Compare commits

..

126 Commits

Author SHA1 Message Date
Paul Bottein
29a638c56e Open more info as default action in entity heading badge 2024-09-30 11:08:26 +02:00
Bart Mesuere
b6efedfc8d Improve the accessibility of the default colors used for graphs (#21839)
* Update the first 10 colors to match the Observable10 scheme

* Add darker and lighter variants
2024-09-30 10:59:13 +02:00
Wendelin
23c21a35d8 Fix script rename name placeholder (#22160) 2024-09-30 07:43:59 +00:00
Simon Lamon
9c7324298b Remove floor context (#22143)
* Remove floor context

* Fixup gallery
2024-09-30 09:33:08 +02:00
Matthias Alphart
e92be566a0 Handle falsy value in ha-yaml-editor (object selector) (#22142)
* Handle falsy value in ha-yaml-editor (object selector)

* handle explicit `null`
2024-09-30 09:28:17 +02:00
dependabot[bot]
4e96ad5f28 Bump actions/checkout from 4.1.7 to 4.2.0 (#22159)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 09:07:50 +02:00
renovate[bot]
f64a1500af Update dependency webpack to v5.95.0 (#22150)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-28 21:23:29 +02:00
renovate[bot]
c9e8619c04 Update dependency @codemirror/view to v6.34.0 (#22144)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-28 21:23:21 +02:00
Bram Kragten
7ab1133b45 Implement missing function for password field 2024-09-27 23:42:44 +02:00
Bram Kragten
77abfd3e61 voice setup tweaks 2024-09-27 23:42:09 +02:00
Bram Kragten
d7aaa41aa4 Add missing voice assistant select action logic (#22139) 2024-09-27 14:40:55 -04:00
Aindriú Mac Giolla Eoin
8223f6b155 Update translationMetadata.json - Added Irish language code (#21898)
Added language code for Irish, native name Gaeilge
2024-09-27 18:12:44 +02:00
renovate[bot]
435eae77fa Update dependency rollup to v2.79.2 [SECURITY] (#22071)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-27 15:29:29 +00:00
Paul Bottein
ead54e445f Reuse flatten logic for trigger ids condition (#22136) 2024-09-27 17:18:06 +02:00
Bram Kragten
7ee5db2be5 Bumped version to 20240927.0 2024-09-27 17:15:57 +02:00
Bram Kragten
fef6f0ac94 migrate nested triggers too (#22135) 2024-09-27 15:13:05 +00:00
Bram Kragten
7a60763786 Voice setup feedback (#22134)
* Voice setup feedback

* Update voice-assistant-setup-step-check.ts
2024-09-27 16:56:38 +02:00
Paul Bottein
94e321a364 Add UI support for trigger list (#22133)
* Add UI support for trigger list

* Update gallery

* Fix gallery
2024-09-27 16:56:22 +02:00
Bram Kragten
1c12c2b714 Fix codemirror fold for empty lines (#22130) 2024-09-27 14:18:48 +02:00
Paul Bottein
442a8f11a7 Improve heading card style and add theme variables (#22129)
improve heading card style and add theme variables
2024-09-27 13:45:18 +02:00
Bram Kragten
4e8b58cd6c Add password field element (#22121)
* Add password field element

* Update ha-password-field.ts
2024-09-27 12:34:28 +02:00
Paul Bottein
a92dab46c2 Allow different types of heading badges (#22109)
* Allow different type of heading item

* Update editor

* Migrate entities to items

* Rename support for string entity

* Refactor

* Rename to badges and add error state

* Update font weight

* Feedback

* Feedback
2024-09-27 12:33:15 +02:00
Joakim Sørensen
468660d235 Adjust username handling in the cloud panel register and login flows (#22118)
* Use lowercase when registering

* Fallback to lowercase username if usernotfound is recieved

* Adjust resend

* handle reset password

* limit with else

* return early
2024-09-27 12:31:48 +02:00
Wendelin
c721afa137 Fix matter device actions (#22117)
* Fix matter device actions when matter integration loads forever

* Fix matter device-actions types path

* Move getMatterDeviceActions inside getDeviceActions in device page
2024-09-27 09:37:07 +00:00
Paul Bottein
ac9654c1de Add heading card when creating a new view (#22123) 2024-09-27 09:19:19 +00:00
Wendelin
570ad38bac Fix automation trigger condition and triggers description (#22122)
* Fix config.triggers in automation-contition-trigger

* Fix config.triggers for automation triggers description
2024-09-27 09:10:33 +00:00
Erik Montnemery
e778a9aa1d Improve statistics issues (#22110) 2024-09-27 11:05:30 +02:00
selvalt7
49576189af Use localizeValue in ha-form-expandable and ha-form-grid (#22114)
Pass localizeValue to ha-form-expandable and ha-form-grid
2024-09-27 10:00:36 +02:00
Bram Kragten
5d71d4c0a1 Bumped version to 20240926.0 2024-09-26 18:26:20 +02:00
Bram Kragten
d334b1ca7b Update voice-assistant-setup-step-update.ts 2024-09-26 18:25:30 +02:00
Wendelin
5551e98388 Add no IP found message to ping a matter device (#22103) 2024-09-26 18:22:28 +02:00
Bram Kragten
59945cb2f8 Add statistic id to statistic issue fix messages (#22104)
* Add statistic id to fix messages

* revert state class check, as it will be solved in another way
2024-09-26 18:07:54 +02:00
Erik Montnemery
500bc959f0 Improve translation strings for statistic issues (#22100) 2024-09-26 16:46:35 +02:00
Bram Kragten
deece20206 Include extended_pan_id when commissioning matter (#22099) 2024-09-26 16:46:15 +02:00
Bram Kragten
fc8945be60 Flatten fields in sections in developer tools actions (#22096)
* flatten fields in sections in developer tools actions

* Update src/panels/developer-tools/action/developer-tools-action.ts

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2024-09-26 12:31:53 +00:00
Bram Kragten
3fbd5f07a9 Fix dialog box callback order (#22097)
* Fix dialog box callback order

* Update dialog-box.ts
2024-09-26 14:17:28 +02:00
Wendelin
ff9af2f980 Fix delete appearance chip (#22098)
* Add filter option to ha-sortable

* Filter chips remove buttons from dragging in ha-entity-state-content-picker
2024-09-26 14:11:34 +02:00
Paul Bottein
62cba99491 Don't use ha-card in card-condition-editor (#22085) 2024-09-26 12:15:58 +02:00
Wendelin
5a5005c09c Fix matter commissioning wording and add prevent misuse alert (#22083)
* Fix matter commissioning wording and add prevent misuse alert

* Update src/translations/en.json

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

* Add small misuese prevent note for matter-commissioning dialog

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2024-09-26 12:15:03 +02:00
Wendelin
dd179e1f4e Add seperator to dialog-repairs-issue-subtitle (#22095) 2024-09-26 12:12:10 +02:00
Paul Bottein
27bdf80168 Fix automation drag and drop (#22093) 2024-09-26 07:59:48 +00:00
renovate[bot]
f70ce7491a Update dependency @rollup/plugin-node-resolve to v15.2.4 (#22092)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-26 09:48:41 +02:00
Jan Rieger
9f17f6a8cf Fix typo (#22086) 2024-09-25 18:02:34 +00:00
Paul Bottein
4e51c7cf96 Use callback instead of changing nested config with sub editor (#22081)
Use callback instead of changing nested config
2024-09-25 16:47:38 +02:00
Bram Kragten
291c026da0 Bumped version to 20240925.0 2024-09-25 16:47:19 +02:00
Bram Kragten
dd88d8633f Optimize helpers filtering (#22080) 2024-09-25 16:41:12 +02:00
Wendelin
254ee8568b Add integration name information to repairs (#22006)
* Add integration name to repairs

* Improve dialog-repairs-issue aria and translations

* Fix type in dialog-repairs-issue

* Remove unused slots in dialog-repairs-issue

* Fix ha-config-repairs avoid nested css

* Fix ha-config-repairs to use ha-md-list

* Add subtitle slot to ha-dialog-header

* Move close icon to left in dialog-data-entry-flow

* Move severity and reportedBy to dialog subtitle in repair-dialog

* Add md buttons to dialog-repairs-issue

* Revert dialog-repairs-issue to use normal ha-buttons

* Revert dialog-entry-flow close icon position

* Improve buttons for dialog-repairs-issue

* Add subtitle to all show-dialog-repair-flow headers

* Fix integration names for repair dialogs

* Fix subtitle title repair dialogs

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2024-09-25 16:12:45 +02:00
Paul Bottein
cd631e8693 Move sub element editor inside hui-element-editor. (#22079)
* Move sub element editor into hui-element-editor

* Migrate feature editor

* Migrate feature editor

* Simplify context
2024-09-25 16:05:41 +02:00
Bram Kragten
765812331b Allow to fix statistic issue from repairs (#22055)
* Allow to fix statistic issue from repairs

* clean up, add names

* Update src/translations/en.json

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>

* address review

* Update src/translations/en.json

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>

---------

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2024-09-25 15:55:49 +02:00
Paul Bottein
7462f8fbe3 Fix entity row editor (#22078) 2024-09-25 15:42:29 +02:00
Bram Kragten
dc940f248c Migrate trigger platform key to trigger (#22054)
* Migrate trigger platform key to trigger

* fix gallery configs

* Update ha-automation-editor.ts

* migrate device automation triggers
2024-09-25 14:20:27 +02:00
Miguel Palhas
2793ca65cd Adds highlight on current indentation mark to code editor (#21972)
* Adds highlight on current indentation mark to code editor

* code review
2024-09-25 11:35:47 +00:00
Miguel Palhas
e687ddab21 Indent-based folds for YAML editor (#21966)
* Indent-based folds for YAML editor

* adding compartment

* code review
2024-09-25 13:15:02 +02:00
karwosts
4bd27e5055 Add detail to the device+entity_id rename dialog (#21952) 2024-09-25 13:08:39 +02:00
Paul Bottein
c6e2e07286 Use YAML editor in card/badge editor (#22075) 2024-09-25 10:59:39 +02:00
Paul Bottein
e77508b8a8 Create ha-divider and use it inside color picker (#22074)
* Create ha-divider and use it inside color picker

* rename divider
2024-09-25 08:59:00 +00:00
Bram Kragten
a5db44a167 Fix initial automation config (#22073) 2024-09-25 08:24:06 +00:00
Bram Kragten
265bbfc95d Triggers doesn't have to be an array, fix flattenTriggers (#22072)
Triggers doesnt have to be an array, fix flattenTriggers
2024-09-25 08:17:37 +00:00
Bram Kragten
305cecb213 Add MVP voice assist flow (#22061)
* Add MVP voice assist flow

* filter on supported features

* check for unavailable

* Update step-flow-create-entry.ts
2024-09-24 20:38:00 +02:00
Paul Bottein
813feff12e Add color option to heading entities (#22068)
* Add uncolored option

* Allow to color icon based on state or custom color

* Use text color for inactive color

* Rename uncolored to none

* Add helper

* Update wording
2024-09-24 20:14:03 +02:00
Bram Kragten
cbce6f633f Migrate base automation config to plurals (#22053)
* Migrate base automation config to plurals

* revert

* Update hat-script-graph.ts

* Make traces work with both new and old config

* Adjust validateConfig
2024-09-24 20:03:53 +02:00
Bram Kragten
1bbf45d35e Remove min width from alert dialog (#22069) 2024-09-24 16:16:07 +00:00
Paul Bottein
76e53e9738 Add more config option to heading entity element (#22063)
Add show state and show icon to heading entity
2024-09-24 18:15:36 +02:00
Paul Bottein
c30e4a6935 Add visibility option to heading entities (#22064)
* Add visibility option to heading entity

* Fix types
2024-09-24 17:12:04 +02:00
Paul Bottein
c4a700a55c Improve element editor and migrate heading-entity editor (#22034)
* Extract load config element

* Improve error by using ha-alert

* Create hui-hase-editor

* Migrate heading entity form to its own editor

* Rename editor

* Rename

* Rename

* Move heading entity to its own component

* Fix default action for heading entity
2024-09-24 11:17:29 +02:00
Raj Laud
a759767d79 Update media-player.ts to display artist name as backup secondary title (#22039)
* Update media-player.ts to display artist name for playlist if playlist name unavailable

* Run prettier
2024-09-23 16:08:18 +00:00
renovate[bot]
7f868c8140 Update dependency eslint to v8.57.1 (#22033)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-23 16:04:59 +00:00
Paul Bottein
f7f37c24e2 Filter selected entities in entities picker using includeEntities property (#22059)
* Filter selected entities in entities picker using includeEntities property

* Don't ignore other property when using include entities
2024-09-23 15:55:13 +00:00
Simon Lamon
be02a8869f Fix lint failures on CI (#21986)
Add ignore
2024-09-23 15:50:43 +00:00
Wendelin
3a9f09cb47 Migrate dialog restart to ha-md-dialog (#22032)
* Fix ha-md-dialog for iOS 12

* Fix ha-md-dialog polyfill loading

* Fix ha-md-dialog open prop

* Fix multiple polyfill loads in ha-md-dialog

* Migrate dialog-restart to ha-md-dialog

* Fix dialog-restart to use ha-md-list

* Fix dialog opens dialog for ha-md-dialog
2024-09-23 15:18:00 +00:00
karwosts
0c2a9d85e0 Better handling of multiple entities in numeric-state condition (#22021)
* Better handling of multiple entities in numeric-state condition

* update translations, fix infinite render loop in form
2024-09-23 15:35:45 +02:00
Simon Lamon
e72356033c Handle url error better when invalid blueprint url is provided (#21778)
* Encode spaces again

* Prettier

* Update src/panels/my/ha-panel-my.ts

* Remove the specific contents

* Remove the error keys, assign error immediately

* Revert "Remove the error keys, assign error immediately"

This reverts commit 27381ff250.
2024-09-23 15:28:57 +02:00
Simon Lamon
3c48559df6 Move patches in dependency section (#22050)
* Move patches in dependency section

* yarn lock
2024-09-23 15:27:46 +02:00
Wendelin
f36d68c677 Fix delete entity alias (#22058)
Fix aliasChanged to save deleted in entity-voice-settings
2024-09-23 15:18:50 +02:00
Yosi Levy
af46b8221e RTL fixes (#22060) 2024-09-23 13:16:58 +02:00
Paul Bottein
d25f72524b Migrate title section to heading (#22017)
* Remove title from UI

* Migrate section title to heading card

* Remove title from edit section dialog

* Update src/panels/lovelace/views/hui-sections-view.ts

* Simplify delete section dialog
2024-09-23 09:54:20 +02:00
dependabot[bot]
0840d8a10e Bump actions/setup-node from 4.0.3 to 4.0.4 (#22057)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-23 09:37:25 +02:00
renovate[bot]
597bf5def0 Update dependency @octokit/plugin-retry to v7.1.2 (#22047)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-22 18:35:14 +02:00
renovate[bot]
3478bd309b Update dependency date-fns to v4.1.0 (#22037)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-21 09:28:08 +02:00
renovate[bot]
64b8b7658d Update dependency @codemirror/commands to v6.6.2 (#22038)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-21 09:27:31 +02:00
renovate[bot]
a1af8718a0 Update dependency @material/web to v2.2.0 (#22041)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-21 09:25:24 +02:00
renovate[bot]
fd9e2b647d Lock file maintenance (#22027)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-19 20:48:19 +02:00
karwosts
caee4ba7bc Load defaults in script more-info (#22014) 2024-09-19 20:01:57 +02:00
karwosts
915036006d Fixes for trace viewer for nested triggers feature (#21765) 2024-09-19 19:48:20 +02:00
renovate[bot]
48887f2066 Update dependency sinon to v19 (#21988)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-19 19:27:41 +02:00
renovate[bot]
68d9ce7923 Update vaadinWebComponents monorepo to v24.4.9 (#21970)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-19 19:26:56 +02:00
renovate[bot]
a36f3c8fb1 Update dependency date-fns to v4 (#22028)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-19 19:25:15 +02:00
renovate[bot]
4dfadea9e9 Update dependency babel-loader to v9.2.1 (#22031)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-19 19:24:48 +02:00
Wendelin
71dc26edab Migrate dialog-light-color-favorite to ha-md-dialog (#22013)
* Migrate dialog-light-color-favorite to ha-md-dialog

* Add aria-label to dialog-light-color-favorite

* Add mobile dialog from bottom animation to dialog-light-color-favorite
2024-09-19 18:28:13 +02:00
Wendelin
f260c95add Add default value to zwave config params (#21990)
* Add default value to zwave config params

* Remove unused ha-switch from zwave node config

* Small fix of duplicate code in zwave node config
2024-09-19 13:17:45 +02:00
Paul Bottein
dc6f1efffb Add badges to section demo (#22029) 2024-09-19 12:01:59 +02:00
Paul Bottein
b7763882f4 Add Heading card (#22008)
* Add header card

* Rename to heading card

* Add heading entities

* Add editor for entities

* Remove unused property

* Fix margin and gap

* Improve content and entities container

* Fix no entities displayed

* Cache form to not loose state

* Use style

* Fix type

* Add support for string entities

* Add tap action support to entities

* Move expandable outside of entities editor

* Fix double processing
2024-09-19 10:46:20 +02:00
Paul Bottein
7de5c46f14 Improve card features editor (#22023)
* Move expansion panel outside of feature component

* Cache main form and feature form
2024-09-19 09:52:59 +02:00
Bram Kragten
5920efa2b2 Add preferred thread credentials to matter external commission (#22022) 2024-09-19 09:34:03 +02:00
Simon Lamon
d2194d55f9 Rename ha-button-menu-new into ha-md-button-menu (#22016)
* ha-button-menu-new => ha-md-button-menu

* linting
2024-09-18 12:02:15 +00:00
Simon Lamon
c0043af4c9 Rename ha-list-new into ha-md-list (#22015)
ha-list-new => ha-md-list
2024-09-18 08:28:05 +00:00
Bram Kragten
dcf763438b Use issue placeholders in issue repair flow, show break warning in re… (#21959)
Use issue placeholders in issue repair flow, show break warning in repair flow
2024-09-18 10:18:00 +02:00
Wendelin
858a00e28c Migrate dialog-box to ha-md-dialog (#22007)
* Migrate dialog-box to ha-md-dialog

* Add aria-labelby to dialog-box

* Add ids for dialog-box aria content
2024-09-18 09:24:27 +02:00
renovate[bot]
ab407e8274 Update Yarn to v4.5.0 (#22012)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-17 18:14:13 +02:00
renovate[bot]
14f96a6262 Update dependency @codemirror/autocomplete to v6.18.1 (#22011)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-17 17:50:01 +02:00
Paulus Schoutsen
2b33c70e04 Ensure device info categories notify, event and assist always show (#21994)
Currently we have the entity category always win. However, we have some functional categories on a device, notify, event, assist. It would be good to always show these togehter.
2024-09-17 09:37:45 +02:00
akloeckner
717443e2d6 Fix typo in ha-selector-color-rgb.ts (#22001) 2024-09-17 04:20:56 +00:00
renovate[bot]
2aba9099a0 Update dependency ua-parser-js to v1.0.39 (#22002)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-17 06:09:54 +02:00
karwosts
3079f126a8 Improve robustness of automation editor description error handling (#21993) 2024-09-16 17:55:16 +02:00
Bram Kragten
1cdfb746bf Optimize entities config performance (#21974)
* Optimize entities config performance

* review
2024-09-16 12:40:52 +00:00
renovate[bot]
39a1844991 Update dependency eslint-plugin-unused-imports to v4.1.4 (#21992)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-16 14:27:50 +02:00
Wendelin
9e4dc0d39e Migrate add/edit resources dialog to @material/web (#21933)
* Remove dashboard resources options from advanced mode

* Add ha-dialog-new, use it for dashboard resources

* Add ha-dialog-new shake; Move resources delete to table

* Improve ha-dialog-new, resource-detail

* Rename ha-dialog-new to ha-md-dialog

* Update src/panels/config/lovelace/resources/dialog-lovelace-resource-detail.ts

Fix dialogClosed method naming

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

* Add ha-md-dialog polyfill

* Fix ha-md-dialog for iOS 12

* Fix ha-md-dialog polyfill loading

* Fix ha-md-dialog open prop

* Fix ha-md-dialog legacy loading

* Improve ha-md-dialog legacy loading

* Fix multiple polyfill loads in ha-md-dialog

* Fix polyfill handleOpen in ha-md-dialog

* Improve polyfill handleOpen in ha-md-dialog

* Improve polyfill handleOpen ordering in ha-md-dialog

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2024-09-16 12:27:13 +00:00
Wendelin
ab91a4b814 Fix onboarding with 0 found integrations (#21977)
* Add onboarding 0 integrations fallback page

* Add translations to onboarding all set

* Migrate mwc to ha-button in onboarding-integrations
2024-09-16 13:19:25 +02:00
renovate[bot]
ca66c02fb3 Update dependency husky to v9.1.6 (#21983)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-15 20:18:08 +02:00
renovate[bot]
97bb052d71 Update dependency sinon to v18.0.1 (#21981)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-14 14:55:21 +02:00
Wendelin
137bb473c0 Fix user change password autofill (#21975)
* Fix user change password autofill

* Fix user change password new password input
2024-09-13 17:17:40 +02:00
Miguel Palhas
326b57f91b Fixes text input icons color in dark mode (#21971) 2024-09-13 11:43:59 +00:00
Paul Bottein
32feab6a70 Add floors to hass (#21960) 2024-09-13 11:07:04 +02:00
Martin Dybal
68a0d04f04 Added hold and double tap actions for tile card icon (#21947) 2024-09-13 09:59:04 +02:00
renovate[bot]
9078ab4026 Update dependency @bundle-stats/plugin-webpack-filter to v4.15.1 (#21968)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-13 06:24:57 +02:00
renovate[bot]
8605684906 Update dependency typescript to v5.6.2 (#21963)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-12 18:53:28 +02:00
AlCalzone
9f17d17d6e Z-Wave JS: Mention the ability to select which security keys to grant (#21958) 2024-09-12 18:51:22 +02:00
Reuben
ba5f176d52 Capitalise ha-relative-time in state-display (#21949)
This matches the capitalisation applied to hui-timestamp-display, and state-info displays
2024-09-12 18:51:15 +02:00
ildar170975
7115d14699 Add padding to bottom of logbook in device page (#21913)
Update ha-config-device-page.ts
2024-09-12 18:51:06 +02:00
dependabot[bot]
23e37daff3 Bump express from 4.19.2 to 4.20.0 (#21956)
Bumps [express](https://github.com/expressjs/express) from 4.19.2 to 4.20.0.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.20.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-11 18:39:36 +00:00
renovate[bot]
ed6c2dfe39 Update dependency marked to v14.1.2 (#21955)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-11 20:26:42 +02:00
Simon Lamon
b48a28f2a6 Fix "unknown" traces in design gallery (#21942) 2024-09-10 22:04:10 +02:00
renovate[bot]
3166fec7db Update dependency eslint-plugin-lit to v1.15.0 (#21940)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-10 17:29:25 +02:00
karwosts
1a67bd0414 Fix script more-info when entity_id != unique_id (#21880)
* Fix script more-info when entity_id != unique_id

* Update src/dialogs/more-info/controls/more-info-script.ts

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

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2024-09-10 09:35:40 +02:00
Paulus Schoutsen
d34c43e292 Add assist_satellite to Assist entities array (#21795) 2024-09-10 06:22:54 +02:00
karwosts
c7cfbb5b6c Fix service advanced options UI (#21925) 2024-09-09 17:19:35 +02:00
257 changed files with 8957 additions and 4052 deletions

View File

@@ -21,12 +21,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.0
with:
ref: dev
- name: Setup Node
uses: actions/setup-node@v4.0.3
uses: actions/setup-node@v4.0.4
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -57,12 +57,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.0
with:
ref: master
- name: Setup Node
uses: actions/setup-node@v4.0.3
uses: actions/setup-node@v4.0.4
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -24,9 +24,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.0
- name: Setup Node
uses: actions/setup-node@v4.0.3
uses: actions/setup-node@v4.0.4
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -58,9 +58,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.0
- name: Setup Node
uses: actions/setup-node@v4.0.3
uses: actions/setup-node@v4.0.4
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -76,9 +76,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.0
- name: Setup Node
uses: actions/setup-node@v4.0.3
uses: actions/setup-node@v4.0.4
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -100,9 +100,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.0
- name: Setup Node
uses: actions/setup-node@v4.0.3
uses: actions/setup-node@v4.0.4
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.0
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.

View File

@@ -22,12 +22,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.0
with:
ref: dev
- name: Setup Node
uses: actions/setup-node@v4.0.3
uses: actions/setup-node@v4.0.4
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -58,12 +58,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.0
with:
ref: master
- name: Setup Node
uses: actions/setup-node@v4.0.3
uses: actions/setup-node@v4.0.4
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -16,10 +16,10 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.0
- name: Setup Node
uses: actions/setup-node@v4.0.3
uses: actions/setup-node@v4.0.4
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -21,10 +21,10 @@ jobs:
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.0
- name: Setup Node
uses: actions/setup-node@v4.0.3
uses: actions/setup-node@v4.0.4
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -20,7 +20,7 @@ jobs:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.0
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5
@@ -28,7 +28,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@v4.0.3
uses: actions/setup-node@v4.0.4
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -23,7 +23,7 @@ jobs:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.0
- name: Verify version
uses: home-assistant/actions/helpers/verify-version@master
@@ -34,7 +34,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@v4.0.3
uses: actions/setup-node@v4.0.4
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.0
- name: Upload Translations
run: |

File diff suppressed because one or more lines are too long

View File

@@ -6,4 +6,4 @@ enableGlobalCache: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.4.1.cjs
yarnPath: .yarn/releases/yarn-4.5.0.cjs

View File

@@ -60,6 +60,12 @@ function copyPolyfills(staticDir) {
npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js.map"),
staticPath("polyfills/")
);
// dialog-polyfill css
copyFileDir(
npmPath("dialog-polyfill/dialog-polyfill.css"),
staticPath("polyfills/")
);
}
function copyLoaderJS(staticDir) {

View File

@@ -139,7 +139,7 @@
</p>
</div>
<div class="section-header">Wat does Home Assistant Cast do?</div>
<div class="section-header">What does Home Assistant Cast do?</div>
<div class="card-content">
<p>
Home Assistant Cast is a receiver application for the Chromecast. When

View File

@@ -36,6 +36,7 @@ import { HassElement } from "../../../../src/state/hass-element";
import { castContext } from "../cast_context";
import "./hc-launch-screen";
import { getPanelTitleFromUrlPath } from "../../../../src/data/panel";
import { checkLovelaceConfig } from "../../../../src/panels/lovelace/common/check-lovelace-config";
const DEFAULT_CONFIG: LovelaceDashboardStrategyConfig = {
strategy: {
@@ -365,7 +366,9 @@ export class HcMain extends HassElement {
this._urlPath || "lovelace"
);
castContext.setApplicationState(title || "");
this._lovelaceConfig = lovelaceConfig;
this._lovelaceConfig = checkLovelaceConfig(
lovelaceConfig
) as LovelaceConfig;
}
private _handleShowDemo(_msg: ShowDemoMessage) {

View File

@@ -111,9 +111,37 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
friendly_name: "Living room Temperature",
},
},
"sensor.outdoor_temperature": {
entity_id: "sensor.outdoor_temperature",
state: "10.5",
attributes: {
state_class: "measurement",
unit_of_measurement: "°C",
device_class: "temperature",
friendly_name: "Outdoor temperature",
},
},
"sensor.outdoor_humidity": {
entity_id: "sensor.outdoor_humidity",
state: "70.4",
attributes: {
state_class: "measurement",
unit_of_measurement: "%",
device_class: "humidity",
friendly_name: "Outdoor humidity",
},
},
"device_tracker.car": {
entity_id: "sensor.outdoor_humidity",
state: "not_home",
attributes: {
friendly_name: "Car",
icon: "mdi:car",
},
},
"media_player.living_room_nest_mini": {
entity_id: "media_player.living_room_nest_mini",
state: "on",
state: "playing",
attributes: {
device_class: "speaker",
volume_level: 0.18,

View File

@@ -9,6 +9,22 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
title: isFrontpageEmbed ? "Home Assistant" : "Demo",
path: "home",
icon: "mdi:home-assistant",
badges: [
{
type: "entity",
entity: "sensor.outdoor_temperature",
color: "red",
},
{
type: "entity",
entity: "sensor.outdoor_humidity",
color: "indigo",
},
{
type: "entity",
entity: "device_tracker.car",
},
],
sections: [
...(isFrontpageEmbed
? []

9
demo/src/stubs/config.ts Normal file
View File

@@ -0,0 +1,9 @@
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockConfig = (hass: MockHomeAssistant) => {
hass.mockWS("validate_config", () => ({
actions: { valid: true },
conditions: { valid: true },
triggers: { valid: true },
}));
};

6
demo/src/stubs/tags.ts Normal file
View File

@@ -0,0 +1,6 @@
import { Tag } from "../../../src/data/tag";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockTags = (hass: MockHomeAssistant) => {
hass.mockWS("tag/list", () => [{ id: "my-tag", name: "My Tag" }] as Tag[]);
};

View File

@@ -217,22 +217,22 @@ export const basicTrace: DemoTrace = {
id: "1615419646544",
alias: "Ensure Party mode",
description: "",
trigger: [
triggers: [
{
platform: "state",
trigger: "state",
entity_id: "input_boolean.toggle_1",
},
],
condition: [
conditions: [
{
condition: "template",
alias: "Test if Paulus is home",
value_template: "{{ true }}",
},
],
action: [
actions: [
{
service: "input_boolean.toggle",
action: "input_boolean.toggle",
target: {
entity_id: "input_boolean.toggle_4",
},
@@ -268,7 +268,7 @@ export const basicTrace: DemoTrace = {
],
default: [
{
service: "input_boolean.toggle",
action: "input_boolean.toggle",
alias: "Toggle 2",
target: {
entity_id: "input_boolean.toggle_2",
@@ -277,7 +277,7 @@ export const basicTrace: DemoTrace = {
],
},
{
service: "input_boolean.toggle",
action: "input_boolean.toggle",
target: {
entity_id: "input_boolean.toggle_4",
},

View File

@@ -31,8 +31,8 @@ export const mockDemoTrace = (
],
},
config: {
trigger: [],
action: [],
triggers: [],
actions: [],
},
context: {
id: "abcd",

View File

@@ -133,17 +133,17 @@ export const motionLightTrace: DemoTrace = {
config: {
mode: "restart",
max_exceeded: "silent",
trigger: [
triggers: [
{
platform: "state",
trigger: "state",
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
from: "off",
to: "on",
},
],
action: [
actions: [
{
service: "light.turn_on",
action: "light.turn_on",
target: {
entity_id: "light.elgato_key_light_air",
},
@@ -162,7 +162,7 @@ export const motionLightTrace: DemoTrace = {
delay: 0,
},
{
service: "light.turn_off",
action: "light.turn_off",
target: {
entity_id: "light.elgato_key_light_air",
},

View File

@@ -48,7 +48,7 @@ const ACTIONS = [
{
wait_for_trigger: [
{
platform: "state",
trigger: "state",
entity_id: "input_boolean.toggle_1",
},
],
@@ -121,7 +121,7 @@ const ACTIONS = [
];
const initialAction: Action = {
service: "light.turn_on",
action: "light.turn_on",
target: {
entity_id: "light.kitchen",
},
@@ -142,7 +142,7 @@ export class DemoAutomationDescribeAction extends LitElement {
<div class="action">
<span>
${this._action
? describeAction(this.hass, [], [], [], this._action)
? describeAction(this.hass, [], [], this._action)
: "<invalid YAML>"}
</span>
<ha-yaml-editor
@@ -155,7 +155,7 @@ export class DemoAutomationDescribeAction extends LitElement {
${ACTIONS.map(
(conf) => html`
<div class="action">
<span>${describeAction(this.hass, [], [], [], conf as any)}</span>
<span>${describeAction(this.hass, [], [], conf as any)}</span>
<pre>${dump(conf)}</pre>
</div>
`

View File

@@ -22,46 +22,52 @@ const ENTITIES = [
];
const triggers = [
{ platform: "state", entity_id: "light.kitchen", from: "off", to: "on" },
{ platform: "mqtt" },
{ trigger: "state", entity_id: "light.kitchen", from: "off", to: "on" },
{ trigger: "mqtt" },
{
platform: "geo_location",
trigger: "geo_location",
source: "test_source",
zone: "zone.home",
event: "enter",
},
{ platform: "homeassistant", event: "start" },
{ trigger: "homeassistant", event: "start" },
{
platform: "numeric_state",
trigger: "numeric_state",
entity_id: "light.kitchen",
attribute: "brightness",
below: 80,
above: 20,
},
{ platform: "sun", event: "sunset" },
{ platform: "time_pattern" },
{ platform: "time_pattern", hours: "*", minutes: "/5", seconds: "10" },
{ platform: "webhook" },
{ platform: "persistent_notification" },
{ trigger: "sun", event: "sunset" },
{ trigger: "time_pattern" },
{ trigger: "time_pattern", hours: "*", minutes: "/5", seconds: "10" },
{ trigger: "webhook" },
{ trigger: "persistent_notification" },
{
platform: "zone",
trigger: "zone",
entity_id: "person.person",
zone: "zone.home",
event: "enter",
},
{ platform: "tag" },
{ platform: "time", at: "15:32" },
{ platform: "template" },
{ platform: "conversation", command: "Turn on the lights" },
{ trigger: "tag" },
{ trigger: "time", at: "15:32" },
{ trigger: "template" },
{ trigger: "conversation", command: "Turn on the lights" },
{
platform: "conversation",
trigger: "conversation",
command: ["Turn on the lights", "Turn the lights on"],
},
{ platform: "event", event_type: "homeassistant_started" },
{ trigger: "event", event_type: "homeassistant_started" },
{
triggers: [
{ trigger: "state", entity_id: "light.kitchen", to: "on" },
{ trigger: "state", entity_id: "light.kitchen", to: "off" },
],
},
];
const initialTrigger: Trigger = {
platform: "state",
trigger: "state",
entity_id: "light.kitchen",
};

View File

@@ -8,6 +8,9 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
import { mockConfig } from "../../../../demo/src/stubs/config";
import { mockTags } from "../../../../demo/src/stubs/tags";
import { mockAuth } from "../../../../demo/src/stubs/auth";
import type { Trigger } from "../../../../src/data/automation";
import { HaGeolocationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-geo_location";
import { HaEventTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-event";
@@ -26,6 +29,7 @@ import { HaStateTrigger } from "../../../../src/panels/config/automation/trigger
import { HaMQTTTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt";
import "../../../../src/panels/config/automation/trigger/ha-automation-trigger";
import { HaConversationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-conversation";
import { HaTriggerList } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-list";
const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
{
@@ -111,11 +115,15 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
triggers: [
{ ...HaConversationTrigger.defaultConfig },
{
platform: "conversation",
trigger: "conversation",
command: ["Turn on the lights", "Turn the lights on"],
},
],
},
{
name: "Trigger list",
triggers: [{ ...HaTriggerList.defaultConfig }],
},
];
@customElement("demo-automation-editor-trigger")
@@ -135,6 +143,9 @@ export class DemoAutomationEditorTrigger extends LitElement {
mockDeviceRegistry(hass);
mockAreaRegistry(hass);
mockHassioSupervisor(hass);
mockConfig(hass);
mockTags(hass);
mockAuth(hass);
}
protected render(): TemplateResult {

View File

@@ -15,6 +15,7 @@ import { LocalizeFunc } from "../../../src/common/translations/localize";
import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-textfield";
import "../../../src/components/ha-password-field";
import "../../../src/components/ha-radio";
import type { HaRadio } from "../../../src/components/ha-radio";
import {
@@ -261,23 +262,21 @@ export class SupervisorBackupContent extends LitElement {
: ""}
${this.backupHasPassword
? html`
<ha-textfield
<ha-password-field
.label=${this._localize("password")}
type="password"
name="backupPassword"
.value=${this.backupPassword}
@change=${this._handleTextValueChanged}
>
</ha-textfield>
</ha-password-field>
${!this.backup
? html`<ha-textfield
? html`<ha-password-field
.label=${this._localize("confirm_password")}
type="password"
name="confirmBackupPassword"
.value=${this.confirmBackupPassword}
@change=${this._handleTextValueChanged}
>
</ha-textfield>`
</ha-password-field>`
: ""}
`
: ""}

View File

@@ -13,10 +13,12 @@ import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel";
import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-password-field";
import "../../../../src/components/ha-radio";
import "../../../../src/components/ha-textfield";
import type { HaTextField } from "../../../../src/components/ha-textfield";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import {
AccessPoints,
@@ -34,7 +36,6 @@ import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { HassioNetworkDialogParams } from "./show-dialog-network";
import type { HaTextField } from "../../../../src/components/ha-textfield";
const IP_VERSIONS = ["ipv4", "ipv6"];
@@ -246,9 +247,8 @@ export class DialogHassioNetwork
${this._wifiConfiguration.auth === "wpa-psk" ||
this._wifiConfiguration.auth === "wep"
? html`
<ha-textfield
<ha-password-field
class="flex-auto"
type="password"
id="psk"
.label=${this.supervisor.localize(
"dialog.network.wifi_password"
@@ -256,7 +256,7 @@ export class DialogHassioNetwork
version="wifi"
@change=${this._handleInputValueChangedWifi}
>
</ha-textfield>
</ha-password-field>
`
: ""}
`

View File

@@ -25,8 +25,8 @@ import type { HomeAssistant } from "../../../../src/types";
import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
import type { HaTextField } from "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-list-new";
import "../../../../src/components/ha-list-item-new";
import "../../../../src/components/ha-md-list";
import "../../../../src/components/ha-md-list-item";
@customElement("dialog-hassio-repositories")
class HassioRepositoriesDialog extends LitElement {
@@ -107,11 +107,11 @@ class HassioRepositoriesDialog extends LitElement {
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<div class="form">
<ha-list-new>
<ha-md-list>
${repositories.length
? repositories.map(
(repo) => html`
<ha-list-item-new class="option">
<ha-md-list-item class="option">
${repo.name}
<div slot="supporting-text">
<div>${repo.maintainer}</div>
@@ -142,11 +142,11 @@ class HassioRepositoriesDialog extends LitElement {
)}
</simple-tooltip>
</div>
</ha-list-item-new>
</ha-md-list-item>
`
)
: html`<ha-list-item-new> No repositories </ha-list-item-new>`}
</ha-list-new>
: html`<ha-md-list-item> No repositories </ha-md-list-item>`}
</ha-md-list>
<div class="layout horizontal bottom">
<ha-textfield
class="flex-auto"
@@ -209,7 +209,7 @@ class HassioRepositoriesDialog extends LitElement {
div.delete ha-icon-button {
color: var(--error-color);
}
ha-list-item-new {
ha-md-list-item {
position: relative;
}
`,

View File

@@ -27,13 +27,13 @@
"dependencies": {
"@babel/runtime": "7.25.6",
"@braintree/sanitize-url": "7.1.0",
"@codemirror/autocomplete": "6.18.0",
"@codemirror/commands": "6.6.1",
"@codemirror/language": "6.10.2",
"@codemirror/autocomplete": "6.18.1",
"@codemirror/commands": "6.6.2",
"@codemirror/language": "6.10.3",
"@codemirror/legacy-modes": "6.4.1",
"@codemirror/search": "6.5.6",
"@codemirror/state": "6.4.1",
"@codemirror/view": "6.33.0",
"@codemirror/view": "6.34.0",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.12.5",
"@formatjs/intl-displaynames": "6.6.8",
@@ -80,16 +80,17 @@
"@material/mwc-top-app-bar": "0.27.0",
"@material/mwc-top-app-bar-fixed": "0.27.0",
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
"@material/web": "2.1.0",
"@material/web": "2.2.0",
"@mdi/js": "7.4.47",
"@mdi/svg": "7.4.47",
"@polymer/paper-item": "3.0.1",
"@polymer/paper-listbox": "3.0.1",
"@polymer/paper-tabs": "3.1.0",
"@polymer/polymer": "3.5.1",
"@replit/codemirror-indentation-markers": "6.5.3",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.4.7",
"@vaadin/vaadin-themable-mixin": "24.4.7",
"@vaadin/combo-box": "24.4.9",
"@vaadin/vaadin-themable-mixin": "24.4.9",
"@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@@ -102,10 +103,11 @@
"comlink": "4.4.1",
"core-js": "3.38.1",
"cropperjs": "1.6.2",
"date-fns": "3.6.0",
"date-fns": "4.1.0",
"date-fns-tz": "3.1.3",
"deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1",
"dialog-polyfill": "0.5.6",
"element-internals-polyfill": "1.3.11",
"fuse.js": "7.0.0",
"google-timezones-json": "1.2.0",
@@ -115,10 +117,10 @@
"intl-messageformat": "10.5.14",
"js-yaml": "4.1.0",
"leaflet": "1.9.4",
"leaflet-draw": "1.0.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
"lit": "2.8.0",
"luxon": "3.5.0",
"marked": "14.1.1",
"marked": "14.1.2",
"memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "0.3.2",
@@ -127,13 +129,13 @@
"qrcode": "1.5.4",
"roboto-fontface": "0.10.0",
"rrule": "2.8.1",
"sortablejs": "1.15.3",
"sortablejs": "patch:sortablejs@npm%3A1.15.3#~/.yarn/patches/sortablejs-npm-1.15.3-3235a8f83b.patch",
"stacktrace-js": "2.0.2",
"superstruct": "2.0.2",
"tinykeys": "3.0.0",
"tsparticles-engine": "2.12.0",
"tsparticles-preset-links": "2.12.0",
"ua-parser-js": "1.0.38",
"ua-parser-js": "1.0.39",
"unfetch": "5.0.0",
"vis-data": "7.1.9",
"vis-network": "9.1.9",
@@ -155,17 +157,17 @@
"@babel/plugin-transform-runtime": "7.25.4",
"@babel/preset-env": "7.25.4",
"@babel/preset-typescript": "7.24.7",
"@bundle-stats/plugin-webpack-filter": "4.15.0",
"@bundle-stats/plugin-webpack-filter": "4.15.1",
"@koa/cors": "5.0.0",
"@lokalise/node-api": "12.7.0",
"@octokit/auth-oauth-device": "7.1.1",
"@octokit/plugin-retry": "7.1.1",
"@octokit/plugin-retry": "7.1.2",
"@octokit/rest": "21.0.2",
"@open-wc/dev-server-hmr": "0.1.4",
"@rollup/plugin-babel": "6.0.4",
"@rollup/plugin-commonjs": "26.0.1",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-node-resolve": "15.2.3",
"@rollup/plugin-node-resolve": "15.2.4",
"@rollup/plugin-replace": "5.0.7",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.17",
@@ -189,20 +191,20 @@
"@typescript-eslint/parser": "7.18.0",
"@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1",
"babel-loader": "9.1.3",
"babel-loader": "9.2.1",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"chai": "5.1.1",
"del": "7.1.0",
"eslint": "8.57.0",
"eslint": "8.57.1",
"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-plugin-import": "2.30.0",
"eslint-plugin-lit": "1.14.0",
"eslint-plugin-lit": "1.15.0",
"eslint-plugin-lit-a11y": "4.1.4",
"eslint-plugin-unused-imports": "4.1.3",
"eslint-plugin-unused-imports": "4.1.4",
"eslint-plugin-wc": "2.1.1",
"fancy-log": "2.0.0",
"fs-extra": "11.2.0",
@@ -213,7 +215,7 @@
"gulp-rename": "2.0.0",
"gulp-zopfli-green": "6.0.2",
"html-minifier-terser": "7.2.0",
"husky": "9.1.5",
"husky": "9.1.6",
"instant-mocha": "1.5.2",
"jszip": "3.10.1",
"lint-staged": "15.2.10",
@@ -227,19 +229,19 @@
"open": "10.1.0",
"pinst": "3.0.0",
"prettier": "3.3.3",
"rollup": "2.79.1",
"rollup": "2.79.2",
"rollup-plugin-string": "3.0.0",
"rollup-plugin-terser": "7.0.2",
"rollup-plugin-visualizer": "5.12.0",
"serve-handler": "6.1.5",
"sinon": "18.0.0",
"sinon": "19.0.2",
"systemjs": "6.15.1",
"tar": "7.4.3",
"terser-webpack-plugin": "5.3.10",
"transform-async-modules-webpack-plugin": "1.1.1",
"ts-lit-plugin": "2.0.2",
"typescript": "5.5.4",
"webpack": "5.94.0",
"typescript": "5.6.2",
"webpack": "5.95.0",
"webpack-cli": "5.1.4",
"webpack-dev-server": "5.1.0",
"webpack-manifest-plugin": "5.0.0",
@@ -254,9 +256,7 @@
"lit": "2.8.0",
"clean-css": "5.3.3",
"@lit/reactive-element": "1.6.3",
"@fullcalendar/daygrid": "6.1.15",
"sortablejs@1.15.3": "patch:sortablejs@npm%3A1.15.3#~/.yarn/patches/sortablejs-npm-1.15.3-3235a8f83b.patch",
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
"@fullcalendar/daygrid": "6.1.15"
},
"packageManager": "yarn@4.4.1"
"packageManager": "yarn@4.5.0"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

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

View File

@@ -1,36 +1,36 @@
import { theme2hex } from "./convert-color";
export const COLORS = [
"#44739e",
"#984ea3",
"#00d2d5",
"#ff7f00",
"#af8d00",
"#7f80cd",
"#b3e900",
"#c42e60",
"#a65628",
"#f781bf",
"#8dd3c7",
"#bebada",
"#fb8072",
"#80b1d3",
"#fdb462",
"#fccde5",
"#bc80bd",
"#ffed6f",
"#c4eaff",
"#cf8c00",
"#1b9e77",
"#d95f02",
"#e7298a",
"#e6ab02",
"#a6761d",
"#0097ff",
"#00d067",
"#f43600",
"#4ba93b",
"#5779bb",
"#4269d0",
"#f4bd4a",
"#ff725c",
"#6cc5b0",
"#a463f2",
"#ff8ab7",
"#9c6b4e",
"#97bbf5",
"#01ab63",
"#9498a0",
"#094bad",
"#c99000",
"#d84f3e",
"#49a28f",
"#048732",
"#d96895",
"#8043ce",
"#7599d1",
"#7a4c31",
"#74787f",
"#6989f4",
"#ffd444",
"#ff957c",
"#8fe9d3",
"#62cc71",
"#ffadda",
"#c884ff",
"#badeff",
"#bf8b6d",
"#b6bac2",
"#927acc",
"#97ee3f",
"#bf3947",

View File

@@ -234,7 +234,12 @@ export const SENSOR_ENTITIES = [
"weather",
];
export const ASSIST_ENTITIES = ["conversation", "stt", "tts"];
export const ASSIST_ENTITIES = [
"assist_satellite",
"conversation",
"stt",
"tts",
];
/** Domains that render an input element instead of a text value when displayed in a row.
* Those rows should then not show a cursor pointer when hovered (which would normally

View File

@@ -26,7 +26,7 @@ class HaDeviceTriggerPicker extends HaDeviceAutomationPicker<DeviceTrigger> {
fetchDeviceTriggers,
(deviceId?: string) => ({
device_id: deviceId || "",
platform: "device",
trigger: "device",
domain: "",
entity_id: "",
})

View File

@@ -1,10 +1,9 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { isValidEntityId } from "../../common/entity/valid_entity_id";
import type { ValueChangedEvent, HomeAssistant } from "../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "./ha-entity-picker";
import type { HaEntityPickerEntityFilterFunc } from "./ha-entity-picker";
@@ -98,10 +97,7 @@ class HaEntitiesPickerLight extends LitElement {
.excludeEntities=${this.excludeEntities}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.entityFilter=${this._getEntityFilter(
this.value,
this.entityFilter
)}
.entityFilter=${this.entityFilter}
.value=${entityId}
.label=${this.pickedEntityLabel}
.disabled=${this.disabled}
@@ -118,10 +114,13 @@ class HaEntitiesPickerLight extends LitElement {
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeEntities=${this.includeEntities}
.excludeEntities=${this.excludeEntities}
.excludeEntities=${this._excludeEntities(
this.value,
this.excludeEntities
)}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.entityFilter=${this._getEntityFilter(this.value, this.entityFilter)}
.entityFilter=${this.entityFilter}
.label=${this.pickEntityLabel}
.helper=${this.helper}
.disabled=${this.disabled}
@@ -133,14 +132,16 @@ class HaEntitiesPickerLight extends LitElement {
`;
}
private _getEntityFilter = memoizeOne(
private _excludeEntities = memoizeOne(
(
value: string[] | undefined,
entityFilter: HaEntityPickerEntityFilterFunc | undefined
): HaEntityPickerEntityFilterFunc =>
(stateObj: HassEntity) =>
(!value || !value.includes(stateObj.entity_id)) &&
(!entityFilter || entityFilter(stateObj))
excludeEntities: string[] | undefined
): string[] | undefined => {
if (value === undefined) {
return excludeEntities;
}
return [...(excludeEntities || []), ...value];
}
);
private get _currentEntities() {

View File

@@ -87,7 +87,7 @@ export class HaEntityPicker extends LitElement {
public includeUnitOfMeasurement?: string[];
/**
* List of allowed entities to show. Will ignore all other filters.
* List of allowed entities to show.
* @type {Array}
* @attr include-entities
*/
@@ -220,30 +220,13 @@ export class HaEntityPicker extends LitElement {
if (includeEntities) {
entityIds = entityIds.filter((entityId) =>
this.includeEntities!.includes(entityId)
includeEntities.includes(entityId)
);
return entityIds
.map((key) => {
const friendly_name = computeStateName(hass!.states[key]) || key;
return {
...hass!.states[key],
friendly_name,
strings: [key, friendly_name],
};
})
.sort((entityA, entityB) =>
caseInsensitiveStringCompare(
entityA.friendly_name,
entityB.friendly_name,
this.hass.locale.language
)
);
}
if (excludeEntities) {
entityIds = entityIds.filter(
(entityId) => !excludeEntities!.includes(entityId)
(entityId) => !excludeEntities.includes(entityId)
);
}

View File

@@ -173,6 +173,7 @@ class HaEntityStatePicker extends LitElement {
no-style
@item-moved=${this._moveItem}
.disabled=${this.disabled}
filter="button.trailing.action"
>
<ha-chip-set>
${repeat(

View File

@@ -1,6 +1,6 @@
import { mdiTextureBox } from "@mdi/js";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { HassEntity } from "home-assistant-js-websocket";
import { LitElement, PropertyValues, TemplateResult, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
@@ -20,12 +20,7 @@ import {
getDeviceEntityDisplayLookup,
} from "../data/device_registry";
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
import {
FloorRegistryEntry,
getFloorAreaLookup,
subscribeFloorRegistry,
} from "../data/floor_registry";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { FloorRegistryEntry, getFloorAreaLookup } from "../data/floor_registry";
import { HomeAssistant, ValueChangedEvent } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-combo-box";
@@ -50,7 +45,7 @@ interface FloorAreaEntry {
}
@customElement("ha-area-floor-picker")
export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
export class HaAreaFloorPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@@ -111,22 +106,12 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
@property({ type: Boolean }) public required = false;
@state() private _floors?: FloorRegistryEntry[];
@state() private _opened?: boolean;
@query("ha-combo-box", true) public comboBox!: HaComboBox;
private _init = false;
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
return [
subscribeFloorRegistry(this.hass.connection, (floors) => {
this._floors = floors;
}),
];
}
public async open() {
await this.updateComplete;
await this.comboBox?.open();
@@ -431,12 +416,12 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
protected updated(changedProps: PropertyValues) {
if (
(!this._init && this.hass && this._floors) ||
(!this._init && this.hass) ||
(this._init && changedProps.has("_opened") && this._opened)
) {
this._init = true;
const areas = this._getAreas(
this._floors!,
Object.values(this.hass.floors),
Object.values(this.hass.areas),
Object.values(this.hass.devices),
Object.values(this.hass.entities),

View File

@@ -124,9 +124,12 @@ export class HaCodeEditor extends ReactiveElement {
const transactions: TransactionSpec[] = [];
if (changedProps.has("mode")) {
transactions.push({
effects: this._loadedCodeMirror!.langCompartment!.reconfigure(
this._mode
),
effects: [
this._loadedCodeMirror!.langCompartment!.reconfigure(this._mode),
this._loadedCodeMirror!.foldingCompartment.reconfigure(
this._getFoldingExtensions()
),
],
});
}
if (changedProps.has("readOnly")) {
@@ -177,6 +180,14 @@ export class HaCodeEditor extends ReactiveElement {
this._loadedCodeMirror.crosshairCursor(),
this._loadedCodeMirror.highlightSelectionMatches(),
this._loadedCodeMirror.highlightActiveLine(),
this._loadedCodeMirror.indentationMarkers({
thickness: 0,
activeThickness: 1,
colors: {
activeLight: "var(--secondary-text-color)",
activeDark: "var(--secondary-text-color)",
},
}),
this._loadedCodeMirror.keymap.of([
...this._loadedCodeMirror.defaultKeymap,
...this._loadedCodeMirror.searchKeymap,
@@ -194,6 +205,9 @@ export class HaCodeEditor extends ReactiveElement {
this.linewrap ? this._loadedCodeMirror.EditorView.lineWrapping : []
),
this._loadedCodeMirror.EditorView.updateListener.of(this._onUpdate),
this._loadedCodeMirror.foldingCompartment.of(
this._getFoldingExtensions()
),
];
if (!this.readOnly) {
@@ -311,6 +325,17 @@ export class HaCodeEditor extends ReactiveElement {
fireEvent(this, "value-changed", { value: this._value });
};
private _getFoldingExtensions = (): Extension => {
if (this.mode === "yaml") {
return [
this._loadedCodeMirror!.foldGutter(),
this._loadedCodeMirror!.foldingOnIndent,
];
}
return [];
};
static get styles(): CSSResultGroup {
return css`
:host(.error-state) .cm-gutters {

View File

@@ -1,14 +1,15 @@
import "@material/mwc-list/mwc-list-item";
import { mdiInvertColorsOff, mdiPalette } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import "./ha-select";
import "./ha-list-item";
import { HomeAssistant } from "../types";
import { LocalizeKeys } from "../common/translations/localize";
import { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
import "./ha-md-divider";
@customElement("ha-color-picker")
export class HaColorPicker extends LitElement {
@@ -20,43 +21,81 @@ export class HaColorPicker extends LitElement {
@property() public value?: string;
@property({ type: Boolean }) public defaultColor = false;
@property({ type: String, attribute: "default_color" })
public defaultColor?: string;
@property({ type: Boolean, attribute: "include_state" })
public includeState = false;
@property({ type: Boolean, attribute: "include_none" })
public includeNone = false;
@property({ type: Boolean }) public disabled = false;
_valueSelected(ev) {
const value = ev.target.value;
if (value) {
fireEvent(this, "value-changed", {
value: value !== "default" ? value : undefined,
});
}
this.value = value === this.defaultColor ? undefined : value;
fireEvent(this, "value-changed", {
value: this.value,
});
}
render() {
const value = this.value || this.defaultColor;
return html`
<ha-select
.icon=${Boolean(this.value)}
.icon=${Boolean(value)}
.label=${this.label}
.value=${this.value || "default"}
.value=${value}
.helper=${this.helper}
.disabled=${this.disabled}
@closed=${stopPropagation}
@selected=${this._valueSelected}
fixedMenuPosition
naturalMenuWidth
.clearable=${!this.defaultColor}
>
${this.value
${value
? html`
<span slot="icon">
${this.renderColorCircle(this.value || "grey")}
${value === "none"
? html`
<ha-svg-icon path=${mdiInvertColorsOff}></ha-svg-icon>
`
: value === "state"
? html`<ha-svg-icon path=${mdiPalette}></ha-svg-icon>`
: this.renderColorCircle(value || "grey")}
</span>
`
: nothing}
${this.defaultColor
? html` <ha-list-item value="default">
${this.hass.localize(`ui.components.color-picker.default_color`)}
</ha-list-item>`
${this.includeNone
? html`
<ha-list-item value="none" graphic="icon">
${this.hass.localize("ui.components.color-picker.none")}
${this.defaultColor === "none"
? ` (${this.hass.localize("ui.components.color-picker.default")})`
: nothing}
<ha-svg-icon
slot="graphic"
path=${mdiInvertColorsOff}
></ha-svg-icon>
</ha-list-item>
`
: nothing}
${this.includeState
? html`
<ha-list-item value="state" graphic="icon">
${this.hass.localize("ui.components.color-picker.state")}
${this.defaultColor === "state"
? ` (${this.hass.localize("ui.components.color-picker.default")})`
: nothing}
<ha-svg-icon slot="graphic" path=${mdiPalette}></ha-svg-icon>
</ha-list-item>
`
: nothing}
${this.includeState || this.includeNone
? html`<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
: nothing}
${Array.from(THEME_COLORS).map(
(color) => html`
@@ -64,6 +103,9 @@ export class HaColorPicker extends LitElement {
${this.hass.localize(
`ui.components.color-picker.colors.${color}` as LocalizeKeys
) || color}
${this.defaultColor === color
? ` (${this.hass.localize("ui.components.color-picker.default")})`
: nothing}
<span slot="graphic">${this.renderColorCircle(color)}</span>
</ha-list-item>
`
@@ -87,10 +129,11 @@ export class HaColorPicker extends LitElement {
return css`
.circle-color {
display: block;
background-color: var(--circle-color);
background-color: var(--circle-color, var(--divider-color));
border-radius: 10px;
width: 20px;
height: 20px;
box-sizing: border-box;
}
ha-select {
width: 100%;

View File

@@ -10,8 +10,13 @@ export class HaDialogHeader extends LitElement {
<section class="header-navigation-icon">
<slot name="navigationIcon"></slot>
</section>
<section class="header-title">
<slot name="title"></slot>
<section class="header-content">
<div class="header-title">
<slot name="title"></slot>
</div>
<div class="header-subtitle">
<slot name="subtitle"></slot>
</div>
</section>
<section class="header-action-items">
<slot name="actionItems"></slot>
@@ -39,17 +44,24 @@ export class HaDialogHeader extends LitElement {
padding: 4px;
box-sizing: border-box;
}
.header-title {
.header-content {
flex: 1;
font-size: 22px;
line-height: 28px;
font-weight: 400;
padding: 10px 4px;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.header-title {
font-size: 22px;
line-height: 28px;
font-weight: 400;
}
.header-subtitle {
font-size: 14px;
line-height: 20px;
color: var(--secondary-text-color);
}
@media all and (min-width: 450px) and (min-height: 500px) {
.header-bar {
padding: 12px;

View File

@@ -1,6 +1,5 @@
import "@material/mwc-menu/mwc-menu-surface";
import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
CSSResultGroup,
LitElement,
@@ -15,13 +14,8 @@ import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { computeRTL } from "../common/util/compute_rtl";
import {
FloorRegistryEntry,
getFloorAreaLookup,
subscribeFloorRegistry,
} from "../data/floor_registry";
import { getFloorAreaLookup } from "../data/floor_registry";
import { RelatedResult, findRelated } from "../data/search";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-check-list-item";
@@ -31,7 +25,7 @@ import "./ha-svg-icon";
import "./ha-tree-indicator";
@customElement("ha-filter-floor-areas")
export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
export class HaFilterFloorAreas extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public value?: {
@@ -47,8 +41,6 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
@state() private _shouldRender = false;
@state() private _floors?: FloorRegistryEntry[];
public willUpdate(properties: PropertyValues) {
super.willUpdate(properties);
@@ -60,7 +52,7 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
}
protected render() {
const areas = this._areas(this.hass.areas, this._floors);
const areas = this._areas(this.hass.areas, this.hass.floors);
return html`
<ha-expansion-panel
@@ -189,14 +181,6 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
this._findRelated();
}
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
return [
subscribeFloorRegistry(this.hass.connection, (floors) => {
this._floors = floors;
}),
];
}
protected updated(changed) {
if (changed.has("expanded") && this.expanded) {
setTimeout(() => {
@@ -220,9 +204,9 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
}
private _areas = memoizeOne(
(areaReg: HomeAssistant["areas"], floors?: FloorRegistryEntry[]) => {
(areaReg: HomeAssistant["areas"], floorReg: HomeAssistant["floors"]) => {
const areas = Object.values(areaReg);
const floors = Object.values(floorReg);
const floorAreaLookup = getFloorAreaLookup(areas);
const unassisgnedAreas = areas.filter(

View File

@@ -1,5 +1,5 @@
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { HassEntity } from "home-assistant-js-websocket";
import { LitElement, PropertyValues, TemplateResult, html } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@@ -24,10 +24,8 @@ import {
FloorRegistryEntry,
createFloorRegistryEntry,
getFloorAreaLookup,
subscribeFloorRegistry,
} from "../data/floor_registry";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { showFloorRegistryDetailDialog } from "../panels/config/areas/show-dialog-floor-registry-detail";
import { HomeAssistant, ValueChangedEvent } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
@@ -53,7 +51,7 @@ const rowRenderer: ComboBoxLitRenderer<FloorRegistryEntry> = (item) =>
</ha-list-item>`;
@customElement("ha-floor-picker")
export class HaFloorPicker extends SubscribeMixin(LitElement) {
export class HaFloorPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@@ -111,8 +109,6 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
@state() private _opened?: boolean;
@state() private _floors?: FloorRegistryEntry[];
@query("ha-combo-box", true) public comboBox!: HaComboBox;
private _suggestion?: string;
@@ -129,14 +125,6 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
await this.comboBox?.focus();
}
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
return [
subscribeFloorRegistry(this.hass.connection, (floors) => {
this._floors = floors;
}),
];
}
private _getFloors = memoizeOne(
(
floors: FloorRegistryEntry[],
@@ -320,12 +308,12 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
protected updated(changedProps: PropertyValues) {
if (
(!this._init && this.hass && this._floors) ||
(!this._init && this.hass) ||
(this._init && changedProps.has("_opened") && this._opened)
) {
this._init = true;
const floors = this._getFloors(
this._floors!,
Object.values(this.hass.floors),
Object.values(this.hass.areas),
Object.values(this.hass.devices),
Object.values(this.hass.entities),
@@ -360,8 +348,7 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
? this.hass.localize("ui.components.floor-picker.floor")
: this.label}
.placeholder=${this.placeholder
? this._floors?.find((floor) => floor.floor_id === this.placeholder)
?.name
? this.hass.floors[this.placeholder]?.name
: undefined}
.renderer=${rowRenderer}
@filter-changed=${this._filterChanged}
@@ -460,7 +447,7 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
floor_id: floor.floor_id,
});
});
const floors = [...this._floors!, floor];
const floors = [...Object.values(this.hass.floors), floor];
this.comboBox.filteredItems = this._getFloors(
floors,
Object.values(this.hass.areas)!,

View File

@@ -30,6 +30,10 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
options?: { path?: string[] }
) => string;
@property({ attribute: false }) public localizeValue?: (
key: string
) => string;
private _renderDescription() {
const description = this.computeHelper?.(this.schema);
return description ? html`<p>${description}</p>` : nothing;
@@ -86,6 +90,7 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
.disabled=${this.disabled}
.computeLabel=${this._computeLabel}
.computeHelper=${this._computeHelper}
.localizeValue=${this.localizeValue}
></ha-form>
</div>
</ha-expansion-panel>

View File

@@ -35,6 +35,10 @@ export class HaFormGrid extends LitElement implements HaFormElement {
schema: HaFormSchema
) => string;
@property({ attribute: false }) public localizeValue?: (
key: string
) => string;
public async focus() {
await this.updateComplete;
this.renderRoot.querySelector("ha-form")?.focus();
@@ -65,6 +69,7 @@ export class HaFormGrid extends LitElement implements HaFormElement {
.disabled=${this.disabled}
.computeLabel=${this.computeLabel}
.computeHelper=${this.computeHelper}
.localizeValue=${this.localizeValue}
></ha-form>
`
)}

View File

@@ -163,6 +163,7 @@ export class HaForm extends LitElement implements HaFormElement {
localize: this.hass?.localize,
computeLabel: this.computeLabel,
computeHelper: this.computeHelper,
localizeValue: this.localizeValue,
context: this._generateContext(item),
...this.getFormProperties(),
})}

View File

@@ -0,0 +1,58 @@
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
type HeadingBadgeType = "text" | "button";
@customElement("ha-heading-badge")
export class HaBadge extends LitElement {
@property() public type: HeadingBadgeType = "text";
protected render() {
return html`
<div
class="heading-badge"
role=${ifDefined(this.type === "button" ? "button" : undefined)}
tabindex=${ifDefined(this.type === "button" ? "0" : undefined)}
>
<slot name="icon"></slot>
<slot></slot>
</div>
`;
}
static get styles(): CSSResultGroup {
return css`
:host {
color: var(--secondary-text-color);
}
[role="button"] {
cursor: pointer;
}
.heading-badge {
display: flex;
flex-direction: row;
white-space: nowrap;
align-items: center;
gap: 3px;
font-family: Roboto;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: 0.1px;
--mdc-icon-size: 14px;
}
::slotted([slot="icon"]) {
--ha-icon-display: block;
color: var(--icon-color, inherit);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-heading-badge": HaBadge;
}
}

View File

@@ -1,5 +1,4 @@
import { Button } from "@material/mwc-button";
import { Corner } from "@material/web/menu/menu";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
@@ -7,28 +6,16 @@ import type { HaIconButton } from "./ha-icon-button";
import "./ha-menu";
import type { HaMenu } from "./ha-menu";
@customElement("ha-button-menu-new")
export class HaButtonMenuNew extends LitElement {
@customElement("ha-md-button-menu")
export class HaMdButtonMenu extends LitElement {
protected readonly [FOCUS_TARGET];
@property({ type: Boolean }) public disabled = false;
@property() public positioning?: "fixed" | "absolute" | "popover";
@property({ type: Boolean, attribute: "no-horizontal-flip" })
public noHorizontalFlip = false;
@property({ type: Boolean, attribute: "no-vertical-flip" })
public noVerticalFlip = false;
@property({ attribute: "anchor-corner" })
public anchorCorner: Corner = Corner.END_START;
@property({ attribute: "menu-corner" })
public menuCorner: Corner = Corner.START_START;
@property({ type: Boolean, attribute: "has-overflow" })
public hasOverflow = false;
@property({ type: Boolean, attribute: "has-overflow" }) public hasOverflow =
false;
@query("ha-menu", true) private _menu!: HaMenu;
@@ -52,10 +39,6 @@ export class HaButtonMenuNew extends LitElement {
<ha-menu
.positioning=${this.positioning}
.hasOverflow=${this.hasOverflow}
.anchorCorner=${this.anchorCorner}
.menuCorner=${this.menuCorner}
.noVerticalFlip=${this.noVerticalFlip}
.noHorizontalFlip=${this.noHorizontalFlip}
>
<slot></slot>
</ha-menu>
@@ -101,6 +84,6 @@ export class HaButtonMenuNew extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"ha-button-menu-new": HaButtonMenuNew;
"ha-md-button-menu": HaMdButtonMenu;
}
}

View File

@@ -0,0 +1,250 @@
import { MdDialog } from "@material/web/dialog/dialog";
import {
type DialogAnimation,
DIALOG_DEFAULT_CLOSE_ANIMATION,
DIALOG_DEFAULT_OPEN_ANIMATION,
} from "@material/web/dialog/internal/animations";
import { css } from "lit";
import { customElement, property } from "lit/decorators";
// workaround to be able to overlay an dialog with another dialog
MdDialog.addInitializer(async (instance) => {
await instance.updateComplete;
const dialogInstance = instance as MdDialog;
// @ts-expect-error dialog is private
dialogInstance.dialog.prepend(dialogInstance.scrim);
// @ts-expect-error scrim is private
dialogInstance.scrim.style.inset = 0;
// @ts-expect-error scrim is private
dialogInstance.scrim.style.zIndex = 0;
const { getOpenAnimation, getCloseAnimation } = dialogInstance;
dialogInstance.getOpenAnimation = () => {
const animations = getOpenAnimation.call(this);
animations.container = [
...(animations.container ?? []),
...(animations.dialog ?? []),
];
animations.dialog = [];
return animations;
};
dialogInstance.getCloseAnimation = () => {
const animations = getCloseAnimation.call(this);
animations.container = [
...(animations.container ?? []),
...(animations.dialog ?? []),
];
animations.dialog = [];
return animations;
};
});
let DIALOG_POLYFILL: Promise<typeof import("dialog-polyfill")>;
/**
* Based on the home assistant design: https://design.home-assistant.io/#components/ha-dialogs
*
*/
@customElement("ha-md-dialog")
export class HaMdDialog extends MdDialog {
/**
* When true the dialog will not close when the user presses the esc key or press out of the dialog.
*/
@property({ attribute: "disable-cancel-action", type: Boolean })
public disableCancelAction = false;
private _polyfillDialogRegistered = false;
constructor() {
super();
this.addEventListener("cancel", this._handleCancel);
if (typeof HTMLDialogElement !== "function") {
this.addEventListener("open", this._handleOpen);
if (!DIALOG_POLYFILL) {
DIALOG_POLYFILL = import("dialog-polyfill");
}
}
// if browser doesn't support animate API disable open/close animations
if (this.animate === undefined) {
this.quick = true;
}
// if browser doesn't support animate API disable open/close animations
if (this.animate === undefined) {
this.quick = true;
}
}
// prevent open in older browsers and wait for polyfill to load
private async _handleOpen(openEvent: Event) {
openEvent.preventDefault();
if (this._polyfillDialogRegistered) {
return;
}
this._polyfillDialogRegistered = true;
this._loadPolyfillStylesheet("/static/polyfills/dialog-polyfill.css");
const dialog = this.shadowRoot?.querySelector(
"dialog"
) as HTMLDialogElement;
const dialogPolyfill = await DIALOG_POLYFILL;
dialogPolyfill.default.registerDialog(dialog);
this.removeEventListener("open", this._handleOpen);
this.show();
}
private async _loadPolyfillStylesheet(href) {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = href;
return new Promise<void>((resolve, reject) => {
link.onload = () => resolve();
link.onerror = () =>
reject(new Error(`Stylesheet failed to load: ${href}`));
this.shadowRoot?.appendChild(link);
});
}
_handleCancel(closeEvent: Event) {
if (this.disableCancelAction) {
closeEvent.preventDefault();
const dialogElement = this.shadowRoot?.querySelector("dialog .container");
if (this.animate !== undefined) {
dialogElement?.animate(
[
{
transform: "rotate(-1deg)",
"animation-timing-function": "ease-in",
},
{
transform: "rotate(1.5deg)",
"animation-timing-function": "ease-out",
},
{
transform: "rotate(0deg)",
"animation-timing-function": "ease-in",
},
],
{
duration: 200,
iterations: 2,
}
);
}
}
}
static override styles = [
...super.styles,
css`
:host {
--md-dialog-container-color: var(--card-background-color);
--md-dialog-headline-color: var(--primary-text-color);
--md-dialog-supporting-text-color: var(--primary-text-color);
--md-sys-color-scrim: #000000;
--md-dialog-headline-weight: 400;
--md-dialog-headline-size: 1.574rem;
--md-dialog-supporting-text-size: 1rem;
--md-dialog-supporting-text-line-height: 1.5rem;
}
:host([type="alert"]) {
min-width: 320px;
}
:host(:not([type="alert"])) {
@media all and (max-width: 450px), all and (max-height: 500px) {
min-width: calc(
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
);
max-width: calc(
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
);
min-height: 100%;
max-height: 100%;
--md-dialog-container-shape: 0;
}
}
:host ::slotted(ha-dialog-header) {
display: contents;
}
slot[name="content"]::slotted(*) {
padding: var(--dialog-content-padding, 24px);
}
.scrim {
z-index: 10; // overlay navigation
}
`,
];
}
// by default the dialog open/close animation will be from/to the top
// but if we have a special mobile dialog which is at the bottom of the screen, an from bottom animation can be used:
const OPEN_FROM_BOTTOM_ANIMATION: DialogAnimation = {
...DIALOG_DEFAULT_OPEN_ANIMATION,
dialog: [
[
// Dialog slide up
[{ transform: "translateY(50px)" }, { transform: "translateY(0)" }],
{ duration: 500, easing: "cubic-bezier(.3,0,0,1)" },
],
],
container: [
[
// Container fade in
[{ opacity: 0 }, { opacity: 1 }],
{ duration: 50, easing: "linear", pseudoElement: "::before" },
],
],
};
const CLOSE_TO_BOTTOM_ANIMATION: DialogAnimation = {
...DIALOG_DEFAULT_CLOSE_ANIMATION,
dialog: [
[
// Dialog slide down
[{ transform: "translateY(0)" }, { transform: "translateY(50px)" }],
{ duration: 150, easing: "cubic-bezier(.3,0,0,1)" },
],
],
container: [
[
// Container fade out
[{ opacity: "1" }, { opacity: "0" }],
{ delay: 100, duration: 50, easing: "linear", pseudoElement: "::before" },
],
],
};
export const getMobileOpenFromBottomAnimation = () => {
const matches = window.matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
return matches ? OPEN_FROM_BOTTOM_ANIMATION : DIALOG_DEFAULT_OPEN_ANIMATION;
};
export const getMobileCloseToBottomAnimation = () => {
const matches = window.matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
return matches ? CLOSE_TO_BOTTOM_ANIMATION : DIALOG_DEFAULT_CLOSE_ANIMATION;
};
declare global {
interface HTMLElementTagNameMap {
"ha-md-dialog": HaMdDialog;
}
}

View File

@@ -0,0 +1,21 @@
import { MdDivider } from "@material/web/divider/divider";
import { css } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-md-divider")
export class HaMdDivider extends MdDivider {
static override styles = [
...super.styles,
css`
:host {
--md-divider-color: var(--divider-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-md-divider": HaMdDivider;
}
}

View File

@@ -2,8 +2,8 @@ import { MdListItem } from "@material/web/list/list-item";
import { css } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-list-item-new")
export class HaListItemNew extends MdListItem {
@customElement("ha-md-list-item")
export class HaMdListItem extends MdListItem {
static override styles = [
...super.styles,
css`
@@ -21,6 +21,6 @@ export class HaListItemNew extends MdListItem {
declare global {
interface HTMLElementTagNameMap {
"ha-list-item-new": HaListItemNew;
"ha-md-list-item": HaMdListItem;
}
}

View File

@@ -2,8 +2,8 @@ import { MdList } from "@material/web/list/list";
import { css } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-list-new")
export class HaListNew extends MdList {
@customElement("ha-md-list")
export class HaMdList extends MdList {
static override styles = [
...super.styles,
css`
@@ -16,6 +16,6 @@ export class HaListNew extends MdList {
declare global {
interface HTMLElementTagNameMap {
"ha-list-new": HaListNew;
"ha-md-list": HaMdList;
}
}

View File

@@ -2,8 +2,8 @@ import { MdMenuItem } from "@material/web/menu/menu-item";
import { css } from "lit";
import { customElement, property } from "lit/decorators";
@customElement("ha-menu-item")
export class HaMenuItem extends MdMenuItem {
@customElement("ha-md-menu-item")
export class HaMdMenuItem extends MdMenuItem {
@property({ attribute: false }) clickAction?: (item?: HTMLElement) => void;
static override styles = [
@@ -41,6 +41,6 @@ export class HaMenuItem extends MdMenuItem {
declare global {
interface HTMLElementTagNameMap {
"ha-menu-item": HaMenuItem;
"ha-md-menu-item": HaMdMenuItem;
}
}

View File

@@ -6,7 +6,7 @@ import {
} from "@material/web/menu/internal/controllers/shared";
import { css } from "lit";
import { customElement } from "lit/decorators";
import type { HaMenuItem } from "./ha-menu-item";
import type { HaMdMenuItem } from "./ha-md-menu-item";
@customElement("ha-menu")
export class HaMenu extends MdMenu {
@@ -22,7 +22,7 @@ export class HaMenu extends MdMenu {
) {
return;
}
(ev.detail.initiator as HaMenuItem).clickAction?.(ev.detail.initiator);
(ev.detail.initiator as HaMdMenuItem).clickAction?.(ev.detail.initiator);
}
static override styles = [

View File

@@ -24,9 +24,11 @@ export class HaOutlinedField extends MdOutlinedField {
}
.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

@@ -1,22 +0,0 @@
import { MdOutlinedSegmentedButtonSet } from "@material/web/labs/segmentedbuttonset/outlined-segmented-button-set";
import { css } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-outlined-segmented-button-set")
export class HaOutlinedSegmentedButtonSet extends MdOutlinedSegmentedButtonSet {
static override styles = [
...super.styles,
css`
:host {
--ha-icon-display: block;
--md-outlined-segmented-button-container-height: 32px;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-outlined-segmented-button-set": HaOutlinedSegmentedButtonSet;
}
}

View File

@@ -1,34 +0,0 @@
import { MdOutlinedSegmentedButton } from "@material/web/labs/segmentedbutton/outlined-segmented-button";
import { css } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-outlined-segmented-button")
export class HaOutlinedSegmentedButton extends MdOutlinedSegmentedButton {
static override styles = [
...super.styles,
css`
:host {
--ha-icon-display: block;
--md-outlined-segmented-button-selected-container-color: var(
--light-primary-color
);
--md-outlined-segmented-button-container-height: 32px;
--md-outlined-segmented-button-disabled-label-text-color: var(
--disabled-text-color
);
--md-outlined-segmented-button-disabled-icon-color: var(
--disabled-text-color
);
--md-outlined-segmented-button-disabled-outline-color: var(
--disabled-text-color
);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-outlined-segmented-button": HaOutlinedSegmentedButton;
}
}

View File

@@ -0,0 +1,185 @@
import { TextAreaCharCounter } from "@material/mwc-textfield/mwc-textfield-base";
import { mdiEye, mdiEyeOff } from "@mdi/js";
import { LitElement, css, html } from "lit";
import {
customElement,
eventOptions,
property,
query,
state,
} from "lit/decorators";
import { HomeAssistant } from "../types";
import "./ha-icon-button";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
@customElement("ha-password-field")
export class HaPasswordField extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public invalid?: boolean;
@property({ attribute: "error-message" }) public errorMessage?: string;
@property({ type: Boolean }) public icon = false;
@property({ type: Boolean }) public iconTrailing = false;
@property() public autocomplete?: string;
@property() public autocorrect?: string;
@property({ attribute: "input-spellcheck" })
public inputSpellcheck?: string;
@property({ type: String }) value = "";
@property({ type: String }) placeholder = "";
@property({ type: String }) label = "";
@property({ type: Boolean, reflect: true }) disabled = false;
@property({ type: Boolean }) required = false;
@property({ type: Number }) minLength = -1;
@property({ type: Number }) maxLength = -1;
@property({ type: Boolean, reflect: true }) outlined = false;
@property({ type: String }) helper = "";
@property({ type: Boolean }) validateOnInitialRender = false;
@property({ type: String }) validationMessage = "";
@property({ type: Boolean }) autoValidate = false;
@property({ type: String }) pattern = "";
@property({ type: Number }) size: number | null = null;
@property({ type: Boolean }) helperPersistent = false;
@property({ type: Boolean }) charCounter: boolean | TextAreaCharCounter =
false;
@property({ type: Boolean }) endAligned = false;
@property({ type: String }) prefix = "";
@property({ type: String }) suffix = "";
@property({ type: String }) name = "";
@property({ type: String, attribute: "input-mode" })
inputMode!: string;
@property({ type: Boolean }) readOnly = false;
@property({ type: String }) autocapitalize = "";
@state() private _unmaskedPassword = false;
@query("ha-textfield") private _textField!: HaTextField;
protected render() {
return html`<ha-textfield
.invalid=${this.invalid}
.errorMessage=${this.errorMessage}
.icon=${this.icon}
.iconTrailing=${this.iconTrailing}
.autocomplete=${this.autocomplete}
.autocorrect=${this.autocorrect}
.inputSpellcheck=${this.inputSpellcheck}
.value=${this.value}
.placeholder=${this.placeholder}
.label=${this.label}
.disabled=${this.disabled}
.required=${this.required}
.minLength=${this.minLength}
.maxLength=${this.maxLength}
.outlined=${this.outlined}
.helper=${this.helper}
.validateOnInitialRender=${this.validateOnInitialRender}
.validationMessage=${this.validationMessage}
.autoValidate=${this.autoValidate}
.pattern=${this.pattern}
.size=${this.size}
.helperPersistent=${this.helperPersistent}
.charCounter=${this.charCounter}
.endAligned=${this.endAligned}
.prefix=${this.prefix}
.name=${this.name}
.inputMode=${this.inputMode}
.readOnly=${this.readOnly}
.autocapitalize=${this.autocapitalize}
.type=${this._unmaskedPassword ? "text" : "password"}
.suffix=${html`<div style="width: 24px"></div>`}
@input=${this._handleInputChange}
></ha-textfield>
<ha-icon-button
toggles
.label=${this.hass?.localize(
this._unmaskedPassword
? "ui.components.selectors.text.hide_password"
: "ui.components.selectors.text.show_password"
) || (this._unmaskedPassword ? "Hide password" : "Show password")}
@click=${this._toggleUnmaskedPassword}
.path=${this._unmaskedPassword ? mdiEyeOff : mdiEye}
></ha-icon-button>`;
}
public checkValidity(): boolean {
return this._textField.checkValidity();
}
public reportValidity(): boolean {
return this._textField.reportValidity();
}
public setCustomValidity(message: string): void {
return this._textField.setCustomValidity(message);
}
public layout(): Promise<void> {
return this._textField.layout();
}
private _toggleUnmaskedPassword(): void {
this._unmaskedPassword = !this._unmaskedPassword;
}
@eventOptions({ passive: true })
private _handleInputChange(ev) {
this.value = ev.target.value;
}
static styles = css`
:host {
display: block;
position: relative;
}
ha-textfield {
width: 100%;
}
ha-icon-button {
position: absolute;
top: 8px;
right: 8px;
inset-inline-start: initial;
inset-inline-end: 8px;
--mdc-icon-button-size: 40px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
direction: var(--direction);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-password-field": HaPasswordField;
}
}

View File

@@ -1,6 +1,7 @@
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { Action } from "../../data/script";
import memoizeOne from "memoize-one";
import { Action, migrateAutomationAction } from "../../data/script";
import { ActionSelector } from "../../data/selector";
import "../../panels/config/automation/action/ha-automation-action";
import { HomeAssistant } from "../../types";
@@ -17,12 +18,19 @@ export class HaActionSelector extends LitElement {
@property({ type: Boolean, reflect: true }) public disabled = false;
private _actions = memoizeOne((action: Action | undefined) => {
if (!action) {
return [];
}
return migrateAutomationAction(action);
});
protected render() {
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<ha-automation-action
.disabled=${this.disabled}
.actions=${this.value || []}
.actions=${this._actions(this.value)}
.hass=${this.hass}
.path=${this.selector.action?.path}
></ha-automation-action>

View File

@@ -31,7 +31,7 @@ export class HaColorRGBSelector extends LitElement {
.label=${this.label || ""}
.required=${this.required}
.helper=${this.helper}
.disalbled=${this.disabled}
.disabled=${this.disabled}
@change=${this._valueChanged}
></ha-textfield>
`;

View File

@@ -7,12 +7,7 @@ import "../ha-code-editor";
import "../ha-input-helper-text";
import "../ha-alert";
const WARNING_STRINGS = [
"template:",
"sensor:",
"state:",
"platform: template",
];
const WARNING_STRINGS = ["template:", "sensor:", "state:", "trigger: template"];
@customElement("ha-selector-template")
export class HaTemplateSelector extends LitElement {

View File

@@ -1,6 +1,7 @@
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { Trigger } from "../../data/automation";
import memoizeOne from "memoize-one";
import { migrateAutomationTrigger, Trigger } from "../../data/automation";
import { TriggerSelector } from "../../data/selector";
import "../../panels/config/automation/trigger/ha-automation-trigger";
import { HomeAssistant } from "../../types";
@@ -17,12 +18,19 @@ export class HaTriggerSelector extends LitElement {
@property({ type: Boolean, reflect: true }) public disabled = false;
private _triggers = memoizeOne((trigger: Trigger | undefined) => {
if (!trigger) {
return [];
}
return migrateAutomationTrigger(trigger);
});
protected render() {
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<ha-automation-trigger
.disabled=${this.disabled}
.triggers=${this.value || []}
.triggers=${this._triggers(this.value)}
.hass=${this.hass}
.path=${this.selector.trigger?.path}
></ha-automation-trigger>

View File

@@ -24,6 +24,8 @@ export class HaSelectorUiColor extends LitElement {
.hass=${this.hass}
.value=${this.value}
.helper=${this.helper}
.includeNone=${this.selector.ui_color?.include_none}
.includeState=${this.selector.ui_color?.include_state}
.defaultColor=${this.selector.ui_color?.default_color}
@value-changed=${this._valueChanged}
></ha-color-picker>

View File

@@ -240,12 +240,24 @@ export class HaServiceControl extends LitElement {
...value,
selector: value.selector as Selector | undefined,
}));
const hasSelector: string[] = [];
fields.forEach((field) => {
if ((field as any).fields) {
Object.entries((field as any).fields).forEach(([key, subField]) => {
if ((subField as any).selector) {
hasSelector.push(key);
}
});
} else if (field.selector) {
hasSelector.push(field.key);
}
});
return {
...serviceDomains[domain][serviceName],
fields,
hasSelector: fields.length
? fields.filter((field) => field.selector).map((field) => field.key)
: [],
hasSelector,
};
}
);
@@ -793,7 +805,8 @@ export class HaServiceControl extends LitElement {
const value = ev.detail.value;
if (
this._value?.data?.[key] === value ||
(!this._value?.data?.[key] && (value === "" || value === undefined))
((!this._value?.data || !(key in this._value.data)) &&
(value === "" || value === undefined))
) {
return;
}

View File

@@ -43,6 +43,13 @@ export class HaSortable extends LitElement {
@property({ type: String, attribute: "handle-selector" })
public handleSelector?: string;
/**
* Selectors that do not lead to dragging (String or Function)
* https://github.com/SortableJS/Sortable?tab=readme-ov-file#filter-option
* */
@property({ type: String, attribute: "filter" })
public filter?: string;
@property({ type: String })
public group?: string | SortableInstance.GroupOptions;
@@ -145,6 +152,9 @@ export class HaSortable extends LitElement {
if (this.group) {
options.group = this.group;
}
if (this.filter) {
options.filter = this.filter;
}
this._sortable = new Sortable(container, options);
}

View File

@@ -35,10 +35,6 @@ import {
computeDeviceName,
} from "../data/device_registry";
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
import {
FloorRegistryEntry,
subscribeFloorRegistry,
} from "../data/floor_registry";
import {
LabelRegistryEntry,
subscribeLabelRegistry,
@@ -103,17 +99,12 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
@query(".add-container", true) private _addContainer?: HTMLDivElement;
@state() private _floors?: FloorRegistryEntry[];
@state() private _labels?: LabelRegistryEntry[];
private _opened = false;
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
return [
subscribeFloorRegistry(this.hass.connection, (floors) => {
this._floors = floors;
}),
subscribeLabelRegistry(this.hass.connection, (labels) => {
this._labels = labels;
}),
@@ -132,9 +123,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
<div class="mdc-chip-set items">
${this.value?.floor_id
? ensureArray(this.value.floor_id).map((floor_id) => {
const floor = this._floors?.find(
(flr) => flr.floor_id === floor_id
);
const floor = this.hass.floors[floor_id];
return this._renderChip(
"floor_id",
floor_id,

View File

@@ -6,7 +6,7 @@ import { mainWindow } from "../common/dom/get_main_window";
@customElement("ha-textfield")
export class HaTextField extends TextFieldBase {
@property({ type: Boolean }) public invalid = false;
@property({ type: Boolean }) public invalid?: boolean;
@property({ attribute: "error-message" }) public errorMessage?: string;
@@ -28,14 +28,24 @@ export class HaTextField extends TextFieldBase {
override updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (
(changedProperties.has("invalid") &&
(this.invalid || changedProperties.get("invalid") !== undefined)) ||
changedProperties.has("invalid") ||
changedProperties.has("errorMessage")
) {
this.setCustomValidity(
this.invalid ? this.errorMessage || "Invalid" : ""
this.invalid
? this.errorMessage || this.validationMessage || "Invalid"
: ""
);
this.reportValidity();
if (
this.invalid ||
this.validateOnInitialRender ||
(changedProperties.has("invalid") &&
changedProperties.get("invalid") !== undefined)
) {
// Only report validity if the field is invalid or the invalid state has changed from
// true to false to prevent setting empty required fields to invalid on first render
this.reportValidity();
}
}
if (changedProperties.has("autocomplete")) {
if (this.autocomplete) {
@@ -109,7 +119,7 @@ export class HaTextField extends TextFieldBase {
color: var(--secondary-text-color);
}
.mdc-text-field__icon {
.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon {
color: var(--secondary-text-color);
}

View File

@@ -7,16 +7,18 @@ import {
nothing,
PropertyValues,
} 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 type { HomeAssistant } from "../types";
import { haStyle } from "../resources/styles";
import "./ha-code-editor";
import { showToast } from "../util/toast";
import { copyToClipboard } from "../common/util/copy-clipboard";
import type { HaCodeEditor } from "./ha-code-editor";
import "./ha-button";
const isEmpty = (obj: Record<string, unknown>): boolean => {
if (typeof obj !== "object") {
if (typeof obj !== "object" || obj === null) {
return false;
}
for (const key in obj) {
@@ -53,16 +55,17 @@ export class HaYamlEditor extends LitElement {
@state() private _yaml = "";
@query("ha-code-editor") _codeEditor?: HaCodeEditor;
public setValue(value): void {
try {
this._yaml =
value && !isEmpty(value)
? dump(value, {
schema: this.yamlSchema,
quotingType: '"',
noRefs: true,
})
: "";
this._yaml = !isEmpty(value)
? dump(value, {
schema: this.yamlSchema,
quotingType: '"',
noRefs: true,
})
: "";
} catch (err: any) {
// eslint-disable-next-line no-console
console.error(err, value);
@@ -71,7 +74,7 @@ export class HaYamlEditor extends LitElement {
}
protected firstUpdated(): void {
if (this.defaultValue) {
if (this.defaultValue !== undefined) {
this.setValue(this.defaultValue);
}
}
@@ -83,6 +86,12 @@ export class HaYamlEditor extends LitElement {
}
}
public focus(): void {
if (this._codeEditor?.codemirror) {
this._codeEditor?.codemirror.focus();
}
}
protected render() {
if (this._yaml === undefined) {
return nothing;
@@ -90,7 +99,7 @@ export class HaYamlEditor extends LitElement {
return html`
${this.label
? html`<p>${this.label}${this.required ? " *" : ""}</p>`
: ""}
: nothing}
<ha-code-editor
.hass=${this.hass}
.value=${this._yaml}
@@ -103,16 +112,20 @@ export class HaYamlEditor extends LitElement {
dir="ltr"
></ha-code-editor>
${this.copyClipboard || this.hasExtraActions
? html`<div class="card-actions">
${this.copyClipboard
? html` <mwc-button @click=${this._copyYaml}>
${this.hass.localize(
"ui.components.yaml-editor.copy_to_clipboard"
)}
</mwc-button>`
: nothing}
<slot name="extra-actions"></slot>
</div>`
? html`
<div class="card-actions">
${this.copyClipboard
? html`
<ha-button @click=${this._copyYaml}>
${this.hass.localize(
"ui.components.yaml-editor.copy_to_clipboard"
)}
</ha-button>
`
: nothing}
<slot name="extra-actions"></slot>
</div>
`
: nothing}
`;
}

View File

@@ -22,7 +22,7 @@ import { LitElement, PropertyValues, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import { Condition, Trigger } from "../../data/automation";
import { Condition, Trigger, flattenTriggers } from "../../data/automation";
import {
Action,
ChooseAction,
@@ -94,7 +94,7 @@ export class HatScriptGraph extends LitElement {
@focus=${this.selectNode(config, path)}
?active=${this.selected === path}
.iconPath=${mdiAsterisk}
.notEnabled=${config.enabled === false}
.notEnabled=${"enabled" in config && config.enabled === false}
.error=${this.trace.trace[path]?.some((tr) => tr.error)}
tabindex=${track ? "0" : "-1"}
></hat-graph-node>
@@ -569,11 +569,16 @@ export class HatScriptGraph extends LitElement {
}
protected render() {
const triggerKey = "triggers" in this.trace.config ? "triggers" : "trigger";
const conditionKey =
"conditions" in this.trace.config ? "conditions" : "condition";
const actionKey = "actions" in this.trace.config ? "actions" : "action";
const paths = Object.keys(this.trackedNodes);
const trigger_nodes =
"trigger" in this.trace.config
? ensureArray(this.trace.config.trigger).map((trigger, i) =>
this.render_trigger(trigger, i)
triggerKey in this.trace.config
? flattenTriggers(ensureArray(this.trace.config[triggerKey])).map(
(trigger, i) => this.render_trigger(trigger, i)
)
: undefined;
try {
@@ -584,14 +589,14 @@ export class HatScriptGraph extends LitElement {
${trigger_nodes}
</hat-graph-branch>`
: ""}
${"condition" in this.trace.config
? html`${ensureArray(this.trace.config.condition)?.map(
${conditionKey in this.trace.config
? html`${ensureArray(this.trace.config[conditionKey])?.map(
(condition, i) => this.render_condition(condition, i)
)}`
: ""}
${"action" in this.trace.config
? html`${ensureArray(this.trace.config.action).map((action, i) =>
this.render_action_node(action, `action/${i}`)
${actionKey in this.trace.config
? html`${ensureArray(this.trace.config[actionKey]).map(
(action, i) => this.render_action_node(action, `action/${i}`)
)}`
: ""}
${"sequence" in this.trace.config

View File

@@ -22,13 +22,8 @@ import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_tim
import { relativeTime } from "../../common/datetime/relative_time";
import { fireEvent } from "../../common/dom/fire_event";
import { toggleAttribute } from "../../common/dom/toggle_attribute";
import {
floorsContext,
fullEntitiesContext,
labelsContext,
} from "../../data/context";
import { fullEntitiesContext, labelsContext } from "../../data/context";
import { EntityRegistryEntry } from "../../data/entity_registry";
import { FloorRegistryEntry } from "../../data/floor_registry";
import { LabelRegistryEntry } from "../../data/label_registry";
import { LogbookEntry } from "../../data/logbook";
import {
@@ -206,7 +201,6 @@ class ActionRenderer {
private hass: HomeAssistant,
private entityReg: EntityRegistryEntry[],
private labelReg: LabelRegistryEntry[],
private floorReg: FloorRegistryEntry[],
private entries: TemplateResult[],
private trace: AutomationTraceExtended,
private logbookRenderer: LogbookRenderer,
@@ -325,7 +319,6 @@ class ActionRenderer {
this.hass,
this.entityReg,
this.labelReg,
this.floorReg,
data,
actionType
),
@@ -493,13 +486,7 @@ class ActionRenderer {
const name =
repeatConfig.alias ||
describeAction(
this.hass,
this.entityReg,
this.labelReg,
this.floorReg,
repeatConfig
);
describeAction(this.hass, this.entityReg, this.labelReg, repeatConfig);
this._renderEntry(repeatPath, name, undefined, disabled);
@@ -597,7 +584,6 @@ class ActionRenderer {
this.hass,
this.entityReg,
this.labelReg,
this.floorReg,
sequenceConfig,
"sequence"
),
@@ -694,10 +680,6 @@ export class HaAutomationTracer extends LitElement {
@consume({ context: labelsContext, subscribe: true })
_labelReg!: LabelRegistryEntry[];
@state()
@consume({ context: floorsContext, subscribe: true })
_floorReg!: FloorRegistryEntry[];
protected render() {
if (!this.trace) {
return nothing;
@@ -715,7 +697,6 @@ export class HaAutomationTracer extends LitElement {
this.hass,
this._entityReg,
this._labelReg,
this._floorReg,
entries,
this.trace,
logbookRenderer,

View File

@@ -0,0 +1,81 @@
import { HassEntity } from "home-assistant-js-websocket";
import { HomeAssistant } from "../types";
import { supportsFeature } from "../common/entity/supports-feature";
import { UNAVAILABLE } from "./entity";
export const enum AssistSatelliteEntityFeature {
ANNOUNCE = 1,
}
export interface WakeWordInterceptMessage {
wake_word_phrase: string;
}
export interface WakeWordOption {
id: string;
wake_word: string;
trained_languages: string[];
}
export interface AssistSatelliteConfiguration {
active_wake_words: string[];
available_wake_words: WakeWordOption[];
max_active_wake_words: number;
pipeline_entity_id: string;
vad_entity_id: string;
}
export const interceptWakeWord = (
hass: HomeAssistant,
entity_id: string,
callback: (result: WakeWordInterceptMessage) => void
) =>
hass.connection.subscribeMessage(callback, {
type: "assist_satellite/intercept_wake_word",
entity_id,
});
export const testAssistSatelliteConnection = (
hass: HomeAssistant,
entity_id: string
) =>
hass.callWS<{
status: "success" | "timeout";
}>({
type: "assist_satellite/test_connection",
entity_id,
});
export const assistSatelliteAnnounce = (
hass: HomeAssistant,
entity_id: string,
message: string
) =>
hass.callService("assist_satellite", "announce", { message }, { entity_id });
export const fetchAssistSatelliteConfiguration = (
hass: HomeAssistant,
entity_id: string
) =>
hass.callWS<AssistSatelliteConfiguration>({
type: "assist_satellite/get_configuration",
entity_id,
});
export const setWakeWords = (
hass: HomeAssistant,
entity_id: string,
wake_word_ids: string[]
) =>
hass.callWS({
type: "assist_satellite/set_wake_words",
entity_id,
wake_word_ids,
});
export const assistSatelliteSupportsSetupFlow = (
assistSatelliteEntity: HassEntity | undefined
) =>
assistSatelliteEntity &&
assistSatelliteEntity.state !== UNAVAILABLE &&
supportsFeature(assistSatelliteEntity, AssistSatelliteEntityFeature.ANNOUNCE);

View File

@@ -3,6 +3,7 @@ import {
HassEntityBase,
} from "home-assistant-js-websocket";
import { navigate } from "../common/navigate";
import { ensureArray } from "../common/array/ensure-array";
import { Context, HomeAssistant } from "../types";
import { BlueprintInput } from "./blueprint";
import { DeviceCondition, DeviceTrigger } from "./device_automation";
@@ -26,8 +27,14 @@ export interface ManualAutomationConfig {
id?: string;
alias?: string;
description?: string;
trigger: Trigger | Trigger[];
triggers: Trigger | Trigger[];
/** @deprecated Use `triggers` instead */
trigger?: Trigger | Trigger[];
conditions?: Condition | Condition[];
/** @deprecated Use `conditions` instead */
condition?: Condition | Condition[];
actions: Action | Action[];
/** @deprecated Use `actions` instead */
action?: Action | Action[];
mode?: (typeof MODES)[number];
max?: number;
@@ -62,16 +69,22 @@ export interface ContextConstraint {
user_id?: string | string[];
}
export interface TriggerList {
triggers: Trigger | Trigger[] | undefined;
}
export interface BaseTrigger {
alias?: string;
platform: string;
/** @deprecated Use `trigger` instead */
platform?: string;
trigger: string;
id?: string;
variables?: Record<string, unknown>;
enabled?: boolean;
}
export interface StateTrigger extends BaseTrigger {
platform: "state";
trigger: "state";
entity_id: string | string[];
attribute?: string;
from?: string | string[];
@@ -80,25 +93,25 @@ export interface StateTrigger extends BaseTrigger {
}
export interface MqttTrigger extends BaseTrigger {
platform: "mqtt";
trigger: "mqtt";
topic: string;
payload?: string;
}
export interface GeoLocationTrigger extends BaseTrigger {
platform: "geo_location";
trigger: "geo_location";
source: string;
zone: string;
event: "enter" | "leave";
}
export interface HassTrigger extends BaseTrigger {
platform: "homeassistant";
trigger: "homeassistant";
event: "start" | "shutdown";
}
export interface NumericStateTrigger extends BaseTrigger {
platform: "numeric_state";
trigger: "numeric_state";
entity_id: string | string[];
attribute?: string;
above?: number;
@@ -108,69 +121,69 @@ export interface NumericStateTrigger extends BaseTrigger {
}
export interface ConversationTrigger extends BaseTrigger {
platform: "conversation";
trigger: "conversation";
command: string | string[];
}
export interface SunTrigger extends BaseTrigger {
platform: "sun";
trigger: "sun";
offset: number;
event: "sunrise" | "sunset";
}
export interface TimePatternTrigger extends BaseTrigger {
platform: "time_pattern";
trigger: "time_pattern";
hours?: number | string;
minutes?: number | string;
seconds?: number | string;
}
export interface WebhookTrigger extends BaseTrigger {
platform: "webhook";
trigger: "webhook";
webhook_id: string;
allowed_methods?: string[];
local_only?: boolean;
}
export interface PersistentNotificationTrigger extends BaseTrigger {
platform: "persistent_notification";
trigger: "persistent_notification";
notification_id?: string;
update_type?: string[];
}
export interface ZoneTrigger extends BaseTrigger {
platform: "zone";
trigger: "zone";
entity_id: string;
zone: string;
event: "enter" | "leave";
}
export interface TagTrigger extends BaseTrigger {
platform: "tag";
trigger: "tag";
tag_id: string;
device_id?: string;
}
export interface TimeTrigger extends BaseTrigger {
platform: "time";
trigger: "time";
at: string;
}
export interface TemplateTrigger extends BaseTrigger {
platform: "template";
trigger: "template";
value_template: string;
for?: string | number | ForDict;
}
export interface EventTrigger extends BaseTrigger {
platform: "event";
trigger: "event";
event_type: string;
event_data?: any;
context?: ContextConstraint;
}
export interface CalendarTrigger extends BaseTrigger {
platform: "calendar";
trigger: "calendar";
event: "start" | "end";
entity_id: string;
offset: string;
@@ -193,7 +206,8 @@ export type Trigger =
| TemplateTrigger
| EventTrigger
| DeviceTrigger
| CalendarTrigger;
| CalendarTrigger
| TriggerList;
interface BaseCondition {
condition: string;
@@ -357,22 +371,97 @@ export const normalizeAutomationConfig = <
>(
config: T
): T => {
config = migrateAutomationConfig(config);
// Normalize data: ensure triggers, actions and conditions are lists
// Happens when people copy paste their automations into the config
for (const key of ["trigger", "condition", "action"]) {
for (const key of ["triggers", "conditions", "actions"]) {
const value = config[key];
if (value && !Array.isArray(value)) {
config[key] = [value];
}
}
if (config.action) {
config.action = migrateAutomationAction(config.action);
return config;
};
export const migrateAutomationConfig = <
T extends Partial<AutomationConfig> | AutomationConfig,
>(
config: T
) => {
if ("trigger" in config) {
if (!("triggers" in config)) {
config.triggers = config.trigger;
}
delete config.trigger;
}
if ("condition" in config) {
if (!("conditions" in config)) {
config.conditions = config.condition;
}
delete config.condition;
}
if ("action" in config) {
if (!("actions" in config)) {
config.actions = config.action;
}
delete config.action;
}
if (config.triggers) {
config.triggers = migrateAutomationTrigger(config.triggers);
}
if (config.actions) {
config.actions = migrateAutomationAction(config.actions);
}
return config;
};
export const migrateAutomationTrigger = (
trigger: Trigger | Trigger[]
): Trigger | Trigger[] => {
if (Array.isArray(trigger)) {
return trigger.map(migrateAutomationTrigger) as Trigger[];
}
if ("triggers" in trigger && trigger.triggers) {
trigger.triggers = migrateAutomationTrigger(trigger.triggers);
}
if ("platform" in trigger) {
if (!("trigger" in trigger)) {
// @ts-ignore
trigger.trigger = trigger.platform;
}
delete trigger.platform;
}
return trigger;
};
export const flattenTriggers = (
triggers: undefined | Trigger | Trigger[]
): Trigger[] => {
if (!triggers) {
return [];
}
const flatTriggers: Trigger[] = [];
ensureArray(triggers).forEach((t) => {
if ("triggers" in t) {
if (t.triggers) {
flatTriggers.push(...flattenTriggers(t.triggers));
}
} else {
flatTriggers.push(t);
}
});
return flatTriggers;
};
export const showAutomationEditor = (data?: Partial<AutomationConfig>) => {
initialAutomationEditorData = data;
navigate("/config/automation/edit/new");

View File

@@ -22,6 +22,7 @@ import {
formatListWithAnds,
formatListWithOrs,
} from "../common/string/format-list";
import { isTriggerList } from "./trigger";
const triggerTranslationBaseKey =
"ui.panel.config.automation.editor.triggers.type";
@@ -68,9 +69,18 @@ export const describeTrigger = (
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
ignoreAlias = false
) => {
): string => {
try {
return tryDescribeTrigger(trigger, hass, entityRegistry, ignoreAlias);
const description = tryDescribeTrigger(
trigger,
hass,
entityRegistry,
ignoreAlias
);
if (typeof description !== "string") {
throw new Error(String(description));
}
return description;
} catch (error: any) {
// eslint-disable-next-line no-console
console.error(error);
@@ -89,12 +99,26 @@ const tryDescribeTrigger = (
entityRegistry: EntityRegistryEntry[],
ignoreAlias = false
) => {
if (isTriggerList(trigger)) {
const triggers = ensureArray(trigger.triggers);
if (!triggers || triggers.length === 0) {
return hass.localize(
`${triggerTranslationBaseKey}.list.description.no_trigger`
);
}
const count = triggers.length;
return hass.localize(`${triggerTranslationBaseKey}.list.description.full`, {
count: count,
});
}
if (trigger.alias && !ignoreAlias) {
return trigger.alias;
}
// Event Trigger
if (trigger.platform === "event" && trigger.event_type) {
if (trigger.trigger === "event" && trigger.event_type) {
const eventTypes: string[] = [];
if (Array.isArray(trigger.event_type)) {
@@ -113,7 +137,7 @@ const tryDescribeTrigger = (
}
// Home Assistant Trigger
if (trigger.platform === "homeassistant" && trigger.event) {
if (trigger.trigger === "homeassistant" && trigger.event) {
return hass.localize(
trigger.event === "start"
? `${triggerTranslationBaseKey}.homeassistant.description.started`
@@ -122,7 +146,7 @@ const tryDescribeTrigger = (
}
// Numeric State Trigger
if (trigger.platform === "numeric_state" && trigger.entity_id) {
if (trigger.trigger === "numeric_state" && trigger.entity_id) {
const entities: string[] = [];
const states = hass.states;
@@ -197,7 +221,7 @@ const tryDescribeTrigger = (
}
// State Trigger
if (trigger.platform === "state") {
if (trigger.trigger === "state") {
const entities: string[] = [];
const states = hass.states;
@@ -320,7 +344,7 @@ const tryDescribeTrigger = (
}
// Sun Trigger
if (trigger.platform === "sun" && trigger.event) {
if (trigger.trigger === "sun" && trigger.event) {
let duration = "";
if (trigger.offset) {
if (typeof trigger.offset === "number") {
@@ -341,12 +365,12 @@ const tryDescribeTrigger = (
}
// Tag Trigger
if (trigger.platform === "tag") {
if (trigger.trigger === "tag") {
return hass.localize(`${triggerTranslationBaseKey}.tag.description.full`);
}
// Time Trigger
if (trigger.platform === "time" && trigger.at) {
if (trigger.trigger === "time" && trigger.at) {
const result = ensureArray(trigger.at).map((at) =>
typeof at !== "string"
? at
@@ -361,7 +385,7 @@ const tryDescribeTrigger = (
}
// Time Pattern Trigger
if (trigger.platform === "time_pattern") {
if (trigger.trigger === "time_pattern") {
if (!trigger.seconds && !trigger.minutes && !trigger.hours) {
return hass.localize(
`${triggerTranslationBaseKey}.time_pattern.description.initial`
@@ -538,7 +562,7 @@ const tryDescribeTrigger = (
}
// Zone Trigger
if (trigger.platform === "zone" && trigger.entity_id && trigger.zone) {
if (trigger.trigger === "zone" && trigger.entity_id && trigger.zone) {
const entities: string[] = [];
const zones: string[] = [];
@@ -581,7 +605,7 @@ const tryDescribeTrigger = (
}
// Geo Location Trigger
if (trigger.platform === "geo_location" && trigger.source && trigger.zone) {
if (trigger.trigger === "geo_location" && trigger.source && trigger.zone) {
const sources: string[] = [];
const zones: string[] = [];
const states = hass.states;
@@ -620,12 +644,12 @@ const tryDescribeTrigger = (
}
// MQTT Trigger
if (trigger.platform === "mqtt") {
if (trigger.trigger === "mqtt") {
return hass.localize(`${triggerTranslationBaseKey}.mqtt.description.full`);
}
// Template Trigger
if (trigger.platform === "template") {
if (trigger.trigger === "template") {
let duration = "";
if (trigger.for) {
duration = describeDuration(hass.locale, trigger.for) ?? "";
@@ -638,14 +662,14 @@ const tryDescribeTrigger = (
}
// Webhook Trigger
if (trigger.platform === "webhook") {
if (trigger.trigger === "webhook") {
return hass.localize(
`${triggerTranslationBaseKey}.webhook.description.full`
);
}
// Conversation Trigger
if (trigger.platform === "conversation") {
if (trigger.trigger === "conversation") {
if (!trigger.command) {
return hass.localize(
`${triggerTranslationBaseKey}.conversation.description.empty`
@@ -664,14 +688,14 @@ const tryDescribeTrigger = (
}
// Persistent Notification Trigger
if (trigger.platform === "persistent_notification") {
if (trigger.trigger === "persistent_notification") {
return hass.localize(
`${triggerTranslationBaseKey}.persistent_notification.description.full`
);
}
// Device Trigger
if (trigger.platform === "device" && trigger.device_id) {
if (trigger.trigger === "device" && trigger.device_id) {
const config = trigger as DeviceTrigger;
const localized = localizeDeviceAutomationTrigger(
hass,
@@ -689,7 +713,7 @@ const tryDescribeTrigger = (
return (
hass.localize(
`ui.panel.config.automation.editor.triggers.type.${trigger.platform}.label`
`ui.panel.config.automation.editor.triggers.type.${trigger.trigger}.label`
) ||
hass.localize(`ui.panel.config.automation.editor.triggers.unknown_trigger`)
);
@@ -700,9 +724,18 @@ export const describeCondition = (
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
ignoreAlias = false
) => {
): string => {
try {
return tryDescribeCondition(condition, hass, entityRegistry, ignoreAlias);
const description = tryDescribeCondition(
condition,
hass,
entityRegistry,
ignoreAlias
);
if (typeof description !== "string") {
throw new Error(String(description));
}
return description;
} catch (error: any) {
// eslint-disable-next-line no-console
console.error(error);
@@ -889,8 +922,14 @@ const tryDescribeCondition = (
// Numeric State Condition
if (condition.condition === "numeric_state" && condition.entity_id) {
const stateObj = hass.states[condition.entity_id];
const entity = stateObj ? computeStateName(stateObj) : condition.entity_id;
const entity_ids = ensureArray(condition.entity_id);
const stateObj = hass.states[entity_ids[0]];
const entity = formatListWithAnds(
hass.locale,
entity_ids.map((id) =>
hass.states[id] ? computeStateName(hass.states[id]) : id || ""
)
);
const attribute = condition.attribute
? computeAttributeNameDisplay(
@@ -905,8 +944,9 @@ const tryDescribeCondition = (
return hass.localize(
`${conditionsTranslationBaseKey}.numeric_state.description.above-below`,
{
attribute: attribute,
entity: entity,
attribute,
entity,
numberOfEntities: entity_ids.length,
above: condition.above,
below: condition.below,
}
@@ -916,8 +956,9 @@ const tryDescribeCondition = (
return hass.localize(
`${conditionsTranslationBaseKey}.numeric_state.description.above`,
{
attribute: attribute,
entity: entity,
attribute,
entity,
numberOfEntities: entity_ids.length,
above: condition.above,
}
);
@@ -926,8 +967,9 @@ const tryDescribeCondition = (
return hass.localize(
`${conditionsTranslationBaseKey}.numeric_state.description.below`,
{
attribute: attribute,
entity: entity,
attribute,
entity,
numberOfEntities: entity_ids.length,
below: condition.below,
}
);

View File

@@ -10,7 +10,7 @@ interface InvalidConfig {
error: string;
}
type ValidKeys = "trigger" | "action" | "condition";
type ValidKeys = "triggers" | "actions" | "conditions";
export const validateConfig = <
T extends Partial<{ [key in ValidKeys]: unknown }>,

View File

@@ -2,7 +2,6 @@ import { createContext } from "@lit-labs/context";
import { HassConfig } from "home-assistant-js-websocket";
import { HomeAssistant } from "../types";
import { EntityRegistryEntry } from "./entity_registry";
import { FloorRegistryEntry } from "./floor_registry";
import { LabelRegistryEntry } from "./label_registry";
export const connectionContext =
@@ -28,6 +27,4 @@ export const panelsContext = createContext<HomeAssistant["panels"]>("panels");
export const fullEntitiesContext =
createContext<EntityRegistryEntry[]>("extendedEntities");
export const floorsContext = createContext<FloorRegistryEntry[]>("floors");
export const labelsContext = createContext<LabelRegistryEntry[]>("labels");

View File

@@ -1,10 +1,20 @@
export interface DataTableFilters {
[key: string]: {
value: string[] | { key: string[] } | undefined;
value: DataTableFiltersValue;
items: Set<string> | undefined;
};
}
export type DataTableFiltersValue = string[] | { key: string[] } | undefined;
export interface DataTableFiltersValues {
[key: string]: DataTableFiltersValue;
}
export interface DataTableFiltersItems {
[key: string]: Set<string> | undefined;
}
export const serializeFilters = (value: DataTableFilters) => {
const serializedValue = {};
Object.entries(value).forEach(([key, val]) => {

View File

@@ -1,7 +1,7 @@
import { computeStateName } from "../common/entity/compute_state_name";
import type { HaFormSchema } from "../components/ha-form/types";
import { HomeAssistant } from "../types";
import { BaseTrigger } from "./automation";
import { BaseTrigger, migrateAutomationTrigger } from "./automation";
import {
computeEntityRegistryName,
entityRegistryByEntityId,
@@ -31,7 +31,7 @@ export interface DeviceCondition extends DeviceAutomation {
export type DeviceTrigger = DeviceAutomation &
BaseTrigger & {
platform: "device";
trigger: "device";
};
export interface DeviceCapabilities {
@@ -51,10 +51,12 @@ export const fetchDeviceConditions = (hass: HomeAssistant, deviceId: string) =>
});
export const fetchDeviceTriggers = (hass: HomeAssistant, deviceId: string) =>
hass.callWS<DeviceTrigger[]>({
type: "device_automation/trigger/list",
device_id: deviceId,
});
hass
.callWS<DeviceTrigger[]>({
type: "device_automation/trigger/list",
device_id: deviceId,
})
.then((triggers) => migrateAutomationTrigger(triggers) as DeviceTrigger[]);
export const fetchDeviceActionCapabilities = (
hass: HomeAssistant,
@@ -91,7 +93,7 @@ const deviceAutomationIdentifiers = [
"subtype",
"event",
"condition",
"platform",
"trigger",
];
export const deviceAutomationsEqual = (

View File

@@ -1,7 +1,4 @@
import { Connection, createCollection } from "home-assistant-js-websocket";
import { Store } from "home-assistant-js-websocket/dist/store";
import { stringCompare } from "../common/string/compare";
import { debounce } from "../common/util/debounce";
import { HomeAssistant } from "../types";
import { AreaRegistryEntry } from "./area_registry";
import { RegistryEntry } from "./registry";
@@ -27,48 +24,6 @@ export interface FloorRegistryEntryMutableParams {
aliases?: string[];
}
const fetchFloorRegistry = (conn: Connection) =>
conn
.sendMessagePromise({
type: "config/floor_registry/list",
})
.then((floors) =>
(floors as FloorRegistryEntry[]).sort((ent1, ent2) => {
if (ent1.level !== ent2.level) {
return (ent1.level ?? 9999) - (ent2.level ?? 9999);
}
return stringCompare(ent1.name, ent2.name);
})
);
const subscribeFloorRegistryUpdates = (
conn: Connection,
store: Store<FloorRegistryEntry[]>
) =>
conn.subscribeEvents(
debounce(
() =>
fetchFloorRegistry(conn).then((areas: FloorRegistryEntry[]) =>
store.setState(areas, true)
),
500,
true
),
"floor_registry_updated"
);
export const subscribeFloorRegistry = (
conn: Connection,
onChange: (floors: FloorRegistryEntry[]) => void
) =>
createCollection<FloorRegistryEntry[]>(
"_floorRegistry",
fetchFloorRegistry,
subscribeFloorRegistryUpdates,
conn,
onChange
);
export const createFloorRegistryEntry = (
hass: HomeAssistant,
values: FloorRegistryEntryMutableParams

View File

@@ -3,10 +3,13 @@ import type { LovelaceCardConfig } from "./card";
import type { LovelaceStrategyConfig } from "./strategy";
export interface LovelaceBaseSectionConfig {
title?: string;
visibility?: Condition[];
column_span?: number;
row_span?: number;
/**
* @deprecated Use heading card instead.
*/
title?: string;
}
export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig {

View File

@@ -2,6 +2,8 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { navigate } from "../common/navigate";
import { HomeAssistant } from "../types";
import { subscribeDeviceRegistry } from "./device_registry";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { getThreadDataSetTLV, listThreadDataSets } from "./thread";
export enum NetworkType {
THREAD = "thread",
@@ -51,10 +53,31 @@ export interface MatterCommissioningParameters {
export const canCommissionMatterExternal = (hass: HomeAssistant) =>
hass.auth.external?.config.canCommissionMatter;
export const startExternalCommissioning = (hass: HomeAssistant) =>
hass.auth.external!.fireMessage({
export const startExternalCommissioning = async (hass: HomeAssistant) => {
if (isComponentLoaded(hass, "thread")) {
const datasets = await listThreadDataSets(hass);
const preferredDataset = datasets.datasets.find(
(dataset) => dataset.preferred
);
if (preferredDataset) {
return hass.auth.external!.fireMessage({
type: "matter/commission",
payload: {
active_operational_dataset: (
await getThreadDataSetTLV(hass, preferredDataset.dataset_id)
).tlv,
border_agent_id: preferredDataset.preferred_border_agent_id,
mac_extended_address: preferredDataset.preferred_extended_address,
extended_pan_id: preferredDataset.extended_pan_id,
},
});
}
}
return hass.auth.external!.fireMessage({
type: "matter/commission",
});
};
export const redirectOnNewMatterDevice = (
hass: HomeAssistant,

View File

@@ -245,7 +245,8 @@ export const computeMediaDescription = (
secondaryTitle = stateObj.attributes.media_artist!;
break;
case "playlist":
secondaryTitle = stateObj.attributes.media_playlist!;
secondaryTitle =
stateObj.attributes.media_playlist || stateObj.attributes.media_artist!;
break;
case "tvshow":
secondaryTitle = stateObj.attributes.media_series_title!;

View File

@@ -47,11 +47,19 @@ export interface StatisticsMetaData {
unit_class: string | null;
}
export const STATISTIC_TYPES: StatisticsValidationResult["type"][] = [
"entity_not_recorded",
"entity_no_longer_recorded",
"state_class_removed",
"units_changed",
"no_state",
];
export type StatisticsValidationResult =
| StatisticsValidationResultNoState
| StatisticsValidationResultEntityNotRecorded
| StatisticsValidationResultEntityNoLongerRecorded
| StatisticsValidationResultUnsupportedStateClass
| StatisticsValidationResultStateClassRemoved
| StatisticsValidationResultUnitsChanged;
export interface StatisticsValidationResultNoState {
@@ -69,9 +77,9 @@ export interface StatisticsValidationResultEntityNotRecorded {
data: { statistic_id: string };
}
export interface StatisticsValidationResultUnsupportedStateClass {
type: "unsupported_state_class";
data: { statistic_id: string; state_class: string };
export interface StatisticsValidationResultStateClassRemoved {
type: "state_class_removed";
data: { statistic_id: string };
}
export interface StatisticsValidationResultUnitsChanged {

View File

@@ -20,6 +20,7 @@ import { navigate } from "../common/navigate";
import { HomeAssistant } from "../types";
import {
Condition,
migrateAutomationTrigger,
ShorthandAndCondition,
ShorthandNotCondition,
ShorthandOrCondition,
@@ -404,7 +405,7 @@ export const getActionType = (action: Action): ActionType => {
if ("set_conversation_response" in action) {
return "set_conversation_response";
}
if ("action" in action) {
if ("action" in action || "service" in action) {
if ("metadata" in action) {
if (is(action, activateSceneActionStruct)) {
return "activate_scene";
@@ -480,5 +481,10 @@ export const migrateAutomationAction = (
}
}
if (actionType === "wait_for_trigger") {
const _action = action as WaitForTriggerAction;
migrateAutomationTrigger(_action.wait_for_trigger);
}
return action;
};

View File

@@ -14,7 +14,6 @@ import {
computeEntityRegistryName,
entityRegistryById,
} from "./entity_registry";
import { FloorRegistryEntry } from "./floor_registry";
import { domainToName } from "./integration";
import { LabelRegistryEntry } from "./label_registry";
import {
@@ -44,21 +43,23 @@ export const describeAction = <T extends ActionType>(
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
labelRegistry: LabelRegistryEntry[],
floorRegistry: FloorRegistryEntry[],
action: ActionTypes[T],
actionType?: T,
ignoreAlias = false
): string => {
try {
return tryDescribeAction(
const description = tryDescribeAction(
hass,
entityRegistry,
labelRegistry,
floorRegistry,
action,
actionType,
ignoreAlias
);
if (typeof description !== "string") {
throw new Error(String(description));
}
return description;
} catch (error: any) {
// eslint-disable-next-line no-console
console.error(error);
@@ -74,7 +75,6 @@ const tryDescribeAction = <T extends ActionType>(
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
labelRegistry: LabelRegistryEntry[],
floorRegistry: FloorRegistryEntry[],
action: ActionTypes[T],
actionType?: T,
ignoreAlias = false
@@ -164,9 +164,7 @@ const tryDescribeAction = <T extends ActionType>(
);
}
} else if (key === "floor_id") {
const floor = floorRegistry.find(
(flr) => flr.floor_id === targetThing
);
const floor = hass.floors[targetThing] ?? undefined;
if (floor?.name) {
targets.push(floor.name);
} else {

View File

@@ -454,7 +454,11 @@ export interface UiActionSelector {
export interface UiColorSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
ui_color: { default_color?: boolean } | null;
ui_color: {
default_color?: string;
include_none?: boolean;
include_state?: boolean;
} | null;
}
export interface UiStateContentSelector {

View File

@@ -3,6 +3,7 @@ import { Context, HomeAssistant } from "../types";
import {
BlueprintAutomationConfig,
ManualAutomationConfig,
flattenTriggers,
} from "./automation";
import { BlueprintScriptConfig, ScriptConfig } from "./script";
@@ -186,11 +187,26 @@ export const getDataFromPath = (
const asNumber = Number(raw);
if (isNaN(asNumber)) {
const tempResult = result[raw];
let tempResult = result[raw];
if (!tempResult && raw === "sequence") {
continue;
}
result = tempResult;
if (!tempResult && raw === "trigger") {
tempResult = result.triggers;
}
if (!tempResult && raw === "condition") {
tempResult = result.conditions;
}
if (!tempResult && raw === "action") {
tempResult = result.actions;
}
if (raw === "trigger") {
result = flattenTriggers(tempResult);
} else {
result = tempResult;
}
continue;
}

View File

@@ -5,6 +5,7 @@ import {
mdiCodeBraces,
mdiDevices,
mdiDotsHorizontal,
mdiFormatListBulleted,
mdiGestureDoubleTap,
mdiMapClock,
mdiMapMarker,
@@ -21,7 +22,7 @@ import {
} from "@mdi/js";
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
import { AutomationElementGroup } from "./automation";
import { AutomationElementGroup, Trigger, TriggerList } from "./automation";
export const TRIGGER_ICONS = {
calendar: mdiCalendar,
@@ -41,6 +42,7 @@ export const TRIGGER_ICONS = {
webhook: mdiWebhook,
persistent_notification: mdiMessageAlert,
zone: mdiMapMarkerRadius,
list: mdiFormatListBulleted,
};
export const TRIGGER_GROUPS: AutomationElementGroup = {
@@ -65,3 +67,6 @@ export const TRIGGER_GROUPS: AutomationElementGroup = {
},
},
} as const;
export const isTriggerList = (trigger: Trigger): trigger is TriggerList =>
"triggers" in trigger;

View File

@@ -0,0 +1,47 @@
import { Connection, createCollection } from "home-assistant-js-websocket";
import { Store } from "home-assistant-js-websocket/dist/store";
import { stringCompare } from "../common/string/compare";
import { debounce } from "../common/util/debounce";
import { FloorRegistryEntry } from "./floor_registry";
const fetchFloorRegistry = (conn: Connection) =>
conn
.sendMessagePromise({
type: "config/floor_registry/list",
})
.then((floors) =>
(floors as FloorRegistryEntry[]).sort((ent1, ent2) => {
if (ent1.level !== ent2.level) {
return (ent1.level ?? 9999) - (ent2.level ?? 9999);
}
return stringCompare(ent1.name, ent2.name);
})
);
const subscribeFloorRegistryUpdates = (
conn: Connection,
store: Store<FloorRegistryEntry[]>
) =>
conn.subscribeEvents(
debounce(
() =>
fetchFloorRegistry(conn).then((areas: FloorRegistryEntry[]) =>
store.setState(areas, true)
),
500,
true
),
"floor_registry_updated"
);
export const subscribeFloorRegistry = (
conn: Connection,
onChange: (floors: FloorRegistryEntry[]) => void
) =>
createCollection<FloorRegistryEntry[]>(
"_floorRegistry",
fetchFloorRegistry,
subscribeFloorRegistryUpdates,
conn,
onChange
);

View File

@@ -252,6 +252,7 @@ export interface ZWaveJSNodeConfigParamMetadata {
type: string;
unit: string;
states: { [key: number]: string };
default: any;
}
export interface ZWaveJSSetConfigParamData {

View File

@@ -31,6 +31,11 @@ export interface FlowConfig {
deleteFlow(hass: HomeAssistant, flowId: string): Promise<unknown>;
renderAbortHeader?(
hass: HomeAssistant,
step: DataEntryFlowStepAbort
): TemplateResult | string;
renderAbortDescription(
hass: HomeAssistant,
step: DataEntryFlowStepAbort
@@ -39,7 +44,7 @@ export interface FlowConfig {
renderShowFormStepHeader(
hass: HomeAssistant,
step: DataEntryFlowStepForm
): string;
): string | TemplateResult;
renderShowFormStepDescription(
hass: HomeAssistant,
@@ -95,14 +100,17 @@ export interface FlowConfig {
renderShowFormProgressHeader(
hass: HomeAssistant,
step: DataEntryFlowStepProgress
): string;
): string | TemplateResult;
renderShowFormProgressDescription(
hass: HomeAssistant,
step: DataEntryFlowStepProgress
): TemplateResult | "";
renderMenuHeader(hass: HomeAssistant, step: DataEntryFlowStepMenu): string;
renderMenuHeader(
hass: HomeAssistant,
step: DataEntryFlowStepMenu
): string | TemplateResult;
renderMenuDescription(
hass: HomeAssistant,

View File

@@ -31,7 +31,11 @@ class StepFlowAbort extends LitElement {
return nothing;
}
return html`
<h2>${this.hass.localize(`component.${this.domain}.title`)}</h2>
<h2>
${this.params.flowConfig.renderAbortHeader
? this.params.flowConfig.renderAbortHeader(this.hass, this.step)
: this.hass.localize(`component.${this.domain}.title`)}
</h2>
<div class="content">
${this.params.flowConfig.renderAbortDescription(this.hass, this.step)}
</div>

View File

@@ -1,6 +1,14 @@
import "@material/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-area-picker";
import { DataEntryFlowStepCreateEntry } from "../../data/data_entry_flow";
@@ -9,10 +17,14 @@ import {
DeviceRegistryEntry,
updateDeviceRegistryEntry,
} from "../../data/device_registry";
import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import { HomeAssistant } from "../../types";
import { showAlertDialog } from "../generic/show-dialog-box";
import { FlowConfig } from "./show-dialog-data-entry-flow";
import { configFlowContentStyles } from "./styles";
import { computeDomain } from "../../common/entity/compute_domain";
import { showVoiceAssistantSetupDialog } from "../voice-assistant-setup/show-voice-assistant-setup-dialog";
import { assistSatelliteSupportsSetupFlow } from "../../data/assist_satellite";
@customElement("step-flow-create-entry")
class StepFlowCreateEntry extends LitElement {
@@ -24,6 +36,46 @@ class StepFlowCreateEntry extends LitElement {
@property({ attribute: false }) public devices!: DeviceRegistryEntry[];
private _deviceEntities = memoizeOne(
(
deviceId: string,
entities: EntityRegistryDisplayEntry[],
domain?: string
): EntityRegistryDisplayEntry[] =>
entities.filter(
(entity) =>
entity.device_id === deviceId &&
(!domain || computeDomain(entity.entity_id) === domain)
)
);
protected willUpdate(changedProps: PropertyValues) {
if (
(changedProps.has("devices") || changedProps.has("hass")) &&
this.devices.length === 1
) {
// integration_type === "device"
const assistSatellites = this._deviceEntities(
this.devices[0].id,
Object.values(this.hass.entities),
"assist_satellite"
);
if (
assistSatellites.length &&
assistSatellites.some((satellite) =>
assistSatelliteSupportsSetupFlow(
this.hass.states[satellite.entity_id]
)
)
) {
this._flowDone();
showVoiceAssistantSetupDialog(this, {
deviceId: this.devices[0].id,
});
}
}
}
protected render(): TemplateResult {
const localize = this.hass.localize;

View File

@@ -1,13 +1,14 @@
import "@material/mwc-button/mwc-button";
import { mdiAlertOutline } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-dialog";
import "../../components/ha-md-dialog";
import type { HaMdDialog } from "../../components/ha-md-dialog";
import "../../components/ha-dialog-header";
import "../../components/ha-svg-icon";
import "../../components/ha-switch";
import "../../components/ha-button";
import { HaTextField } from "../../components/ha-textfield";
import { HomeAssistant } from "../../types";
import { DialogBoxParams } from "./show-dialog-box";
@@ -18,8 +19,12 @@ class DialogBox extends LitElement {
@state() private _params?: DialogBoxParams;
@state() private _closeState?: "canceled" | "confirmed";
@query("ha-textfield") private _textField?: HaTextField;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public async showDialog(params: DialogBoxParams): Promise<void> {
this._params = params;
}
@@ -42,33 +47,33 @@ class DialogBox extends LitElement {
const confirmPrompt = this._params.confirmation || this._params.prompt;
const dialogTitle =
this._params.title ||
(this._params.confirmation &&
this.hass.localize("ui.dialogs.generic.default_confirmation_title"));
return html`
<ha-dialog
<ha-md-dialog
open
?scrimClickAction=${confirmPrompt}
?escapeKeyAction=${confirmPrompt}
.disableCancelAction=${confirmPrompt || false}
@closed=${this._dialogClosed}
defaultAction="ignore"
.heading=${html`${this._params.warning
? html`<ha-svg-icon
.path=${mdiAlertOutline}
style="color: var(--warning-color)"
></ha-svg-icon> `
: ""}${this._params.title
? this._params.title
: this._params.confirmation &&
this.hass.localize(
"ui.dialogs.generic.default_confirmation_title"
)}`}
type="alert"
aria-labelledby="dialog-box-title"
aria-describedby="dialog-box-description"
>
<div>
${this._params.text
? html`
<p class=${this._params.prompt ? "no-bottom-padding" : ""}>
${this._params.text}
</p>
`
: ""}
<div slot="headline">
<span .title=${dialogTitle} id="dialog-box-title">
${this._params.warning
? html`<ha-svg-icon
.path=${mdiAlertOutline}
style="color: var(--warning-color)"
></ha-svg-icon> `
: nothing}
${dialogTitle}
</span>
</div>
<div slot="content" id="dialog-box-description">
${this._params.text ? html` <p>${this._params.text}</p> ` : ""}
${this._params.prompt
? html`
<ha-textfield
@@ -87,63 +92,68 @@ class DialogBox extends LitElement {
`
: ""}
</div>
${confirmPrompt &&
html`
<mwc-button
@click=${this._dismiss}
slot="secondaryAction"
<div slot="actions">
${confirmPrompt &&
html`
<ha-button
@click=${this._dismiss}
?dialogInitialFocus=${!this._params.prompt &&
this._params.destructive}
>
${this._params.dismissText
? this._params.dismissText
: this.hass.localize("ui.dialogs.generic.cancel")}
</ha-button>
`}
<ha-button
@click=${this._confirm}
?dialogInitialFocus=${!this._params.prompt &&
this._params.destructive}
!this._params.destructive}
class=${classMap({
destructive: this._params.destructive || false,
})}
>
${this._params.dismissText
? this._params.dismissText
: this.hass.localize("ui.dialogs.generic.cancel")}
</mwc-button>
`}
<mwc-button
@click=${this._confirm}
?dialogInitialFocus=${!this._params.prompt &&
!this._params.destructive}
slot="primaryAction"
class=${classMap({
destructive: this._params.destructive || false,
})}
>
${this._params.confirmText
? this._params.confirmText
: this.hass.localize("ui.dialogs.generic.ok")}
</mwc-button>
</ha-dialog>
${this._params.confirmText
? this._params.confirmText
: this.hass.localize("ui.dialogs.generic.ok")}
</ha-button>
</div>
</ha-md-dialog>
`;
}
private _dismiss(): void {
private _cancel(): void {
if (this._params?.cancel) {
this._params.cancel();
}
this._close();
}
private _dismiss(): void {
this._closeState = "canceled";
this._closeDialog();
this._cancel();
}
private _confirm(): void {
this._closeState = "confirmed";
this._closeDialog();
if (this._params!.confirm) {
this._params!.confirm(this._textField?.value);
}
this._close();
}
private _dialogClosed(ev) {
if (ev.detail.action === "ignore") {
return;
}
this._dismiss();
}
private _close(): void {
if (!this._params) {
return;
}
this._params = undefined;
private _closeDialog() {
fireEvent(this, "dialog-closed", { dialog: this.localName });
this._dialog?.close();
}
private _dialogClosed() {
if (!this._closeState) {
fireEvent(this, "dialog-closed", { dialog: this.localName });
this._cancel();
}
this._closeState = undefined;
this._params = undefined;
}
static get styles(): CSSResultGroup {
@@ -168,15 +178,6 @@ class DialogBox extends LitElement {
.destructive {
--mdc-theme-primary: var(--error-color);
}
ha-dialog {
/* Place above other dialogs */
--dialog-z-index: 104;
}
@media all and (min-width: 600px) {
ha-dialog {
--mdc-dialog-min-width: 400px;
}
}
ha-textfield {
width: 100%;
}

View File

@@ -1,15 +1,18 @@
import { mdiClose } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, state, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog";
import {
getMobileOpenFromBottomAnimation,
getMobileCloseToBottomAnimation,
} from "../../../../components/ha-md-dialog";
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-icon-button-toggle";
import type { EntityRegistryEntry } from "../../../../data/entity_registry";
import {
formatTempColor,
LightColor,
LightColorMode,
LightEntity,
@@ -38,15 +41,7 @@ class DialogLightColorFavorite extends LitElement {
@state() private _modes: LightPickerMode[] = [];
@state() private _currentValue?: string;
private _colorHovered(ev: CustomEvent<HASSDomEvents["color-hovered"]>) {
if (ev.detail && "color_temp_kelvin" in ev.detail) {
this._currentValue = formatTempColor(ev.detail.color_temp_kelvin);
} else {
this._currentValue = undefined;
}
}
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public async showDialog(
dialogParams: LightColorFavoriteDialogParams
@@ -58,10 +53,7 @@ class DialogLightColorFavorite extends LitElement {
}
public closeDialog(): void {
this._dialogParams = undefined;
this._entry = undefined;
this._color = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
this._dialog?.close();
}
private _updateModes() {
@@ -130,9 +122,20 @@ class DialogLightColorFavorite extends LitElement {
private async _cancel() {
this._dialogParams?.cancel?.();
}
private _cancelDialog() {
this._cancel();
this.closeDialog();
}
private _dialogClosed(): void {
this._dialogParams = undefined;
this._entry = undefined;
this._color = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
private async _save() {
if (!this._color) {
this._cancel();
@@ -156,82 +159,83 @@ class DialogLightColorFavorite extends LitElement {
}
return html`
<ha-dialog
<ha-md-dialog
open
@closed=${this._cancel}
.heading=${this._dialogParams?.title ?? ""}
flexContent
@cancel=${this._cancel}
@closed=${this._dialogClosed}
aria-labelledby="dialog-light-color-favorite-title"
.getOpenAnimation=${getMobileOpenFromBottomAnimation}
.getCloseAnimation=${getMobileCloseToBottomAnimation}
>
<ha-dialog-header slot="heading">
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
dialogAction="cancel"
@click=${this.closeDialog}
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
<span slot="title">${this._dialogParams?.title}</span>
<span slot="title" id="dialog-light-color-favorite-title"
>${this._dialogParams?.title}</span
>
</ha-dialog-header>
<div class="header">
<span class="value">${this._currentValue}</span>
${this._modes.length > 1
? html`
<div class="modes">
${this._modes.map(
(value) => html`
<ha-icon-button-toggle
border-only
.selected=${value === this._mode}
.label=${this.hass.localize(
`ui.dialogs.more_info_control.light.color_picker.mode.${value}`
)}
.mode=${value}
@click=${this._modeChanged}
>
<span
class="wheel ${classMap({ [value]: true })}"
></span>
</ha-icon-button-toggle>
`
)}
</div>
`
: nothing}
<div slot="content">
<div class="header">
${this._modes.length > 1
? html`
<div class="modes">
${this._modes.map(
(value) => html`
<ha-icon-button-toggle
border-only
.selected=${value === this._mode}
.label=${this.hass.localize(
`ui.dialogs.more_info_control.light.color_picker.mode.${value}`
)}
.mode=${value}
@click=${this._modeChanged}
>
<span
class="wheel ${classMap({ [value]: true })}"
></span>
</ha-icon-button-toggle>
`
)}
</div>
`
: nothing}
</div>
<div class="content">
${this._mode === "color_temp"
? html`
<light-color-temp-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
@color-changed=${this._colorChanged}
>
</light-color-temp-picker>
`
: nothing}
${this._mode === "color"
? html`
<light-color-rgb-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
@color-changed=${this._colorChanged}
>
</light-color-rgb-picker>
`
: nothing}
</div>
</div>
<div class="content">
${this._mode === "color_temp"
? html`
<light-color-temp-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
@color-changed=${this._colorChanged}
@color-hovered=${this._colorHovered}
>
</light-color-temp-picker>
`
: nothing}
${this._mode === "color"
? html`
<light-color-rgb-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
@color-changed=${this._colorChanged}
@color-hovered=${this._colorHovered}
>
</light-color-rgb-picker>
`
: nothing}
<div slot="actions">
<ha-button @click=${this._cancelDialog}>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button @click=${this._save} .disabled=${!this._color}
>${this.hass.localize("ui.common.save")}</ha-button
>
</div>
<ha-button slot="secondaryAction" dialogAction="cancel">
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._save}
.disabled=${!this._color}
>${this.hass.localize("ui.common.save")}</ha-button
>
</ha-dialog>
</ha-md-dialog>
`;
}
@@ -239,19 +243,23 @@ class DialogLightColorFavorite extends LitElement {
return [
haStyleDialog,
css`
ha-dialog {
--dialog-content-padding: 0;
ha-md-dialog {
min-width: 420px; /* prevent width jumps when switching modes */
max-height: min(
600px,
100% - 48px
); /* prevent scrolling on desktop */
}
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-dialog {
--dialog-surface-margin-top: 100px;
--mdc-dialog-min-height: auto;
--mdc-dialog-max-height: calc(100% - 100px);
--ha-dialog-border-radius: var(
--ha-dialog-bottom-sheet-border-radius,
28px 28px 0 0
);
ha-md-dialog {
min-width: 100%;
min-height: auto;
max-height: calc(100% - 100px);
margin-bottom: 0;
--md-dialog-container-shape-start-start: 28px;
--md-dialog-container-shape-start-end: 28px;
}
}
@@ -287,21 +295,6 @@ class DialogLightColorFavorite extends LitElement {
rgb(255, 160, 0) 100%
);
}
.value {
pointer-events: none;
position: absolute;
top: 0;
left: 0;
right: 0;
margin: auto;
font-style: normal;
font-weight: 500;
font-size: 16px;
height: 48px;
line-height: 48px;
letter-spacing: 0.1px;
text-align: center;
}
`,
];
}

View File

@@ -21,6 +21,7 @@ import { isUnavailableState } from "../../../data/entity";
import { computeObjectId } from "../../../common/entity/compute_object_id";
import { listenMediaQuery } from "../../../common/dom/media_query";
import "../components/ha-more-info-state-header";
import { ExtEntityRegistryEntry } from "../../../data/entity_registry";
@customElement("more-info-script")
class MoreInfoScript extends LitElement {
@@ -28,6 +29,8 @@ class MoreInfoScript extends LitElement {
@property({ attribute: false }) public stateObj?: ScriptEntity;
@property({ attribute: false }) public entry?: ExtEntityRegistryEntry;
@state() private _scriptData: Record<string, any> = {};
@state() private narrow = false;
@@ -59,8 +62,9 @@ class MoreInfoScript extends LitElement {
const stateObj = this.stateObj;
const fields =
this.hass.services.script[computeObjectId(this.stateObj.entity_id)]
?.fields;
this.hass.services.script[
this.entry?.unique_id || computeObjectId(this.stateObj.entity_id)
]?.fields;
const hasFields = fields && Object.keys(fields).length > 0;
@@ -138,17 +142,30 @@ class MoreInfoScript extends LitElement {
protected override willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (!changedProperties.has("stateObj")) {
return;
if (changedProperties.has("stateObj")) {
const oldState = changedProperties.get("stateObj") as
| HassEntity
| undefined;
const newState = this.stateObj;
if (
newState &&
(!oldState || oldState.entity_id !== newState.entity_id)
) {
this._scriptData = {
action:
this.entry?.entity_id === newState.entity_id
? `script.${this.entry.unique_id}`
: newState.entity_id,
};
}
}
const oldState = changedProperties.get("stateObj") as
| HassEntity
| undefined;
const newState = this.stateObj;
if (newState && (!oldState || oldState.entity_id !== newState.entity_id)) {
this._scriptData = { action: newState.entity_id, data: {} };
if (this.entry?.unique_id && changedProperties.has("entry")) {
const action = `script.${this.entry?.unique_id}`;
if (this._scriptData?.action !== action) {
this._scriptData = { ...this._scriptData, action };
}
}
}
@@ -161,7 +178,7 @@ class MoreInfoScript extends LitElement {
ev.stopPropagation();
this.hass.callService(
"script",
computeObjectId(this.stateObj!.entity_id),
this.entry?.unique_id || computeObjectId(this.stateObj!.entity_id),
this._scriptData.data
);
}

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