Compare commits

...

275 Commits

Author SHA1 Message Date
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
f416b1b5da Merge branch 'dev' into rc 2024-12-24 16:23:46 +01:00
Bram Kragten
6a345d86a6 Bumped version to 20241224.0 2024-12-24 16:23:30 +01:00
Bram Kragten
a04e9b68bd Fixes for device quick bar (#23441)
Fixes #23432 and Fixes #23431
2024-12-24 17:22:06 +02:00
Bram Kragten
f7dbd38c2e Sort addons in backup by name (#23442)
* Sort addons in backup by name

* Update ha-backup-addons-picker.ts
2024-12-24 14:44:27 +00:00
Bram Kragten
7150016375 fix missing credentials step (#23440) 2024-12-24 15:26:43 +01:00
Bram Kragten
c3507abd9c Guard for cloud integration not loaded (#23439) 2024-12-24 15:17:40 +01:00
Bram Kragten
fc8a8b28c2 await copy clipboard (#23438) 2024-12-24 15:15:17 +01:00
Bram Kragten
637fe37ef4 Change emergency kit format (#23428)
* Change emergency kit format

* Update src/panels/config/backup/ha-config-backup-settings.ts

* review
2024-12-24 13:55:38 +00:00
Bram Kragten
657bfc82ca Backup text updates (#23437)
* Backup text updates

* Update ha-config-backup-backups.ts
2024-12-24 13:50:03 +00:00
Bram Kragten
44423812f4 Set defaults for background settings, restore old behavior (#23436)
Fixes https://github.com/home-assistant/frontend/issues/23421
2024-12-24 14:48:28 +01:00
Bram Kragten
a6a76155e5 Fix chart tooltips (#23435) 2024-12-24 08:47:40 -05:00
Bram Kragten
8c18d816b6 Fix banner when first backup was unsuccessful (#23430)
* Fix banner when first backup was unsuccesful

* Update ha-backup-config-agents.ts

* remove fix widths
2024-12-24 08:45:43 -05:00
Bram Kragten
361caafab9 fix saving new automation/script (#23433) 2024-12-24 08:44:23 -05:00
Jan-Philipp Benecke
94f679e387 Add area to automation/script save dialog (#23416)
Add area chip to automation/script save dialog
2024-12-24 11:21:03 +01:00
Bram Kragten
85bd6432a9 Update hui-generic-entity-row.ts 2024-12-24 10:19:38 +01:00
Norbert Rittel
00c1dfa1d2 Add missing localizations to ha-script-trace.ts (#23418)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2024-12-24 09:55:09 +01:00
Norbert Rittel
fb72e1fb9c Wrong lowercase and adjacent space in dialog-media-manage.ts (#23372) 2024-12-24 09:54:26 +01:00
Yosi Levy
152d2d0bdf Rtl fixes (#23425) 2024-12-24 09:42:26 +01:00
renovate[bot]
e06aa52a21 Update dependency ua-parser-js to v1.0.40 (#23427)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-24 10:17:47 +02:00
renovate[bot]
49aa935490 Update dependency @braintree/sanitize-url to v7.1.1 (#23417)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-24 10:07:34 +02:00
Norbert Rittel
cead1e355d Fix missing localization of "Message" field in Cloud > Try text-to-speech dialog (#23422)
* Add missing "Message" string for cloud tts dialog to en.json

The field label for the Message field in the Cloud > Try text-to-speech dialog is missing localization.

This commit adds the necessary localizable string.

* Update dialog-cloud-tts-try.ts to include localizable string
2024-12-24 10:05:31 +02:00
Norbert Rittel
edc99b5d6c Lovelace - Resources: Add localizable string for "Delete" column header (#23404) 2024-12-23 21:39:10 +01:00
Simon Lamon
b38963b214 Fix allow-svg attribute (#23415) 2024-12-23 21:37:24 +01:00
Charles Garwood
0d99531855 Grammar fix for assign category dialog (#23412) 2024-12-23 21:36:41 +01:00
Simon Lamon
9cde7637ca Weekly backup on tuesday: spelling mistake (#23410) 2024-12-23 21:36:17 +01:00
Paul Bottein
edc08994b3 Format scheduled backup time using locale settings (#23407) 2024-12-23 17:40:12 +01:00
Paul Bottein
fc0907ef72 Fix skeleton in dark mode (#23406)
* Fix skeleton in dark mode

* Fix
2024-12-23 15:23:44 +00:00
Bram Kragten
bdb28246fc Update ha-config-backup-overview.ts 2024-12-23 16:16:22 +01:00
Bram Kragten
0582798cde Fix backup summary content not centered (#23405) 2024-12-23 15:07:07 +00:00
Simon Lamon
331385794c View background settings: Change radio buttons to dropdowns (#23403) 2024-12-23 15:30:46 +01:00
Simon Lamon
8e71bd7e82 View background settings: Change radio buttons to dropdowns (#23403) 2024-12-23 15:30:17 +01:00
Bram Kragten
664cc9b33d Bumped version to 20241223.1 2024-12-23 15:26:30 +01:00
Bram Kragten
6757cda7f0 Text changes backups (#23397)
* Text changes backups

* Update dialog-change-backup-encryption-key.ts

* Update ha-backup-config-agents.ts

* Update header margin

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2024-12-23 14:26:18 +00:00
Bram Kragten
97e6313890 Min height list items backups detail smaller (#23398) 2024-12-23 15:08:00 +01:00
Norbert Rittel
3cfba7b960 Replace "State" with "Status" for consistency with Entity picker (#23399)
The Filters pane for entities contains a "Status" section which gives several options to narrow down the list of entities, including "Disabled".

For devices, however, the matching selector is currently labelled "State", although it has "Disabled" as the only choice.

This conflicts with the device / entity state definition in HA and is inconsistent, getting worse in translations.
2024-12-23 15:06:50 +01:00
Paul Bottein
5fb384ad31 Backup status banner improvement (#23400)
* Improve overdue backups and don't consider backup with failed agents as success

* Improve margin and loader

* Improve margin

* Show agent name if only one off site location is configured
2024-12-23 15:06:36 +01:00
Bram Kragten
76cb9ce807 Change sidebar header color (#23401) 2024-12-23 14:04:19 +00:00
Norbert Rittel
0e61596f5c Replace "Add" button with "Create" in Create floor / area dialogs (#23395) 2024-12-23 14:25:14 +01:00
Bram Kragten
061b6af812 Fix search bar styling (#23396) 2024-12-23 14:23:51 +01:00
Bram Kragten
fd95ab5518 Revert "Bump softprops/action-gh-release from 2.1.0 to 2.2.0" (#23393)
Revert "Bump softprops/action-gh-release from 2.1.0 to 2.2.0 (#23300)"

This reverts commit 65458538a7.
2024-12-23 11:59:05 +00:00
Bram Kragten
5a0225b86a fix merge 2024-12-23 12:07:28 +01:00
Bram Kragten
8b79fc5848 Merge branch 'master' into dev 2024-12-23 12:00:53 +01:00
Bram Kragten
5513da51a8 Bumped version to 20241223.0 2024-12-23 11:58:57 +01:00
Bram Kragten
56f9165323 Move order of restore around (#23391) 2024-12-23 10:56:54 +00:00
Paul Bottein
6afcd4d770 Add icon and description for network mount locations (#23390)
* Use custom icon and name for network storage

* Add note to local backup location dialog
2024-12-23 11:53:31 +01:00
Bram Kragten
52e1f9315e Fix selection mode backups on mobile (#23389)
* Fix selection mode backups on mobile

* Update ha-config-backup-details.ts

* unselect when deleting
2024-12-23 11:45:23 +01:00
Steve Repsher
a00f645921 Revert "Add node_modules cache in ci (#22826)" (#23370)
This reverts commit 4655929fed.
2024-12-23 11:27:04 +01:00
Bram Kragten
faf3bb2644 Swap setup python and verfify version (#23388) 2024-12-23 10:09:38 +01:00
Bram Kragten
31d98ec935 Always show HA toggle in backup data settings (#23387) 2024-12-23 09:37:50 +01:00
Mark Steward
7d0a269f1b Use null instead of default, to avoid stomping on netmask (#23382) 2024-12-23 09:22:11 +01:00
karwosts
147098f0fd ha-form-multi_select accessibility improvements (#21023) 2024-12-23 09:21:27 +01:00
dependabot[bot]
d4cbfd9583 Bump actions/upload-artifact from 4.4.3 to 4.5.0 (#23386) 2024-12-23 08:33:48 +01:00
Norbert Rittel
a35ba38a2d Make quotation marks localizable in Add badge dialog (#23383)
* Add quotation and question marks to en.json for localization

Currently the quotation marks are hard-coded. This commit puts them into the strings file and adds the missing question mark at the end.

* Remove hard-coded quotation marks from hui-dialog-create-badge.ts
2024-12-23 06:38:18 +01:00
Dave T
8fa36c8226 Add stream preview element to generic camera (#21463)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2024-12-22 17:14:29 +00:00
karwosts
5ccc3365fe Show all disabled helpers in config/helpers (#23377) 2024-12-22 18:04:16 +01:00
renovate[bot]
dcf97d4667 Update dependency @codemirror/view to v6.36.1 (#23381)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-22 17:25:32 +01:00
Petar Petrov
4b7acbb766 Energy Sankey chart card (#23002)
* WIP: sankey chart

* basic sankey chart

* dynamic size of last section

* basic energy-sankey card

* add floors, areas & passthrough

* order by floor level, add colors & exess energy nodes

* tweak nodes

* add tooltips and better layout and responsiveness

* WIP vertical layout

* fix height when not in sections

* handle labels in vertical mode

* remove from energy dashboard for now

* lint fix

* PR comments

* use ResizeController instead of ResizeObserver

* look up device area

* code clarity improvement
2024-12-22 17:25:13 +01:00
Simon Lamon
7900eb4054 View background settings (#23133)
Co-authored-by: karwosts <karwosts@gmail.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2024-12-22 16:19:54 +00:00
Bram Kragten
53caef8f92 Update developer-tools-statistics.ts 2024-12-22 17:06:46 +01:00
Jan-Philipp Benecke
14bebc76b0 Add toggle switch for exposing of new entities to Assist (#23127) 2024-12-22 16:06:13 +00:00
Norbert Rittel
8b6382448f Use "limit" instead of "mode" for Numeric state trigger and condition (#23360) 2024-12-22 17:00:48 +01:00
Jan-Philipp Benecke
5cd6f22e99 Add category and labels to automation/script save and rename dialog (#23240)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2024-12-22 16:59:04 +01:00
Wendelin
523c38a83e Add tests for common/config (#23349)
* Add tests for common/config

* Revert version dev hook
2024-12-22 16:05:17 +01:00
Wendelin
0a28bbdd72 Improve picture select crop error handling (#23352)
* Improve crop editor error handling

* Add tests for data/image_upload

* Fix tests imports
2024-12-22 15:45:37 +01:00
renovate[bot]
e58bef7795 Update dependency globals to v15.14.0 (#23375)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-21 15:36:45 +00:00
Bram Kragten
d403532fc1 Fix link on all backups button (#23374) 2024-12-21 16:35:00 +01:00
karwosts
4de8b562bd Ability to show map icons for geolocation sources (#23247) 2024-12-21 16:29:53 +01:00
renovate[bot]
31e85836f0 Update CodeMirror (#23369)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-21 16:29:06 +01:00
Franck Nijhof
e1359781a5 Add state completion support for Assist Satellite entities (#23371) 2024-12-21 10:17:59 +01:00
Paul Bottein
379bc3a4e3 Add links to backup overview settings (#23368) 2024-12-20 21:12:05 +01:00
Bram Kragten
b3b0006ba3 Filter backups by type (#23366)
* filter backups by type

* Add filter pane

* Make sure we show all when you click show all
2024-12-20 20:52:11 +01:00
Bram Kragten
cd44b33201 Merge branch 'rc' 2024-12-20 18:50:55 +01:00
Paul Bottein
33df805168 Revert "Add overview settings link to backup settings sections"
This reverts commit fad435ea10.
2024-12-20 18:21:24 +01:00
Paul Bottein
fad435ea10 Add overview settings link to backup settings sections 2024-12-20 18:20:20 +01:00
Bram Kragten
b35f9944ea Change backup restore flow (#23354)
* Change backup restore flow

* adapt and finish

* Update dialog-restore-backup.ts
2024-12-20 16:39:44 +01:00
Paul Bottein
dc799bf691 Don't allow user to choose cloud if ha settings is not set (#23364) 2024-12-20 16:39:35 +01:00
Simon Lamon
1f7929bb3d Rspack v1.1.8 (#23363) 2024-12-20 16:20:25 +01:00
Paul Bottein
fb228dc918 Multiple backup adjustments (#23361)
* Always show welcome screen during onboarding

* Fix content overflow in dialog

* Rename copies to backups, drop 7 days

* Remove including new and learn more button

* Some other fixes

* Allow changing default local location from new backup page

* Dont show addon version in settings and onboarding

* Add margin for ios

* Display old encryption key before change

* Refactor descriptipn
2024-12-20 15:52:55 +01:00
Wendelin
c66f5e2d8a Fix spacing of markdown textfield helpers (#23359) 2024-12-20 15:41:09 +01:00
ildar170975
d71b29d089 border-radius fixes for state-badge (#23212) 2024-12-20 15:06:03 +01:00
renovate[bot]
6b230b6142 Lock file maintenance (#23337)
* Lock file maintenance

* Resolve codemirror in current version

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2024-12-20 13:16:23 +00:00
Paul Bottein
c532a9023a Revert remove hass debug connection in dev (#23356) 2024-12-20 14:05:33 +01:00
Paul Bottein
ecc704e6ac Fix duplicate agents ids during onboarding (#23357)
Fix duplicate agents ids during onboardind
2024-12-20 13:04:32 +00:00
Bram Kragten
5470c8f250 Bumped version to 20241127.9 2024-12-20 13:54:18 +01:00
Bram Kragten
d21f249aac Only use pipelines that have the default coversation agent (#23320)
only use pipelines that have the default coversation agent
2024-12-20 13:53:31 +01:00
renovate[bot]
907299b139 Update dependency @codemirror/language to v6.10.7 (#23350)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-20 13:02:03 +01:00
Wendelin
37aa2bd869 Improve onboarding backup restore (#23340)
* Improve onboarding backup restore

* Fix onboarding backup restore

* Fix restoring value in onboarding-restore-backup
2024-12-20 12:13:16 +01:00
Petar Petrov
f1f53b9f24 Set default netmask when parsing an address (#23348) 2024-12-20 11:28:12 +01:00
Paul Bottein
49d9c7f392 Update icons for backups (#23351) 2024-12-20 11:27:49 +01:00
karwosts
65860a3142 Allow selecting previously uploaded image for picture upload (#23072) 2024-12-20 10:55:24 +01:00
ildar170975
3b52d3d302 Fix visibility for shown entities on device card (2) (#23198) 2024-12-20 10:44:57 +01:00
Steve Repsher
2fe6203eae Remove leftover packages from Rollup builds (#23344) 2024-12-19 15:16:08 -05:00
Paul Bottein
92b02e39c9 Add overview summary for backups (#23343) 2024-12-19 19:54:50 +01:00
renovate[bot]
b693fd1edc Update dependency @rsdoctor/rspack-plugin to v0.4.12 (#23341)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-19 17:21:23 +02:00
libe.net
b84e00b312 Unit Test for shiftDateRange (#23324)
Unit Test for Pull 23228

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2024-12-19 15:42:31 +01:00
karwosts
973fd51639 Sortable energy individual devices (#23330)
* Sortable energy individual devices

* change key fn
2024-12-19 15:51:56 +02:00
Simon Lamon
e0494ccb57 Introduce a target picker for the logbook card (#23007)
* target picker logbookcard wip

* migration

* fixes

* change property

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

* prettier
2024-12-19 14:41:32 +01:00
Paul Bottein
95559cbc2a Setup backup overview page (#23331)
* Add overview page

* Remove configure button

* Reorganize files

* Add backups summary

* Add settings overview

* Fixes

* Update wording

* Setup onboarding before creating a backup
2024-12-19 13:37:05 +00:00
Paul Bottein
3da13b823a Only show local addons folder settings if at least one addon is local (#23333)
Only show local addons folder settings if an addon is local
2024-12-19 14:29:35 +01:00
Paul Bottein
c022871ead Add none option for backup addons (#23335) 2024-12-19 11:26:17 +01:00
Mimi
c4fcbf0613 Optimize the style of hui-energy-period-selector (#23332) 2024-12-19 07:29:32 +00:00
renovate[bot]
79921745a8 Update dependency magic-string to v0.30.17 (#23339)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-19 09:09:24 +02:00
Paul Bottein
40a4255045 Rename backup strategy (#23329)
* Rename backup strategy

* Feedback
2024-12-18 18:02:42 +01:00
Petar Petrov
2d902a0688 Fix integrations dialog (#23328)
* Fix integrations dialog

* tweak comments
2024-12-18 15:53:30 +01:00
Petar Petrov
8a46ef6168 Improve chart zoom (#23326) 2024-12-18 15:50:34 +01:00
karwosts
ba3d37b550 Fix costs in energy csv download (#23322) 2024-12-18 08:46:55 +02:00
renovate[bot]
88d0247217 Update dependency marked to v15.0.4 (#23323)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-18 08:33:06 +02:00
Krisjanis Lejejs
1c076d22a6 Add MFA login flow support for cloud component (#23188)
* Add MFA login flow support for cloud component

* Update MFA login in voice assistant setup flow

* Sync errors with core

* Add translations to the TOTP dialog
2024-12-17 20:55:07 +01:00
Julian
0ecdae2551 Fix inconsistency in Thermostat and Humidifier card names (#22416) 2024-12-17 19:15:35 +01:00
libe.net
6f8ba6afac Date-range-picker Design to use Prev and Next Buttons in History and Logbook on mobiles (#23228)
* date-range-picker prev and next design

* correct width and margins

* logbook width correction

* Update src/components/ha-date-range-picker.ts

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

* Update src/components/ha-date-range-picker.ts

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

* Update src/components/ha-date-range-picker.ts

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

* changes from comments

* share date-range logic for energy and history / logbook

* switch to energy-dashboard timespan-function

* changed shiftDateRange to differenceInMilliseconds

* used gap instead margin-right

---------

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2024-12-17 17:40:32 +01:00
Bram Kragten
ad1c32a880 Only use pipelines that have the default coversation agent (#23320)
only use pipelines that have the default coversation agent
2024-12-17 17:50:29 +02:00
Petar Petrov
48819a59e7 Fix new automation dialog in Firefox (#23319) 2024-12-17 15:00:00 +01:00
Bram Kragten
2718801c69 Update light-color-rgb-picker.ts 2024-12-17 14:47:38 +01:00
renovate[bot]
2b43f5f8c8 Update dependency eslint to v9.17.0 (#23316)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-17 09:18:31 +02:00
Paul Bottein
0ef23cd712 Remove backup toggle on the update more info dialog (#23314) 2024-12-16 15:47:19 +00:00
Petar Petrov
7370d1e0dd Fix touch control of chart zoom (#23302)
* Fix touch control of chart zoom

* fix
2024-12-16 16:03:32 +01:00
karwosts
da7d3e118c Allow for deleting image_upload images in media panel (#23086) 2024-12-16 16:36:50 +02:00
renovate[bot]
d4188d9aee Update dependency terser-webpack-plugin to v5.3.11 (#23313)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-16 16:27:27 +02:00
Marcin
8722157623 Focus on card search input when adding new card (#23309)
* Focus on card search input when adding new card

* Update src/panels/lovelace/editor/card-editor/hui-card-picker.ts

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>

* update and fix

* not on mobile

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2024-12-16 15:54:29 +02:00
Petar Petrov
6e2f0d8c9b Enable chart animations (#23311) 2024-12-16 14:37:31 +01:00
Paul Bottein
08459394a6 Add backup sync status in detail page (#23312) 2024-12-16 14:36:59 +01:00
Jan-Philipp Benecke
ee292f900f For consistency align close button to the left in ha-dialog (#23284) 2024-12-16 15:35:06 +02:00
Steve Repsher
3aaf08ac03 Defer loading of fallback domain icons (#23298) 2024-12-16 15:22:53 +02:00
Paul Bottein
d31f4a5f1d Improve backup onboarding (#23282)
* Sort agents, enable cloud and local by default

* Enable cloud by default

* Improve wording

* Hide fab if onboarding is not complete

* Add recommended settings

* Use one step encryption key during onboarding

* Add description for cloud

* Update change encryption key dialog
2024-12-16 14:08:51 +01:00
Bram Kragten
875ab0cb97 Cloud login: Wait with navigate until confirm action is done (#23310)
Wait with navigate until confirm action is done
2024-12-16 13:08:30 +00:00
renovate[bot]
678af025ac Update vaadinWebComponents monorepo to v24.6.0 (#23306)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-16 13:41:31 +01:00
renovate[bot]
1fd38d085f Update dependency @lokalise/node-api to v13 (#23299)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-16 13:39:35 +01:00
Paul Bottein
ec324ab09f Disable backup by default for update more info (#23307) 2024-12-16 12:24:09 +01:00
Paul Bottein
efcd57934a Improve backup data-table on mobile (#23283)
* Improve backup data-table on mobile

* Group by default

* Fix top-header slot on mobile
2024-12-16 12:23:17 +01:00
Norbert Rittel
1efe61445f Fix typo "dasboard" and missing hyphen in en.json (#23303)
Add the missing "h" to "Choose a dasboard"
Add the missing hyphen to "auto generated"
2024-12-16 09:44:17 +01:00
Wendelin
6db9bf800a Fix ha-button-menu menuCorner attribute (#23301)
Fix menuCorner attribute
2024-12-16 09:47:36 +02:00
dependabot[bot]
65458538a7 Bump softprops/action-gh-release from 2.1.0 to 2.2.0 (#23300)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-16 08:04:32 +01:00
Norbert Rittel
9ddeb3734f Fix grammar mismatch in OAuth description (#23297) 2024-12-15 21:45:40 +01:00
Steve Repsher
c89fc188a5 Remove fixed device class icons (#23291) 2024-12-14 16:15:10 +01:00
Norbert Rittel
d366471058 Make Device Condition description consistent with Trigger and Action (#23293)
Make device Condition description consistent with Trigger and Action

The first item in the lists of the triggers, conditions and actions is always the device type:

- When something happens to a device. Great way to start.
- Set of conditions provided by your device. Great way to start
- Do something on a device. Great way to start.

The condition does use "your" instead of "a" which is inconsistent and sounds irritating. As there are usually many different devices in a setup the use of "a" is much preferred as the actual device is picked up at the next step.

This commit makes the condition string consistent by replacing "your" with "a".
2024-12-14 16:14:24 +01:00
renovate[bot]
9951c162a1 Update dependency eslint-import-resolver-webpack to v0.13.10 (#23292)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-14 09:23:38 +01:00
renovate[bot]
0c7d689b5a Update dependency lint-staged to v15.2.11 (#23289)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-13 20:23:43 +01:00
renovate[bot]
655ce05efe Update dependency magic-string to v0.30.15 (#23287)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-13 08:44:33 +01:00
renovate[bot]
2a6e562d37 Update rspack monorepo to v1.1.6 (#23288)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-13 08:44:10 +01:00
renovate[bot]
98d1954812 Update formatjs monorepo (#23269)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-12 17:49:02 +01:00
renovate[bot]
72a2f54598 Update dependency @codemirror/view to v6.35.3 (#23280)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-12 17:47:25 +01:00
Bram Kragten
9364ea060a Merge branch 'rc' 2024-12-12 17:44:28 +01:00
Bram Kragten
f8dfdcb090 Bumped version to 20241127.8 2024-12-12 17:43:43 +01:00
Paulus Schoutsen
3d78a7821a Improve piper audio generation (#23281)
* Improve piper audio generation

* update logic

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2024-12-12 17:43:26 +01:00
Bram Kragten
e923d610ce Add missing translations to voice wizard (#23278) 2024-12-12 17:43:25 +01:00
Paul Bottein
8b25fe88a4 Use list item for integration quality scale (#23236) 2024-12-12 17:43:24 +01:00
Wendelin
3768be55ff Fix landingpage, supervisor release permission (#23223) 2024-12-12 17:43:23 +01:00
Paulus Schoutsen
47cf17ab50 Improve piper audio generation (#23281)
* Improve piper audio generation

* update logic

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2024-12-12 17:41:11 +01:00
Wendelin
d2ec24f32a Automation/Script editor add settings,category overflow actions (#23272)
* Add settings, category actions

* Add script category and settings actions

* Correct overflow actions order

* Use consume instead of subscribe
2024-12-12 17:40:52 +01:00
Bram Kragten
69ed736080 Add missing translations to voice wizard (#23278) 2024-12-12 17:38:53 +01:00
Wendelin
7d3c77008d Mobile styles for backup summary (#23276)
* Add mobile backup summary styles

* revert padding
2024-12-12 17:02:51 +01:00
Wendelin
27dbabc6bf Fix create custom backup twice on dialog-new-backup (#23279) 2024-12-12 15:55:07 +00:00
Wendelin
27ce395d68 Common-color-tests (#23258) 2024-12-12 14:44:17 +01:00
Steve Repsher
145a536156 Remove unused polyfills and adjust support for Intl.getCanonicalLocales (#23264) 2024-12-12 15:07:50 +02:00
ildar170975
e9b2a83411 Automation/Script editor border-radius fix (#23267)
* Update ha-automation-action-row.ts

* Update ha-automation-condition-row.ts

* Update ha-automation-trigger-row.ts

* Update ha-script-field-row.ts

* Update ha-script-field-row.ts

* Update ha-automation-condition-row.ts

* Update ha-automation-trigger-row.ts

* Update ha-automation-action-row.ts

* Update ha-automation-trigger-row.ts

* Update ha-automation-condition-row.ts

* Update ha-automation-action-row.ts

* Update ha-script-field-row.ts
2024-12-12 15:03:19 +02:00
Petar Petrov
784b7e4d04 Add zoom & pan to charts (#23183)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2024-12-12 13:40:47 +01:00
renovate[bot]
f4bf999ae2 Update dependency @codemirror/state to v6.5.0 (#23277)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-12 12:37:43 +00:00
Petar Petrov
9c04f57e35 Fix getting custom config parameters for Z-Wave JS (#23244) 2024-12-12 13:37:22 +01:00
Petar Petrov
f0ddc408e8 Remove custom TimeLineScale for chartjs (#23270) 2024-12-12 12:43:22 +01:00
Petar Petrov
df4d5a4567 Simplify dialog history logic (#23271) 2024-12-12 12:41:57 +01:00
Bram Kragten
0a413cba03 Fix more dialogs (#23273)
* Remove unneeded close dialog calls

* more
2024-12-12 09:00:38 +00:00
Bram Kragten
7e2217b542 Fix navigate timing when dialogs where opened (#23261)
* Fix navigate timing when dialogs where opened

* Revert navigate changes
2024-12-12 08:35:34 +00:00
Petar Petrov
93947d76a2 Fix navigate with replace when there are open dialogs (#23268) 2024-12-12 09:25:33 +01:00
Petar Petrov
18cce45b88 Fix for auto closing dialogs on navigate (#23262) 2024-12-11 22:47:23 +01:00
Paul Bottein
86f1af6682 Merge feature branch with backup changes to dev (#23239)
* Add dialog to upload a backup file (#22405)

* Add dialog to upload a backup file

* Prosess feedback

* Remoe unused definition

* Early pushout of changes to the backup panel (#22321)

* Eary pushout of changes to the backup panel

* Add location icons

* Path is optional

* Set backupSlug from route

* No need for subscription mixin

* update

* Reorder

* init details

* Fix import

* Improve backup screen and navigation (#22827)

* Add location page

* Start dashboard

* Move list to dashboard

* Add mocked config page

* Fix hardcoded boolean

* Add summary card

* Use new format for BackupAgent

* Use new API

* Rename to ha-backup-summary-card

* Use new api

* Fix backup agents

* Rename backup slug to backup id (#22876)

* Add delete backup action to datatable (#22867)

* Create generate backup dialog (#22866)

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

* Add backup details page (#22884)

* Add new backup dialog to choose between automatic and manual (#22895)

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>

* Improve download backup (#22905)

* Rename remove to delete in backup websocket type (#22902)

* Use bytes for backup size (#22909)

* Use default backup instead of automatic backup (#22915)

* Update generate backup api (#22943)

Use new backup api for generate backup

* Improve details page for new backup (#22946)

* Add content of backup in detail page

* Add restore button

* Add note

* Use disabled

* Fix backup generate

* Use options to WS command backup/restore (#22950)

* Add addons picker in generate backup dialog (#22951)

* Add addons picker in generate backup dialog

* Change condition

* Fix label

* Fix local addons

* Review

* Add local addon in addon mode is all

* Fix local addon folder

* Use addon picker inside data picker

* Fetch addons info in detail page

* Fetch addon inside component

* Rename agents picker

* Restrict generate backup content for core backup (#22958)

* Fix addon mode all

* Use event to check if a backup is in progress (#22960)

* Use event to check if a backup is in progress

* Update src/panels/config/backup/ha-config-backup-dashboard.ts

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

---------

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

* Force enable home assistant settings when history is selected in backup (#22961)

* Backup default config (#22954)

* WIP default config

* Add addons

* save data

* add icon

* basics of change encryption key

* Update dialog-change-backup-password.ts

* use default config when manually triggering default backup

* limit to hassio

* enforce encryption key, manual use manual one

* Update ha-config-backup-dashboard.ts

* Add suggested password and copy buttons

* Add download emergency kit button

* review

* fix

* Update ha-config-backup-default-config.ts

* Update ha-config-backup-default-config.ts

* Update default backup settings (#23109)

* Only display addons and folder for hassio (#23118)

* Use new backup dashboard page for hassio backup (#23161)

* Add support for copies and days for backup retention (#23128)

* Add upload dialog for backup (#23139)

* Improve generate backup dialog (#23167)

* Propose to use encryption key if available when restoring a backup (#23164)

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

* Add encryption key onboarding (#23180)

* Fix attributes broken by the warning fixes (#23182)

* Don't allow any more eslint warnings (#23181)

* Use dedicated endpoint to generate backup with default settings (#23224)

* Add onboarding dialog for backups (#23225)

* Add onboarding flow for backups

* Add welcome screen

* Add progress and status for backup dashboard (#23222)

* Handle backup state

* Add summary card

* Use difference in days

* Rename local backups and show icon (#23238)

* Improve backup onboarding (#23241)

* Do not navigate to config page after onboarding

* Use casita image and center text

* fix lint

* Rename stored and default to strategy backup

* Update

* Fix icon and add type in datatable

* Use strategy in more places

* Fix list item overflow

---------

Co-authored-by: Joakim Sørensen <ludeeus@ludeeus.dev>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2024-12-11 15:52:37 -05:00
Bram Kragten
c8f58c7bc9 Prevent leaving the editor if there are unsaved changes (#23170)
* Prevent leaving the editor if there are unsaved changes

* Process code review

* use first composePath target

* fix function calls

* Use query instead

* Remove id on sidebar

* suggestions

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2024-12-11 16:58:25 +01:00
Wendelin
e5e168803a Remove not needed admin checks (#23259) 2024-12-11 16:50:35 +01:00
Bram Kragten
3436a023f6 suggestions 2024-12-11 16:08:03 +01:00
Jan-Philipp Benecke
84322a21fe Remove id on sidebar 2024-12-11 15:59:46 +01:00
Jan-Philipp Benecke
ec20f7e2c4 Use query instead 2024-12-11 15:59:46 +01:00
Jan-Philipp Benecke
3579d82e8e fix function calls 2024-12-11 15:59:46 +01:00
Jan-Philipp Benecke
70532ac3bf use first composePath target 2024-12-11 15:59:46 +01:00
Jan-Philipp Benecke
96b9d25bc5 Process code review 2024-12-11 15:59:46 +01:00
Jan-Philipp Benecke
91777d45b0 Prevent leaving the editor if there are unsaved changes 2024-12-11 15:59:46 +01:00
ildar170975
0582b8430d Update developer-tools-template.ts: add independent scrollbars for left & right panels for large screens (#23242)
* Update developer-tools-template.ts

* whitespace in pre

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

* prettier

* Update developer-tools-template.ts

* Update developer-tools-template.ts

* prettier

* prettier!!!!

* prettier again

* prettier fight

* prettier

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

* prettier

* prettier

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2024-12-11 15:25:39 +02:00
Bram Kragten
63d97398c1 Fix dialog navigation by making navigate() async (#23254)
* Fix dialog navigation by making `navigate()` async

* close all dialogs on `navigate`

* log warning when navigation is blocked

* fix
2024-12-11 14:13:20 +01:00
Steve Repsher
3552417b39 Compress legacy build and shared static files using Brotli (#23233)
* Revert "Remove Zopfli compression (#23157)"

This reverts commit 4092f56ea5.

* Revert "Fix compression of hassio builds (#21869)"

This reverts commit b69f0964c9.

* Compress legacy build and shared static files using Brotli
2024-12-11 15:05:17 +02:00
Petar Petrov
a6cbbfe1a4 fix 2024-12-11 13:59:47 +02:00
Petar Petrov
48f5d17060 log warning when navigation is blocked 2024-12-11 13:36:47 +02:00
Petar Petrov
c713106948 close all dialogs on navigate 2024-12-11 13:35:09 +02:00
Petar Petrov
142e674020 Fix dialog navigation by making navigate() async 2024-12-11 11:56:32 +02:00
Wendelin
e4fc21c991 Move view to another dashboard (#23245)
* Add move view to dashboard

* Remove unneeded return in error flow

* Update src/panels/lovelace/editor/select-dashboard/hui-dialog-select-dashboard.ts

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

* Update src/panels/lovelace/editor/select-dashboard/hui-dialog-select-dashboard.ts

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

* Use ha-list-item for button-menu

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2024-12-11 10:30:57 +02:00
Bram Kragten
f9844e8e58 Merge branch 'rc' 2024-12-09 15:59:29 +01:00
Bram Kragten
86f9909ac9 Bumped version to 20241127.7 2024-12-09 15:59:07 +01:00
Paulus Schoutsen
629ae3fbf3 Fix voice debug link (#23214) 2024-12-09 15:58:46 +01:00
Christopher Fenner
ddd2c177b5 Correct overwriting integration labelling on integrations page (#23206)
* Update ha-config-integration-page.ts

fixes #22776

* update icon color

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2024-12-09 15:58:45 +01:00
ildar170975
829de4a073 Fix "Integration entries" page for yaml-based integrations (#23201)
* Update ha-config-integration-page.ts

* prettier
2024-12-09 15:58:44 +01:00
karwosts
0df8b96133 No script entities in scenes (#23192) 2024-12-09 15:58:43 +01:00
Wendelin
443921a97c Automate supervisor & landing-page release (#22959)
* Automate supervisor & landing-page release

* Add no prerelease condition to supervisor/landing-page release

* Prepare release workflow for testing

* Add release permissions to create PR

* Add supervisor, landingpage release to assets

* Create test draft release to test

* Fix hassio release path

* Fix workflow permission for test reasons

* Revert test settings
2024-12-09 15:58:43 +01:00
Bram Kragten
54bc0525f7 Merge branch 'rc' 2024-12-06 16:18:55 +01:00
Bram Kragten
72f8f020fc Bumped version to 20241127.6 2024-12-06 16:18:37 +01:00
Bram Kragten
7f31acf764 Fix label selector when required (#23186) 2024-12-06 16:18:21 +01:00
Bram Kragten
8c5862e4ce 20241127.5 (#23178) 2024-12-06 10:45:15 +01:00
Bram Kragten
f3e0df93b5 Bumped version to 20241127.5 2024-12-06 10:37:14 +01:00
Paul Bottein
c6c5ea34d3 Fix text color in ha-md-select in dark mode (#23174) 2024-12-06 10:36:53 +01:00
Bram Kragten
287a068ada Add localisation to voice wizard (#23169)
* Add localization to voice wizard

* more
2024-12-06 10:36:52 +01:00
karwosts
8ca52820b1 Missing horiz swing mode import (#23168) 2024-12-06 10:36:51 +01:00
Bram Kragten
cd8900dd26 Voice addon install: try to find discovered flow (#23146) 2024-12-06 10:36:51 +01:00
Paul Bottein
6d2e7f9fbd Improve trigger and action description for conversation (#23141) 2024-12-06 10:36:50 +01:00
Bram Kragten
1016c87c60 20241127.4 (#23137) 2024-12-04 14:20:32 +01:00
Bram Kragten
dbda1d75f9 Bumped version to 20241127.4 2024-12-04 14:12:49 +01:00
Wendelin
a9c25b49b9 Add red delete button to delete zone confirmation dialog (#23136) 2024-12-04 14:12:25 +01:00
Bram Kragten
fbff95345c Use action instead of selected for select entity row (#23135) 2024-12-04 14:12:24 +01:00
Paul Bottein
8f4e65d392 Don't use duration formatting for second unit (#23132)
Don't use duration formatting for sec unit
2024-12-04 14:12:23 +01:00
Paul Bottein
3a2cb51f8d Reduce button target zone in media player more info (#23130) 2024-12-04 14:12:22 +01:00
Jan-Philipp Benecke
6c9cfed49f Explicitly set file name for camera snapshot (#23124)
* Explicitly set file name for camera snapshot

* Process code review
2024-12-04 14:12:21 +01:00
Paul Bottein
257cab1061 Fix create section on iOS (#23123) 2024-12-04 14:12:21 +01:00
Wendelin
5f6577a24c Remove static font from ha-badge (#23120) 2024-12-04 14:12:20 +01:00
Jan-Philipp Benecke
f8d6f0fae4 Show in assist pipeline debug when intent is preferred and processed locally (#23115) 2024-12-04 14:12:19 +01:00
renovate[bot]
5b7eeb6ac1 Update dependency marked to v15.0.3 (#23088)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-04 14:12:18 +01:00
Bram Kragten
191feed920 Merge branch 'rc' 2024-12-03 14:44:05 +01:00
Bram Kragten
13c1763277 Bumped version to 20241127.3 2024-12-03 14:43:51 +01:00
Wendelin
ac9085c7a4 Fix ha-target-picker hideTitle (#23116) 2024-12-03 14:43:40 +01:00
Bram Kragten
8beb93b695 Voice local: Small refactor and return when local already exists (#23113) 2024-12-03 14:43:39 +01:00
Marcin
4f76e66cc0 Updated English translations for scene editor. (#23110) 2024-12-03 14:43:38 +01:00
Bram Kragten
1ebd13027c 20241127.2 (#23104) 2024-12-02 21:39:46 +01:00
Bram Kragten
04beef5e36 Bumped version to 20241127.2 2024-12-02 21:31:47 +01:00
Bram Kragten
0a578c5847 Voice wizard local: Add error message, fix hostname (#23103)
Add error message, fix hostname
2024-12-02 21:31:29 +01:00
Wendelin
41924d8ec6 Add automatic retry to stream logs (#23098) 2024-12-02 21:31:29 +01:00
Paul Bottein
6ff1a6fecc Don't show alert in voice assistant dialog (#23097) 2024-12-02 21:31:28 +01:00
Wendelin
05eb6e15a5 Save scene before switching to live edit (#23094)
Save scene changes before live edit, align delete icons for entities.
2024-12-02 21:31:27 +01:00
Alex Jurkiewicz
6d01728d54 history-graph-card-editor: Correct hours_to_show validation (#23090)
history-graph-card-editor: Correct hours_to_show validation

Allow all floating point numbers from 0 up.

Fixes #15933.
2024-12-02 21:31:26 +01:00
karwosts
2550bff4e9 Init new scenes in live edit mode (#23051)
* Init new scenes in live edit mode

* Update src/panels/config/scene/ha-scene-editor.ts

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>

* imports

---------

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2024-12-02 21:31:25 +01:00
Bram Kragten
6e003907fb 20241127.1 (#23045) 2024-11-28 17:01:24 +01:00
Bram Kragten
f6c15dc990 Bumped version to 20241127.1 2024-11-28 16:53:58 +01:00
Wendelin
1061769144 Simplify depends_on_cloud translation (#23042) 2024-11-28 16:52:56 +01:00
Wendelin
1edfec08e1 Add internal, legacy to IQS (#23040) 2024-11-28 16:52:55 +01:00
Wendelin
239cad9b47 Fix iqs naming and docs link anchor (#23036) 2024-11-28 16:52:54 +01:00
Paul Bottein
8d7c175d70 Only use duration poly-fill when necessary (#23030) 2024-11-28 16:52:43 +01:00
Wendelin
795bbefba6 Fix platinum color and spacing of integration logo (#23029) 2024-11-28 16:51:57 +01:00
karwosts
e1b34eaa33 Dont floor duration for milliseconds (#23028)
* Dont floor duration for milliseconds

* remove ms
2024-11-28 16:51:22 +01:00
Bram Kragten
91d5d2f1eb 20241127.0 (#23027) 2024-11-27 15:28:49 +01:00
247 changed files with 14558 additions and 3463 deletions

View File

@@ -26,20 +26,14 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
- name: Setup Node
id: setup-node
uses: actions/setup-node@v4.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
- uses: actions/cache@v4.2.0
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: "node_modules"
key: ${{ runner.os }}-yarn-${{ hashFiles('.yarnrc.yml') }}-${{ steps.setup-node.outputs.node-version }}-${{ hashFiles('yarn.lock') }}
restore-keys: ${{ runner.os }}-yarn-${{ hashFiles('.yarnrc.yml') }}-${{ steps.setup-node.outputs.node-version }}
- name: Install dependencies
if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install --immutable
- name: Check for duplicate dependencies
run: yarn dedupe --check
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
- name: Setup lint cache
@@ -66,19 +60,11 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
- name: Setup Node
id: setup-node
uses: actions/setup-node@v4.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
- uses: actions/cache@v4.2.0
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: "node_modules"
key: ${{ runner.os }}-yarn-${{ hashFiles('.yarnrc.yml') }}-${{ steps.setup-node.outputs.node-version }}-${{ hashFiles('yarn.lock') }}
restore-keys: ${{ runner.os }}-yarn-${{ hashFiles('.yarnrc.yml') }}-${{ steps.setup-node.outputs.node-version }}
- name: Install dependencies
if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install --immutable
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data
@@ -92,26 +78,18 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
- name: Setup Node
id: setup-node
uses: actions/setup-node@v4.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
- uses: actions/cache@v4.2.0
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: "node_modules"
key: ${{ runner.os }}-yarn-${{ hashFiles('.yarnrc.yml') }}-${{ steps.setup-node.outputs.node-version }}-${{ hashFiles('yarn.lock') }}
restore-keys: ${{ runner.os }}-yarn-${{ hashFiles('.yarnrc.yml') }}-${{ steps.setup-node.outputs.node-version }}
- name: Install dependencies
if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install --immutable
- name: Build Application
run: ./node_modules/.bin/gulp build-app
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@v4.4.3
uses: actions/upload-artifact@v4.5.0
with:
name: frontend-bundle-stats
path: build/stats/*.json
@@ -124,26 +102,18 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
- name: Setup Node
id: setup-node
uses: actions/setup-node@v4.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
- uses: actions/cache@v4.2.0
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: "node_modules"
key: ${{ runner.os }}-yarn-${{ hashFiles('.yarnrc.yml') }}-${{ steps.setup-node.outputs.node-version }}-${{ hashFiles('yarn.lock') }}
restore-keys: ${{ runner.os }}-yarn-${{ hashFiles('.yarnrc.yml') }}-${{ steps.setup-node.outputs.node-version }}
- name: Install dependencies
if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install --immutable
- name: Build Application
run: ./node_modules/.bin/gulp build-hassio
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@v4.4.3
uses: actions/upload-artifact@v4.5.0
with:
name: supervisor-bundle-stats
path: build/stats/*.json

View File

@@ -57,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts
uses: actions/upload-artifact@v4.4.3
uses: actions/upload-artifact@v4.5.0
with:
name: wheels
path: dist/home_assistant_frontend*.whl
if-no-files-found: error
- name: Upload translations
uses: actions/upload-artifact@v4.4.3
uses: actions/upload-artifact@v4.5.0
with:
name: translations
path: translations.tar.gz

View File

@@ -25,14 +25,14 @@ jobs:
- name: Checkout the repository
uses: actions/checkout@v4.2.2
- name: Verify version
uses: home-assistant/actions/helpers/verify-version@master
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Verify version
uses: home-assistant/actions/helpers/verify-version@master
- name: Setup Node
uses: actions/setup-node@v4.1.0
with:

View File

@@ -5,7 +5,7 @@ import paths from "../paths.cjs";
const POLYFILL_DIR = join(paths.polymer_dir, "src/resources/polyfills");
// List of polyfill keys with supported browser targets for the functionality
const PolyfillSupport = {
const polyfillSupport = {
// Note states and shadowRoot properties should be supported.
"element-internals": {
android: 90,
@@ -18,17 +18,6 @@ const PolyfillSupport = {
safari: 17.4,
samsung: 15.0,
},
"element-append": {
android: 54,
chrome: 54,
edge: 17,
firefox: 49,
ios: 10.0,
opera: 41,
opera_mobile: 41,
safari: 10.0,
samsung: 6.0,
},
"element-getattributenames": {
android: 61,
chrome: 61,
@@ -51,27 +40,18 @@ const PolyfillSupport = {
safari: 12.0,
samsung: 10.0,
},
fetch: {
android: 42,
chrome: 42,
edge: 14,
firefox: 39,
ios: 10.3,
opera: 29,
opera_mobile: 29,
safari: 10.1,
samsung: 4.0,
},
// FormatJS polyfill detects fix for https://bugs.chromium.org/p/v8/issues/detail?id=10682,
// so adjusted to several months after that was marked fixed
"intl-getcanonicallocales": {
android: 54,
chrome: 54,
edge: 16,
android: 90,
chrome: 90,
edge: 90,
firefox: 48,
ios: 10.3,
opera: 41,
opera_mobile: 41,
opera: 76,
opera_mobile: 64,
safari: 10.1,
samsung: 6.0,
samsung: 15.0,
},
"intl-locale": {
android: 74,
@@ -87,17 +67,6 @@ const PolyfillSupport = {
"intl-other": {
// Not specified (i.e. always try polyfill) since compatibility depends on supported locales
},
proxy: {
android: 49,
chrome: 49,
edge: 12,
firefox: 18,
ios: 10.0,
opera: 36,
opera_mobile: 36,
safari: 10.0,
samsung: 5.0,
},
"resize-observer": {
android: 64,
chrome: 64,
@@ -115,8 +84,6 @@ const PolyfillSupport = {
// corresponding polyfill key and actual module to import
const polyfillMap = {
global: {
fetch: { key: "fetch", module: "unfetch/polyfill" },
Proxy: { key: "proxy", module: "proxy-polyfill" },
ResizeObserver: {
key: "resize-observer",
module: join(POLYFILL_DIR, "resize-observer.ts"),
@@ -128,7 +95,7 @@ const polyfillMap = {
module: "element-internals-polyfill",
},
...Object.fromEntries(
["append", "getAttributeNames", "toggleAttribute"].map((prop) => {
["getAttributeNames", "toggleAttribute"].map((prop) => {
const key = `element-${prop.toLowerCase()}`;
return [prop, { key, module: join(POLYFILL_DIR, `${key}.ts`) }];
})
@@ -168,7 +135,7 @@ export default defineProvider(
const resolvePolyfill = createMetaResolver(polyfillMap);
return {
name: "custom-polyfill",
polyfills: PolyfillSupport,
polyfills: polyfillSupport,
usageGlobal(meta, utils) {
const polyfill = resolvePolyfill(meta);
if (polyfill && shouldInjectPolyfill(polyfill.desc.key)) {

View File

@@ -13,28 +13,41 @@ const brotliOptions = {
},
};
const compressDistBrotli = (rootDir, modernDir, compressServiceWorker = true) =>
const compressModern = (rootDir, modernDir) =>
gulp
.src([`${modernDir}/**/${filesGlob}`, `${rootDir}/sw-modern.js`], {
base: rootDir,
allowEmpty: true,
})
.pipe(brotli(brotliOptions))
.pipe(gulp.dest(rootDir));
const compressOther = (rootDir, modernDir) =>
gulp
.src(
[
`${modernDir}/**/${filesGlob}`,
compressServiceWorker ? `${rootDir}/sw-modern.js` : undefined,
].filter(Boolean),
{
base: rootDir,
}
`${rootDir}/**/${filesGlob}`,
`!${modernDir}/**/${filesGlob}`,
`!${rootDir}/{sw-modern,service_worker}.js`,
`${rootDir}/{authorize,onboarding}.html`,
],
{ base: rootDir, allowEmpty: true }
)
.pipe(brotli(brotliOptions))
.pipe(gulp.dest(rootDir));
const compressAppBrotli = () =>
compressDistBrotli(paths.app_output_root, paths.app_output_latest);
const compressHassioBrotli = () =>
compressDistBrotli(
paths.hassio_output_root,
paths.hassio_output_latest,
false
);
const compressAppModern = () =>
compressModern(paths.app_output_root, paths.app_output_latest);
const compressHassioModern = () =>
compressModern(paths.hassio_output_root, paths.hassio_output_latest);
gulp.task("compress-app", compressAppBrotli);
gulp.task("compress-hassio", compressHassioBrotli);
const compressAppOther = () =>
compressOther(paths.app_output_root, paths.app_output_latest);
const compressHassioOther = () =>
compressOther(paths.hassio_output_root, paths.hassio_output_latest);
gulp.task("compress-app", gulp.parallel(compressAppModern, compressAppOther));
gulp.task(
"compress-hassio",
gulp.parallel(compressHassioModern, compressHassioOther)
);

View File

@@ -67,12 +67,6 @@ function copyPolyfills(staticDir) {
);
}
function copyLoaderJS(staticDir) {
const staticPath = genStaticPath(staticDir);
copyFileDir(npmPath("systemjs/dist/s.min.js"), staticPath("js"));
copyFileDir(npmPath("systemjs/dist/s.min.js.map"), staticPath("js"));
}
function copyFonts(staticDir) {
const staticPath = genStaticPath(staticDir);
// Local fonts
@@ -140,8 +134,6 @@ gulp.task("copy-static-app", async () => {
const staticDir = paths.app_output_static;
// Basic static files
fs.copySync(polyPath("public"), paths.app_output_root);
copyLoaderJS(staticDir);
copyPolyfills(staticDir);
copyFonts(staticDir);
copyTranslations(staticDir);
@@ -164,8 +156,6 @@ gulp.task("copy-static-demo", async () => {
);
// Copy demo static files
fs.copySync(path.resolve(paths.demo_dir, "public"), paths.demo_output_root);
copyLoaderJS(paths.demo_output_static);
copyPolyfills(paths.demo_output_static);
copyMapPanel(paths.demo_output_static);
copyFonts(paths.demo_output_static);
@@ -179,8 +169,6 @@ gulp.task("copy-static-cast", async () => {
fs.copySync(polyPath("public/static"), paths.cast_output_static);
// Copy cast static files
fs.copySync(path.resolve(paths.cast_dir, "public"), paths.cast_output_root);
copyLoaderJS(paths.cast_output_static);
copyPolyfills(paths.cast_output_static);
copyMapPanel(paths.cast_output_static);
copyFonts(paths.cast_output_static);

View File

@@ -14,6 +14,7 @@ import "../../../../src/panels/lovelace/views/hui-view";
import "../../../../src/panels/lovelace/views/hui-view-container";
import type { HomeAssistant } from "../../../../src/types";
import "./hc-launch-screen";
import "../../../../src/panels/lovelace/views/hui-view-background";
(window as any).loadCardHelpers = () =>
import("../../../../src/panels/lovelace/custom-card-helpers");
@@ -57,11 +58,8 @@ class HcLovelace extends LitElement {
const background = viewConfig.background || this.lovelaceConfig.background;
return html`
<hui-view-container
.hass=${this.hass}
.background=${background}
.theme=${viewConfig.theme}
>
<hui-view-container .hass=${this.hass} .theme=${viewConfig.theme}>
<hui-view-background .background=${background}> </hui-view-background>
<hui-view
.hass=${this.hass}
.lovelace=${lovelace}

View File

@@ -1,7 +1,7 @@
import { mdiFolderUpload } from "@mdi/js";
import type { TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/ha-circular-progress";
import "../../../src/components/ha-file-upload";
@@ -10,10 +10,12 @@ import { uploadBackup } from "../../../src/data/hassio/backup";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../src/types";
import type { LocalizeFunc } from "../../../src/common/translations/localize";
declare global {
interface HASSDomEvents {
"backup-uploaded": { backup: HassioBackup };
"backup-cleared": void;
}
}
@@ -21,6 +23,8 @@ declare global {
export class HassioUploadBackup extends LitElement {
public hass?: HomeAssistant;
@property({ attribute: false }) public localize?: LocalizeFunc;
@state() public value: string | null = null;
@state() private _uploading = false;
@@ -32,13 +36,26 @@ export class HassioUploadBackup extends LitElement {
.uploading=${this._uploading}
.icon=${mdiFolderUpload}
accept="application/x-tar"
label="Upload backup"
supports="Supports .TAR files"
.label=${this.localize?.(
"ui.panel.page-onboarding.restore.upload_backup"
) || "Upload backup"}
.supports=${this.localize?.(
"ui.panel.page-onboarding.restore.upload_supports"
) || "Supports .TAR files"}
.secondary=${this.localize?.(
"ui.panel.page-onboarding.restore.upload_drop"
) || "Or drop your file here"}
@file-picked=${this._uploadFile}
@files-cleared=${this._clear}
></ha-file-upload>
`;
}
private _clear() {
this.value = null;
fireEvent(this, "backup-cleared");
}
private async _uploadFile(ev) {
const file = ev.detail.files[0];

View File

@@ -65,7 +65,7 @@ const _computeAddons = (addons): AddonCheckboxItem[] =>
@customElement("supervisor-backup-content")
export class SupervisorBackupContent extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public localize?: LocalizeFunc;
@@ -186,12 +186,13 @@ export class SupervisorBackupContent extends LitElement {
.iconPath=${mdiHomeAssistant}
.version=${this.backup
? this.backup.homeassistant
: this.hass.config.version}
: this.hass?.config.version}
>
</supervisor-formfield-label>`}
>
<ha-checkbox
.checked=${this.homeAssistant}
.checked=${this.onboarding || this.homeAssistant}
.disabled=${this.onboarding}
@change=${this._toggleHomeAssistant}
>
</ha-checkbox>
@@ -334,7 +335,7 @@ export class SupervisorBackupContent extends LitElement {
| HassioFullBackupCreateParams {
const data: any = {};
if (!this.backup) {
if (!this.backup && this.hass) {
data.name =
this.backupName ||
formatDate(new Date(), this.hass.locale, this.hass.config);
@@ -364,7 +365,9 @@ export class SupervisorBackupContent extends LitElement {
if (folders?.length) {
data.folders = folders;
}
data.homeassistant = this.homeAssistant;
// onboarding needs at least homeassistant to restore
data.homeassistant = this.onboarding || this.homeAssistant;
return data;
}
@@ -386,6 +389,7 @@ export class SupervisorBackupContent extends LitElement {
.iconPath=${section === "addons" ? mdiPuzzle : mdiFolder}
.imageUrl=${section === "addons" &&
!this.onboarding &&
this.hass &&
atLeastVersion(this.hass.config.version, 0, 105) &&
addons?.get(item.slug)?.icon
? `/api/hassio/addons/${item.slug}/icon`

View File

@@ -8,9 +8,11 @@ import { atLeastVersion } from "../../../../src/common/config/version";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { stopPropagation } from "../../../../src/common/dom/stop_propagation";
import { slugify } from "../../../../src/common/string/slugify";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-md-dialog";
import "../../../../src/components/ha-dialog-header";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-button-menu";
import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-icon-button";
@@ -19,6 +21,7 @@ import type { HassioBackupDetail } from "../../../../src/data/hassio/backup";
import {
fetchHassioBackupInfo,
removeBackup,
restoreBackup,
} from "../../../../src/data/hassio/backup";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import {
@@ -33,6 +36,7 @@ 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")
class HassioBackupDialog
@@ -52,13 +56,20 @@ class HassioBackupDialog
@query("supervisor-backup-content")
private _backupContent!: SupervisorBackupContent;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public async showDialog(dialogParams: HassioBackupDialogParams) {
this._backup = await fetchHassioBackupInfo(this.hass, dialogParams.slug);
this._dialogParams = dialogParams;
this._backup = await fetchHassioBackupInfo(this.hass, dialogParams.slug);
if (!this._backup) {
this._error = this._localize("no_backup_found");
} else if (this._dialogParams.onboarding && !this._backup.homeassistant) {
this._error = this._localize("restore_no_home_assistant");
}
this._restoringBackup = false;
}
public closeDialog() {
private _dialogClosed(): void {
this._backup = undefined;
this._dialogParams = undefined;
this._restoringBackup = false;
@@ -66,6 +77,10 @@ class HassioBackupDialog
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
public closeDialog(): void {
this._dialog?.close();
}
private _localize(key: BackupOrRestoreKey) {
return (
this._dialogParams!.supervisor?.localize(`backup.${key}`) ||
@@ -78,100 +93,80 @@ class HassioBackupDialog
return nothing;
}
return html`
<ha-dialog
<ha-md-dialog
open
scrimClickAction
@closed=${this.closeDialog}
.heading=${this._backup.name}
.disableCancelAction=${!this._error}
@closed=${this._dialogClosed}
>
<div slot="heading">
<ha-header-bar>
<span slot="title">${this._backup.name}</span>
<ha-icon-button
.label=${this._localize("close")}
.path=${mdiClose}
slot="actionItems"
dialogAction="cancel"
></ha-icon-button>
</ha-header-bar>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
.label=${this._localize("close")}
.path=${mdiClose}
@click=${this.closeDialog}
.disabled=${this._restoringBackup}
></ha-icon-button>
<span slot="title" .title=${this._backup.name}
>${this._backup.name}</span
>
${!this._dialogParams.onboarding && this._dialogParams.supervisor
? html`<ha-button-menu
slot="actionItems"
fixed
@action=${this._handleMenuAction}
@closed=${stopPropagation}
>
<ha-icon-button
.label=${this._dialogParams.supervisor.localize(
"backup.more_actions"
)}
.path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>
<mwc-list-item
>${this._dialogParams.supervisor.localize(
"backup.download_backup"
)}</mwc-list-item
>
<mwc-list-item class="error"
>${this._dialogParams.supervisor.localize(
"backup.delete_backup_title"
)}</mwc-list-item
>
</ha-button-menu>`
: nothing}
</ha-dialog-header>
<div slot="content">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: this._restoringBackup
? html`<div class="loading">
<ha-circular-progress indeterminate></ha-circular-progress>
</div>`
: html`
<supervisor-backup-content
.hass=${this.hass}
.supervisor=${this._dialogParams.supervisor}
.backup=${this._backup}
.onboarding=${this._dialogParams.onboarding || false}
.localize=${this._dialogParams.localize}
dialogInitialFocus
>
</supervisor-backup-content>
`}
</div>
${this._restoringBackup
? html`<ha-circular-progress indeterminate></ha-circular-progress>`
: html`
<supervisor-backup-content
.hass=${this.hass}
.supervisor=${this._dialogParams.supervisor}
.backup=${this._backup}
.onboarding=${this._dialogParams.onboarding || false}
.localize=${this._dialogParams.localize}
dialogInitialFocus
>
</supervisor-backup-content>
`}
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
<mwc-button
.disabled=${this._restoringBackup}
slot="secondaryAction"
@click=${this._restoreClicked}
>
${this._localize("restore")}
</mwc-button>
${!this._dialogParams.onboarding && this._dialogParams.supervisor
? html`<ha-button-menu
fixed
slot="primaryAction"
@action=${this._handleMenuAction}
@closed=${stopPropagation}
>
<ha-icon-button
.label=${this._dialogParams.supervisor.localize(
"backup.more_actions"
)}
.path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>
<mwc-list-item
>${this._dialogParams.supervisor.localize(
"backup.download_backup"
)}</mwc-list-item
>
<mwc-list-item class="error"
>${this._dialogParams.supervisor.localize(
"backup.delete_backup_title"
)}</mwc-list-item
>
</ha-button-menu>`
: nothing}
</ha-dialog>
<div slot="actions">
<ha-button
.disabled=${this._restoringBackup || !!this._error}
@click=${this._restoreClicked}
>
${this._localize("restore")}
</ha-button>
</div>
</ha-md-dialog>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
ha-circular-progress {
display: block;
text-align: center;
}
ha-header-bar {
--mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--mdc-theme-surface);
flex-shrink: 0;
display: block;
}
ha-icon-button {
color: var(--secondary-text-color);
}
`,
];
}
private _handleMenuAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
@@ -184,18 +179,9 @@ class HassioBackupDialog
}
private async _restoreClicked() {
const backupDetails = this._backupContent.backupDetails();
this._restoringBackup = true;
this._dialogParams?.onRestoring?.();
if (this._backupContent.backupType === "full") {
await this._fullRestoreClicked(backupDetails);
} else {
await this._partialRestoreClicked(backupDetails);
}
this._restoringBackup = false;
}
const backupDetails = this._backupContent.backupDetails();
private async _partialRestoreClicked(backupDetails) {
const supervisor = this._dialogParams?.supervisor;
if (supervisor !== undefined && supervisor.info.state !== "running") {
await showAlertDialog(this, {
@@ -204,91 +190,45 @@ class HassioBackupDialog
state: supervisor.info.state,
}),
});
this._restoringBackup = false;
return;
}
if (
!(await showConfirmationDialog(this, {
title: this._localize("confirm_restore_partial_backup_title"),
text: this._localize("confirm_restore_partial_backup_text"),
title: this._localize(
this._backupContent.backupType === "full"
? "confirm_restore_full_backup_title"
: "confirm_restore_partial_backup_title"
),
text: this._localize(
this._backupContent.backupType === "full"
? "confirm_restore_full_backup_text"
: "confirm_restore_partial_backup_text"
),
confirmText: this._localize("restore"),
dismissText: this._localize("cancel"),
}))
) {
this._restoringBackup = false;
return;
}
if (!this._dialogParams?.onboarding) {
try {
await this.hass!.callApi(
"POST",
`hassio/${
atLeastVersion(this.hass!.config.version, 2021, 9)
? "backups"
: "snapshots"
}/${this._backup!.slug}/restore/partial`,
backupDetails
);
this.closeDialog();
} catch (error: any) {
this._error = error.body.message;
}
} else {
this._dialogParams?.onRestoring?.();
await fetch(`/api/hassio/backups/${this._backup!.slug}/restore/partial`, {
method: "POST",
body: JSON.stringify(backupDetails),
});
this.closeDialog();
}
}
private async _fullRestoreClicked(backupDetails) {
const supervisor = this._dialogParams?.supervisor;
if (supervisor !== undefined && supervisor.info.state !== "running") {
await showAlertDialog(this, {
title: supervisor.localize("backup.could_not_restore"),
text: supervisor.localize("backup.restore_blocked_not_running", {
state: supervisor.info.state,
}),
});
return;
}
if (
!(await showConfirmationDialog(this, {
title: this._localize("confirm_restore_full_backup_title"),
text: this._localize("confirm_restore_full_backup_text"),
confirmText: this._localize("restore"),
dismissText: this._localize("cancel"),
}))
) {
return;
}
if (!this._dialogParams?.onboarding) {
this.hass!.callApi(
"POST",
`hassio/${
atLeastVersion(this.hass!.config.version, 2021, 9)
? "backups"
: "snapshots"
}/${this._backup!.slug}/restore/full`,
backupDetails
).then(
() => {
this.closeDialog();
},
(error) => {
this._error = error.body.message;
}
try {
await restoreBackup(
this.hass,
this._backupContent.backupType,
this._backup!.slug,
backupDetails,
!!this.hass && atLeastVersion(this.hass.config.version, 2021, 9)
);
} else {
this._dialogParams?.onRestoring?.();
fetch(`/api/hassio/backups/${this._backup!.slug}/restore/full`, {
method: "POST",
body: JSON.stringify(backupDetails),
});
this.closeDialog();
} catch (error: any) {
this._error =
error?.body?.message || this._localize("restore_start_failed");
} finally {
this._restoringBackup = false;
}
}
@@ -361,7 +301,36 @@ class HassioBackupDialog
private get _computeName() {
return this._backup
? this._backup.name || this._backup.slug
: "Unnamed backup";
: this._localize("unnamed_backup");
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
ha-circular-progress {
display: block;
text-align: center;
}
ha-header-bar {
--mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--mdc-theme-surface);
flex-shrink: 0;
display: block;
}
ha-icon-button {
color: var(--secondary-text-color);
}
.loading {
width: 100%;
display: flex;
height: 100%;
justify-content: center;
align-items: center;
}
`,
];
}
}

View File

@@ -27,24 +27,24 @@
"type": "module",
"dependencies": {
"@babel/runtime": "7.26.0",
"@braintree/sanitize-url": "7.1.0",
"@codemirror/autocomplete": "6.18.3",
"@braintree/sanitize-url": "7.1.1",
"@codemirror/autocomplete": "6.18.4",
"@codemirror/commands": "6.7.1",
"@codemirror/language": "6.10.6",
"@codemirror/language": "6.10.7",
"@codemirror/legacy-modes": "6.4.2",
"@codemirror/search": "6.5.8",
"@codemirror/state": "6.4.1",
"@codemirror/view": "6.35.2",
"@codemirror/state": "6.5.0",
"@codemirror/view": "6.36.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.16.6",
"@formatjs/intl-displaynames": "6.8.6",
"@formatjs/intl-durationformat": "0.6.5",
"@formatjs/intl-getcanonicallocales": "2.5.3",
"@formatjs/intl-listformat": "7.7.6",
"@formatjs/intl-locale": "4.2.6",
"@formatjs/intl-numberformat": "8.14.6",
"@formatjs/intl-pluralrules": "5.3.6",
"@formatjs/intl-relativetimeformat": "11.4.6",
"@formatjs/intl-datetimeformat": "6.17.1",
"@formatjs/intl-displaynames": "6.8.8",
"@formatjs/intl-durationformat": "0.7.1",
"@formatjs/intl-getcanonicallocales": "2.5.4",
"@formatjs/intl-listformat": "7.7.8",
"@formatjs/intl-locale": "4.2.8",
"@formatjs/intl-numberformat": "8.15.1",
"@formatjs/intl-pluralrules": "5.4.1",
"@formatjs/intl-relativetimeformat": "11.4.8",
"@fullcalendar/core": "6.1.15",
"@fullcalendar/daygrid": "6.1.15",
"@fullcalendar/interaction": "6.1.15",
@@ -91,8 +91,8 @@
"@polymer/polymer": "3.5.2",
"@replit/codemirror-indentation-markers": "6.5.3",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.5.5",
"@vaadin/vaadin-themable-mixin": "24.5.5",
"@vaadin/combo-box": "24.6.0",
"@vaadin/vaadin-themable-mixin": "24.6.0",
"@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@@ -102,6 +102,7 @@
"app-datepicker": "5.1.1",
"barcode-detector": "2.3.1",
"chart.js": "4.4.7",
"chartjs-plugin-zoom": "2.2.0",
"color-name": "2.0.0",
"comlink": "4.4.2",
"core-js": "3.39.0",
@@ -117,17 +118,16 @@
"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.8",
"intl-messageformat": "10.7.10",
"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",
"lit": "2.8.0",
"lit-html": "2.8.0",
"luxon": "3.5.0",
"marked": "15.0.3",
"marked": "15.0.4",
"memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "0.3.2",
"punycode": "2.3.1",
"qr-scanner": "1.4.2",
"qrcode": "1.5.4",
@@ -139,8 +139,7 @@
"tinykeys": "3.0.0",
"tsparticles-engine": "2.12.0",
"tsparticles-preset-links": "2.12.0",
"ua-parser-js": "1.0.39",
"unfetch": "5.0.0",
"ua-parser-js": "1.0.40",
"vis-data": "7.1.9",
"vis-network": "9.1.9",
"vue": "2.7.16",
@@ -162,13 +161,13 @@
"@babel/preset-env": "7.26.0",
"@babel/preset-typescript": "7.26.0",
"@bundle-stats/plugin-webpack-filter": "4.17.0",
"@lokalise/node-api": "12.8.0",
"@lokalise/node-api": "13.0.0",
"@octokit/auth-oauth-device": "7.1.1",
"@octokit/plugin-retry": "7.1.2",
"@octokit/rest": "21.0.2",
"@rsdoctor/rspack-plugin": "0.4.11",
"@rspack/cli": "1.1.5",
"@rspack/core": "1.1.5",
"@rsdoctor/rspack-plugin": "0.4.12",
"@rspack/cli": "1.1.8",
"@rspack/core": "1.1.8",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.20",
"@types/chromecast-caf-sender": "1.0.11",
@@ -182,7 +181,6 @@
"@types/luxon": "3.4.2",
"@types/mocha": "10.0.10",
"@types/qrcode": "1.5.5",
"@types/serve-handler": "6.1.4",
"@types/sortablejs": "1.15.8",
"@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39",
@@ -194,11 +192,11 @@
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"del": "8.0.0",
"eslint": "9.16.0",
"eslint": "9.17.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "18.0.0",
"eslint-config-prettier": "9.1.0",
"eslint-import-resolver-webpack": "0.13.9",
"eslint-import-resolver-webpack": "0.13.10",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-lit": "1.15.0",
"eslint-plugin-lit-a11y": "4.1.4",
@@ -215,22 +213,18 @@
"husky": "9.1.7",
"jsdom": "25.0.1",
"jszip": "3.10.1",
"lint-staged": "15.2.10",
"lint-staged": "15.2.11",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",
"magic-string": "0.30.14",
"map-stream": "0.0.7",
"object-hash": "3.0.0",
"open": "10.1.0",
"pinst": "3.0.0",
"prettier": "3.4.2",
"rspack-manifest-plugin": "5.0.2",
"serve-handler": "6.1.6",
"sinon": "19.0.2",
"systemjs": "6.15.1",
"tar": "7.4.3",
"terser-webpack-plugin": "5.3.10",
"terser-webpack-plugin": "5.3.11",
"ts-lit-plugin": "2.0.2",
"typescript": "5.7.2",
"vitest": "2.1.8",
@@ -247,7 +241,7 @@
"clean-css": "5.3.3",
"@lit/reactive-element": "1.6.3",
"@fullcalendar/daygrid": "6.1.15",
"globals": "15.13.0"
"globals": "15.14.0"
},
"packageManager": "yarn@4.5.3"
}

View File

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

View File

@@ -1,14 +1,19 @@
type NonUndefined<T> = T extends undefined ? never : T;
type NonNullUndefined<T> = T extends undefined
? never
: T extends null
? never
: T;
/**
* Ensure that the input is an array or wrap it in an array
* @param value - The value to ensure is an array
*/
export function ensureArray(value: undefined): undefined;
export function ensureArray<T>(value: T | T[]): NonUndefined<T>[];
export function ensureArray<T>(value: T | readonly T[]): NonUndefined<T>[];
export function ensureArray(value: null): null;
export function ensureArray<T>(value: T | T[]): NonNullUndefined<T>[];
export function ensureArray<T>(value: T | readonly T[]): NonNullUndefined<T>[];
export function ensureArray(value) {
if (value === undefined || Array.isArray(value)) {
if (value === undefined || value === null || Array.isArray(value)) {
return value;
}
return [value];

View File

@@ -90,9 +90,9 @@ export const lab2rgb = (
x = Xn * lab_xyz(x);
z = Zn * lab_xyz(z);
const r = xyz_rgb(3.2404542 * x - 1.5371385 * y - 0.4985314 * z); // D65 -> sRGB
const g = xyz_rgb(-0.969266 * x + 1.8760108 * y + 0.041556 * z);
const b_ = xyz_rgb(0.0556434 * x - 0.2040259 * y + 1.0572252 * z);
const r = Math.round(xyz_rgb(3.2404542 * x - 1.5371385 * y - 0.4985314 * z)); // D65 -> sRGB
const g = Math.round(xyz_rgb(-0.969266 * x + 1.8760108 * y + 0.041556 * z));
const b_ = Math.round(xyz_rgb(0.0556434 * x - 0.2040259 * y + 1.0572252 * z));
return [r, g, b_];
};

View File

@@ -8,9 +8,9 @@ export const temperature2rgb = (
): [number, number, number] => {
const value = temperature / 100;
return [
temperatureRed(value),
temperatureGreen(value),
temperatureBlue(value),
Math.round(temperatureRed(value)),
Math.round(temperatureGreen(value)),
Math.round(temperatureBlue(value)),
];
};
@@ -59,10 +59,10 @@ const matchMaxScale = (
};
export const mired2kelvin = (miredTemperature: number) =>
Math.floor(1000000 / miredTemperature);
miredTemperature === 0 ? 1000000 : Math.floor(1000000 / miredTemperature);
export const kelvin2mired = (kelvintTemperature: number) =>
Math.floor(1000000 / kelvintTemperature);
export const kelvin2mired = (kelvinTemperature: number) =>
kelvinTemperature === 0 ? 1000000 : Math.floor(1000000 / kelvinTemperature);
export const rgbww2rgb = (
rgbww: [number, number, number, number, number],

View File

@@ -14,8 +14,8 @@ export const hexBlend = (c1: string, c2: string, blend = 50): string => {
c1 = expandHex(c1);
c2 = expandHex(c2);
for (let i = 0; i <= 5; i += 2) {
const h1 = parseInt(c1.substr(i, 2), 16);
const h2 = parseInt(c2.substr(i, 2), 16);
const h1 = parseInt(c1.substring(i, i + 2), 16);
const h2 = parseInt(c2.substring(i, i + 2), 16);
let hex = Math.floor(h2 + (h1 - h2) * (blend / 100)).toString(16);
while (hex.length < 2) hex = "0" + hex;
color += hex;

View File

@@ -1,12 +1,13 @@
// From https://github.com/gka/chroma.js
// Copyright (c) 2011-2019, Gregor Aisch
export const labDarken = (
lab: [number, number, number],
amount = 1
): [number, number, number] => [lab[0] - 18 * amount, lab[1], lab[2]];
export type LabColor = [number, number, number];
export const labBrighten = (
lab: [number, number, number],
amount = 1
): [number, number, number] => labDarken(lab, -amount);
export const labDarken = (lab: LabColor, amount = 1): LabColor => [
lab[0] - 18 * amount,
lab[1],
lab[2],
];
export const labBrighten = (lab: LabColor, amount = 1): LabColor =>
labDarken(lab, -amount);

View File

@@ -8,20 +8,27 @@ export const canShowPage = (hass: HomeAssistant, page: PageNavigation) =>
!hideAdvancedPage(hass, page) &&
isNotLoadedIntegration(hass, page);
const isLoadedIntegration = (hass: HomeAssistant, page: PageNavigation) =>
export const isLoadedIntegration = (
hass: HomeAssistant,
page: PageNavigation
) =>
!page.component ||
ensureArray(page.component).some((integration) =>
isComponentLoaded(hass, integration)
);
const isNotLoadedIntegration = (hass: HomeAssistant, page: PageNavigation) =>
export const isNotLoadedIntegration = (
hass: HomeAssistant,
page: PageNavigation
) =>
!page.not_component ||
!ensureArray(page.not_component).some((integration) =>
isComponentLoaded(hass, integration)
);
const isCore = (page: PageNavigation) => page.core;
const isAdvancedPage = (page: PageNavigation) => page.advancedOnly;
const userWantsAdvanced = (hass: HomeAssistant) => hass.userData?.showAdvanced;
const hideAdvancedPage = (hass: HomeAssistant, page: PageNavigation) =>
export const isCore = (page: PageNavigation) => page.core;
export const isAdvancedPage = (page: PageNavigation) => page.advancedOnly;
export const userWantsAdvanced = (hass: HomeAssistant) =>
hass.userData?.showAdvanced;
export const hideAdvancedPage = (hass: HomeAssistant, page: PageNavigation) =>
isAdvancedPage(page) && !userWantsAdvanced(hass);

View File

@@ -1,202 +1,9 @@
/** Constants to be used in the frontend. */
import {
mdiAccount,
mdiAirFilter,
mdiAlert,
mdiAngleAcute,
mdiAppleSafari,
mdiArrowLeftRight,
mdiBell,
mdiBookmark,
mdiBrightness5,
mdiBullhorn,
mdiButtonPointer,
mdiCalendar,
mdiCalendarClock,
mdiCarCoolantLevel,
mdiCash,
mdiChatSleep,
mdiClipboardList,
mdiClock,
mdiCog,
mdiCommentAlert,
mdiCounter,
mdiCurrentAc,
mdiDatabase,
mdiEarHearing,
mdiEye,
mdiFlash,
mdiFlower,
mdiFormatListBulleted,
mdiFormatListCheckbox,
mdiFormTextbox,
mdiForumOutline,
mdiGauge,
mdiGoogleAssistant,
mdiGoogleCirclesCommunities,
mdiHomeAutomation,
mdiImage,
mdiImageFilterFrames,
mdiLightbulb,
mdiLightningBolt,
mdiMapMarkerRadius,
mdiMeterGas,
mdiMicrophoneMessage,
mdiMolecule,
mdiMoleculeCo,
mdiMoleculeCo2,
mdiPalette,
mdiPh,
mdiPipe,
mdiProgressClock,
mdiRayVertex,
mdiRemote,
mdiRobot,
mdiRobotMower,
mdiRobotVacuum,
mdiRoomService,
mdiScriptText,
mdiSineWave,
mdiSpeakerMessage,
mdiSpeedometer,
mdiSunWireless,
mdiThermometer,
mdiThermometerLines,
mdiThermostat,
mdiTimerOutline,
mdiToggleSwitch,
mdiTransmissionTower,
mdiWater,
mdiWaterPercent,
mdiWeatherPartlyCloudy,
mdiWeatherPouring,
mdiWeatherRainy,
mdiWeatherWindy,
mdiWeight,
mdiWhiteBalanceSunny,
mdiWifi,
} from "@mdi/js";
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
// Constants should be alphabetically sorted by name.
// Arrays with values should be alphabetically sorted if order doesn't matter.
// Each constant should have a description what it is supposed to be used for.
/** Icon to use when no icon specified for service. */
export const DEFAULT_SERVICE_ICON = mdiRoomService;
/** Icon to use when no icon specified for domain. */
export const DEFAULT_DOMAIN_ICON = mdiBookmark;
/** Icons for each domain */
export const FIXED_DOMAIN_ICONS = {
air_quality: mdiAirFilter,
alert: mdiAlert,
automation: mdiRobot,
calendar: mdiCalendar,
climate: mdiThermostat,
configurator: mdiCog,
conversation: mdiForumOutline,
counter: mdiCounter,
date: mdiCalendar,
datetime: mdiCalendarClock,
demo: mdiHomeAssistant,
device_tracker: mdiAccount,
google_assistant: mdiGoogleAssistant,
group: mdiGoogleCirclesCommunities,
homeassistant: mdiHomeAssistant,
homekit: mdiHomeAutomation,
image_processing: mdiImageFilterFrames,
image: mdiImage,
input_boolean: mdiToggleSwitch,
input_button: mdiButtonPointer,
input_datetime: mdiCalendarClock,
input_number: mdiRayVertex,
input_select: mdiFormatListBulleted,
input_text: mdiFormTextbox,
lawn_mower: mdiRobotMower,
light: mdiLightbulb,
notify: mdiCommentAlert,
number: mdiRayVertex,
persistent_notification: mdiBell,
person: mdiAccount,
plant: mdiFlower,
proximity: mdiAppleSafari,
remote: mdiRemote,
scene: mdiPalette,
schedule: mdiCalendarClock,
script: mdiScriptText,
select: mdiFormatListBulleted,
sensor: mdiEye,
simple_alarm: mdiBell,
siren: mdiBullhorn,
stt: mdiMicrophoneMessage,
sun: mdiWhiteBalanceSunny,
text: mdiFormTextbox,
time: mdiClock,
timer: mdiTimerOutline,
todo: mdiClipboardList,
tts: mdiSpeakerMessage,
vacuum: mdiRobotVacuum,
wake_word: mdiChatSleep,
weather: mdiWeatherPartlyCloudy,
zone: mdiMapMarkerRadius,
};
export const FIXED_DEVICE_CLASS_ICONS = {
apparent_power: mdiFlash,
aqi: mdiAirFilter,
atmospheric_pressure: mdiThermometerLines,
// battery: mdiBattery, => not included by design since `sensorIcon()` will dynamically determine the icon
carbon_dioxide: mdiMoleculeCo2,
carbon_monoxide: mdiMoleculeCo,
current: mdiCurrentAc,
data_rate: mdiTransmissionTower,
data_size: mdiDatabase,
date: mdiCalendar,
distance: mdiArrowLeftRight,
duration: mdiProgressClock,
energy: mdiLightningBolt,
frequency: mdiSineWave,
gas: mdiMeterGas,
humidity: mdiWaterPercent,
illuminance: mdiBrightness5,
irradiance: mdiSunWireless,
moisture: mdiWaterPercent,
monetary: mdiCash,
nitrogen_dioxide: mdiMolecule,
nitrogen_monoxide: mdiMolecule,
nitrous_oxide: mdiMolecule,
ozone: mdiMolecule,
ph: mdiPh,
pm1: mdiMolecule,
pm10: mdiMolecule,
pm25: mdiMolecule,
power: mdiFlash,
power_factor: mdiAngleAcute,
precipitation: mdiWeatherRainy,
precipitation_intensity: mdiWeatherPouring,
pressure: mdiGauge,
reactive_power: mdiFlash,
shopping_List: mdiFormatListCheckbox,
signal_strength: mdiWifi,
sound_pressure: mdiEarHearing,
speed: mdiSpeedometer,
sulphur_dioxide: mdiMolecule,
temperature: mdiThermometer,
timestamp: mdiClock,
volatile_organic_compounds: mdiMolecule,
volatile_organic_compounds_parts: mdiMolecule,
voltage: mdiSineWave,
volume: mdiCarCoolantLevel,
volume_flow_rate: mdiPipe,
water: mdiWater,
weight: mdiWeight,
wind_speed: mdiWeatherWindy,
};
/** Domains that have a state card. */
export const DOMAINS_WITH_CARD = [
"alert",

View File

@@ -1,3 +1,12 @@
import {
addMilliseconds,
addMonths,
isFirstDayOfMonth,
isLastDayOfMonth,
differenceInMilliseconds,
differenceInMonths,
endOfMonth,
} from "date-fns";
import { toZonedTime, fromZonedTime } from "date-fns-tz";
import type { HassConfig } from "home-assistant-js-websocket";
import type { FrontendLocaleData } from "../../data/translation";
@@ -55,3 +64,55 @@ export const calcDateDifferenceProperty = (
? toZonedTime(startDate, config.time_zone)
: startDate
);
export const shiftDateRange = (
startDate: Date,
endDate: Date,
forward: boolean,
locale: FrontendLocaleData,
config: any
): { start: Date; end: Date } => {
let start: Date;
let end: Date;
if (
(calcDateProperty(
startDate,
isFirstDayOfMonth,
locale,
config
) as boolean) &&
(calcDateProperty(endDate, isLastDayOfMonth, locale, config) as boolean)
) {
const difference =
((calcDateDifferenceProperty(
endDate,
startDate,
differenceInMonths,
locale,
config
) as number) +
1) *
(forward ? 1 : -1);
start = calcDate(startDate, addMonths, locale, config, difference);
end = calcDate(
calcDate(endDate, addMonths, locale, config, difference),
endOfMonth,
locale,
config
);
} else {
const difference =
((calcDateDifferenceProperty(
endDate,
startDate,
differenceInMilliseconds,
locale,
config
) as number) +
1) *
(forward ? 1 : -1);
start = calcDate(startDate, addMilliseconds, locale, config, difference);
end = calcDate(endDate, addMilliseconds, locale, config, difference);
}
return { start, end };
};

View File

@@ -15,6 +15,7 @@ export const FIXED_DOMAIN_STATES = {
"pending",
"triggered",
],
assist_satellite: ["idle", "listening", "responding", "processing"],
automation: ["on", "off"],
binary_sensor: ["on", "off"],
button: [],

View File

@@ -1,3 +1,4 @@
import { closeAllDialogs } from "../dialogs/make-dialog-manager";
import { fireEvent } from "./dom/fire_event";
import { mainWindow } from "./dom/get_main_window";
@@ -13,15 +14,35 @@ export interface NavigateOptions {
data?: any;
}
export const navigate = (path: string, options?: NavigateOptions) => {
// max time to wait for dialogs to close before navigating
const DIALOG_WAIT_TIMEOUT = 500;
export const navigate = async (
path: string,
options?: NavigateOptions,
timestamp = Date.now()
) => {
const { history } = mainWindow;
if (history.state?.dialog && Date.now() - timestamp < DIALOG_WAIT_TIMEOUT) {
const closed = await closeAllDialogs();
if (!closed) {
// eslint-disable-next-line no-console
console.warn("Navigation blocked, because dialog refused to close");
return false;
}
return new Promise<boolean>((resolve) => {
// need to wait for history state to be updated in case a dialog was closed
setTimeout(() => {
navigate(path, options, timestamp).then(resolve);
});
});
}
const replace = options?.replace || false;
if (__DEMO__) {
if (replace) {
mainWindow.history.replaceState(
mainWindow.history.state?.root
? { root: true }
: (options?.data ?? null),
history.replaceState(
history.state?.root ? { root: true } : (options?.data ?? null),
"",
`${mainWindow.location.pathname}#${path}`
);
@@ -29,15 +50,16 @@ export const navigate = (path: string, options?: NavigateOptions) => {
mainWindow.location.hash = path;
}
} else if (replace) {
mainWindow.history.replaceState(
mainWindow.history.state?.root ? { root: true } : (options?.data ?? null),
history.replaceState(
history.state?.root ? { root: true } : (options?.data ?? null),
"",
path
);
} else {
mainWindow.history.pushState(options?.data ?? null, "", path);
history.pushState(options?.data ?? null, "", path);
}
fireEvent(mainWindow, "location-changed", {
replace,
});
return true;
};

View File

@@ -1,4 +1,4 @@
export const copyToClipboard = async (str) => {
export const copyToClipboard = async (str, rootEl?: HTMLElement) => {
if (navigator.clipboard) {
try {
await navigator.clipboard.writeText(str);
@@ -8,10 +8,12 @@ export const copyToClipboard = async (str) => {
}
}
const root = rootEl ?? document.body;
const el = document.createElement("textarea");
el.value = str;
document.body.appendChild(el);
root.appendChild(el);
el.select();
document.execCommand("copy");
document.body.removeChild(el);
root.removeChild(el);
};

View File

@@ -3,7 +3,7 @@
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
// leading edge and on the trailing.
export const debounce = <T extends any[]>(
func: (...args: T) => void,
@@ -14,9 +14,7 @@ export const debounce = <T extends any[]>(
const debouncedFunc = (...args: T): void => {
const later = () => {
timeout = undefined;
if (!immediate) {
func(...args);
}
func(...args);
};
const callNow = immediate && !timeout;
clearTimeout(timeout);

View File

@@ -1,7 +1,25 @@
class TimeoutError extends Error {
public timeout: number;
constructor(timeout: number, ...params) {
super(...params);
// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, TimeoutError);
}
this.name = "TimeoutError";
// Custom debugging information
this.timeout = timeout;
this.message = `Timed out in ${timeout} ms.`;
}
}
export const promiseTimeout = (ms: number, promise: Promise<any> | any) => {
const timeout = new Promise((_resolve, reject) => {
setTimeout(() => {
reject(`Timed out in ${ms} ms.`);
reject(new TimeoutError(ms));
}, ms);
});

View File

@@ -10,10 +10,13 @@ import { css, html, nothing, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { mdiRestart } from "@mdi/js";
import { fireEvent } from "../../common/dom/fire_event";
import { clamp } from "../../common/number/clamp";
import type { HomeAssistant } from "../../types";
import { debounce } from "../../common/util/debounce";
import { isMac } from "../../util/is_mac";
import "../ha-icon-button";
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
@@ -60,10 +63,16 @@ export class HaChartBase extends LitElement {
@state() private _chartHeight?: number;
@state() private _legendHeight?: number;
@state() private _tooltip?: Tooltip;
@state() private _hiddenDatasets: Set<number> = new Set();
@state() private _showZoomHint = false;
@state() private _isZoomed = false;
private _paddingUpdateCount = 0;
private _paddingUpdateLock = false;
@@ -201,16 +210,30 @@ export class HaChartBase extends LitElement {
}
this.chart.data = this.data;
}
if (changedProps.has("options")) {
if (changedProps.has("options") && !this.chart.isZoomedOrPanned()) {
// this resets the chart zoom because min/max scales changed
// so we only do it if the user is not zooming or panning
this.chart.options = this._createOptions();
}
this.chart.update("none");
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (changedProperties.has("data") || changedProperties.has("options")) {
if (this.options?.plugins?.legend?.display) {
this._legendHeight =
this.renderRoot.querySelector(".chart-legend")?.clientHeight;
} else {
this._legendHeight = 0;
}
}
}
protected render() {
return html`
${this.options?.plugins?.legend?.display === true
? html`<div class="chartLegend">
? html`<div class="chart-legend">
<ul>
${this._datasetOrder.map((index) => {
const dataset = this.data.datasets[index];
@@ -242,14 +265,14 @@ export class HaChartBase extends LitElement {
</div>`
: ""}
<div
class="animationContainer"
class="animation-container"
style=${styleMap({
height: `${this.height || this._chartHeight || 0}px`,
overflow: this._chartHeight ? "initial" : "hidden",
})}
>
<div
class="chartContainer"
class="chart-container"
style=${styleMap({
height: `${
this.height ?? this._chartHeight ?? this.clientWidth / 2
@@ -259,11 +282,39 @@ export class HaChartBase extends LitElement {
"padding-inline-start": `${this._paddingYAxisInternal}px`,
"padding-inline-end": 0,
})}
@wheel=${this._handleChartScroll}
>
<canvas></canvas>
<canvas
class=${classMap({
"not-zoomed": !this._isZoomed,
})}
></canvas>
<div
class="zoom-hint ${classMap({
visible: this._showZoomHint,
})}"
>
<div>
${isMac
? this.hass.localize(
"ui.components.history_charts.zoom_hint_mac"
)
: this.hass.localize("ui.components.history_charts.zoom_hint")}
</div>
</div>
${this._isZoomed && this.chartType !== "timeline"
? html`<ha-icon-button
class="zoom-reset"
.path=${mdiRestart}
@click=${this._handleZoomReset}
title=${this.hass.localize(
"ui.components.history_charts.zoom_reset"
)}
></ha-icon-button>`
: nothing}
${this._tooltip
? html`<div
class="chartTooltip ${classMap({
class="chart-tooltip ${classMap({
[this._tooltip.yAlign]: true,
})}"
style=${styleMap({
@@ -273,7 +324,7 @@ export class HaChartBase extends LitElement {
>
<div class="title">${this._tooltip.title}</div>
${this._tooltip.beforeBody
? html`<div class="beforeBody">
? html`<div class="before-body">
${this._tooltip.beforeBody}
</div>`
: ""}
@@ -343,9 +394,13 @@ export class HaChartBase extends LitElement {
}
}
private _createOptions() {
private _createOptions(): ChartOptions {
const modifierKey = isMac ? "meta" : "ctrl";
return {
maintainAspectRatio: false,
animation: {
duration: 500,
},
...this.options,
plugins: {
...this.options?.plugins,
@@ -358,6 +413,53 @@ export class HaChartBase extends LitElement {
...this.options?.plugins?.legend,
display: false,
},
zoom: {
...this.options?.plugins?.zoom,
pan: {
enabled: true,
},
zoom: {
pinch: {
enabled: true,
},
drag: {
enabled: true,
modifierKey,
threshold: 2,
},
wheel: {
enabled: true,
modifierKey,
speed: 0.05,
},
mode:
this.chartType !== "timeline" &&
(this.options?.scales?.y as any)?.type === "category"
? "y"
: "x",
onZoomComplete: () => {
const isZoomed = this.chart?.isZoomedOrPanned() ?? false;
if (this._isZoomed && !isZoomed) {
setTimeout(() => {
// make sure the scales are properly reset after full zoom out
// they get bugged when zooming in/out multiple times and panning
this.chart?.resetZoom();
});
}
this._isZoomed = isZoomed;
},
},
limits: {
x: {
min: "original",
max: (this.options?.scales?.x as any)?.max ?? "original",
},
y: {
min: "original",
max: "original",
},
},
},
},
};
}
@@ -382,6 +484,17 @@ export class HaChartBase extends LitElement {
];
}
private _handleChartScroll(ev: MouseEvent) {
const modifier = isMac ? "metaKey" : "ctrlKey";
this._tooltip = undefined;
if (!ev[modifier] && !this._showZoomHint) {
this._showZoomHint = true;
setTimeout(() => {
this._showZoomHint = false;
}, 1000);
}
}
private _legendClick(ev) {
if (!this.chart) {
return;
@@ -416,15 +529,20 @@ export class HaChartBase extends LitElement {
this._tooltip = undefined;
return;
}
const boundingBox = this.getBoundingClientRect();
this._tooltip = {
...context.tooltip,
top: this.chart!.canvas.offsetTop + context.tooltip.caretY + 12 + "px",
top:
boundingBox.y +
(this._legendHeight || 0) +
context.tooltip.caretY +
12 +
"px",
left:
this.chart!.canvas.offsetLeft +
clamp(
context.tooltip.caretX,
100,
this.clientWidth - 100 - this._paddingYAxisInternal
boundingBox.x + context.tooltip.caretX,
boundingBox.x + 100,
boundingBox.x + boundingBox.width - 100
) -
100 +
"px",
@@ -439,24 +557,35 @@ export class HaChartBase extends LitElement {
}
}
private _handleZoomReset() {
this.chart?.resetZoom();
}
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
position: var(--chart-base-position, relative);
position: relative;
}
.animationContainer {
.animation-container {
overflow: hidden;
height: 0;
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
}
.chart-container {
position: relative;
}
canvas {
max-height: var(--chart-max-height, 400px);
}
.chartLegend {
canvas.not-zoomed {
/* allow scrolling if the chart is not zoomed */
touch-action: pan-y !important;
}
.chart-legend {
text-align: center;
}
.chartLegend li {
.chart-legend li {
cursor: pointer;
display: inline-grid;
grid-auto-flow: column;
@@ -465,16 +594,16 @@ export class HaChartBase extends LitElement {
align-items: center;
color: var(--secondary-text-color);
}
.chartLegend .hidden {
.chart-legend .hidden {
text-decoration: line-through;
}
.chartLegend .label {
.chart-legend .label {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.chartLegend .bullet,
.chartTooltip .bullet {
.chart-legend .bullet,
.chart-tooltip .bullet {
border-width: 1px;
border-style: solid;
border-radius: 50%;
@@ -488,13 +617,13 @@ export class HaChartBase extends LitElement {
margin-inline-start: initial;
direction: var(--direction);
}
.chartTooltip .bullet {
.chart-tooltip .bullet {
align-self: baseline;
}
.chartTooltip {
.chart-tooltip {
padding: 8px;
font-size: 90%;
position: absolute;
position: fixed;
background: rgba(80, 80, 80, 0.9);
color: white;
border-radius: 4px;
@@ -507,17 +636,17 @@ export class HaChartBase extends LitElement {
box-sizing: border-box;
direction: var(--direction);
}
.chartLegend ul,
.chartTooltip ul {
.chart-legend ul,
.chart-tooltip ul {
display: inline-block;
padding: 0 0px;
margin: 8px 0 0 0;
width: 100%;
}
.chartTooltip ul {
.chart-tooltip ul {
margin: 0 4px;
}
.chartTooltip li {
.chart-tooltip li {
display: flex;
white-space: pre-line;
word-break: break-word;
@@ -525,20 +654,55 @@ export class HaChartBase extends LitElement {
line-height: 16px;
padding: 4px 0;
}
.chartTooltip .title {
.chart-tooltip .title {
text-align: center;
font-weight: 500;
word-break: break-word;
direction: ltr;
}
.chartTooltip .footer {
.chart-tooltip .footer {
font-weight: 500;
}
.chartTooltip .beforeBody {
.chart-tooltip .before-body {
text-align: center;
font-weight: 300;
word-break: break-all;
}
.zoom-hint {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 500ms cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
}
.zoom-hint.visible {
opacity: 1;
}
.zoom-hint > div {
color: white;
font-size: 1.5em;
font-weight: 500;
padding: 8px;
border-radius: 8px;
background: rgba(0, 0, 0, 0.3);
box-shadow: 0 0 32px 32px rgba(0, 0, 0, 0.3);
}
.zoom-reset {
position: absolute;
top: 16px;
right: 4px;
background: var(--card-background-color);
border-radius: 4px;
--mdc-icon-button-size: 32px;
color: var(--primary-color);
border: 1px solid var(--divider-color);
}
`;
}
}

View File

@@ -0,0 +1,544 @@
import { customElement, property } from "lit/decorators";
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";
export type Node = {
id: string;
value: number;
index: number; // like z-index but for x/y
label?: string;
tooltip?: string;
color?: string;
passThrough?: boolean;
};
export type Link = { source: string; target: string; value?: number };
export type SankeyChartData = {
nodes: Node[];
links: Link[];
};
type ProcessedNode = Node & {
x: number;
y: number;
size: number;
};
type ProcessedLink = Link & {
value: number;
offset: {
source: number;
target: number;
};
passThroughNodeIds: string[];
};
type Section = {
nodes: ProcessedNode[];
offset: number;
index: number;
totalValue: number;
statePerPixel: number;
};
const MIN_SIZE = 3;
const DEFAULT_COLOR = "var(--primary-color)";
const NODE_WIDTH = 15;
const FONT_SIZE = 12;
const MIN_DISTANCE = FONT_SIZE / 2;
@customElement("sankey-chart")
export class SankeyChart extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public data: SankeyChartData = {
nodes: [],
links: [],
};
@property({ type: Boolean }) public vertical = false;
@property({ attribute: false }) public loadingText?: string;
private _statePerPixel = 0;
private _textMeasureCanvas?: HTMLCanvasElement;
private _sizeController = new ResizeController(this, {
callback: (entries) => entries[0]?.contentRect,
});
disconnectedCallback() {
super.disconnectedCallback();
this._textMeasureCanvas = undefined;
}
willUpdate() {
this._statePerPixel = 0;
}
render() {
if (!this._sizeController.value) {
return this.loadingText ?? nothing;
}
const { width, height } = this._sizeController.value;
const { nodes, paths } = this._processNodesAndPaths(
this.data.nodes,
this.data.links
);
return html`
<svg
width=${width}
height=${height}
viewBox="0 0 ${width} ${height}"
preserveAspectRatio="none"
>
<defs>
${paths.map(
(path, i) => svg`
<linearGradient id="gradient${path.sourceNode.id}.${path.targetNode.id}.${i}" gradientTransform="${
this.vertical ? "rotate(90)" : ""
}">
<stop offset="0%" stop-color="${path.sourceNode.color}"></stop>
<stop offset="100%" stop-color="${path.targetNode.color}"></stop>
</linearGradient>
`
)}
</defs>
${paths.map(
(path, i) =>
svg`
<path d="${path.path.map(([cmd, x, y]) => `${cmd}${x},${y}`).join(" ")} Z"
fill="url(#gradient${path.sourceNode.id}.${path.targetNode.id}.${i})" fill-opacity="0.4" />
`
)}
${nodes.map((node) =>
node.passThrough
? nothing
: svg`
<g transform="translate(${node.x},${node.y})">
<rect
class="node"
width=${this.vertical ? node.size : NODE_WIDTH}
height=${this.vertical ? NODE_WIDTH : node.size}
style="fill: ${node.color}"
>
<title>${node.tooltip}</title>
</rect>
${
this.vertical
? nothing
: svg`
<text
class="node-label"
x=${NODE_WIDTH + 5}
y=${node.size / 2}
text-anchor="start"
dominant-baseline="middle"
>${node.label}</text>
`
}
</g>
`
)}
</svg>
${this.vertical
? nodes.map((node) => {
if (!node.label) {
return nothing;
}
const labelWidth = MIN_DISTANCE + node.size;
const fontSize = this._getVerticalLabelFontSize(
node.label,
labelWidth
);
return html`<div
class="node-label vertical"
style="
left: ${node.x - MIN_DISTANCE / 2}px;
top: ${node.y + NODE_WIDTH}px;
width: ${labelWidth}px;
height: ${FONT_SIZE * 3}px;
font-size: ${fontSize}px;
line-height: ${fontSize}px;
"
title=${node.label}
>
${node.label}
</div>`;
})
: nothing}
`;
}
private _processNodesAndPaths = memoizeOne(
(rawNodes: Node[], rawLinks: Link[]) => {
const filteredNodes = rawNodes.filter((n) => n.value > 0);
const indexes = [...new Set(filteredNodes.map((n) => n.index))].sort();
const { links, passThroughNodes } = this._processLinks(
filteredNodes,
indexes,
rawLinks
);
const nodes = this._processNodes(
[...filteredNodes, ...passThroughNodes],
indexes
);
const paths = this._processPaths(nodes, links);
return { nodes, paths };
}
);
private _processLinks(nodes: Node[], indexes: number[], rawLinks: Link[]) {
const accountedIn = new Map<string, number>();
const accountedOut = new Map<string, number>();
const links: ProcessedLink[] = [];
const passThroughNodes: Node[] = [];
rawLinks.forEach((link) => {
const sourceNode = nodes.find((n) => n.id === link.source);
const targetNode = nodes.find((n) => n.id === link.target);
if (!sourceNode || !targetNode) {
return;
}
const sourceAccounted = accountedOut.get(sourceNode.id) || 0;
const targetAccounted = accountedIn.get(targetNode.id) || 0;
// if no value is provided, we infer it from the remaining capacity of the source and target nodes
const sourceRemaining = sourceNode.value - sourceAccounted;
const targetRemaining = targetNode.value - targetAccounted;
// ensure the value is not greater than the remaining capacity of the nodes
const value = Math.min(
link.value ?? sourceRemaining,
sourceRemaining,
targetRemaining
);
accountedIn.set(targetNode.id, targetAccounted + value);
accountedOut.set(sourceNode.id, sourceAccounted + value);
// handle links across sections
const sourceIndex = indexes.findIndex((i) => i === sourceNode.index);
const targetIndex = indexes.findIndex((i) => i === targetNode.index);
const passThroughSections = indexes.slice(sourceIndex + 1, targetIndex);
// create pass-through nodes to reserve space
const passThroughNodeIds = passThroughSections.map((index) => {
const node = {
passThrough: true,
id: `${sourceNode.id}-${targetNode.id}-${index}`,
value,
index,
};
passThroughNodes.push(node);
return node.id;
});
if (value > 0) {
links.push({
...link,
value,
offset: {
source: sourceAccounted / (sourceNode.value || 1),
target: targetAccounted / (targetNode.value || 1),
},
passThroughNodeIds,
});
}
});
return { links, passThroughNodes };
}
private _processNodes(filteredNodes: Node[], indexes: number[]) {
// add MIN_DISTANCE as padding
const sectionSize = this.vertical
? this._sizeController.value!.width - MIN_DISTANCE * 2
: this._sizeController.value!.height - MIN_DISTANCE * 2;
const nodesPerSection: Record<number, Node[]> = {};
filteredNodes.forEach((node) => {
if (!nodesPerSection[node.index]) {
nodesPerSection[node.index] = [node];
} else {
nodesPerSection[node.index].push(node);
}
});
const sectionFlexSize = this._getSectionFlexSize(
Object.values(nodesPerSection)
);
const sections: Section[] = indexes.map((index, i) => {
const nodes: ProcessedNode[] = nodesPerSection[index].map(
(node: Node) => ({
...node,
color: node.color || DEFAULT_COLOR,
x: 0,
y: 0,
size: 0,
})
);
const availableSpace =
sectionSize - (nodes.length * MIN_DISTANCE - MIN_DISTANCE);
const totalValue = nodes.reduce(
(acc: number, node: Node) => acc + node.value,
0
);
const { nodes: sizedNodes, statePerPixel } = this._setNodeSizes(
nodes,
availableSpace,
totalValue
);
return {
nodes: sizedNodes,
offset: sectionFlexSize * i,
index,
totalValue,
statePerPixel,
};
});
sections.forEach((section) => {
// calc sizes again with the best statePerPixel
let totalSize = 0;
if (section.statePerPixel !== this._statePerPixel) {
section.nodes.forEach((node) => {
const size = Math.max(
MIN_SIZE,
Math.floor(node.value / this._statePerPixel)
);
totalSize += size;
node.size = size;
});
} else {
totalSize = section.nodes.reduce((sum, b) => sum + b.size, 0);
}
// calc margin betwee boxes
const emptySpace = sectionSize - totalSize;
const spacerSize = emptySpace / (section.nodes.length - 1);
// account for MIN_DISTANCE padding and center single node sections
let offset =
section.nodes.length > 1 ? MIN_DISTANCE : emptySpace / 2 + MIN_DISTANCE;
// calc positions - swap x/y for vertical layout
section.nodes.forEach((node) => {
if (this.vertical) {
node.x = offset;
node.y = section.offset;
} else {
node.x = section.offset;
node.y = offset;
}
offset += node.size + spacerSize;
});
});
return sections.flatMap((section) => section.nodes);
}
private _processPaths(nodes: ProcessedNode[], links: ProcessedLink[]) {
const flowDirection = this.vertical ? "y" : "x";
const orthDirection = this.vertical ? "x" : "y"; // orthogonal to the flow
const nodesById = new Map(nodes.map((n) => [n.id, n]));
return links.map((link) => {
const { source, target, value, offset, passThroughNodeIds } = link;
const pathNodes = [source, ...passThroughNodeIds, target].map(
(id) => nodesById.get(id)!
);
const offsets = [
offset.source,
...link.passThroughNodeIds.map(() => 0),
offset.target,
];
const sourceNode = pathNodes[0];
const targetNode = pathNodes[pathNodes.length - 1];
let path: [string, number, number][] = [
[
"M",
sourceNode[flowDirection] + NODE_WIDTH,
sourceNode[orthDirection] + offset.source * sourceNode.size,
],
]; // starting point
// traverse the path forwards. stop before the last node
for (let i = 0; i < pathNodes.length - 1; i++) {
const node = pathNodes[i];
const nextNode = pathNodes[i + 1];
const flowMiddle =
(nextNode[flowDirection] - node[flowDirection]) / 2 +
node[flowDirection];
const orthStart = node[orthDirection] + offsets[i] * node.size;
const orthEnd =
nextNode[orthDirection] + offsets[i + 1] * nextNode.size;
path.push(
["L", node[flowDirection] + NODE_WIDTH, orthStart],
["C", flowMiddle, orthStart],
["", flowMiddle, orthEnd],
["", nextNode[flowDirection], orthEnd]
);
}
// traverse the path backwards. stop before the first node
for (let i = pathNodes.length - 1; i > 0; i--) {
const node = pathNodes[i];
const prevNode = pathNodes[i - 1];
const flowMiddle =
(node[flowDirection] - prevNode[flowDirection]) / 2 +
prevNode[flowDirection];
const orthStart =
node[orthDirection] +
offsets[i] * node.size +
Math.max((value / (node.value || 1)) * node.size, 0);
const orthEnd =
prevNode[orthDirection] +
offsets[i - 1] * prevNode.size +
Math.max((value / (prevNode.value || 1)) * prevNode.size, 0);
path.push(
["L", node[flowDirection], orthStart],
["C", flowMiddle, orthStart],
["", flowMiddle, orthEnd],
["", prevNode[flowDirection] + NODE_WIDTH, orthEnd]
);
}
if (this.vertical) {
// Just swap x and y coordinates for vertical layout
path = path.map((c) => [c[0], c[2], c[1]]);
}
return {
sourceNode,
targetNode,
value,
path,
};
});
}
private _setNodeSizes(
nodes: ProcessedNode[],
availableSpace: number,
totalValue: number
): { nodes: ProcessedNode[]; statePerPixel: number } {
const statePerPixel = totalValue / availableSpace;
if (statePerPixel > this._statePerPixel) {
this._statePerPixel = statePerPixel;
}
let deficitHeight = 0;
const result = nodes.map((node) => {
if (node.size === MIN_SIZE) {
return node;
}
let size = Math.floor(node.value / this._statePerPixel);
if (size < MIN_SIZE) {
deficitHeight += MIN_SIZE - size;
size = MIN_SIZE;
}
return {
...node,
size,
};
});
if (deficitHeight > 0) {
return this._setNodeSizes(
result,
availableSpace - deficitHeight,
totalValue
);
}
return { nodes: result, statePerPixel: this._statePerPixel };
}
private _getSectionFlexSize(nodesPerSection: Node[][]): number {
const fullSize = this.vertical
? this._sizeController.value!.height
: this._sizeController.value!.width;
if (nodesPerSection.length < 2) {
return fullSize;
}
let lastSectionFlexSize: number;
if (this.vertical) {
lastSectionFlexSize = FONT_SIZE * 2 + NODE_WIDTH; // estimated based on the font size + some margin
} else {
// Estimate the width needed for the last section based on label length
const lastIndex = nodesPerSection.length - 1;
const lastSectionNodes = nodesPerSection[lastIndex];
const TEXT_PADDING = 5; // Padding between node and text
lastSectionFlexSize =
lastSectionNodes.length > 0
? Math.max(
...lastSectionNodes.map(
(node) =>
NODE_WIDTH +
TEXT_PADDING +
(node.label ? this._getTextWidth(node.label) : 0)
)
)
: 0;
}
// Calculate the flex size for other sections
const remainingSize = fullSize - lastSectionFlexSize;
const flexSize = remainingSize / (nodesPerSection.length - 1);
// if the last section is bigger than the others, we make them all the same size
// this is to prevent the last section from squishing the others
return lastSectionFlexSize < flexSize
? flexSize
: 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
.split(" ")
.reduce(
(longest, current) =>
longest.length > current.length ? longest : current,
""
);
const wordWidth = this._getTextWidth(longestWord);
return Math.min(FONT_SIZE, (labelWidth / wordWidth) * FONT_SIZE);
}
static styles = css`
:host {
display: block;
flex: 1;
background: var(--ha-card-background, var(--card-background-color, #000));
overflow: hidden;
position: relative;
}
svg {
overflow: visible;
position: absolute;
}
.node-label {
font-size: ${FONT_SIZE}px;
fill: var(--primary-text-color, white);
}
.node-label.vertical {
position: absolute;
text-align: center;
overflow: hidden;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"sankey-chart": SankeyChart;
}
}

View File

@@ -99,7 +99,6 @@ export class StateHistoryChartLine extends LitElement {
) {
this._chartOptions = {
parsing: false,
animation: false,
interaction: {
mode: "nearest",
axis: "xy",
@@ -114,7 +113,7 @@ export class StateHistoryChartLine extends LitElement {
},
},
min: this.startTime,
suggestedMax: this.endTime,
max: this.endTime,
ticks: {
maxRotation: 0,
sampleSize: 5,

View File

@@ -103,10 +103,9 @@ export class StateHistoryChartTimeline extends LitElement {
this._chartOptions = {
maintainAspectRatio: false,
parsing: false,
animation: false,
scales: {
x: {
type: "timeline",
type: "time",
position: "bottom",
adapters: {
date: {

View File

@@ -194,7 +194,6 @@ export class StatisticsChart extends LitElement {
private _createOptions(unit?: string) {
this._chartOptions = {
parsing: false,
animation: false,
interaction: {
mode: "nearest",
axis: "x",

View File

@@ -17,7 +17,6 @@ declare module "chart.js" {
datasetOptions: BarControllerDatasetOptions;
defaultDataPoint: TimeLineData;
parsedDataType: any;
scales: "timeline";
};
}
}

View File

@@ -1,55 +0,0 @@
import { TimeScale } from "chart.js";
import type { TimeLineData } from "./const";
export class TimeLineScale extends TimeScale {
static id = "timeline";
static defaults = {
position: "bottom",
tooltips: {
mode: "nearest",
},
ticks: {
autoSkip: true,
},
};
determineDataLimits() {
const options = this.options;
// @ts-ignore
const adapter = this._adapter;
const unit = options.time.unit || "day";
let { min, max } = this.getUserBounds();
const chart = this.chart;
// Convert data to timestamps
chart.data.datasets.forEach((dataset, index) => {
if (!chart.isDatasetVisible(index)) {
return;
}
for (const data of dataset.data as TimeLineData[]) {
let timestamp0 = adapter.parse(data.start, this);
let timestamp1 = adapter.parse(data.end, this);
if (timestamp0 > timestamp1) {
[timestamp0, timestamp1] = [timestamp1, timestamp0];
}
if (min > timestamp0 && timestamp0) {
min = timestamp0;
}
if (max < timestamp1 && timestamp1) {
max = timestamp1;
}
}
});
// In case there is no valid min/max, var's use today limits
min =
isFinite(min) && !isNaN(min) ? min : +adapter.startOf(Date.now(), unit);
max = isFinite(max) && !isNaN(max) ? max : +adapter.endOf(Date.now(), unit);
// Make sure that max is strictly higher than min (required by the lookup table)
this.min = adapter.parse(options.min, this) ?? Math.min(min, max - 1);
this.max = adapter.parse(options.max, this) ?? Math.max(min + 1, max);
}
}

View File

@@ -515,7 +515,7 @@ export class HaDataTable extends LitElement {
return html`<div class="mdc-data-table__row">${row.content}</div>`;
}
if (row.empty) {
return html`<div class="mdc-data-table__row"></div>`;
return html`<div class="mdc-data-table__row empty-row"></div>`;
}
return html`
<div
@@ -960,6 +960,13 @@ export class HaDataTable extends LitElement {
width: var(--table-row-width, 100%);
}
.mdc-data-table__row.empty-row {
height: var(
--data-table-empty-row-height,
var(--data-table-row-height, 52px)
);
}
.mdc-data-table__row ~ .mdc-data-table__row {
border-top: 1px solid var(--divider-color);
}

View File

@@ -222,7 +222,9 @@ export class HaDevicePicker extends LitElement {
return {
id: device.id,
name: name,
name:
name ||
this.hass.localize("ui.components.device-picker.unnamed_device"),
area:
device.area_id && areas[device.area_id]
? areas[device.area_id].name

View File

@@ -113,71 +113,74 @@ export class StateBadge extends LitElement {
this.icon = true;
if (stateObj && this.overrideImage === undefined) {
// hide icon if we have entity picture
if (
(stateObj.attributes.entity_picture_local ||
stateObj.attributes.entity_picture) &&
!this.overrideIcon
) {
let imageUrl =
stateObj.attributes.entity_picture_local ||
stateObj.attributes.entity_picture;
if (stateObj) {
const domain = computeDomain(stateObj.entity_id);
if (this.overrideImage === undefined) {
// hide icon if we have entity picture
if (
(stateObj.attributes.entity_picture_local ||
stateObj.attributes.entity_picture) &&
!this.overrideIcon
) {
let imageUrl =
stateObj.attributes.entity_picture_local ||
stateObj.attributes.entity_picture;
if (this.hass) {
imageUrl = this.hass.hassUrl(imageUrl);
}
if (domain === "camera") {
imageUrl = cameraUrlWithWidthHeight(imageUrl, 80, 80);
}
backgroundImage = `url(${imageUrl})`;
this.icon = false;
} else if (this.color) {
// Externally provided overriding color wins over state color
iconStyle.color = this.color;
} else if (this._stateColor) {
const color = stateColorCss(stateObj);
if (color) {
iconStyle.color = color;
}
if (stateObj.attributes.rgb_color) {
iconStyle.color = `rgb(${stateObj.attributes.rgb_color.join(",")})`;
}
if (stateObj.attributes.brightness) {
const brightness = stateObj.attributes.brightness;
if (typeof brightness !== "number") {
const errorMessage = `Type error: state-badge expected number, but type of ${
stateObj.entity_id
}.attributes.brightness is ${typeof brightness} (${brightness})`;
// eslint-disable-next-line
console.warn(errorMessage);
}
iconStyle.filter = stateColorBrightness(stateObj);
}
if (stateObj.attributes.hvac_action) {
const hvacAction = stateObj.attributes.hvac_action;
if (hvacAction in CLIMATE_HVAC_ACTION_TO_MODE) {
iconStyle.color = stateColorCss(
stateObj,
CLIMATE_HVAC_ACTION_TO_MODE[hvacAction]
)!;
} else {
delete iconStyle.color;
}
}
}
} else if (this.overrideImage) {
let imageUrl = this.overrideImage;
if (this.hass) {
imageUrl = this.hass.hassUrl(imageUrl);
}
const domain = computeDomain(stateObj.entity_id);
if (domain === "camera") {
imageUrl = cameraUrlWithWidthHeight(imageUrl, 80, 80);
}
backgroundImage = `url(${imageUrl})`;
this.icon = false;
if (domain === "update") {
this.style.borderRadius = "0";
} else if (domain === "media_player") {
this.style.borderRadius = "8%";
}
} else if (this.color) {
// Externally provided overriding color wins over state color
iconStyle.color = this.color;
} else if (this._stateColor) {
const color = stateColorCss(stateObj);
if (color) {
iconStyle.color = color;
}
if (stateObj.attributes.rgb_color) {
iconStyle.color = `rgb(${stateObj.attributes.rgb_color.join(",")})`;
}
if (stateObj.attributes.brightness) {
const brightness = stateObj.attributes.brightness;
if (typeof brightness !== "number") {
const errorMessage = `Type error: state-badge expected number, but type of ${
stateObj.entity_id
}.attributes.brightness is ${typeof brightness} (${brightness})`;
// eslint-disable-next-line
console.warn(errorMessage);
}
iconStyle.filter = stateColorBrightness(stateObj);
}
if (stateObj.attributes.hvac_action) {
const hvacAction = stateObj.attributes.hvac_action;
if (hvacAction in CLIMATE_HVAC_ACTION_TO_MODE) {
iconStyle.color = stateColorCss(
stateObj,
CLIMATE_HVAC_ACTION_TO_MODE[hvacAction]
)!;
} else {
delete iconStyle.color;
}
}
}
} else if (this.overrideImage) {
let imageUrl = this.overrideImage;
if (this.hass) {
imageUrl = this.hass.hassUrl(imageUrl);
if (domain === "update") {
this.style.borderRadius = "0";
} else if (domain === "media_player" || domain === "camera") {
this.style.borderRadius = "8%";
}
backgroundImage = `url(${imageUrl})`;
this.icon = false;
}
this._iconStyle = iconStyle;

View File

@@ -14,7 +14,8 @@ export class HaButtonMenu extends LitElement {
@property() public corner: Corner = "BOTTOM_START";
@property({ attribute: false }) public menuCorner: MenuCorner = "START";
@property({ attribute: "menu-corner" }) public menuCorner: MenuCorner =
"START";
@property({ type: Number }) public x: number | null = null;

View File

@@ -8,7 +8,7 @@ export class HaCircularProgress extends MdCircularProgress {
@property({ attribute: "aria-label", type: String }) public ariaLabel =
"Loading";
@property() public size: "tiny" | "small" | "medium" | "large" = "medium";
@property() public size?: "tiny" | "small" | "medium" | "large";
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
@@ -21,7 +21,6 @@ export class HaCircularProgress extends MdCircularProgress {
case "small":
this.style.setProperty("--md-circular-progress-size", "28px");
break;
// medium is default size
case "medium":
this.style.setProperty("--md-circular-progress-size", "48px");
break;

View File

@@ -5,8 +5,6 @@ import "@material/mwc-list/mwc-list-item";
import { mdiCalendar } from "@mdi/js";
import {
addDays,
addMonths,
addYears,
endOfDay,
endOfMonth,
endOfWeek,
@@ -15,25 +13,23 @@ import {
startOfMonth,
startOfWeek,
startOfYear,
differenceInMilliseconds,
addMilliseconds,
subMilliseconds,
roundToNearestHours,
isThisYear,
} from "date-fns";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { calcDate } from "../common/datetime/calc_date";
import { calcDate, shiftDateRange } from "../common/datetime/calc_date";
import { firstWeekdayIndex } from "../common/datetime/first_weekday";
import { formatDate } from "../common/datetime/format_date";
import { formatDateTime } from "../common/datetime/format_date_time";
import {
formatShortDateTimeWithYear,
formatShortDateTime,
} from "../common/datetime/format_date_time";
import { useAmPm } from "../common/datetime/use_am_pm";
import type { HomeAssistant } from "../types";
import "./date-range-picker";
import "./ha-icon-button";
import "./ha-svg-icon";
import "./ha-textfield";
import "./ha-textarea";
import "./ha-icon-button-next";
import "./ha-icon-button-prev";
@@ -141,9 +137,6 @@ export class HaDateRangePicker extends LitElement {
[this.hass.localize(
"ui.components.date-range-picker.ranges.this_week"
)]: [weekStart, weekEnd],
[this.hass.localize(
"ui.components.date-range-picker.ranges.last_week"
)]: [addDays(weekStart, -7), addDays(weekEnd, -7)],
...(this.extendedPresets
? {
[this.hass.localize(
@@ -168,28 +161,6 @@ export class HaDateRangePicker extends LitElement {
}
),
],
[this.hass.localize(
"ui.components.date-range-picker.ranges.last_month"
)]: [
calcDate(
addMonths(today, -1),
startOfMonth,
this.hass.locale,
this.hass.config,
{
weekStartsOn,
}
),
calcDate(
addMonths(today, -1),
endOfMonth,
this.hass.locale,
this.hass.config,
{
weekStartsOn,
}
),
],
[this.hass.localize(
"ui.components.date-range-picker.ranges.this_year"
)]: [
@@ -206,28 +177,6 @@ export class HaDateRangePicker extends LitElement {
weekStartsOn,
}),
],
[this.hass.localize(
"ui.components.date-range-picker.ranges.last_year"
)]: [
calcDate(
addYears(today, -1),
startOfYear,
this.hass.locale,
this.hass.config,
{
weekStartsOn,
}
),
calcDate(
addYears(today, -1),
endOfYear,
this.hass.locale,
this.hass.config,
{
weekStartsOn,
}
),
],
}
: {}),
};
@@ -261,54 +210,49 @@ export class HaDateRangePicker extends LitElement {
>
<div slot="input" class="date-range-inputs" @click=${this._handleClick}>
${!this.minimal
? html`<ha-svg-icon .path=${mdiCalendar}></ha-svg-icon>
<ha-icon-button-prev
.label=${this.hass.localize("ui.common.previous")}
class="prev"
@click=${this._handlePrev}
>
</ha-icon-button-prev>
<ha-textfield
.value=${this.timePicker
? formatDateTime(
? html`<ha-textarea
mobile-multiline
.value=${(isThisYear(this.startDate)
? formatShortDateTime(
this.startDate,
this.hass.locale,
this.hass.config
)
: formatDate(
: formatShortDateTimeWithYear(
this.startDate,
this.hass.locale,
this.hass.config
)}
)) +
" - \n" +
(isThisYear(this.endDate)
? formatShortDateTime(
this.endDate,
this.hass.locale,
this.hass.config
)
: formatShortDateTimeWithYear(
this.endDate,
this.hass.locale,
this.hass.config
))}
.label=${this.hass.localize(
"ui.components.date-range-picker.start_date"
)}
.disabled=${this.disabled}
@click=${this._handleInputClick}
readonly
></ha-textfield>
<ha-textfield
.value=${this.timePicker
? formatDateTime(
this.endDate,
this.hass.locale,
this.hass.config
)
: formatDate(
this.endDate,
this.hass.locale,
this.hass.config
)}
.label=${this.hass.localize(
) +
" - " +
this.hass.localize(
"ui.components.date-range-picker.end_date"
)}
.disabled=${this.disabled}
@click=${this._handleInputClick}
readonly
></ha-textfield>
></ha-textarea>
<ha-icon-button-prev
.label=${this.hass.localize("ui.common.previous")}
@click=${this._handlePrev}
>
</ha-icon-button-prev>
<ha-icon-button-next
.label=${this.hass.localize("ui.common.next")}
class="next"
@click=${this._handleNext}
>
</ha-icon-button-next>`
@@ -342,40 +286,28 @@ export class HaDateRangePicker extends LitElement {
`;
}
private _handleNext(): void {
const dateRange = [
roundToNearestHours(this.endDate),
subMilliseconds(
roundToNearestHours(
addMilliseconds(
this.endDate,
Math.max(
3600000,
differenceInMilliseconds(this.endDate, this.startDate)
)
)
),
1
),
];
const dateRangePicker = this._dateRangePicker;
dateRangePicker.clickRange(dateRange);
dateRangePicker.clickedApply();
private _handleNext(ev: MouseEvent): void {
if (ev && ev.stopPropagation) ev.stopPropagation();
this._shift(true);
}
private _handlePrev(): void {
const dateRange = [
roundToNearestHours(
subMilliseconds(
this.startDate,
Math.max(
3600000,
differenceInMilliseconds(this.endDate, this.startDate)
)
)
),
subMilliseconds(roundToNearestHours(this.startDate), 1),
];
private _handlePrev(ev: MouseEvent): void {
if (ev && ev.stopPropagation) ev.stopPropagation();
this._shift(false);
}
private _shift(forward: boolean) {
if (!this.startDate) return;
const { start, end } = shiftDateRange(
this.startDate,
this.endDate,
forward,
this.hass.locale,
this.hass.config
);
this.startDate = start;
this.endDate = end;
const dateRange = [start, end];
const dateRangePicker = this._dateRangePicker;
dateRangePicker.clickRange(dateRange);
dateRangePicker.clickedApply();
@@ -430,12 +362,6 @@ export class HaDateRangePicker extends LitElement {
static get styles(): CSSResultGroup {
return css`
ha-svg-icon {
margin-right: 8px;
margin-inline-end: 8px;
margin-inline-start: initial;
direction: var(--direction);
}
ha-icon-button {
direction: var(--direction);
@@ -444,6 +370,7 @@ export class HaDateRangePicker extends LitElement {
.date-range-inputs {
display: flex;
align-items: center;
gap: 8px;
}
.date-range-ranges {
@@ -457,17 +384,13 @@ export class HaDateRangePicker extends LitElement {
border-top: 1px solid var(--divider-color);
}
ha-textfield {
ha-textarea {
display: inline-block;
max-width: 250px;
min-width: 220px;
width: 340px;
}
ha-textfield:last-child {
margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: initial;
direction: var(--direction);
@media only screen and (max-width: 460px) {
ha-textarea {
width: 100%
}
@media only screen and (max-width: 800px) {
@@ -476,18 +399,6 @@ export class HaDateRangePicker extends LitElement {
border-bottom: 1px solid var(--divider-color);
}
}
@media only screen and (max-width: 500px) {
ha-textfield {
min-width: inherit;
}
ha-svg-icon,
.prev,
.next {
display: none;
}
}
`;
}
}

View File

@@ -15,13 +15,13 @@ export const createCloseHeading = (
title: string | TemplateResult
) => html`
<div class="header_title">
<span>${title}</span>
<ha-icon-button
.label=${hass?.localize("ui.dialogs.generic.close") ?? "Close"}
.path=${mdiClose}
dialogAction="close"
class="header_button"
></ha-icon-button>
<span>${title}</span>
</div>
`;
@@ -104,6 +104,9 @@ export class HaDialog extends DialogBase {
.mdc-dialog__title {
padding: 24px 24px 0 24px;
}
.mdc-dialog__title:has(span) {
padding: 12px 12px 0;
}
.mdc-dialog__actions {
padding: 12px 24px 12px 24px;
}
@@ -138,10 +141,8 @@ export class HaDialog extends DialogBase {
flex-direction: column;
}
.header_title {
position: relative;
padding-right: 40px;
padding-inline-end: 40px;
padding-inline-start: initial;
display: flex;
align-items: center;
direction: var(--direction);
}
.header_title span {
@@ -149,11 +150,9 @@ export class HaDialog extends DialogBase {
text-overflow: ellipsis;
white-space: nowrap;
display: block;
padding-left: 4px;
}
.header_button {
position: absolute;
right: -12px;
top: -12px;
text-decoration: none;
color: inherit;
inset-inline-start: initial;

View File

@@ -2,8 +2,11 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { until } from "lit/directives/until";
import { DEFAULT_DOMAIN_ICON, FIXED_DOMAIN_ICONS } from "../common/const";
import { domainIcon } from "../data/icons";
import {
DEFAULT_DOMAIN_ICON,
domainIcon,
FALLBACK_DOMAIN_ICONS,
} from "../data/icons";
import type { HomeAssistant } from "../types";
import { brandsUrl } from "../util/brands-url";
import "./ha-icon";
@@ -47,9 +50,9 @@ export class HaDomainIcon extends LitElement {
}
private _renderFallback() {
if (this.domain! in FIXED_DOMAIN_ICONS) {
if (this.domain! in FALLBACK_DOMAIN_ICONS) {
return html`
<ha-svg-icon .path=${FIXED_DOMAIN_ICONS[this.domain!]}></ha-svg-icon>
<ha-svg-icon .path=${FALLBACK_DOMAIN_ICONS[this.domain!]}></ha-svg-icon>
`;
}
if (this.brandFallback) {

View File

@@ -19,6 +19,10 @@ export class HaFab extends FabBase {
margin-inline-end: 12px;
direction: var(--direction);
}
:disabled {
--mdc-theme-secondary: var(--disabled-text-color);
pointer-events: none;
}
`,
// safari workaround - must be explicit
mainWindow.document.dir === "rtl"

View File

@@ -15,6 +15,7 @@ import { bytesToString } from "../util/bytes-to-string";
declare global {
interface HASSDomEvents {
"file-picked": { files: File[] };
"files-cleared": void;
}
}
@@ -56,6 +57,21 @@ export class HaFileUpload extends LitElement {
}
}
private get _name() {
if (this.value === undefined) {
return "";
}
if (typeof this.value === "string") {
return this.value;
}
const files =
this.value instanceof FileList
? Array.from(this.value)
: ensureArray(this.value);
return files.map((file) => file.name).join(", ");
}
public render(): TemplateResult {
return html`
${this.uploading
@@ -65,7 +81,7 @@ export class HaFileUpload extends LitElement {
>${this.value
? this.hass?.localize(
"ui.components.file-upload.uploading_name",
{ name: this.value.toString() }
{ name: this._name }
)
: this.hass?.localize(
"ui.components.file-upload.uploading"
@@ -201,6 +217,7 @@ export class HaFileUpload extends LitElement {
this._input!.value = "";
this.value = undefined;
fireEvent(this, "change");
fireEvent(this, "files-cleared");
}
static get styles() {
@@ -305,6 +322,15 @@ export class HaFileUpload extends LitElement {
.progress {
color: var(--secondary-text-color);
}
button.link {
background: none;
border: none;
padding: 0;
font-size: 14px;
color: var(--primary-color);
text-decoration: underline;
cursor: pointer;
}
`;
}
}

View File

@@ -1,9 +1,9 @@
import "@material/mwc-list/mwc-list";
import type { SelectedDetail } from "@material/mwc-list";
import type { List, SelectedDetail } from "@material/mwc-list";
import { mdiFilterVariantRemove } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
@@ -32,6 +32,8 @@ export class HaFilterStates extends LitElement {
@state() private _shouldRender = false;
@query("mwc-list") private _list!: List;
protected render() {
if (!this.states) {
return nothing;
@@ -84,12 +86,21 @@ export class HaFilterStates extends LitElement {
`;
}
protected updated(changed) {
protected willUpdate(changed) {
if (changed.has("expanded") && this.expanded) {
setTimeout(() => {
this._shouldRender = true;
}
}
protected updated(changed) {
if ((changed.has("expanded") || changed.has("states")) && this.expanded) {
setTimeout(async () => {
if (!this.expanded) return;
this.renderRoot.querySelector("mwc-list")!.style.height =
`${this.clientHeight - 49}px`;
const list = this._list;
if (!list) {
return;
}
list.style.height = `${this.clientHeight - 49}px`;
}, 300);
}
}

View File

@@ -5,12 +5,14 @@ import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-button-menu";
import "../ha-check-list-item";
import type { HaCheckListItem } from "../ha-check-list-item";
import "../ha-checkbox";
import type { HaCheckbox } from "../ha-checkbox";
import "../ha-formfield";
import "../ha-svg-icon";
import "../ha-icon-button";
import "../ha-textfield";
import "../ha-md-button-menu";
import "../ha-md-menu-item";
import type {
HaFormElement,
HaFormMultiSelectData,
@@ -73,13 +75,10 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
}
return html`
<ha-button-menu
<ha-md-button-menu
.disabled=${this.disabled}
fixed
@opened=${this._handleOpen}
@closed=${this._handleClose}
multi
activatable
@opening=${this._handleOpen}
@closing=${this._handleClose}
>
<ha-textfield
slot="trigger"
@@ -94,28 +93,56 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
.disabled=${this.disabled}
tabindex="-1"
></ha-textfield>
<ha-svg-icon
<ha-icon-button
slot="trigger"
.label=${this.label}
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
></ha-svg-icon>
></ha-icon-button>
${options.map((item: string | [string, string]) => {
const value = optionValue(item);
const selected = data.includes(value);
return html`<ha-check-list-item
left
.selected=${selected}
.activated=${selected}
@request-selected=${this._selectedChanged}
return html`<ha-md-menu-item
type="option"
aria-checked=${selected}
.value=${value}
.disabled=${this.disabled}
.action=${selected ? "remove" : "add"}
.activated=${selected}
@click=${this._toggleItem}
@keydown=${this._keydown}
keep-open
>
<ha-checkbox
slot="start"
tabindex="-1"
.checked=${selected}
></ha-checkbox>
${optionLabel(item)}
</ha-check-list-item>`;
</ha-md-menu-item>`;
})}
</ha-button-menu>
</ha-md-button-menu>
`;
}
protected _keydown(ev) {
if (ev.code === "Space" || ev.code === "Enter") {
ev.preventDefault();
this._toggleItem(ev);
}
}
protected _toggleItem(ev) {
const oldData = this.data || [];
let newData: string[];
if (ev.currentTarget.action === "add") {
newData = [...oldData, ev.currentTarget.value];
} else {
newData = oldData.filter((d) => d !== ev.currentTarget.value);
}
fireEvent(this, "value-changed", {
value: newData,
});
}
protected firstUpdated() {
this.updateComplete.then(() => {
const { formElement, mdcRoot } =
@@ -139,17 +166,6 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
}
}
private _selectedChanged(ev: CustomEvent): void {
ev.stopPropagation();
if (ev.detail.source === "property") {
return;
}
this._handleValueChanged(
(ev.target as HaCheckListItem).value,
ev.detail.selected
);
}
private _valueChanged(ev: CustomEvent): void {
const { value, checked } = ev.target as HaCheckbox;
this._handleValueChanged(value, checked);
@@ -195,7 +211,7 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
:host([own-margin]) {
margin-bottom: 5px;
}
ha-button-menu {
ha-md-button-menu {
display: block;
cursor: pointer;
}
@@ -208,22 +224,23 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
}
ha-textfield {
display: block;
width: 100%;
pointer-events: none;
}
ha-svg-icon {
ha-icon-button {
color: var(--input-dropdown-icon-color);
position: absolute;
right: 1em;
top: 1em;
top: 4px;
cursor: pointer;
inset-inline-end: 1em;
inset-inline-start: initial;
direction: var(--direction);
}
:host([opened]) ha-svg-icon {
:host([opened]) ha-icon-button {
color: var(--primary-color);
}
:host([opened]) ha-button-menu {
:host([opened]) ha-md-button-menu {
--mdc-text-field-idle-line-color: var(--input-hover-line-color);
--mdc-text-field-label-ink-color: var(--primary-color);
}

View File

@@ -62,6 +62,7 @@ export class HaGauge extends LitElement {
if (
!this._updated ||
(!changedProperties.has("value") &&
!changedProperties.has("valueText") &&
!changedProperties.has("label") &&
!changedProperties.has("_segment_label"))
) {

View File

@@ -181,7 +181,15 @@ class HaHLSPlayer extends LitElement {
let playlist_url: string;
if (match !== null && matchTwice === null) {
// Only send the regular playlist url if we match exactly once
playlist_url = new URL(match[3], this._url).href;
// In case we arrive here with a relative URL, we need to provide a valid
// base/absolute URL to avoid the URL() constructor throwing an error.
let base_url: string;
try {
base_url = new URL(this._url).href;
} catch (error) {
base_url = new URL(this._url, window.location.href).href;
}
playlist_url = new URL(match[3], base_url).href;
} else {
playlist_url = this._url;
}

View File

@@ -3,6 +3,7 @@ import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
import { fireEvent } from "../common/dom/fire_event";
import type { HaIconButton } from "./ha-icon-button";
import "./ha-menu";
import type { HaMenu } from "./ha-menu";
@@ -40,12 +41,22 @@ export class HaMdButtonMenu extends LitElement {
<ha-menu
.positioning=${this.positioning}
.hasOverflow=${this.hasOverflow}
@opening=${this._handleOpening}
@closing=${this._handleClosing}
>
<slot></slot>
</ha-menu>
`;
}
private _handleOpening(): void {
fireEvent(this, "opening", undefined, { composed: false });
}
private _handleClosing(): void {
fireEvent(this, "closing", undefined, { composed: false });
}
private _handleClick(): void {
if (this.disabled) {
return;
@@ -88,3 +99,10 @@ declare global {
"ha-md-button-menu": HaMdButtonMenu;
}
}
declare global {
interface HASSDomEvents {
opening: undefined;
closing: undefined;
}
}

View File

@@ -0,0 +1,31 @@
import { MdFilledTextField } from "@material/web/textfield/filled-text-field";
import { css } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-md-textfield")
export class HaMdTextfield extends MdFilledTextField {
static override styles = [
...super.styles,
css`
:host {
--ha-icon-display: block;
--md-sys-color-primary: var(--primary-text-color);
--md-sys-color-secondary: var(--secondary-text-color);
--md-sys-color-surface: var(--card-background-color);
--md-sys-color-on-surface-variant: var(--secondary-text-color);
--md-sys-color-surface-container-highest: var(--input-fill-color);
--md-sys-color-on-surface: var(--input-ink-color);
--md-sys-color-surface-container: var(--input-fill-color);
--md-sys-color-secondary-container: var(--input-fill-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-md-textfield": HaMdTextfield;
}
}

View File

@@ -1,5 +1,6 @@
import { mdiBackupRestore, mdiFolder, mdiHarddisk, mdiPlayBox } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../common/config/is_component_loaded";
@@ -173,6 +174,16 @@ class HaMountPicker extends LitElement {
fireEvent(this, "change");
}, 0);
}
static get styles(): CSSResultGroup {
return [
css`
ha-select {
width: 100%;
}
`,
];
}
}
declare global {

View File

@@ -22,14 +22,6 @@ export class HaOutlinedField extends MdOutlinedField {
border-end-start-radius: var(--_container-shape-end-start);
border-end-end-radius: var(--_container-shape-end-end);
}
.with-start .start {
margin-inline-end: var(--ha-outlined-field-start-margin, 4px);
margin-inline-start: initial;
}
.with-end .end {
margin-inline-start: var(--ha-outlined-field-end-margin, 4px);
margin-inline-end: initial;
}
`,
];
}

View File

@@ -28,8 +28,9 @@ export class HaOutlinedTextField extends MdOutlinedTextField {
--md-outlined-field-container-shape-end-end: 10px;
--md-outlined-field-container-shape-end-start: 10px;
--md-outlined-field-focus-outline-width: 1px;
--ha-outlined-field-start-margin: -4px;
--ha-outlined-field-end-margin: -4px;
--md-outlined-field-with-leading-content-leading-space: 8px;
--md-outlined-field-with-trailing-content-trailing-space: 8px;
--md-outlined-field-content-space: 8px;
--mdc-icon-size: var(--md-input-chip-icon-size, 18px);
}
.input {

View File

@@ -143,6 +143,10 @@ export class HaPasswordField extends LitElement {
></ha-icon-button>`;
}
public focus(): void {
this._textField.focus();
}
public checkValidity(): boolean {
return this._textField.checkValidity();
}

View File

@@ -2,9 +2,16 @@ import { mdiImagePlus } from "@mdi/js";
import type { TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { MediaPickedEvent } from "../data/media-player";
import { fireEvent } from "../common/dom/fire_event";
import { haStyle } from "../resources/styles";
import { createImage, generateImageThumbnailUrl } from "../data/image_upload";
import {
MEDIA_PREFIX,
getIdFromUrl,
createImage,
generateImageThumbnailUrl,
getImageData,
} from "../data/image_upload";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import type { CropOptions } from "../dialogs/image-cropper-dialog/show-image-cropper-dialog";
import { showImageCropperDialog } from "../dialogs/image-cropper-dialog/show-image-cropper-dialog";
@@ -12,6 +19,7 @@ import type { HomeAssistant } from "../types";
import "./ha-button";
import "./ha-circular-progress";
import "./ha-file-upload";
import { showMediaBrowserDialog } from "./media-player/show-media-browser-dialog";
@customElement("ha-picture-upload")
export class HaPictureUpload extends LitElement {
@@ -29,6 +37,9 @@ export class HaPictureUpload extends LitElement {
@property({ type: Boolean }) public crop = false;
@property({ type: Boolean, attribute: "select-media" }) public selectMedia =
false;
@property({ attribute: false }) public cropOptions?: CropOptions;
@property({ type: Boolean }) public original = false;
@@ -39,13 +50,31 @@ export class HaPictureUpload extends LitElement {
public render(): TemplateResult {
if (!this.value) {
const secondary =
this.secondary ||
(this.selectMedia
? html`${this.hass.localize(
"ui.components.picture-upload.secondary",
{
select_media: html`<button
class="link"
@click=${this._chooseMedia}
>
${this.hass.localize(
"ui.components.picture-upload.select_media"
)}
</button>`,
}
)}`
: undefined);
return html`
<ha-file-upload
.hass=${this.hass}
.icon=${mdiImagePlus}
.label=${this.label ||
this.hass.localize("ui.components.picture-upload.label")}
.secondary=${this.secondary}
.secondary=${secondary}
.supports=${this.supports ||
this.hass.localize("ui.components.picture-upload.supported_formats")}
.uploading=${this._uploading}
@@ -66,7 +95,7 @@ export class HaPictureUpload extends LitElement {
<ha-button
@click=${this._handleChangeClick}
.label=${this.hass.localize(
"ui.components.picture-upload.change_picture"
"ui.components.picture-upload.clear_picture"
)}
>
</ha-button>
@@ -93,7 +122,7 @@ export class HaPictureUpload extends LitElement {
this.value = null;
}
private async _cropFile(file: File) {
private async _cropFile(file: File, mediaId?: string) {
if (!["image/png", "image/jpeg", "image/gif"].includes(file.type)) {
showAlertDialog(this, {
text: this.hass.localize(
@@ -109,7 +138,16 @@ export class HaPictureUpload extends LitElement {
aspectRatio: NaN,
},
croppedCallback: (croppedFile) => {
this._uploadFile(croppedFile);
if (mediaId && croppedFile === file) {
this.value = generateImageThumbnailUrl(
mediaId,
this.size,
this.original
);
fireEvent(this, "change");
} else {
this._uploadFile(croppedFile);
}
},
});
}
@@ -141,6 +179,50 @@ export class HaPictureUpload extends LitElement {
}
}
private _chooseMedia = () => {
showMediaBrowserDialog(this, {
action: "pick",
entityId: "browser",
navigateIds: [
{ media_content_id: undefined, media_content_type: undefined },
{
media_content_id: MEDIA_PREFIX,
media_content_type: "app",
},
],
minimumNavigateLevel: 2,
mediaPickedCallback: async (pickedMedia: MediaPickedEvent) => {
const mediaId = getIdFromUrl(pickedMedia.item.media_content_id);
if (mediaId) {
if (this.crop) {
const url = generateImageThumbnailUrl(mediaId, undefined, true);
let data;
try {
data = await getImageData(url);
} catch (err: any) {
showAlertDialog(this, {
text: err.toString(),
});
return;
}
const metadata = {
type: pickedMedia.item.media_content_type,
};
const file = new File([data], pickedMedia.item.title, metadata);
this._cropFile(file, mediaId);
} else {
this.value = generateImageThumbnailUrl(
mediaId,
this.size,
this.original
);
fireEvent(this, "change");
}
}
},
});
};
static get styles() {
return [
haStyle,

View File

@@ -164,7 +164,6 @@ export class HaRelatedItems extends LitElement {
return html`
<a
href=${`/config/integrations/integration/${entry.domain}#config_entry=${entry.entry_id}`}
@click=${this._navigateAwayClose}
>
<ha-list-item hasMeta graphic="icon">
<img
@@ -191,7 +190,6 @@ export class HaRelatedItems extends LitElement {
(integration) =>
html`<a
href=${`/config/integrations/integration/${integration}`}
@click=${this._navigateAwayClose}
>
<ha-list-item hasMeta graphic="icon">
<img
@@ -223,10 +221,7 @@ export class HaRelatedItems extends LitElement {
return nothing;
}
return html`
<a
href="/config/devices/device/${relatedDeviceId}"
@click=${this._navigateAwayClose}
>
<a href="/config/devices/device/${relatedDeviceId}">
<ha-list-item hasMeta graphic="icon">
<ha-svg-icon
.path=${mdiDevices}
@@ -251,10 +246,7 @@ export class HaRelatedItems extends LitElement {
return nothing;
}
return html`
<a
href="/config/areas/area/${relatedAreaId}"
@click=${this._navigateAwayClose}
>
<a href="/config/areas/area/${relatedAreaId}">
<ha-list-item
hasMeta
.graphic=${area.picture ? "avatar" : "icon"}
@@ -364,10 +356,7 @@ export class HaRelatedItems extends LitElement {
const blueprintMeta = this._blueprints
? this._blueprints.automation[path]
: undefined;
return html`<a
href="/config/blueprint/dashboard"
@click=${this._navigateAwayClose}
>
return html`<a href="/config/blueprint/dashboard">
<ha-list-item hasMeta graphic="icon">
<ha-svg-icon
.path=${mdiPaletteSwatch}
@@ -421,10 +410,7 @@ export class HaRelatedItems extends LitElement {
const blueprintMeta = this._blueprints
? this._blueprints.script[path]
: undefined;
return html`<a
href="/config/blueprint/dashboard"
@click=${this._navigateAwayClose}
>
return html`<a href="/config/blueprint/dashboard">
<ha-list-item hasMeta graphic="icon">
<ha-svg-icon
.path=${mdiPaletteSwatch}
@@ -468,14 +454,6 @@ export class HaRelatedItems extends LitElement {
`;
}
private async _navigateAwayClose() {
// allow new page to open before closing dialog
await new Promise((resolve) => {
setTimeout(resolve, 0);
});
fireEvent(this, "close-dialog");
}
private async _findRelated() {
this._related = await findRelated(this.hass, this.itemType, this.itemId);
if (this._related.config_entry) {

View File

@@ -0,0 +1,98 @@
import "@material/mwc-list/mwc-list-item";
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import type { ButtonToggleSelector, SelectOption } from "../../data/selector";
import type { HomeAssistant, ToggleButton } from "../../types";
import "../ha-button-toggle-group";
@customElement("ha-selector-button_toggle")
export class HaButtonToggleSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: ButtonToggleSelector;
@property() public value?: string;
@property() public label?: string;
@property() public helper?: string;
@property({ attribute: false })
public localizeValue?: (key: string) => string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
const options =
this.selector.button_toggle?.options?.map((option) =>
typeof option === "object"
? (option as SelectOption)
: ({ value: option, label: option } as SelectOption)
) || [];
const translationKey = this.selector.button_toggle?.translation_key;
if (this.localizeValue && translationKey) {
options.forEach((option) => {
const localizedLabel = this.localizeValue!(
`${translationKey}.options.${option.value}`
);
if (localizedLabel) {
option.label = localizedLabel;
}
});
}
if (this.selector.button_toggle?.sort) {
options.sort((a, b) =>
caseInsensitiveStringCompare(
a.label,
b.label,
this.hass.locale.language
)
);
}
const toggleButtons: ToggleButton[] = options.map((item: SelectOption) => ({
label: item.label,
value: item.value,
}));
return html`
${this.label}
<ha-button-toggle-group
.buttons=${toggleButtons}
.active=${this.value}
@value-changed=${this._valueChanged}
></ha-button-toggle-group>
`;
}
private _valueChanged(ev) {
ev.stopPropagation();
const value = ev.detail?.value || ev.target.value;
if (this.disabled || value === undefined || value === (this.value ?? "")) {
return;
}
fireEvent(this, "value-changed", {
value: value,
});
}
static styles = css`
:host {
position: relative;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-button_toggle": HaButtonToggleSelector;
}
}

View File

@@ -96,6 +96,7 @@ export class HaImageSelector extends LitElement {
.value=${this.value?.startsWith(URL_PREFIX) ? this.value : null}
.original=${this.selector.image?.original}
.cropOptions=${this.selector.image?.crop}
select-media
@change=${this._pictureChanged}
></ha-picture-upload>
`}

View File

@@ -51,6 +51,7 @@ const LOAD_ELEMENTS = {
icon: () => import("./ha-selector-icon"),
media: () => import("./ha-selector-media"),
theme: () => import("./ha-selector-theme"),
button_toggle: () => import("./ha-selector-button-toggle"),
trigger: () => import("./ha-selector-trigger"),
tts: () => import("./ha-selector-tts"),
tts_voice: () => import("./ha-selector-tts-voice"),

View File

@@ -89,7 +89,7 @@ export class HaServiceControl extends LitElement {
@property({ attribute: "show-advanced", type: Boolean }) public showAdvanced =
false;
@property({ attribute: false, type: Boolean, reflect: true })
@property({ attribute: "hide-picker", type: Boolean, reflect: true })
public hidePicker = false;
@property({ attribute: "hide-description", type: Boolean })

View File

@@ -1,9 +1,12 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { until } from "lit/directives/until";
import { DEFAULT_SERVICE_ICON, FIXED_DOMAIN_ICONS } from "../common/const";
import { computeDomain } from "../common/entity/compute_domain";
import { serviceIcon } from "../data/icons";
import {
DEFAULT_SERVICE_ICON,
FALLBACK_DOMAIN_ICONS,
serviceIcon,
} from "../data/icons";
import type { HomeAssistant } from "../types";
import "./ha-icon";
import "./ha-svg-icon";
@@ -44,7 +47,7 @@ export class HaServiceIcon extends LitElement {
return html`
<ha-svg-icon
.path=${FIXED_DOMAIN_ICONS[domain] || DEFAULT_SERVICE_ICON}
.path=${FALLBACK_DOMAIN_ICONS[domain] || DEFAULT_SERVICE_ICON}
></ha-svg-icon>
`;
}

View File

@@ -48,6 +48,7 @@ import "./ha-menu-button";
import "./ha-sortable";
import "./ha-svg-icon";
import "./user/ha-user-badge";
import { preventDefault } from "../common/dom/prevent_default";
const SHOW_AFTER_SPACER = ["config", "developer-tools"];
@@ -404,6 +405,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
@focusout=${this._listboxFocusOut}
@scroll=${this._listboxScroll}
@keydown=${this._listboxKeydown}
@iron-activate=${preventDefault}
>
${this.editMode
? this._renderPanelsEdit(beforeSpacer)
@@ -869,7 +871,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
border-bottom: 1px solid var(--divider-color);
background-color: var(
--sidebar-menu-button-background-color,
var(--primary-background-color)
inherit
);
font-size: 20px;
align-items: center;

View File

@@ -2,9 +2,12 @@ import type { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { until } from "lit/directives/until";
import { DEFAULT_DOMAIN_ICON, FIXED_DOMAIN_ICONS } from "../common/const";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { entityIcon } from "../data/icons";
import {
DEFAULT_DOMAIN_ICON,
entityIcon,
FALLBACK_DOMAIN_ICONS,
} from "../data/icons";
import type { HomeAssistant } from "../types";
import "./ha-icon";
import "./ha-svg-icon";
@@ -49,7 +52,7 @@ export class HaStateIcon extends LitElement {
return html`
<ha-svg-icon
.path=${FIXED_DOMAIN_ICONS[domain] || DEFAULT_DOMAIN_ICON}
.path=${FALLBACK_DOMAIN_ICONS[domain] || DEFAULT_DOMAIN_ICON}
></ha-svg-icon>
`;
}

View File

@@ -53,6 +53,12 @@ export class HaTextArea extends TextAreaBase {
inset-inline-end: initial !important;
transform-origin: var(--float-start) top;
}
@media only screen and (min-width: 459px) {
:host([mobile-multiline]) .mdc-text-field__input {
white-space: nowrap;
max-height: 16px;
}
}
`,
];
}

View File

@@ -207,6 +207,9 @@ export class HaTextField extends TextFieldBase {
.mdc-text-field__affix--prefix {
color: var(--mdc-text-field-label-ink-color);
}
#helper-text ha-markdown {
display: inline-block;
}
`,
// safari workaround - must be explicit
mainWindow.document.dir === "rtl"

View File

@@ -1,9 +1,13 @@
import { LitElement, html, css } from "lit";
import { property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import type { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-state-icon";
class HaEntityMarker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "entity-id" }) public entityId?: string;
@property({ attribute: "entity-name" }) public entityName?: string;
@@ -12,6 +16,8 @@ class HaEntityMarker extends LitElement {
@property({ attribute: "entity-color" }) public entityColor?: string;
@property({ attribute: "show-icon", type: Boolean }) public showIcon = false;
protected render() {
return html`
<div
@@ -26,7 +32,12 @@ class HaEntityMarker extends LitElement {
"background-image": `url(${this.entityPicture})`,
})}
></div>`
: this.entityName}
: this.showIcon && this.entityId
? html`<ha-state-icon
.hass=${this.hass}
.stateObj=${this.hass?.states[this.entityId]}
></ha-state-icon>`
: this.entityName}
</div>
`;
}

View File

@@ -52,7 +52,7 @@ export interface HaMapPaths {
export interface HaMapEntity {
entity_id: string;
color: string;
label_mode?: "name" | "state";
label_mode?: "name" | "state" | "icon";
name?: string;
focus?: boolean;
}
@@ -523,23 +523,24 @@ export class HaMap extends ReactiveElement {
.join("")
.substr(0, 3));
const entityMarker = document.createElement("ha-entity-marker");
entityMarker.hass = this.hass;
entityMarker.showIcon =
typeof entity !== "string" && entity.label_mode === "icon";
entityMarker.entityId = getEntityId(entity);
entityMarker.entityName = entityName;
entityMarker.entityPicture =
entityPicture && (typeof entity === "string" || !entity.label_mode)
? this.hass.hassUrl(entityPicture)
: "";
if (typeof entity !== "string") {
entityMarker.entityColor = entity.color;
}
// create marker with the icon
const marker = Leaflet.marker([latitude, longitude], {
icon: Leaflet.divIcon({
html: `
<ha-entity-marker
entity-id="${getEntityId(entity)}"
entity-name="${entityName}"
entity-picture="${
entityPicture ? this.hass.hassUrl(entityPicture) : ""
}"
${
typeof entity !== "string"
? `entity-color="${entity.color}"`
: ""
}
></ha-entity-marker>
`,
html: entityMarker,
iconSize: [48, 48],
className: "",
}),

View File

@@ -13,7 +13,10 @@ import { MediaClassBrowserSettings } from "../../data/media-player";
import {
browseLocalMediaPlayer,
removeLocalMedia,
isLocalMediaSourceContentId,
isImageUploadMediaSourceContentId,
} from "../../data/media_source";
import { deleteImage, getIdFromUrl } from "../../data/image_upload";
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
@@ -114,7 +117,7 @@ class DialogMediaManage extends LitElement {
: html`
<ha-button
class="danger"
slot="title"
slot="navigationIcon"
.disabled=${this._deleting}
.label=${this.hass.localize(
`ui.components.media-browser.file_management.${
@@ -207,12 +210,10 @@ class DialogMediaManage extends LitElement {
href="/config/storage"
@click=${this.closeDialog}
>
${this.hass
.localize(
"ui.components.media-browser.file_management.tip_storage_panel"
)
.toLowerCase()}
</a>`,
${this.hass.localize(
"ui.components.media-browser.file_management.tip_storage_panel"
)}</a
>`,
}
)}
</ha-tip>`
@@ -270,7 +271,14 @@ class DialogMediaManage extends LitElement {
try {
await Promise.all(
toDelete.map(async (item) => {
await removeLocalMedia(this.hass, item.media_content_id);
if (isLocalMediaSourceContentId(item.media_content_id)) {
await removeLocalMedia(this.hass, item.media_content_id);
} else if (isImageUploadMediaSourceContentId(item.media_content_id)) {
const media_id = getIdFromUrl(item.media_content_id);
if (media_id) {
await deleteImage(this.hass, media_id);
}
}
this._currentItem = {
...this._currentItem!,
children: this._currentItem!.children!.filter((i) => i !== item),

View File

@@ -85,7 +85,7 @@ class DialogMediaPlayerBrowse extends LitElement {
@opened=${this._dialogOpened}
>
<ha-dialog-header show-border slot="heading">
${this._navigateIds.length > 1
${this._navigateIds.length > (this._params.minimumNavigateLevel ?? 1)
? html`
<ha-icon-button
slot="navigationIcon"

View File

@@ -4,7 +4,10 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { MediaPlayerItem } from "../../data/media-player";
import { isLocalMediaSourceContentId } from "../../data/media_source";
import {
isLocalMediaSourceContentId,
isImageUploadMediaSourceContentId,
} from "../../data/media_source";
import type { HomeAssistant } from "../../types";
import "../ha-svg-icon";
import { showMediaManageDialog } from "./show-media-manage-dialog";
@@ -26,7 +29,11 @@ class MediaManageButton extends LitElement {
protected render() {
if (
!this.currentItem ||
!isLocalMediaSourceContentId(this.currentItem.media_content_id || "")
!(
isLocalMediaSourceContentId(this.currentItem.media_content_id || "") ||
(this.hass!.user?.is_admin &&
isImageUploadMediaSourceContentId(this.currentItem.media_content_id))
)
) {
return nothing;
}

View File

@@ -10,6 +10,7 @@ export interface MediaPlayerBrowseDialogParams {
entityId: string;
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => void;
navigateIds?: MediaPlayerItemId[];
minimumNavigateLevel?: number;
}
export const showMediaBrowserDialog = (

View File

@@ -20,8 +20,8 @@ export class HatGraphNode extends LitElement {
@property({ attribute: false, reflect: true, type: Boolean }) notEnabled =
false;
@property({ attribute: false, reflect: true, type: Boolean }) graphStart =
false;
@property({ attribute: "graph-start", reflect: true, type: Boolean })
graphStart = false;
@property({ type: Boolean, attribute: "nofocus" }) noFocus = false;
@@ -112,7 +112,7 @@ export class HatGraphNode extends LitElement {
var(--hat-graph-node-size) + var(--hat-graph-spacing) + 1px
);
}
:host([graphStart]) {
:host([graph-start]) {
height: calc(var(--hat-graph-node-size) + 2px);
}
:host([track]) {

View File

@@ -91,7 +91,7 @@ export class HatScriptGraph extends LitElement {
}
return html`
<hat-graph-node
graphStart
graph-start
?track=${track}
@focus=${this._selectNode(config, path)}
?active=${this.selected === path}
@@ -354,8 +354,8 @@ export class HatScriptGraph extends LitElement {
></hat-graph-node>
<div
style=${`width: ${NODE_SIZE + SPACING}px;`}
graphStart
graphEnd
graph-start
graph-end
></div>
<div ?track=${trackPass}></div>
<hat-graph-node

View File

@@ -737,18 +737,22 @@ const tryDescribeTrigger = (
? computeStateName(hass.states[trigger.entity_id])
: trigger.entity_id;
let offsetChoice = trigger.offset.startsWith("-") ? "before" : "after";
let offset: string | string[] = trigger.offset.startsWith("-")
? trigger.offset.substring(1).split(":")
: trigger.offset.split(":");
const duration = {
hours: offset.length > 0 ? +offset[0] : 0,
minutes: offset.length > 1 ? +offset[1] : 0,
seconds: offset.length > 2 ? +offset[2] : 0,
};
offset = formatDurationLong(hass.locale, duration);
if (offset === "") {
offsetChoice = "other";
let offsetChoice: string = "other";
let offset: string | string[] = "";
if (trigger.offset) {
offsetChoice = trigger.offset.startsWith("-") ? "before" : "after";
offset = trigger.offset.startsWith("-")
? trigger.offset.substring(1).split(":")
: trigger.offset.split(":");
const duration = {
hours: offset.length > 0 ? +offset[0] : 0,
minutes: offset.length > 1 ? +offset[1] : 0,
seconds: offset.length > 2 ? +offset[2] : 0,
};
offset = formatDurationLong(hass.locale, duration);
if (offset === "") {
offsetChoice = "other";
}
}
return hass.localize(

View File

@@ -1,36 +1,340 @@
import { setHours, setMinutes } from "date-fns";
import type { HassConfig } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
import { formatTime } from "../common/datetime/format_time";
import type { LocalizeFunc } from "../common/translations/localize";
import type { HomeAssistant } from "../types";
import { domainToName } from "./integration";
import type { FrontendLocaleData } from "./translation";
import {
formatDateTime,
formatDateTimeNumeric,
} from "../common/datetime/format_date_time";
import { fileDownload } from "../util/file_download";
export const enum BackupScheduleState {
NEVER = "never",
DAILY = "daily",
MONDAY = "mon",
TUESDAY = "tue",
WEDNESDAY = "wed",
THURSDAY = "thu",
FRIDAY = "fri",
SATURDAY = "sat",
SUNDAY = "sun",
}
export interface BackupConfig {
last_attempted_automatic_backup: string | null;
last_completed_automatic_backup: string | null;
create_backup: {
agent_ids: string[];
include_addons: string[] | null;
include_all_addons: boolean;
include_database: boolean;
include_folders: string[] | null;
name: string | null;
password: string | null;
};
retention: {
copies?: number | null;
days?: number | null;
};
schedule: {
state: BackupScheduleState;
};
}
export interface BackupMutableConfig {
create_backup?: {
agent_ids?: string[];
include_addons?: string[];
include_all_addons?: boolean;
include_database?: boolean;
include_folders?: string[];
name?: string | null;
password?: string | null;
};
retention?: {
copies?: number | null;
days?: number | null;
};
schedule?: BackupScheduleState;
}
export interface BackupAgent {
agent_id: string;
}
export interface BackupContent {
slug: string;
backup_id: string;
date: string;
name: string;
protected: boolean;
size: number;
path: string;
agent_ids?: string[];
failed_agent_ids?: string[];
with_automatic_settings: boolean;
}
export interface BackupData {
backing_up: boolean;
backups: BackupContent[];
addons: BackupAddon[];
database_included: boolean;
folders: string[];
homeassistant_version: string;
homeassistant_included: boolean;
}
export const getBackupDownloadUrl = (slug: string) =>
`/api/backup/download/${slug}`;
export interface BackupAddon {
name: string;
slug: string;
version: string;
}
export const fetchBackupInfo = (hass: HomeAssistant): Promise<BackupData> =>
export interface BackupContentExtended extends BackupContent, BackupData {}
export interface BackupInfo {
backups: BackupContent[];
backing_up: boolean;
}
export interface BackupDetails {
backup: BackupContentExtended;
}
export interface BackupAgentsInfo {
agents: BackupAgent[];
}
export type GenerateBackupParams = {
agent_ids: string[];
include_addons?: string[];
include_all_addons?: boolean;
include_database?: boolean;
include_folders?: string[];
include_homeassistant?: boolean;
name?: string;
password?: string;
};
export type RestoreBackupParams = {
backup_id: string;
agent_id: string;
password?: string;
restore_addons?: string[];
restore_database?: boolean;
restore_folders?: string[];
restore_homeassistant?: boolean;
};
export const fetchBackupConfig = (hass: HomeAssistant) =>
hass.callWS<{ config: BackupConfig }>({ type: "backup/config/info" });
export const updateBackupConfig = (
hass: HomeAssistant,
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 fetchBackupInfo = (hass: HomeAssistant): Promise<BackupInfo> =>
hass.callWS({
type: "backup/info",
});
export const removeBackup = (
export const fetchBackupDetails = (
hass: HomeAssistant,
slug: string
): Promise<void> =>
id: string
): Promise<BackupDetails> =>
hass.callWS({
type: "backup/remove",
slug,
type: "backup/details",
backup_id: id,
});
export const generateBackup = (hass: HomeAssistant): Promise<BackupContent> =>
export const fetchBackupAgentsInfo = (
hass: HomeAssistant
): Promise<BackupAgentsInfo> =>
hass.callWS({
type: "backup/agents/info",
});
export const deleteBackup = (hass: HomeAssistant, id: string): Promise<void> =>
hass.callWS({
type: "backup/delete",
backup_id: id,
});
export const generateBackup = (
hass: HomeAssistant,
params: GenerateBackupParams
): Promise<{ backup_id: string }> =>
hass.callWS({
type: "backup/generate",
...params,
});
export const generateBackupWithAutomaticSettings = (
hass: HomeAssistant
): Promise<void> =>
hass.callWS({
type: "backup/generate_with_automatic_settings",
});
export const restoreBackup = (
hass: HomeAssistant,
params: RestoreBackupParams
): Promise<{ backup_id: string }> =>
hass.callWS({
type: "backup/restore",
...params,
});
export const uploadBackup = async (
hass: HomeAssistant,
file: File,
agent_ids: string[]
): Promise<void> => {
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 resp = await 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[]) => {
const localAgents = agents.filter(
(agent) => agent.split(".")[0] === "backup"
);
return localAgents[0] || agents[0];
};
export const CORE_LOCAL_AGENT = "backup.local";
export const HASSIO_LOCAL_AGENT = "hassio.local";
export const CLOUD_AGENT = "cloud.cloud";
export const isLocalAgent = (agentId: string) =>
[CORE_LOCAL_AGENT, HASSIO_LOCAL_AGENT].includes(agentId);
export const isNetworkMountAgent = (agentId: string) => {
const [domain, name] = agentId.split(".");
return domain === "hassio" && name !== "local";
};
export const computeBackupAgentName = (
localize: LocalizeFunc,
agentId: string,
agentIds?: string[]
) => {
if (isLocalAgent(agentId)) {
return "This system";
}
const [domain, name] = agentId.split(".");
if (isNetworkMountAgent(agentId)) {
return name;
}
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;
return showName ? `${domainName}: ${name}` : domainName;
};
export const compareAgents = (a: string, b: string) => {
const isLocalA = isLocalAgent(a);
const isLocalB = isLocalAgent(b);
const isNetworkMountAgentA = isNetworkMountAgent(a);
const isNetworkMountAgentB = isNetworkMountAgent(b);
const getPriority = (isLocal: boolean, isNetworkMount: boolean) => {
if (isLocal) return 1;
if (isNetworkMount) return 2;
return 3;
};
const priorityA = getPriority(isLocalA, isNetworkMountAgentA);
const priorityB = getPriority(isLocalB, isNetworkMountAgentB);
if (priorityA !== priorityB) {
return priorityA - priorityB;
}
return a.localeCompare(b);
};
export const generateEncryptionKey = () => {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const pattern = "xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx";
let result = "";
const randomArray = new Uint8Array(pattern.length);
crypto.getRandomValues(randomArray);
randomArray.forEach((number, index) => {
result += pattern[index] === "-" ? "-" : chars[number % chars.length];
});
return result;
};
export const generateEmergencyKit = (
hass: HomeAssistant,
encryptionKey: string
) =>
"data:text/plain;charset=utf-8," +
encodeURIComponent(`Home Assistant Backup Emergency Kit
This emergency kit contains your backup encryption key. You need this key
to be able to restore your Home Assistant backups.
Date: ${formatDateTime(new Date(), hass.locale, hass.config)}
Instance:
${hass.config.location_name}
URL:
${hass.auth.data.hassUrl}
Encryption key:
${encryptionKey}
For more information visit: https://www.home-assistant.io/more-info/backup-emergency-kit`);
export const geneateEmergencyKitFileName = (
hass: HomeAssistant,
append?: string
) =>
`home_assistant_backup_emergency_kit_${append ? `${append}_` : ""}${formatDateTimeNumeric(new Date(), hass.locale, hass.config).replace(",", "").replace(" ", "_")}.txt`;
export const downloadEmergencyKit = (
hass: HomeAssistant,
key: string,
appendFileName?: string
) =>
fileDownload(
generateEmergencyKit(hass, key),
geneateEmergencyKitFileName(hass, appendFileName)
);
export const getFormattedBackupTime = memoizeOne(
(locale: FrontendLocaleData, config: HassConfig) => {
const date = setMinutes(setHours(new Date(), 4), 45);
return formatTime(date, locale, config);
}
);

View File

@@ -0,0 +1,77 @@
import type { HomeAssistant } from "../types";
export type BackupManagerState =
| "idle"
| "create_backup"
| "receive_backup"
| "restore_backup";
export type CreateBackupStage =
| "addon_repositories"
| "addons"
| "await_addon_restarts"
| "docker_config"
| "finishing_file"
| "folders"
| "home_assistant"
| "upload_to_agents";
export type CreateBackupState = "completed" | "failed" | "in_progress";
export type ReceiveBackupStage = "receive_file" | "upload_to_agents";
export type ReceiveBackupState = "completed" | "failed" | "in_progress";
export type RestoreBackupStage =
| "addon_repositories"
| "addons"
| "await_addon_restarts"
| "await_home_assistant_restart"
| "check_home_assistant"
| "docker_config"
| "download_from_agent"
| "folders"
| "home_assistant"
| "remove_delta_addons";
export type RestoreBackupState = "completed" | "failed" | "in_progress";
type IdleEvent = {
manager_state: "idle";
};
type CreateBackupEvent = {
manager_state: "create_backup";
stage: CreateBackupStage | null;
state: CreateBackupState;
};
type ReceiveBackupEvent = {
manager_state: "receive_backup";
stage: ReceiveBackupStage | null;
state: ReceiveBackupState;
};
type RestoreBackupEvent = {
manager_state: "restore_backup";
stage: RestoreBackupStage | null;
state: RestoreBackupState;
};
export type ManagerStateEvent =
| IdleEvent
| CreateBackupEvent
| ReceiveBackupEvent
| RestoreBackupEvent;
export const subscribeBackupEvents = (
hass: HomeAssistant,
callback: (event: ManagerStateEvent) => void
) =>
hass.connection.subscribeMessage<ManagerStateEvent>(callback, {
type: "backup/subscribe_events",
});
export const DEFAULT_MANAGER_STATE: ManagerStateEvent = {
manager_state: "idle",
};

View File

@@ -70,18 +70,27 @@ export interface CloudWebhook {
managed?: boolean;
}
export const cloudLogin = (
hass: HomeAssistant,
email: string,
password: string
) =>
interface CloudLoginBase {
hass: HomeAssistant;
email: string;
}
export interface CloudLoginPassword extends CloudLoginBase {
password: string;
}
export interface CloudLoginMFA extends CloudLoginBase {
code: string;
}
export const cloudLogin = ({
hass,
...rest
}: CloudLoginPassword | CloudLoginMFA) =>
hass.callApi<{ success: boolean; cloud_pipeline?: string }>(
"POST",
"cloud/login",
{
email,
password,
}
rest
);
export const cloudLogout = (hass: HomeAssistant) =>

View File

@@ -1,5 +1,6 @@
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";
@@ -105,11 +106,13 @@ export const fetchHassioBackupInfo = async (
);
}
// When called from onboarding we don't have hass
const resp = await fetch(`/api/hassio/backups/${backup}/info`, {
method: "GET",
});
const data = (await resp.json()).data;
return data;
return hassioApiResultExtractor(
await handleFetchPromise(
fetch(`/api/hassio/backups/${backup}/info`, {
method: "GET",
})
)
);
};
export const reloadHassioBackups = async (hass: HomeAssistant) => {
@@ -236,3 +239,26 @@ export const uploadBackup = async (
}
return resp.json();
};
export const restoreBackup = async (
hass: HomeAssistant | undefined,
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),
})
);
}
};

View File

@@ -118,7 +118,9 @@ export const accesspointScan = async (
export const parseAddress = (address: string) => {
const [ip, cidr] = address.split("/");
return { ip, mask: cidrToNetmask(cidr, address.includes(":")) };
const isIPv6 = ip.includes(":");
const mask = cidr ? cidrToNetmask(cidr, isIPv6) : null;
return { ip, mask };
};
export const formatAddress = (ip: string, mask: string) =>

View File

@@ -1,4 +1,51 @@
import {
mdiAccount,
mdiAirFilter,
mdiAlert,
mdiAppleSafari,
mdiBell,
mdiBookmark,
mdiBullhorn,
mdiButtonPointer,
mdiCalendar,
mdiCalendarClock,
mdiChatSleep,
mdiClipboardList,
mdiClock,
mdiCog,
mdiCommentAlert,
mdiCounter,
mdiEye,
mdiFlower,
mdiFormatListBulleted,
mdiFormTextbox,
mdiForumOutline,
mdiGoogleAssistant,
mdiGoogleCirclesCommunities,
mdiHomeAutomation,
mdiImage,
mdiImageFilterFrames,
mdiLightbulb,
mdiMapMarkerRadius,
mdiMicrophoneMessage,
mdiPalette,
mdiRayVertex,
mdiRemote,
mdiRobot,
mdiRobotMower,
mdiRobotVacuum,
mdiRoomService,
mdiScriptText,
mdiSpeakerMessage,
mdiThermostat,
mdiTimerOutline,
mdiToggleSwitch,
mdiWeatherPartlyCloudy,
mdiWhiteBalanceSunny,
} from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { atLeastVersion } from "../common/config/version";
import { computeDomain } from "../common/entity/compute_domain";
import { computeObjectId } from "../common/entity/compute_object_id";
import { computeStateDomain } from "../common/entity/compute_state_domain";
@@ -8,8 +55,69 @@ import type {
EntityRegistryDisplayEntry,
EntityRegistryEntry,
} from "./entity_registry";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { atLeastVersion } from "../common/config/version";
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
/** Icon to use when no icon specified for service. */
export const DEFAULT_SERVICE_ICON = mdiRoomService;
/** Icon to use when no icon specified for domain. */
export const DEFAULT_DOMAIN_ICON = mdiBookmark;
/** Fallback icons for each domain */
export const FALLBACK_DOMAIN_ICONS = {
air_quality: mdiAirFilter,
alert: mdiAlert,
automation: mdiRobot,
calendar: mdiCalendar,
climate: mdiThermostat,
configurator: mdiCog,
conversation: mdiForumOutline,
counter: mdiCounter,
date: mdiCalendar,
datetime: mdiCalendarClock,
demo: mdiHomeAssistant,
device_tracker: mdiAccount,
google_assistant: mdiGoogleAssistant,
group: mdiGoogleCirclesCommunities,
homeassistant: mdiHomeAssistant,
homekit: mdiHomeAutomation,
image_processing: mdiImageFilterFrames,
image: mdiImage,
input_boolean: mdiToggleSwitch,
input_button: mdiButtonPointer,
input_datetime: mdiCalendarClock,
input_number: mdiRayVertex,
input_select: mdiFormatListBulleted,
input_text: mdiFormTextbox,
lawn_mower: mdiRobotMower,
light: mdiLightbulb,
notify: mdiCommentAlert,
number: mdiRayVertex,
persistent_notification: mdiBell,
person: mdiAccount,
plant: mdiFlower,
proximity: mdiAppleSafari,
remote: mdiRemote,
scene: mdiPalette,
schedule: mdiCalendarClock,
script: mdiScriptText,
select: mdiFormatListBulleted,
sensor: mdiEye,
simple_alarm: mdiBell,
siren: mdiBullhorn,
stt: mdiMicrophoneMessage,
sun: mdiWhiteBalanceSunny,
text: mdiFormTextbox,
time: mdiClock,
timer: mdiTimerOutline,
todo: mdiClipboardList,
tts: mdiSpeakerMessage,
vacuum: mdiRobotVacuum,
wake_word: mdiChatSleep,
weather: mdiWeatherPartlyCloudy,
zone: mdiMapMarkerRadius,
};
const resources: {
entity: Record<string, Promise<PlatformIcons>>;

View File

@@ -9,6 +9,7 @@ interface Image {
}
export const URL_PREFIX = "/api/image/serve/";
export const MEDIA_PREFIX = "media-source://image_upload";
export interface ImageMutableParams {
name: string;
@@ -22,6 +23,8 @@ export const getIdFromUrl = (url: string): string | undefined => {
if (idx >= 0) {
id = id.substring(0, idx);
}
} else if (url.startsWith(MEDIA_PREFIX)) {
id = url.substring(MEDIA_PREFIX.length + 1);
}
return id;
};
@@ -77,3 +80,17 @@ export const deleteImage = (hass: HomeAssistant, id: string) =>
type: "image/delete",
image_id: id,
});
export const getImageData = async (url: string) => {
const response = await fetch(url);
if (!response.ok) {
throw new Error(
`Failed to fetch image: ${
response.statusText ? response.statusText : response.status
}`
);
}
return response.blob();
};

View File

@@ -7,8 +7,22 @@ export interface ShowViewConfig {
user?: string;
}
interface LovelaceViewBackgroundConfig {
export interface LovelaceViewBackgroundConfig {
image?: string;
opacity?: number;
size?: "auto" | "cover" | "contain";
alignment?:
| "top left"
| "top center"
| "top right"
| "center left"
| "center"
| "center right"
| "bottom left"
| "bottom center"
| "bottom right";
repeat?: "repeat" | "no-repeat";
attachment?: "scroll" | "fixed";
}
export interface LovelaceBaseViewConfig {

View File

@@ -27,6 +27,9 @@ export const browseLocalMediaPlayer = (
export const isLocalMediaSourceContentId = (mediaId: string) =>
mediaId.startsWith("media-source://media_source");
export const isImageUploadMediaSourceContentId = (mediaId: string) =>
mediaId.startsWith("media-source://image_upload");
export const uploadLocalMedia = async (
hass: HomeAssistant,
media_content_id: string,

View File

@@ -1,7 +1,7 @@
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../types";
const HAS_CUSTOM_PREVIEW = ["template"];
const HAS_CUSTOM_PREVIEW = ["generic_camera", "template"];
export interface GenericPreview {
state: string;

View File

@@ -40,7 +40,7 @@ export const baseActionStruct = object({
enabled: optional(boolean()),
});
const targetStruct = object({
export const targetStruct = object({
entity_id: optional(union([string(), array(string())])),
device_id: optional(union([string(), array(string())])),
area_id: optional(union([string(), array(string())])),

View File

@@ -26,6 +26,7 @@ export type Selector =
| AreaFilterSelector
| AttributeSelector
| BooleanSelector
| ButtonToggleSelector
| ColorRGBSelector
| ColorTempSelector
| ConditionSelector
@@ -68,7 +69,8 @@ export type Selector =
| TTSVoiceSelector
| UiActionSelector
| UiColorSelector
| UiStateContentSelector;
| UiStateContentSelector
| BackupLocationSelector;
export interface ActionSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
@@ -107,6 +109,14 @@ export interface BooleanSelector {
boolean: {} | null;
}
export interface ButtonToggleSelector {
button_toggle: {
options: readonly string[] | readonly SelectOption[];
translation_key?: string;
sort?: boolean;
} | null;
}
export interface ColorRGBSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
color_rgb: {} | null;

View File

@@ -715,11 +715,13 @@ export const getZwaveNodeRawConfigParameter = (
device_id: string,
property: number
): Promise<number> =>
hass.callWS({
type: "zwave_js/get_raw_config_parameter",
device_id,
property,
});
hass
.callWS<{ value: number }>({
type: "zwave_js/get_raw_config_parameter",
device_id,
property,
})
.then((res) => res.value);
export const reinterviewZwaveNode = (
hass: HomeAssistant,

View File

@@ -3,7 +3,7 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-dialog";
import { createCloseHeading } from "../../components/ha-dialog";
import "../../components/ha-formfield";
import "../../components/ha-switch";
import type { HaSwitch } from "../../components/ha-switch";
@@ -52,14 +52,14 @@ class DialogConfigEntrySystemOptions extends LitElement {
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${this.hass.localize(
"ui.dialogs.config_entry_system_options.title",
{
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.dialogs.config_entry_system_options.title", {
integration:
this.hass.localize(
`component.${this._params.entry.domain}.title`
) || this._params.entry.domain,
}
})
)}
>
${this._error ? html` <div class="error">${this._error}</div> ` : ""}

View File

@@ -1,5 +1,6 @@
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { LitElement, html } from "lit";
import type { nothing, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { FlowType } from "../../../data/data_entry_flow";
import type { GenericPreview } from "../../../data/preview";
@@ -11,7 +12,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert";
@customElement("flow-preview-generic")
class FlowPreviewGeneric extends LitElement {
export class FlowPreviewGeneric extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public flowType!: FlowType;
@@ -26,9 +27,9 @@ class FlowPreviewGeneric extends LitElement {
@property({ attribute: false }) public stepData!: Record<string, any>;
@state() private _preview?: HassEntity;
@state() protected _preview?: HassEntity;
@state() private _error?: string;
@state() protected _error?: string;
private _unsub?: Promise<UnsubscribeFunc>;
@@ -46,7 +47,7 @@ class FlowPreviewGeneric extends LitElement {
}
}
protected render() {
protected render(): TemplateResult | typeof nothing {
if (this._error) {
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
}

View File

@@ -0,0 +1,53 @@
import { html, nothing } from "lit";
import { customElement } from "lit/decorators";
import { FlowPreviewGeneric } from "./flow-preview-generic";
import "../../../components/ha-hls-player";
import "../../../components/ha-circular-progress";
@customElement("flow-preview-generic_camera")
class FlowPreviewGenericCamera extends FlowPreviewGeneric {
protected override render() {
if (!this._preview) {
return nothing;
}
if (this._error) {
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
}
const stillUrl = this._preview.attributes.still_url;
const streamUrl = this._preview.attributes.stream_url;
return html` ${stillUrl
? html`<p>Still image:</p>
<p>
<img src=${stillUrl} alt="Still preview" />
</p>`
: ""}
${streamUrl
? html`<p>Stream:</p>
<ha-circular-progress
class="render-spinner"
id="hls-load-spinner"
indeterminate
size="large"
></ha-circular-progress>
<ha-hls-player
autoplay
playsinline
.hass=${this.hass}
.url=${streamUrl}
@load=${this._videoLoaded}
></ha-hls-player>`
: ""}`;
}
private _videoLoaded() {
this.shadowRoot!.getElementById("hls-load-spinner")?.remove();
}
}
declare global {
interface HTMLElementTagNameMap {
"flow-preview-generic_camera": FlowPreviewGenericCamera;
}
}

View File

@@ -50,7 +50,7 @@ export const showConfigFlowDialog = (
return description
? html`
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
<ha-markdown allow-svg breaks .content=${description}></ha-markdown>
`
: step.reason;
},
@@ -71,7 +71,7 @@ export const showConfigFlowDialog = (
);
return description
? html`
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
<ha-markdown allow-svg breaks .content=${description}></ha-markdown>
`
: "";
},
@@ -163,7 +163,7 @@ export const showConfigFlowDialog = (
${description
? html`
<ha-markdown
allowsvg
allow-svg
breaks
.content=${description}
></ha-markdown>
@@ -184,7 +184,7 @@ export const showConfigFlowDialog = (
${description
? html`
<ha-markdown
allowsvg
allow-svg
breaks
.content=${description}
></ha-markdown>
@@ -214,7 +214,7 @@ export const showConfigFlowDialog = (
);
return description
? html`
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
<ha-markdown allow-svg breaks .content=${description}></ha-markdown>
`
: "";
},
@@ -234,7 +234,7 @@ export const showConfigFlowDialog = (
);
return description
? html`
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
<ha-markdown allow-svg breaks .content=${description}></ha-markdown>
`
: "";
},

View File

@@ -61,7 +61,7 @@ export const showOptionsFlowDialog = (
? html`
<ha-markdown
breaks
allowsvg
allow-svg
.content=${description}
></ha-markdown>
`
@@ -85,7 +85,7 @@ export const showOptionsFlowDialog = (
return description
? html`
<ha-markdown
allowsvg
allow-svg
breaks
.content=${description}
></ha-markdown>
@@ -183,7 +183,7 @@ export const showOptionsFlowDialog = (
return description
? html`
<ha-markdown
allowsvg
allow-svg
breaks
.content=${description}
></ha-markdown>
@@ -207,7 +207,7 @@ export const showOptionsFlowDialog = (
return description
? html`
<ha-markdown
allowsvg
allow-svg
breaks
.content=${description}
></ha-markdown>

View File

@@ -51,7 +51,6 @@ class StepFlowAbort extends LitElement {
}
private async _handleMissingCreds() {
this._flowDone();
// Prompt to enter credentials and restart integration setup
showAddApplicationCredentialDialog(this.params.dialogParentElement!, {
selectedDomain: this.domain,
@@ -64,6 +63,7 @@ class StepFlowAbort extends LitElement {
});
},
});
this._flowDone();
}
private _flowDone(): void {

View File

@@ -3,7 +3,7 @@ import Cropper from "cropperjs";
// @ts-ignore
import cropperCss from "cropperjs/dist/cropper.css";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, unsafeCSS } from "lit";
import { css, html, nothing, LitElement, unsafeCSS } from "lit";
import { customElement, property, state, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import "../../components/ha-dialog";
@@ -23,6 +23,8 @@ export class HaImagecropperDialog extends LitElement {
private _cropper?: Cropper;
@state() private _isTargetAspectRatio?: boolean;
public showDialog(params: HaImageCropperDialogParams): void {
this._params = params;
this._open = true;
@@ -33,6 +35,7 @@ export class HaImagecropperDialog extends LitElement {
this._params = undefined;
this._cropper?.destroy();
this._cropper = undefined;
this._isTargetAspectRatio = false;
}
protected updated(changedProperties: PropertyValues) {
@@ -47,6 +50,7 @@ export class HaImagecropperDialog extends LitElement {
dragMode: "move",
minCropBoxWidth: 50,
ready: () => {
this._isTargetAspectRatio = this._checkMatchAspectRatio();
URL.revokeObjectURL(this._image!.src);
},
});
@@ -55,6 +59,25 @@ export class HaImagecropperDialog extends LitElement {
}
}
private _checkMatchAspectRatio(): boolean {
const targetRatio = this._params?.options.aspectRatio;
if (!targetRatio) {
return true;
}
const imageData = this._cropper!.getImageData();
if (imageData.aspectRatio === targetRatio) {
return true;
}
// If the image is not exactly the aspect ratio see if it is within a pixel.
if (imageData.naturalWidth > imageData.naturalHeight) {
const targetHeight = imageData.naturalWidth / targetRatio;
return Math.abs(targetHeight - imageData.naturalHeight) <= 1;
}
const targetWidth = imageData.naturalHeight * targetRatio;
return Math.abs(targetWidth - imageData.naturalWidth) <= 1;
}
protected render(): TemplateResult {
return html`<ha-dialog
@closed=${this.closeDialog}
@@ -72,6 +95,12 @@ export class HaImagecropperDialog extends LitElement {
<mwc-button slot="secondaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.cancel")}
</mwc-button>
${this._isTargetAspectRatio
? html`<mwc-button slot="primaryAction" @click=${this._useOriginal}>
${this.hass.localize("ui.dialogs.image_cropper.use_original")}
</mwc-button>`
: nothing}
<mwc-button slot="primaryAction" @click=${this._cropImage}>
${this.hass.localize("ui.dialogs.image_cropper.crop")}
</mwc-button>
@@ -95,6 +124,11 @@ export class HaImagecropperDialog extends LitElement {
);
}
private _useOriginal() {
this._params!.croppedCallback(this._params!.file);
this.closeDialog();
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,

View File

@@ -87,15 +87,23 @@ export const showDialog = async (
};
}
// Get the focus targets after the dialog closes
LOADED[dialogTag].closedFocusTargets = ancestorsWithProperty(
deepActiveElement(),
FOCUS_TARGET
);
const { state } = mainWindow.history;
// if the same dialog is already open, don't push state
if (addHistory) {
const { history } = mainWindow;
if (history.state?.dialog && !OPEN_DIALOG_STACK.length) {
// theres is a dialog state in history, but no dialogs open
// wait for history.back() to update the state
await new Promise((resolve) => {
setTimeout(resolve);
});
return showDialog(
element,
root,
dialogTag,
dialogParams,
dialogImport,
addHistory
);
}
OPEN_DIALOG_STACK.push({
element,
root,
@@ -105,16 +113,22 @@ export const showDialog = async (
addHistory,
});
const newState = { dialog: dialogTag };
if (state?.dialog) {
// if the dialog is already open, replace the name
mainWindow.history.replaceState(newState, "");
if (history.state?.dialog) {
// if a dialog is already open, replace the name
history.replaceState(newState, "");
} else {
// if the dialog is not open, push a new state so back() will close the dialog
mainWindow.history.replaceState({ ...state, opensDialog: true }, "");
mainWindow.history.pushState(newState, "");
// if a dialog is not open, push a new state so back() will close the dialog
history.replaceState({ ...history.state, opensDialog: true }, "");
history.pushState(newState, "");
}
}
// Get the focus targets after the dialog closes
LOADED[dialogTag].closedFocusTargets = ancestorsWithProperty(
deepActiveElement(),
FOCUS_TARGET
);
const dialogElement = await LOADED[dialogTag].element;
// Append it again so it's the last element in the root,
@@ -125,25 +139,6 @@ export const showDialog = async (
return true;
};
export const showDialogFromHistory = async (dialogTag: string) => {
const dialogState = OPEN_DIALOG_STACK.find(
(state) => state.dialogTag === dialogTag
);
if (dialogState) {
showDialog(
dialogState.element,
dialogState.root,
dialogTag,
dialogState.dialogParams,
dialogState.dialogImport,
false
);
} else {
// remove the dialog from history if already closed
mainWindow.history.back();
}
};
export const closeDialog = async (dialogTag: string): Promise<boolean> => {
if (!(dialogTag in LOADED)) {
return true;
@@ -171,10 +166,23 @@ export const closeLastDialog = async () => {
""
);
}
return closed;
}
return true;
};
const _handleClosed = async (ev: HASSDomEvent<DialogClosedParams>) => {
export const closeAllDialogs = async () => {
for (let i = OPEN_DIALOG_STACK.length - 1; i >= 0; i--) {
// eslint-disable-next-line no-await-in-loop
const closed = await closeDialog(OPEN_DIALOG_STACK[i].dialogTag);
if (!closed) {
return false;
}
}
return true;
};
const _handleClosed = (ev: HASSDomEvent<DialogClosedParams>) => {
// If not closed by navigating back, remove the open state from history
const dialogIndex = OPEN_DIALOG_STACK.findIndex(
(state) => state.dialogTag === ev.detail.dialog
@@ -189,7 +197,8 @@ const _handleClosed = async (ev: HASSDomEvent<DialogClosedParams>) => {
{ dialog: OPEN_DIALOG_STACK[OPEN_DIALOG_STACK.length - 1].dialogTag },
""
);
} else {
} else if (dialogIndex !== -1) {
// if the dialog is the last one and it was indeed open, go back
mainWindow.history.back();
}
}
@@ -216,6 +225,7 @@ export const makeDialogManager = (
};
const _handleClosedFocus = async (ev: HASSDomEvent<DialogClosedParams>) => {
if (!LOADED[ev.detail.dialog]) return;
const closedFocusTargets = LOADED[ev.detail.dialog].closedFocusTargets;
delete LOADED[ev.detail.dialog].closedFocusTargets;
if (!closedFocusTargets) return;

View File

@@ -13,7 +13,6 @@ import {
} from "../../../../common/color/convert-color";
import { fireEvent } from "../../../../common/dom/fire_event";
import { throttle } from "../../../../common/util/throttle";
import "../../../../components/ha-button-toggle-group";
import "../../../../components/ha-hs-color-picker";
import "../../../../components/ha-icon";
import "../../../../components/ha-icon-button-prev";

View File

@@ -213,9 +213,10 @@ class MoreInfoMediaPlayer extends LitElement {
ha-icon-button[action="turn_off"],
ha-icon-button[action="turn_on"] {
margin-inline-end: auto;
margin-right: auto;
margin-left: inherit;
margin-inline-start: inherit;
margin-inline-end: auto;
}
.controls {

View File

@@ -99,6 +99,7 @@ class MoreInfoScript extends LitElement {
${this.hass.localize("ui.card.script.run_script")}
</div>
<ha-service-control
hide-picker
hide-description
.hass=${this.hass}
.value=${this._scriptData}

View File

@@ -12,8 +12,6 @@ import "../../../components/ha-faded";
import "../../../components/ha-formfield";
import "../../../components/ha-markdown";
import "../../../components/ha-settings-row";
import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch";
import { isUnavailableState } from "../../../data/entity";
import type { UpdateEntity } from "../../../data/update";
import {
@@ -136,22 +134,6 @@ class MoreInfoUpdate extends LitElement {
: nothing}
</div>
<div class="footer">
${supportsFeature(this.stateObj, UpdateEntityFeature.BACKUP)
? html`
<ha-settings-row>
<span slot="heading">
${this.hass.localize(
"ui.dialogs.more_info_control.update.create_backup"
)}
</span>
<ha-switch
id="create-backup"
checked
.disabled=${updateIsInstalling(this.stateObj)}
></ha-switch>
</ha-settings-row>
`
: nothing}
<div class="actions">
${this.stateObj.state === BINARY_STATE_OFF &&
this.stateObj.attributes.skipped_version
@@ -224,28 +206,11 @@ class MoreInfoUpdate extends LitElement {
}
}
get _shouldCreateBackup(): boolean | null {
if (!supportsFeature(this.stateObj!, UpdateEntityFeature.BACKUP)) {
return null;
}
const createBackupSwitch = this.shadowRoot?.getElementById(
"create-backup"
) as HaSwitch;
if (createBackupSwitch) {
return createBackupSwitch.checked;
}
return true;
}
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

View File

@@ -338,7 +338,7 @@ export class MoreInfoDialog extends LitElement {
></ha-icon-button>
<ha-button-menu
corner="BOTTOM_END"
menuCorner="END"
menu-corner="END"
slot="actionItems"
@closed=${stopPropagation}
fixed
@@ -426,7 +426,7 @@ export class MoreInfoDialog extends LitElement {
? html`
<ha-button-menu
corner="BOTTOM_END"
menuCorner="END"
menu-corner="END"
slot="actionItems"
@closed=${stopPropagation}
fixed
@@ -545,11 +545,7 @@ export class MoreInfoDialog extends LitElement {
/* Set the top top of the dialog to a fixed position, so it doesnt jump when the content changes size */
--vertical-align-dialog: flex-start;
--dialog-surface-margin-top: 40px;
/* This is needed for the tooltip of the history charts to be positioned correctly */
--dialog-surface-position: static;
--dialog-content-position: static;
--dialog-content-padding: 0;
--chart-base-position: static;
}
.content {

View File

@@ -3,7 +3,6 @@ import type { PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { createSearchParam } from "../../common/url/search-params";
import type { ChartResizeOptions } from "../../components/chart/ha-chart-base";
@@ -77,7 +76,7 @@ export class MoreInfoHistory extends LitElement {
</div>
${__DEMO__
? nothing
: html`<a href=${this._showMoreHref} @click=${this._close}
: html`<a href=${this._showMoreHref}
>${this.hass.localize(
"ui.dialogs.more_info_control.show_more"
)}</a
@@ -244,10 +243,6 @@ export class MoreInfoHistory extends LitElement {
this._setRedrawTimer();
}
private _close(): void {
setTimeout(() => fireEvent(this, "close-dialog"), 500);
}
static styles = css`
.header {
display: flex;

View File

@@ -4,7 +4,6 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { fireEvent } from "../../common/dom/fire_event";
import { createSearchParam } from "../../common/url/search-params";
import "../../panels/logbook/ha-logbook";
import type { HomeAssistant } from "../../types";
@@ -36,7 +35,7 @@ export class MoreInfoLogbook extends LitElement {
<div class="title">
${this.hass.localize("ui.dialogs.more_info_control.logbook")}
</div>
<a href=${this._showMoreHref} @click=${this._close}
<a href=${this._showMoreHref}
>${this.hass.localize("ui.dialogs.more_info_control.show_more")}</a
>
</div>
@@ -67,10 +66,6 @@ export class MoreInfoLogbook extends LitElement {
}
}
private _close(): void {
setTimeout(() => fireEvent(this, "close-dialog"), 500);
}
static get styles() {
return [
css`

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