Compare commits

...

78 Commits

Author SHA1 Message Date
Paulus Schoutsen
1b158d8310 Merge pull request #12619 from home-assistant/cherry-pick-search
20220504.1
2022-05-07 14:14:39 -07:00
Zack
9d2fcec458 Version Bump 2022-05-06 22:30:34 -05:00
Zack Barett
60cd6c65f0 Revert #10991 (#12618) 2022-05-06 22:00:01 -05:00
Bram Kragten
a39af9c307 Merge pull request #12582 from home-assistant/dev 2022-05-04 13:28:04 +02:00
Bram Kragten
a4f8e886bc Bumped version to 20220504.0 2022-05-04 13:14:25 +02:00
Bram Kragten
cc0c96b8b4 Make update notification better, add progress (#12581) 2022-05-04 11:09:54 +00:00
Philip Allgaier
445f0e23fe System menu description tweaks (#12578) 2022-05-04 11:07:41 +00:00
Bram Kragten
6f240297d1 Remove hassio config panel (#12580) 2022-05-04 10:20:41 +00:00
Paulus Schoutsen
6da4981b70 Clone on duplicate (#12574) 2022-05-04 12:04:59 +02:00
Zack Barett
cfadf4d700 Fixes issue where the grid cards wouldnt be sized correctly (#12571) 2022-05-04 12:04:24 +02:00
Zack Barett
7e60de0531 Add padding when no updates (#12575) 2022-05-04 11:59:24 +02:00
Zack Barett
aaef6d7b91 Fix Overlapping Media List items (#12569) 2022-05-03 23:10:40 +02:00
Paulus Schoutsen
02af4c2156 Bump Master to 20220503.0 (#12567)
* Use selectors for add-on configurations (#12234)

* replace ToggleSwitch with new LightSwitch (#12218)

* Fix statistics chart for sum stat without state (#12238)

* Use selectors for add-on network configuration (#12235)

* Use selectors for add-on network configuration

* Show container port as UOM if advanced user

* adjust

* Only show "required" indicator if we have a selector label (#12241)

* Lineup sidebar badges

* Exclude hidden entities from area card

* Fix entity and device selector with `multiple: true`

* Adjust import

* Guard for partial translations (#12296)

* Fix add-on security rating range (#12300)

* Use more text selector types for add-on configuration (#12303)

* Prevent empty brackets if no manufacturer during config entry creation (#12288)

* Fix endless loading screen in zwave-js config (#12295)

* Update cloud text (#12305)

* Select default mode if none set (#12306)

* Decode view path URL (#12310)

* Always render title field (#12319)

* Use new mdi icons for smoke and co detection (#12323)

* Split only on first comma in media browser (#12331)

* Allow tapping on the name on a picture entity card (#12332)

* RTL calendar fix - arrows fix and views fix (#12314)

* RTL calendar fix - arrows fix and views fix

* Removed path attributes

* Quickly search for entities from the Overview Dashboard (#12324)

* Allow selecting multiple entities for state trigger (#12334)

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Add Template selector (#12348)

* Add basic frontend support for siren (#12345)

* Fix strict error handling in developer tools templates (#12352)

* Bump HAWS to 7.0.3 (#12358)

* Add clear skipped to update more-info dialog (#12361)

* Adding blueprint input description markdown/multi-line support (#12291)

* Github no longer supports the (insecure) git protocol (#12359)

* Add if/else automation/script action (#12301)

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Add stop script/automation action (#12299)

* Getting started on Configuration Changes (#12309)

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

* Config menu updates to get it ready for nightly (#12368)

* Bumped version to 20220420.0 (#12369)

* Use template selector in wait_template (#12366)

* Add jinja2 editor to template triggers/conditions (#12365)

Co-authored-by: Zack <zackbarett@hey.com>

* Fix for monetary entities (#12378)

* Add entity search tip to dev-tools set state (#12355)

* Added ability to retry on initialization errors. (#12103)

* RTL fixes (#12367)

* zwave_js: Add title tag to config box heading (#12387)

* Accept new value when hitting ENTER to close a prompt dialog (#12360)

Co-authored-by: Zack Barett <zackbarett@hey.com>

* RTL reading orders and alignments in system log (#12388)

* Add automation editor for calendar trigger (#12343)

* Add calendar event end trigger to automation editor (#12389)

* Show vacuum state in more-info dialog for StateVacuumEntity (#12391)

* Add Empty list item for None (#12356)

* Force LTR on time & number inputs (#12393)

* Fix Dashboard URLs (#12394)

* Update zwavejs controller model (#12390)

* Configuration Menu Updates 3 (#12377)

* Bumped version to 20220424.0

* Config Menu: Addressing Comments in #12377 (#12399)

* Add shorthand condition to the gallery (#12400)

* Virtualize Media Player Grid (#11898)

* Hide supervisor only config, fix backup config page (#12401)

* Fix broken cards being able to crash entire view (#11440)

* Add supervisor network interface settings (#12403)

* Fix zones (#12409)

* Add supervisor hostname config (#12407)

* Add Hardware Page to Configuration System Menu (#12405)

* Add Supervisor logs to core page (#12410)

* Allow Showing Skipped Updates on Updates Page (#12415)

* Configuration Menu Cleanup items (#12413)

* Backup Page - Will load which is available (#12414)

* Move System Health to a page (#12412)

* Show what updates are skipped (#12418)

* Don't show tabs in supervisor (#12417)

* Better gauge segment coloring (#11570)

* Move Data Disk Moving to Storage (#12416)

* Add supervisor, OS version info to about page (#12421)

* Add supervisor, OS version info to about page

* description

* description

* Allow for checking for updates (#12422)

* Fix title and description for menu step in options flow (#12420)

* link to updates page (#12423)

* Show usage stats in System Health (#12424)

* Bumped version to 20220425.0 (#12425)

* Format sensors with state class duration (#12426)

* Guard against non OS installation (#12427)

* Typo in en.json (#12428)

* Move unsupported and unhealthy alerts (#12431)

* Fix log syntax highlight when fetching logs from supervisor (#12430)

* Resources lovelace should just go back (#12432)

* Redirect hassio system my links to new locations (#12429)

* Fix backup back path (#12435)

* Add join/leave beta to updates panel (#12436)

* Fix settings row width (#12438)

* Dont show tabs when less than 2 (#12439)

* Set border radius in config to 8px (#12437)

* Fix incorrect text if no backups are found (#12441)

* Add header to supervisor backups page (#12444)

* Fix content display for `ha-network` after #12438 (#12445)

* Fix content display for `ha-network` after #12438

* Add var default

* Add title to backups config page (#12442)

* Fix integration page on mobile (#12447)

* Add "m" keyboard shortcut to get to the create my link page (#12451)

* Terms based entities search (#10991)

* Small edits on config menu (#12440)

* Fix for backup overflow (#12454)

* Update the hint for key C (#12458)

* Fix when creating new area in picker #11392 (#12457)

* Fix more info input number #12396 (#12456)

* Update Configuration badge color to be accent color to match (#12455)

* Move Provider Selection to Menu on top header (#12443)

* Move the analytics link (#12459)

* Fix Updates Page Toast - Move to overflow (#12453)

* Move Zones Edit to General config + add general config page (#12452)

* Move Zones Edit to General config + add general

* Update src/translations/en.json

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* add paper tooltip back for yaml

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Tweak menu descriptions (#12460)

* Fix my link for config dashboard and profile (#12461)

* Fix my link for config dashboard and profile

* add server control redirect

Co-authored-by: Zack <zackbarett@hey.com>

* Fix icon alignment in nav list (#12463)

* Add a tip for my shortcut (#12462)

* Move Restart to Overflow and yaml config advanced (#12446)

* Move Restart to Overflow and yaml config advanced

* Move around YAML Config page

* Move to developer tools

* Make card actions

* Update Translations

* Bumped version to 20220427.0

* Use correct label for update config menu (#12465)

* Make helper option button more user friendly (#12468)

* Add hass-quick-bar-trigger event to trigger quickbar from supervisor (#12467)

* Use startsWith for m shortcut for partial match (#12464)

* Add supervisor redirects to m keyboard shortcut (#12466)

* Safeguard against non-existant area in device handling (#12475)

* RTL fix for log buttons (#12474)

* Fix YAML Config Invalid button (#12476)

* Small config fixes (#12472)

* Visual tweaks to YAML validation results (#12479)

* Add some bottom padding to YAML conf dev tools page (#12477)

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Fix Restarting Home Assistant (#12480)

* Fix Restarting Home ASsistant

* Update src/panels/config/core/ha-config-system-navigation.ts

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Update src/panels/developer-tools/yaml_configuration/developer-yaml-config.ts

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* reviews

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Move General Up in the system menu (#12483)

* Media panel fix (#12485)

* add my redirects for new config pages (#12481)

* Add template editor to Markdown card editor (#12490)

* Address minor comments about config menu (#12492)

* Hide and sort secondary device automations (#12496)

* Evaluate condition shorthands in editors (#12473)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Add support for enabling/disabling trigger/condition/action (#12493)

* Add support for enabling/disabling trigger/condition/action

* Add more visual indication of disabled

* review

* margin

* Dont make overflow transparent

* Change color of bar

* Add parallel automation/script action (#12491)

* Add Board Names, Move All Hardware (#12484)

Co-authored-by: Joakim Sørensen <ludeeus@ludeeus.dev>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Change Restart to be a button, update dialogs (#12499)

* Bumped version to 20220428.0 (#12501)

* Fix Wrap menu and remove menu title (#12505)

* form-string password fix (#12507)

* Use media query for config menu mobile (#12510)

* Fix incorrect 3-dot menu labels (config hardware & storage) (#12512)

* Media browser RTL fixes (#12506)

* Fix `continue_on_timeout` default on `wait_template` automation visual editor (#12511)

* Support shorthand logical operators in script sequences (#12509)

* Only show Card Content if OS exist (#12513)

* Add condition shorthand to action types (#12514)

* Fix for external url not logged into cloud (#12516)

* Restart Home ASsistant button - Make less red and less big (#12515)

* Add actions to design gallery (#12518)

* Add actions to design gallery

* Update describe-action.ts

* Move integrations to System Health (#12504)

* Add if, parallel and stop action to trace graph (#12520)

* Bumped version to 20220429.0 (#12521)

* Change color of persons for real this time (#12527)

* Ignore modifier keys when forwarding events to quickbar (#12525)

* Add optional repository_url to supervisor_addon my link (#12524)

* Calendar-card fix (#12532)

* Handle condition shorthands in trace graphs (#12533)

* Make the "Aborted: Reauthentication successful" more user friendly (#12530)

Replace the "Aborted" in the title with the integration name to make the user error
messages more user friendly. The message itself ("Reauthentication successful" or "Missing configuraiton, etc) error
message is descriptive enought that we can replace the title with the integration
name and still preserve the meeting. The advance is that this doesn't confuse users
who are surprised by it saying "Aborted" when things were successful

https://github.com/home-assistant/core/issues/47135

* Prevent color temp selector mired exception (#12536)

* Fix some issues and feedback with About and system health (#12537)

* Add descriptions for actions (#12541)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Add repeat to trace timeline (#12547)

* Change name to Settings (#12548)

* Add trace timeline for if (#12543)

* Fix script graph parallel (#12545)

* Handle if in repeat (#12544)

* Add parallel action to trace timeline (#12549)

* Indicate things are disabled in trace graph (#12550)

* Indicate things are disabled in trace graph

* Update hat-script-graph.ts

* Bumped version to 20220502.0

* Add add-on logs to log selector (#12556)

* Fix Webhook Overflow (#12551)

* Search in Overflow on Mobile (#12552)

* Use ha-tip for yaml move tip (#12559)

* Update Quickbar Section Logic to include all (#12553)

* Use outline for cards on config pages (#12558)

* Add supervisor redirects to quickbar (#12557)

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

* Fix searching in hassio logs (#12560)

* Add disabled support to trace timeline and step details (#12555)

* Add new system menu descriptions (#12564)

* Add missing outlined to supervisor panel (#12565)

* Bumped version to 20220503.0 (#12566)

Co-authored-by: Joakim Sørensen <ludeeus@ludeeus.dev>
Co-authored-by: Marius <33354141+Mariusthvdb@users.noreply.github.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Philip Allgaier <mail@spacegaier.de>
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Kuba Wolanin <hi@kubawolanin.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Yosi Levy <37745463+yosilevy@users.noreply.github.com>
Co-authored-by: Raman Gupta <7243222+raman325@users.noreply.github.com>
Co-authored-by: Simon Vallières <simon@vallieres.ca>
Co-authored-by: Eric Stern <stormalong@gmail.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Wesley Vos <17592840+Wesley-Vos@users.noreply.github.com>
Co-authored-by: Mark Lopez <m@silvenga.com>
Co-authored-by: Allen Porter <allen@thebends.org>
Co-authored-by: yangqian <yanyangqian@gmail.com>
Co-authored-by: Thomas Lovén <thomasloven@gmail.com>
Co-authored-by: Netzwerkfehler <16437929+Netzwerkfehler@users.noreply.github.com>
Co-authored-by: Artem Sorokin <artem@sorokin.pp.ru>
Co-authored-by: Jaroslav Hanslík <kukulich@kukulich.cz>
Co-authored-by: Johann Vanackere <johann.vanackere@gmail.com>
Co-authored-by: Bruno Maia <bruno.mm.maia@gmail.com>
2022-05-03 11:14:32 -07:00
Zack Barett
58c5ce2638 Bumped version to 20220503.0 (#12566) 2022-05-03 11:14:12 -07:00
Joakim Sørensen
a9d01c7b55 Add missing outlined to supervisor panel (#12565) 2022-05-03 17:06:21 +00:00
Zack Barett
c5de8a4361 Add new system menu descriptions (#12564) 2022-05-03 16:44:43 +00:00
Bram Kragten
b53645ce92 Add disabled support to trace timeline and step details (#12555) 2022-05-03 09:50:33 -05:00
Bram Kragten
de34a5a597 Fix searching in hassio logs (#12560) 2022-05-03 07:30:01 -05:00
Joakim Sørensen
bd8e15bdd1 Add supervisor redirects to quickbar (#12557)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-05-03 11:57:09 +00:00
Bram Kragten
45c7e0eeeb Use outline for cards on config pages (#12558) 2022-05-03 06:44:55 -05:00
Zack Barett
a35a380ec7 Update Quickbar Section Logic to include all (#12553) 2022-05-03 13:25:46 +02:00
Bram Kragten
02e67d1146 Use ha-tip for yaml move tip (#12559) 2022-05-03 11:22:48 +00:00
Zack Barett
a5411f7ac4 Search in Overflow on Mobile (#12552) 2022-05-03 13:17:47 +02:00
Zack Barett
e8da203fe1 Fix Webhook Overflow (#12551) 2022-05-03 13:17:02 +02:00
Joakim Sørensen
10aa0a8829 Add add-on logs to log selector (#12556) 2022-05-03 13:13:20 +02:00
Paulus Schoutsen
85a37e2d2f Bumped version to 20220502.0 2022-05-02 15:08:01 -07:00
Bram Kragten
ba8621fa2c Indicate things are disabled in trace graph (#12550)
* Indicate things are disabled in trace graph

* Update hat-script-graph.ts
2022-05-02 15:07:36 -07:00
Bram Kragten
43e80f1a2e Add parallel action to trace timeline (#12549) 2022-05-02 15:07:01 -07:00
Bram Kragten
3a305a44b6 Handle if in repeat (#12544) 2022-05-02 14:48:28 -07:00
Bram Kragten
e99143139e Fix script graph parallel (#12545) 2022-05-02 14:47:43 -07:00
Bram Kragten
f0c7232704 Add trace timeline for if (#12543) 2022-05-02 14:47:17 -07:00
Zack Barett
b2186592df Change name to Settings (#12548) 2022-05-02 23:29:06 +02:00
Bram Kragten
e51e3e79d5 Add repeat to trace timeline (#12547) 2022-05-02 17:16:32 +00:00
Bram Kragten
3b6b4d7664 Add descriptions for actions (#12541)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-05-02 15:06:55 +00:00
Zack Barett
239e71b414 Fix some issues and feedback with About and system health (#12537) 2022-05-02 12:54:55 +02:00
Philip Allgaier
080cad0ccd Prevent color temp selector mired exception (#12536) 2022-05-01 22:21:25 +00:00
Allen Porter
dd49fd2788 Make the "Aborted: Reauthentication successful" more user friendly (#12530)
Replace the "Aborted" in the title with the integration name to make the user error
messages more user friendly. The message itself ("Reauthentication successful" or "Missing configuraiton, etc) error
message is descriptive enought that we can replace the title with the integration
name and still preserve the meeting. The advance is that this doesn't confuse users
who are surprised by it saying "Aborted" when things were successful

https://github.com/home-assistant/core/issues/47135
2022-05-01 11:02:32 -05:00
Thomas Lovén
a571fb5528 Handle condition shorthands in trace graphs (#12533) 2022-05-01 10:59:46 -05:00
Yosi Levy
1369c1ae8c Calendar-card fix (#12532) 2022-05-01 10:59:12 -05:00
Joakim Sørensen
f5864181af Add optional repository_url to supervisor_addon my link (#12524) 2022-04-30 16:53:43 -05:00
Joakim Sørensen
a4a0d7cf19 Ignore modifier keys when forwarding events to quickbar (#12525) 2022-04-30 16:52:14 -05:00
Bram Kragten
092dfd1e87 Change color of persons for real this time (#12527) 2022-04-30 14:31:43 -05:00
Zack Barett
a29ac33810 Bumped version to 20220429.0 (#12521) 2022-04-29 15:37:42 -07:00
Bram Kragten
1421df2a5a Add if, parallel and stop action to trace graph (#12520) 2022-04-29 16:30:40 -05:00
Zack Barett
591b8cc503 Move integrations to System Health (#12504) 2022-04-29 20:53:24 +02:00
Bram Kragten
011467ece0 Add actions to design gallery (#12518)
* Add actions to design gallery

* Update describe-action.ts
2022-04-29 20:51:44 +02:00
Zack Barett
f52e8c3392 Restart Home ASsistant button - Make less red and less big (#12515) 2022-04-29 19:15:43 +02:00
Zack Barett
c8b87b65bd Fix for external url not logged into cloud (#12516) 2022-04-29 16:19:53 +00:00
Thomas Lovén
98cc82db44 Add condition shorthand to action types (#12514) 2022-04-29 15:40:03 +00:00
Zack Barett
f510e2a8e0 Only show Card Content if OS exist (#12513) 2022-04-29 16:49:47 +02:00
Franck Nijhof
3438912ba5 Support shorthand logical operators in script sequences (#12509) 2022-04-29 09:47:44 -05:00
Bruno Maia
671c8e387f Fix continue_on_timeout default on wait_template automation visual editor (#12511) 2022-04-29 14:37:35 +00:00
Yosi Levy
0108ec65cf Media browser RTL fixes (#12506) 2022-04-29 09:27:06 -05:00
Philip Allgaier
39f7034578 Fix incorrect 3-dot menu labels (config hardware & storage) (#12512) 2022-04-29 09:24:37 -05:00
Bram Kragten
bf8affaf2b Use media query for config menu mobile (#12510) 2022-04-29 07:41:27 -05:00
Yosi Levy
e16a61eb53 form-string password fix (#12507) 2022-04-29 11:50:19 +02:00
Zack Barett
cadbe45bab Fix Wrap menu and remove menu title (#12505) 2022-04-28 19:23:23 -07:00
Zack Barett
51f971337d Bumped version to 20220428.0 (#12501) 2022-04-28 13:50:08 -07:00
Zack Barett
1f3c23de29 Change Restart to be a button, update dialogs (#12499) 2022-04-28 13:43:00 -07:00
Zack Barett
bdfb17d957 Add Board Names, Move All Hardware (#12484)
Co-authored-by: Joakim Sørensen <ludeeus@ludeeus.dev>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-04-28 20:42:18 +00:00
Franck Nijhof
8c97aee1fe Add parallel automation/script action (#12491) 2022-04-28 15:09:03 -05:00
Bram Kragten
38b4090daa Add support for enabling/disabling trigger/condition/action (#12493)
* Add support for enabling/disabling trigger/condition/action

* Add more visual indication of disabled

* review

* margin

* Dont make overflow transparent

* Change color of bar
2022-04-28 18:37:58 +02:00
Thomas Lovén
b8c55f2f65 Evaluate condition shorthands in editors (#12473)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-04-28 08:36:17 -07:00
Bram Kragten
7ca379e0a1 Hide and sort secondary device automations (#12496) 2022-04-28 08:53:56 -05:00
Bram Kragten
1617a9dfed Address minor comments about config menu (#12492) 2022-04-28 08:44:01 -05:00
Franck Nijhof
2c9411c6c3 Add template editor to Markdown card editor (#12490) 2022-04-28 12:40:39 +02:00
Zack Barett
67626d4a06 add my redirects for new config pages (#12481) 2022-04-28 12:39:35 +02:00
Yosi Levy
8135611688 Media panel fix (#12485) 2022-04-28 05:16:18 +00:00
Zack Barett
3ccbf6983e Move General Up in the system menu (#12483) 2022-04-27 22:08:21 -07:00
Zack Barett
e4f91195d8 Fix Restarting Home Assistant (#12480)
* Fix Restarting Home ASsistant

* Update src/panels/config/core/ha-config-system-navigation.ts

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Update src/panels/developer-tools/yaml_configuration/developer-yaml-config.ts

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* reviews

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-04-27 15:55:04 -07:00
Philip Allgaier
2751f8f33b Add some bottom padding to YAML conf dev tools page (#12477)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-04-27 22:18:25 +02:00
Philip Allgaier
57f2df3b3e Visual tweaks to YAML validation results (#12479) 2022-04-27 19:57:41 +00:00
Zack Barett
6822f0d067 Small config fixes (#12472) 2022-04-27 12:22:57 -07:00
Zack Barett
cfba957313 Fix YAML Config Invalid button (#12476) 2022-04-27 13:57:57 -05:00
Zack Barett
d02cd122a9 Merge pull request #12233 from home-assistant/dev
Co-authored-by: Joakim Sørensen <ludeeus@ludeeus.dev>
Co-authored-by: Philip Allgaier <mail@spacegaier.de>
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-04-05 18:02:02 -05:00
Joakim Sørensen
8e962fdecb Use installed_version for update entities (#12194) 2022-04-01 19:29:26 +02:00
Bram Kragten
1f65193a97 Merge pull request #12193 from home-assistant/dev 2022-04-01 18:56:12 +02:00
Bram Kragten
24484d0e74 20220330.0 (#12165)
* Add a docs icon to the config flow dialog

* Use same help icon everywhere

* Fix quickbar overlaying, fix click handling (#11900)

* Supervisor mobile click accessibility (#11915)

* Convert objects to string in config flow error (#11908)

* Fix datepicker triangle (#11920)

* Always show tab labels (#11919)

* Remove zwave and ozw panels (#11911)

Remove zwave and ozw panels

* Convert lovelace config dialogs to ha-form (#11910)

* Guard setting up config flow for an unsupported domain (#11937)

* Show triggered vars on click (#11924)

* Allow marking YAML editor as read only (#11960)

* Convert inputs (#11907)

* Convert inputs

* Update dialog-thingtalk.ts

* imports

* Remove some additional old zwave code (#11941)

* Correct media upload error + add file name (#11949)

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

* Fix humidifier more info mode dropdown (#11964)

* Make min width of select configurable (#11965)

* Fix for Statistics Editor (#11942)

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

* A11y expansion panel (#11967)

* Convert file upload to mdc (#11906)

* Add location selector, convert zone editor (#11902)

* Add systemd_resolved unsupported reason (#11971)

* replace default switch icon 

to  make it stand out against a power entity which uses the same mdiFlash https://github.com/home-assistant/core/issues/67620#issuecomment-1061949527

suggest the Outline version, so create a subtle difference with the on/off icons.

* Allow selecting multiple entities (#11986)

* Fix theme setting (#11977)

* Update Style of Design Page (#11982)

* change icon to mimic physical device

and follow comments

* Use entities-picker in entity selector (#11990)

* #11971 Change order of alarm panel buttons (#11998)

* Fix zwave_js 'add/remove device' disabled bug (#12000)

* Fix zwave_js 'add/remove device' disabled bug

* revert extra change

* Fix zwave_js set config dropdown default value (#11974)

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>

* Fix changing cost number in energy settings (#12009)

* Fix Dashboard Editing (#12011)

* Fix For Selecting Device Class (#12010)

* Fix: Allow for deleting Input_select options (#12007)

* Script ID update with Alias (#12008)

* HAWS 6.1 (#12016)

* Bumped version to 20220301.1

* Bumped version to 20220301.2

* Bumped version to 20220312.0

* Add shade to device class overrides (#11874)

* Fix: Changing Blueprint Automation Name (#12036)

* Fix @changed where using ev.detail (#12043)

* Add all cover device classes (#12042)

* Rename Lovelace Dashboard to just Dashboard (#12044)

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Add Color Temp Selector (#12041)

* Utilize Hide Hidden Entities

* Reviews

* add to demo

* Add `Brand` folder and `Our story` page (#11978)

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Add HA to public folder and show in markdown

* Update Translations

* Disabled by

* remove 1

* Add Description of chosen

* Add icons and buttons

* Add Color RGB Selector (#12039)

* Add Date Selector

* Add ha-form context (#12062)

* test condition (#11925)

* Revamp URL form (#12060)

* Add support for menu data entry flow option (#12055)

* Add translation

* add to basic editor and update advanced style

* clean up

* Entity Status

* Add Devices Picker (#12056)

* Remvoe redunency

* Bumped version to 20220316.0

* Bump HAWS to 7.0.0 (#12067)

* Create new Logo page

* Add files via upload

* Ignore diagnostics not found exceptions (#12066)

* Bump HAWS to 7.0.1

* Update lock

* Add Date Time Selector (#12070)

* Add radio Form Logic to Select Selector (#12063)

* Bumped version to 20220317.0 (#12074)

* Update gallery/src/pages/brand/logo.markdown

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Update gallery/src/pages/brand/logo.markdown

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Update logo.markdown

* Fetch history with `no_attributes` for entities that do not need them (#12082)

* Update required version of MDI to 6.6.95

* Upload release assets (#11566)

Co-authored-by: Joakim Sørensen <hi@ludeeus.dev>

* Update styles for hui-editor

Update the background-color and text-color of the app-toolbar in
hui-editor to match the styles of hui-root while in edit-mode.

Previously, these properties were set using undefined css variables that
could not be changed via themes (--dark-background-color and
--dark-text-color).

* Fix gas energy graph units if stats added by external source (#11892)

* Change Netlify preview URL (#12095)

* Update src/dialogs/config-flow/dialog-data-entry-flow.ts

* Stack Action Inputs in the Button Editor (#12076)

* Stack Action Inputs in the Button Editor

* update style

* Update for other editors

* Add support for update entities (#12059)

* Add support for update entities

* Apply suggestions from code review

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Add to gallery

* implement xx%

* Adjustments for skipped

* Add progress bar

* Add UPDATE_SUPPORT_INSTALL

* Allow skipping without install support

* Add version to service call if supported

* Adjust changelog link

* Use Installing

* adjustments

* Use unavailable

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Add support for integration type (#12077)

* Update When entity can change enabled or hidden (#12096)

* Add entity include and exclude to selector (#12078)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* change from hidden to not shown (#12097)

* Add statistic adjust dialog (#12101)

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Fix Duration Selector Default (#12098)

* Fix Duration Default

* USe initial form data function

* Bumped version to 20220322.0 (#12102)

* Create user types page and rename the category (#12089)

Co-authored-by: Zack Barett <zackbarett@hey.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Remove `setup.py` (#11593)

* Fix selecting 0 with number selector

* Update lock file with MDI updates

* Use update entities for showing updates on configuration panel (#12100)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Fix loading traces for automation with custom id (#12112)

* Only show docs link when showing a form

* Exclude restored automations from dashboard (#12113)

* Support descriptions in flow menu steps (#12108)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Sort selectors (#12120)

* Update type for backend (#12122)

* Fix issue where theme select does not appear when user's theme is deleted (#12104)

* Fix possibility to enable entity disabled by integration (#12121)

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Allow rendering helper text from strings.json (#12119)

* Allow rendering helper text from strings.json

* Persistent helpers

* Update src/components/ha-base-time-input.ts

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Update src/components/ha-base-time-input.ts

Co-authored-by: Zack Barett <zackbarett@hey.com>

* Add Day to duration selector (#12125)

* Add variables to automation trigger type

* Fix z-index map, always set icon for location selector (#12137)

* Make padding on settings row content consistent (#12139)

* Add Area Multiple Selector option (#12138)

* break theme picker out of lovelace (#12140)

* Allow binary sensor device class updates (#12124)

* Add selector initial values (#12142)

* Add badge to configuration sidebar to indicate pending updates (#12146)

* Bumped version to 20220329.0 (#12152)

* Add entity source API (#12149)

* Update adjust statistic dialog (#12118)

* Update text for adjust statistic dialog

* Change everything

* Import type

* Max show 5

* Revert back the API change

* Hide adjust button if no sum

* Adjustments

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

* Render optional

Co-authored-by: Zack <zackbarett@hey.com>

* Fetch release notes for update entities that provides it (#12148)

* Fetch release notes for update entities that provides it

* lint

* Add support for new timer properties (#11940)

* Fix theme settings on design page (#12154)

* Allow ha-alert to be used in our markdown render (#12153)

* Allow device_tracker entities to use state_color (#12127)

* Automation description text overflow (#12040)

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

* Update Pickers and selectors with required (#12151)

* Update Pickers and selectors with required

* Use native * for device and entity

* Add support for my links to create a helper config entry (#12155)

* Use brand icon instead of domain icon for helpers (#12157)

* Import components that are allowed to be defined in markdown (#12158)

* Add options to selectors gallery (#12156)

* Add helpers to list when searching in add integration (#12159)

* List Selector (#12099)

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

* Add shuffle and repeat-mode of media_player to UI (#12052)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Add panel to Backup integration (#11671)

Co-authored-by: Zack Barett <zackbarett@hey.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Fix for Mobile View of Entities Table (#12160)

* Allow Sensor Units to be updated via Entity Registry (#12143)

* Add switch as x to entity settings (#12161)

Co-authored-by: Zack <zackbarett@hey.com>

* Bumped version to 20220330.0 (#12164)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
Co-authored-by: Robin Wittebol <robinwittebol@live.nl>
Co-authored-by: Raman Gupta <7243222+raman325@users.noreply.github.com>
Co-authored-by: Philip Allgaier <philip.allgaier@gmx.de>
Co-authored-by: Joakim Sørensen <ludeeus@ludeeus.dev>
Co-authored-by: Marius <33354141+Mariusthvdb@users.noreply.github.com>
Co-authored-by: Emil Stjerneman <emil@stjerneman.com>
Co-authored-by: Charles Garwood <cgarwood@gmail.com>
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
Co-authored-by: jpearl <jpearl@users.noreply.github.com>
Co-authored-by: Matthias de Baat <matthias.debaat@nabucasa.com>
Co-authored-by: Matthias de Baat <hello@matthiasdebaat.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Michael Irigoyen <michael@irigoyen.dev>
Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
Co-authored-by: Joakim Sørensen <hi@ludeeus.dev>
Co-authored-by: Nick Iacullo <duckycrayfish@gmail.com>
Co-authored-by: Pawel <pszafer@gmail.com>
Co-authored-by: Erik <erik@montnemery.com>
Co-authored-by: Brynley McDonald <brynley+github@zephire.nz>
Co-authored-by: blair <1585872+blairun@users.noreply.github.com>
Co-authored-by: NachtaktiverHalbaffe <57433516+NachtaktiverHalbaffe@users.noreply.github.com>
2022-03-30 20:49:00 +02:00
127 changed files with 3145 additions and 1076 deletions

View File

@@ -62,6 +62,45 @@ const ACTIONS = [
entity_id: "input_boolean.toggle_4", entity_id: "input_boolean.toggle_4",
}, },
}, },
{
parallel: [
{ scene: "scene.kitchen_morning" },
{
service: "media_player.play_media",
target: { entity_id: "media_player.living_room" },
data: { media_content_id: "", media_content_type: "" },
metadata: { title: "Happy Song" },
},
],
},
{
stop: "No one is home!",
},
{ repeat: { count: 3, sequence: [{ delay: "00:00:01" }] } },
{
repeat: {
for_each: ["bread", "butter", "cheese"],
sequence: [{ delay: "00:00:01" }],
},
},
{
if: [{ condition: "state" }],
then: [{ delay: "00:00:01" }],
else: [{ delay: "00:00:05" }],
},
{
choose: [
{
conditions: [{ condition: "state" }],
sequence: [{ delay: "00:00:01" }],
},
{
conditions: [{ condition: "sun" }],
sequence: [{ delay: "00:00:05" }],
},
],
default: [{ delay: "00:00:03" }],
},
]; ];
@customElement("demo-automation-describe-action") @customElement("demo-automation-describe-action")

View File

@@ -20,6 +20,10 @@ import { HaWaitForTriggerAction } from "../../../../src/panels/config/automation
import { HaWaitAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_template"; import { HaWaitAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_template";
import { Action } from "../../../../src/data/script"; import { Action } from "../../../../src/data/script";
import { HaConditionAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-condition"; import { HaConditionAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-condition";
import { HaParallelAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-parallel";
import { HaIfAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-if";
import { HaStopAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-stop";
import { HaPlayMediaAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-play_media";
const SCHEMAS: { name: string; actions: Action[] }[] = [ const SCHEMAS: { name: string; actions: Action[] }[] = [
{ name: "Event", actions: [HaEventAction.defaultConfig] }, { name: "Event", actions: [HaEventAction.defaultConfig] },
@@ -28,11 +32,15 @@ const SCHEMAS: { name: string; actions: Action[] }[] = [
{ name: "Condition", actions: [HaConditionAction.defaultConfig] }, { name: "Condition", actions: [HaConditionAction.defaultConfig] },
{ name: "Delay", actions: [HaDelayAction.defaultConfig] }, { name: "Delay", actions: [HaDelayAction.defaultConfig] },
{ name: "Scene", actions: [HaSceneAction.defaultConfig] }, { name: "Scene", actions: [HaSceneAction.defaultConfig] },
{ name: "Play media", actions: [HaPlayMediaAction.defaultConfig] },
{ name: "Wait", actions: [HaWaitAction.defaultConfig] }, { name: "Wait", actions: [HaWaitAction.defaultConfig] },
{ name: "WaitForTrigger", actions: [HaWaitForTriggerAction.defaultConfig] }, { name: "WaitForTrigger", actions: [HaWaitForTriggerAction.defaultConfig] },
{ name: "Repeat", actions: [HaRepeatAction.defaultConfig] }, { name: "Repeat", actions: [HaRepeatAction.defaultConfig] },
{ name: "If-Then", actions: [HaIfAction.defaultConfig] },
{ name: "Choose", actions: [HaChooseAction.defaultConfig] }, { name: "Choose", actions: [HaChooseAction.defaultConfig] },
{ name: "Variables", actions: [{ variables: { hello: "1" } }] }, { name: "Variables", actions: [{ variables: { hello: "1" } }] },
{ name: "Parallel", actions: [HaParallelAction.defaultConfig] },
{ name: "Stop", actions: [HaStopAction.defaultConfig] },
]; ];
@customElement("demo-automation-editor-action") @customElement("demo-automation-editor-action")
@@ -86,6 +94,6 @@ class DemoHaAutomationEditorAction extends LitElement {
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"demo-ha-automation-editor-action": DemoHaAutomationEditorAction; "demo-automation-editor-action": DemoHaAutomationEditorAction;
} }
} }

View File

@@ -68,6 +68,7 @@ class HassioAddonRepositoryEl extends LitElement {
${addons.map( ${addons.map(
(addon) => html` (addon) => html`
<ha-card <ha-card
outlined
.addon=${addon} .addon=${addon}
class=${addon.available ? "" : "not_available"} class=${addon.available ? "" : "not_available"}
@click=${this._addonTapped} @click=${this._addonTapped}

View File

@@ -50,6 +50,7 @@ class HassioAddonAudio extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<ha-card <ha-card
outlined
.header=${this.supervisor.localize("addon.configuration.audio.header")} .header=${this.supervisor.localize("addon.configuration.audio.header")}
> >
<div class="card-content"> <div class="card-content">

View File

@@ -162,7 +162,7 @@ class HassioAddonConfig extends LitElement {
); );
return html` return html`
<h1>${this.addon.name}</h1> <h1>${this.addon.name}</h1>
<ha-card> <ha-card outlined>
<div class="header"> <div class="header">
<h2> <h2>
${this.supervisor.localize("addon.configuration.options.header")} ${this.supervisor.localize("addon.configuration.options.header")}

View File

@@ -58,6 +58,7 @@ class HassioAddonNetwork extends LitElement {
return html` return html`
<ha-card <ha-card
outlined
.header=${this.supervisor.localize( .header=${this.supervisor.localize(
"addon.configuration.network.header" "addon.configuration.network.header"
)} )}

View File

@@ -38,7 +38,7 @@ class HassioAddonDocumentationDashboard extends LitElement {
} }
return html` return html`
<div class="content"> <div class="content">
<ha-card> <ha-card outlined>
${this._error ${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>` ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""} : ""}

View File

@@ -17,7 +17,9 @@ import {
HassioAddonDetails, HassioAddonDetails,
} from "../../../src/data/hassio/addon"; } from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common"; import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { setSupervisorOption } from "../../../src/data/hassio/supervisor";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-error-screen"; import "../../../src/layouts/hass-error-screen";
import "../../../src/layouts/hass-loading-screen"; import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-tabs-subpage"; import "../../../src/layouts/hass-tabs-subpage";
@@ -166,6 +168,42 @@ class HassioAddonDashboard extends LitElement {
protected async firstUpdated(): Promise<void> { protected async firstUpdated(): Promise<void> {
if (this.route.path === "") { if (this.route.path === "") {
const requestedAddon = extractSearchParam("addon"); const requestedAddon = extractSearchParam("addon");
const requestedAddonRepository = extractSearchParam("repository_url");
if (
requestedAddonRepository &&
!this.supervisor.supervisor.addons_repositories.find(
(repo) => repo === requestedAddonRepository
)
) {
if (
!(await showConfirmationDialog(this, {
title: this.supervisor.localize("my.add_addon_repository_title"),
text: this.supervisor.localize(
"my.add_addon_repository_description",
{ addon: requestedAddon, repository: requestedAddonRepository }
),
confirmText: this.supervisor.localize("common.add"),
dismissText: this.supervisor.localize("common.cancel"),
}))
) {
this._error = this.supervisor.localize(
"my.error_repository_not_found"
);
return;
}
try {
await setSupervisorOption(this.hass, {
addons_repositories: [
...this.supervisor.supervisor.addons_repositories,
requestedAddonRepository,
],
});
} catch (err: any) {
this._error = extractApiErrorMessage(err);
}
}
if (requestedAddon) { if (requestedAddon) {
const addonsInfo = await fetchHassioAddonsInfo(this.hass); const addonsInfo = await fetchHassioAddonsInfo(this.hass);
const validAddon = addonsInfo.addons.some( const validAddon = addonsInfo.addons.some(

View File

@@ -166,7 +166,7 @@ class HassioAddonInfo extends LitElement {
` `
: ""} : ""}
<ha-card> <ha-card outlined>
<div class="card-content"> <div class="card-content">
<div class="addon-header"> <div class="addon-header">
${!this.narrow ? this.addon.name : ""} ${!this.narrow ? this.addon.name : ""}
@@ -649,7 +649,7 @@ class HassioAddonInfo extends LitElement {
${this.addon.long_description ${this.addon.long_description
? html` ? html`
<ha-card> <ha-card outlined>
<div class="card-content"> <div class="card-content">
<ha-markdown <ha-markdown
.content=${this.addon.long_description} .content=${this.addon.long_description}

View File

@@ -34,7 +34,7 @@ class HassioAddonLogs extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<h1>${this.addon.name}</h1> <h1>${this.addon.name}</h1>
<ha-card> <ha-card outlined>
${this._error ${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>` ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""} : ""}

View File

@@ -26,7 +26,7 @@ class HassioAddons extends LitElement {
<div class="card-group"> <div class="card-group">
${!this.supervisor.supervisor.addons?.length ${!this.supervisor.supervisor.addons?.length
? html` ? html`
<ha-card> <ha-card outlined>
<div class="card-content"> <div class="card-content">
<button class="link" @click=${this._openStore}> <button class="link" @click=${this._openStore}>
${this.supervisor.localize("dashboard.no_addons")} ${this.supervisor.localize("dashboard.no_addons")}
@@ -38,7 +38,11 @@ class HassioAddons extends LitElement {
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name)) .sort((a, b) => caseInsensitiveStringCompare(a.name, b.name))
.map( .map(
(addon) => html` (addon) => html`
<ha-card .addon=${addon} @click=${this._addonTapped}> <ha-card
outlined
.addon=${addon}
@click=${this._addonTapped}
>
<div class="card-content"> <div class="card-content">
<hassio-card-content <hassio-card-content
.hass=${this.hass} .hass=${this.hass}

View File

@@ -85,7 +85,7 @@ export class HassioUpdate extends LitElement {
return html``; return html``;
} }
return html` return html`
<ha-card> <ha-card outlined>
<div class="card-content"> <div class="card-content">
<div class="icon"> <div class="icon">
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon> <ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>

View File

@@ -74,7 +74,11 @@ export class HassioMain extends SupervisorBaseElement {
}); });
// Forward keydown events to the main window for quickbar access // Forward keydown events to the main window for quickbar access
document.body.addEventListener("keydown", (ev) => { document.body.addEventListener("keydown", (ev: KeyboardEvent) => {
if (ev.altKey || ev.ctrlKey || ev.shiftKey || ev.metaKey) {
// Ignore if modifier keys are pressed
return;
}
// @ts-ignore // @ts-ignore
fireEvent(mainWindow, "hass-quick-bar-trigger", ev, { fireEvent(mainWindow, "hass-quick-bar-trigger", ev, {
bubbles: false, bubbles: false,

View File

@@ -42,6 +42,9 @@ export const REDIRECTS: Redirects = {
params: { params: {
addon: "string", addon: "string",
}, },
optional_params: {
repository_url: "url",
},
}, },
supervisor_ingress: { supervisor_ingress: {
redirect: "/hassio/ingress", redirect: "/hassio/ingress",
@@ -124,6 +127,14 @@ class HassioMyRedirect extends LitElement {
} }
resultParams[key] = params[key]; resultParams[key] = params[key];
}); });
Object.entries(redirect.optional_params || {}).forEach(([key, type]) => {
if (params[key]) {
if (!this._checkParamType(type, params[key])) {
throw Error();
}
resultParams[key] = params[key];
}
});
return `?${createSearchParam(resultParams)}`; return `?${createSearchParam(resultParams)}`;
} }

View File

@@ -48,7 +48,7 @@ class HassioCoreInfo extends LitElement {
]; ];
return html` return html`
<ha-card header="Core"> <ha-card header="Core" outlined>
<div class="card-content"> <div class="card-content">
<div> <div>
<ha-settings-row> <ha-settings-row>

View File

@@ -66,7 +66,7 @@ class HassioHostInfo extends LitElement {
}, },
]; ];
return html` return html`
<ha-card header="Host"> <ha-card header="Host" outlined>
<div class="card-content"> <div class="card-content">
<div> <div>
${this.supervisor.host.features.includes("hostname") ${this.supervisor.host.features.includes("hostname")

View File

@@ -57,7 +57,7 @@ class HassioSupervisorInfo extends LitElement {
}, },
]; ];
return html` return html`
<ha-card header="Supervisor"> <ha-card header="Supervisor" outlined>
<div class="card-content"> <div class="card-content">
<div> <div>
<ha-settings-row> <ha-settings-row>

View File

@@ -65,7 +65,7 @@ class HassioSupervisorLog extends LitElement {
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
return html` return html`
<ha-card> <ha-card outlined>
${this._error ${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>` ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""} : ""}

View File

@@ -128,6 +128,7 @@ class UpdateAvailableCard extends LitElement {
return html` return html`
<ha-card <ha-card
outlined
.header=${this.supervisor.localize("update_available.update_name", { .header=${this.supervisor.localize("update_available.update_name", {
name: this._name, name: this._name,
})} })}

View File

@@ -106,7 +106,6 @@
"deep-clone-simple": "^1.1.1", "deep-clone-simple": "^1.1.1",
"deep-freeze": "^0.0.1", "deep-freeze": "^0.0.1",
"fuse.js": "^6.0.0", "fuse.js": "^6.0.0",
"fuzzysort": "^1.2.1",
"google-timezones-json": "^1.0.2", "google-timezones-json": "^1.0.2",
"hls.js": "^1.1.5", "hls.js": "^1.1.5",
"home-assistant-js-websocket": "^7.0.3", "home-assistant-js-websocket": "^7.0.3",

View File

@@ -1,6 +1,6 @@
[metadata] [metadata]
name = home-assistant-frontend name = home-assistant-frontend
version = 20220427.0 version = 20220504.1
author = The Home Assistant Authors author = The Home Assistant Authors
author_email = hello@home-assistant.io author_email = hello@home-assistant.io
license = Apache-2.0 license = Apache-2.0

View File

@@ -0,0 +1,244 @@
// MIT License
// Copyright (c) 2015 - present Microsoft Corporation
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/
/**
* An inlined enum containing useful character codes (to be used with String.charCodeAt).
* Please leave the const keyword such that it gets inlined when compiled to JavaScript!
*/
export enum CharCode {
Null = 0,
/**
* The `\b` character.
*/
Backspace = 8,
/**
* The `\t` character.
*/
Tab = 9,
/**
* The `\n` character.
*/
LineFeed = 10,
/**
* The `\r` character.
*/
CarriageReturn = 13,
Space = 32,
/**
* The `!` character.
*/
ExclamationMark = 33,
/**
* The `"` character.
*/
DoubleQuote = 34,
/**
* The `#` character.
*/
Hash = 35,
/**
* The `$` character.
*/
DollarSign = 36,
/**
* The `%` character.
*/
PercentSign = 37,
/**
* The `&` character.
*/
Ampersand = 38,
/**
* The `'` character.
*/
SingleQuote = 39,
/**
* The `(` character.
*/
OpenParen = 40,
/**
* The `)` character.
*/
CloseParen = 41,
/**
* The `*` character.
*/
Asterisk = 42,
/**
* The `+` character.
*/
Plus = 43,
/**
* The `,` character.
*/
Comma = 44,
/**
* The `-` character.
*/
Dash = 45,
/**
* The `.` character.
*/
Period = 46,
/**
* The `/` character.
*/
Slash = 47,
Digit0 = 48,
Digit1 = 49,
Digit2 = 50,
Digit3 = 51,
Digit4 = 52,
Digit5 = 53,
Digit6 = 54,
Digit7 = 55,
Digit8 = 56,
Digit9 = 57,
/**
* The `:` character.
*/
Colon = 58,
/**
* The `;` character.
*/
Semicolon = 59,
/**
* The `<` character.
*/
LessThan = 60,
/**
* The `=` character.
*/
Equals = 61,
/**
* The `>` character.
*/
GreaterThan = 62,
/**
* The `?` character.
*/
QuestionMark = 63,
/**
* The `@` character.
*/
AtSign = 64,
A = 65,
B = 66,
C = 67,
D = 68,
E = 69,
F = 70,
G = 71,
H = 72,
I = 73,
J = 74,
K = 75,
L = 76,
M = 77,
N = 78,
O = 79,
P = 80,
Q = 81,
R = 82,
S = 83,
T = 84,
U = 85,
V = 86,
W = 87,
X = 88,
Y = 89,
Z = 90,
/**
* The `[` character.
*/
OpenSquareBracket = 91,
/**
* The `\` character.
*/
Backslash = 92,
/**
* The `]` character.
*/
CloseSquareBracket = 93,
/**
* The `^` character.
*/
Caret = 94,
/**
* The `_` character.
*/
Underline = 95,
/**
* The ``(`)`` character.
*/
BackTick = 96,
a = 97,
b = 98,
c = 99,
d = 100,
e = 101,
f = 102,
g = 103,
h = 104,
i = 105,
j = 106,
k = 107,
l = 108,
m = 109,
n = 110,
o = 111,
p = 112,
q = 113,
r = 114,
s = 115,
t = 116,
u = 117,
v = 118,
w = 119,
x = 120,
y = 121,
z = 122,
/**
* The `{` character.
*/
OpenCurlyBrace = 123,
/**
* The `|` character.
*/
Pipe = 124,
/**
* The `}` character.
*/
CloseCurlyBrace = 125,
/**
* The `~` character.
*/
Tilde = 126,
}

View File

@@ -0,0 +1,551 @@
/* eslint-disable no-console */
// MIT License
// Copyright (c) 2015 - present Microsoft Corporation
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { CharCode } from "./char-code";
const _debug = false;
export interface Match {
start: number;
end: number;
}
const _maxLen = 128;
function initTable() {
const table: number[][] = [];
const row: number[] = [];
for (let i = 0; i <= _maxLen; i++) {
row[i] = 0;
}
for (let i = 0; i <= _maxLen; i++) {
table.push(row.slice(0));
}
return table;
}
function isSeparatorAtPos(value: string, index: number): boolean {
if (index < 0 || index >= value.length) {
return false;
}
const code = value.codePointAt(index);
switch (code) {
case CharCode.Underline:
case CharCode.Dash:
case CharCode.Period:
case CharCode.Space:
case CharCode.Slash:
case CharCode.Backslash:
case CharCode.SingleQuote:
case CharCode.DoubleQuote:
case CharCode.Colon:
case CharCode.DollarSign:
case CharCode.LessThan:
case CharCode.OpenParen:
case CharCode.OpenSquareBracket:
return true;
case undefined:
return false;
default:
if (isEmojiImprecise(code)) {
return true;
}
return false;
}
}
function isWhitespaceAtPos(value: string, index: number): boolean {
if (index < 0 || index >= value.length) {
return false;
}
const code = value.charCodeAt(index);
switch (code) {
case CharCode.Space:
case CharCode.Tab:
return true;
default:
return false;
}
}
function isUpperCaseAtPos(pos: number, word: string, wordLow: string): boolean {
return word[pos] !== wordLow[pos];
}
export function isPatternInWord(
patternLow: string,
patternPos: number,
patternLen: number,
wordLow: string,
wordPos: number,
wordLen: number,
fillMinWordPosArr = false
): boolean {
while (patternPos < patternLen && wordPos < wordLen) {
if (patternLow[patternPos] === wordLow[wordPos]) {
if (fillMinWordPosArr) {
// Remember the min word position for each pattern position
_minWordMatchPos[patternPos] = wordPos;
}
patternPos += 1;
}
wordPos += 1;
}
return patternPos === patternLen; // pattern must be exhausted
}
enum Arrow {
Diag = 1,
Left = 2,
LeftLeft = 3,
}
/**
* An array representing a fuzzy match.
*
* 0. the score
* 1. the offset at which matching started
* 2. `<match_pos_N>`
* 3. `<match_pos_1>`
* 4. `<match_pos_0>` etc
*/
// export type FuzzyScore = [score: number, wordStart: number, ...matches: number[]];// [number, number, number];
export type FuzzyScore = Array<number>;
export function fuzzyScore(
pattern: string,
patternLow: string,
patternStart: number,
word: string,
wordLow: string,
wordStart: number,
firstMatchCanBeWeak: boolean
): FuzzyScore | undefined {
const patternLen = pattern.length > _maxLen ? _maxLen : pattern.length;
const wordLen = word.length > _maxLen ? _maxLen : word.length;
if (
patternStart >= patternLen ||
wordStart >= wordLen ||
patternLen - patternStart > wordLen - wordStart
) {
return undefined;
}
// Run a simple check if the characters of pattern occur
// (in order) at all in word. If that isn't the case we
// stop because no match will be possible
if (
!isPatternInWord(
patternLow,
patternStart,
patternLen,
wordLow,
wordStart,
wordLen,
true
)
) {
return undefined;
}
// Find the max matching word position for each pattern position
// NOTE: the min matching word position was filled in above, in the `isPatternInWord` call
_fillInMaxWordMatchPos(
patternLen,
wordLen,
patternStart,
wordStart,
patternLow,
wordLow
);
let row: number;
let column = 1;
let patternPos: number;
let wordPos: number;
const hasStrongFirstMatch = [false];
// There will be a match, fill in tables
for (
row = 1, patternPos = patternStart;
patternPos < patternLen;
row++, patternPos++
) {
// Reduce search space to possible matching word positions and to possible access from next row
const minWordMatchPos = _minWordMatchPos[patternPos];
const maxWordMatchPos = _maxWordMatchPos[patternPos];
const nextMaxWordMatchPos =
patternPos + 1 < patternLen ? _maxWordMatchPos[patternPos + 1] : wordLen;
for (
column = minWordMatchPos - wordStart + 1, wordPos = minWordMatchPos;
wordPos < nextMaxWordMatchPos;
column++, wordPos++
) {
let score = Number.MIN_SAFE_INTEGER;
let canComeDiag = false;
if (wordPos <= maxWordMatchPos) {
score = _doScore(
pattern,
patternLow,
patternPos,
patternStart,
word,
wordLow,
wordPos,
wordLen,
wordStart,
_diag[row - 1][column - 1] === 0,
hasStrongFirstMatch
);
}
let diagScore = 0;
if (score !== Number.MAX_SAFE_INTEGER) {
canComeDiag = true;
diagScore = score + _table[row - 1][column - 1];
}
const canComeLeft = wordPos > minWordMatchPos;
const leftScore = canComeLeft
? _table[row][column - 1] + (_diag[row][column - 1] > 0 ? -5 : 0)
: 0; // penalty for a gap start
const canComeLeftLeft =
wordPos > minWordMatchPos + 1 && _diag[row][column - 1] > 0;
const leftLeftScore = canComeLeftLeft
? _table[row][column - 2] + (_diag[row][column - 2] > 0 ? -5 : 0)
: 0; // penalty for a gap start
if (
canComeLeftLeft &&
(!canComeLeft || leftLeftScore >= leftScore) &&
(!canComeDiag || leftLeftScore >= diagScore)
) {
// always prefer choosing left left to jump over a diagonal because that means a match is earlier in the word
_table[row][column] = leftLeftScore;
_arrows[row][column] = Arrow.LeftLeft;
_diag[row][column] = 0;
} else if (canComeLeft && (!canComeDiag || leftScore >= diagScore)) {
// always prefer choosing left since that means a match is earlier in the word
_table[row][column] = leftScore;
_arrows[row][column] = Arrow.Left;
_diag[row][column] = 0;
} else if (canComeDiag) {
_table[row][column] = diagScore;
_arrows[row][column] = Arrow.Diag;
_diag[row][column] = _diag[row - 1][column - 1] + 1;
} else {
throw new Error(`not possible`);
}
}
}
if (_debug) {
printTables(pattern, patternStart, word, wordStart);
}
if (!hasStrongFirstMatch[0] && !firstMatchCanBeWeak) {
return undefined;
}
row--;
column--;
const result: FuzzyScore = [_table[row][column], wordStart];
let backwardsDiagLength = 0;
let maxMatchColumn = 0;
while (row >= 1) {
// Find the column where we go diagonally up
let diagColumn = column;
do {
const arrow = _arrows[row][diagColumn];
if (arrow === Arrow.LeftLeft) {
diagColumn -= 2;
} else if (arrow === Arrow.Left) {
diagColumn -= 1;
} else {
// found the diagonal
break;
}
} while (diagColumn >= 1);
// Overturn the "forwards" decision if keeping the "backwards" diagonal would give a better match
if (
backwardsDiagLength > 1 && // only if we would have a contiguous match of 3 characters
patternLow[patternStart + row - 1] === wordLow[wordStart + column - 1] && // only if we can do a contiguous match diagonally
!isUpperCaseAtPos(diagColumn + wordStart - 1, word, wordLow) && // only if the forwards chose diagonal is not an uppercase
backwardsDiagLength + 1 > _diag[row][diagColumn] // only if our contiguous match would be longer than the "forwards" contiguous match
) {
diagColumn = column;
}
if (diagColumn === column) {
// this is a contiguous match
backwardsDiagLength++;
} else {
backwardsDiagLength = 1;
}
if (!maxMatchColumn) {
// remember the last matched column
maxMatchColumn = diagColumn;
}
row--;
column = diagColumn - 1;
result.push(column);
}
if (wordLen === patternLen) {
// the word matches the pattern with all characters!
// giving the score a total match boost (to come up ahead other words)
result[0] += 2;
}
// Add 1 penalty for each skipped character in the word
const skippedCharsCount = maxMatchColumn - patternLen;
result[0] -= skippedCharsCount;
return result;
}
function _doScore(
pattern: string,
patternLow: string,
patternPos: number,
patternStart: number,
word: string,
wordLow: string,
wordPos: number,
wordLen: number,
wordStart: number,
newMatchStart: boolean,
outFirstMatchStrong: boolean[]
): number {
if (patternLow[patternPos] !== wordLow[wordPos]) {
return Number.MIN_SAFE_INTEGER;
}
let score = 1;
let isGapLocation = false;
if (wordPos === patternPos - patternStart) {
// common prefix: `foobar <-> foobaz`
// ^^^^^
score = pattern[patternPos] === word[wordPos] ? 7 : 5;
} else if (
isUpperCaseAtPos(wordPos, word, wordLow) &&
(wordPos === 0 || !isUpperCaseAtPos(wordPos - 1, word, wordLow))
) {
// hitting upper-case: `foo <-> forOthers`
// ^^ ^
score = pattern[patternPos] === word[wordPos] ? 7 : 5;
isGapLocation = true;
} else if (
isSeparatorAtPos(wordLow, wordPos) &&
(wordPos === 0 || !isSeparatorAtPos(wordLow, wordPos - 1))
) {
// hitting a separator: `. <-> foo.bar`
// ^
score = 5;
} else if (
isSeparatorAtPos(wordLow, wordPos - 1) ||
isWhitespaceAtPos(wordLow, wordPos - 1)
) {
// post separator: `foo <-> bar_foo`
// ^^^
score = 5;
isGapLocation = true;
}
if (score > 1 && patternPos === patternStart) {
outFirstMatchStrong[0] = true;
}
if (!isGapLocation) {
isGapLocation =
isUpperCaseAtPos(wordPos, word, wordLow) ||
isSeparatorAtPos(wordLow, wordPos - 1) ||
isWhitespaceAtPos(wordLow, wordPos - 1);
}
//
if (patternPos === patternStart) {
// first character in pattern
if (wordPos > wordStart) {
// the first pattern character would match a word character that is not at the word start
// so introduce a penalty to account for the gap preceding this match
score -= isGapLocation ? 3 : 5;
}
} else if (newMatchStart) {
// this would be the beginning of a new match (i.e. there would be a gap before this location)
score += isGapLocation ? 2 : 0;
} else {
// this is part of a contiguous match, so give it a slight bonus, but do so only if it would not be a prefered gap location
score += isGapLocation ? 0 : 1;
}
if (wordPos + 1 === wordLen) {
// we always penalize gaps, but this gives unfair advantages to a match that would match the last character in the word
// so pretend there is a gap after the last character in the word to normalize things
score -= isGapLocation ? 3 : 5;
}
return score;
}
function printTable(
table: number[][],
pattern: string,
patternLen: number,
word: string,
wordLen: number
): string {
function pad(s: string, n: number, _pad = " ") {
while (s.length < n) {
s = _pad + s;
}
return s;
}
let ret = ` | |${word
.split("")
.map((c) => pad(c, 3))
.join("|")}\n`;
for (let i = 0; i <= patternLen; i++) {
if (i === 0) {
ret += " |";
} else {
ret += `${pattern[i - 1]}|`;
}
ret +=
table[i]
.slice(0, wordLen + 1)
.map((n) => pad(n.toString(), 3))
.join("|") + "\n";
}
return ret;
}
function printTables(
pattern: string,
patternStart: number,
word: string,
wordStart: number
): void {
pattern = pattern.substr(patternStart);
word = word.substr(wordStart);
console.log(printTable(_table, pattern, pattern.length, word, word.length));
console.log(printTable(_arrows, pattern, pattern.length, word, word.length));
console.log(printTable(_diag, pattern, pattern.length, word, word.length));
}
const _minWordMatchPos = initArr(2 * _maxLen); // min word position for a certain pattern position
const _maxWordMatchPos = initArr(2 * _maxLen); // max word position for a certain pattern position
const _diag = initTable(); // the length of a contiguous diagonal match
const _table = initTable();
const _arrows = <Arrow[][]>initTable();
function initArr(maxLen: number) {
const row: number[] = [];
for (let i = 0; i <= maxLen; i++) {
row[i] = 0;
}
return row;
}
function _fillInMaxWordMatchPos(
patternLen: number,
wordLen: number,
patternStart: number,
wordStart: number,
patternLow: string,
wordLow: string
) {
let patternPos = patternLen - 1;
let wordPos = wordLen - 1;
while (patternPos >= patternStart && wordPos >= wordStart) {
if (patternLow[patternPos] === wordLow[wordPos]) {
_maxWordMatchPos[patternPos] = wordPos;
patternPos--;
}
wordPos--;
}
}
export interface FuzzyScorer {
(
pattern: string,
lowPattern: string,
patternPos: number,
word: string,
lowWord: string,
wordPos: number,
firstMatchCanBeWeak: boolean
): FuzzyScore | undefined;
}
export function createMatches(score: undefined | FuzzyScore): Match[] {
if (typeof score === "undefined") {
return [];
}
const res: Match[] = [];
const wordPos = score[1];
for (let i = score.length - 1; i > 1; i--) {
const pos = score[i] + wordPos;
const last = res[res.length - 1];
if (last && last.end === pos) {
last.end = pos + 1;
} else {
res.push({ start: pos, end: pos + 1 });
}
}
return res;
}
/**
* A fast function (therefore imprecise) to check if code points are emojis.
* Generated using https://github.com/alexdima/unicode-utils/blob/master/generate-emoji-test.js
*/
export function isEmojiImprecise(x: number): boolean {
return (
(x >= 0x1f1e6 && x <= 0x1f1ff) ||
x === 8986 ||
x === 8987 ||
x === 9200 ||
x === 9203 ||
(x >= 9728 && x <= 10175) ||
x === 11088 ||
x === 11093 ||
(x >= 127744 && x <= 128591) ||
(x >= 128640 && x <= 128764) ||
(x >= 128992 && x <= 129003) ||
(x >= 129280 && x <= 129535) ||
(x >= 129648 && x <= 129750)
);
}

View File

@@ -1,4 +1,52 @@
import fuzzysort from "fuzzysort"; import { fuzzyScore } from "./filter";
/**
* Determine whether a sequence of letters exists in another string,
* in that order, allowing for skipping. Ex: "chdr" exists in "chandelier")
*
* @param {string} filter - Sequence of letters to check for
* @param {ScorableTextItem} item - Item against whose strings will be checked
*
* @return {number} Score representing how well the word matches the filter. Return of 0 means no match.
*/
export const fuzzySequentialMatch = (
filter: string,
item: ScorableTextItem
) => {
let topScore = Number.NEGATIVE_INFINITY;
for (const word of item.strings) {
const scores = fuzzyScore(
filter,
filter.toLowerCase(),
0,
word,
word.toLowerCase(),
0,
true
);
if (!scores) {
continue;
}
// The VS Code implementation of filter returns a 0 for a weak match.
// But if .filter() sees a "0", it considers that a failed match and will remove it.
// So, we set score to 1 in these cases so the match will be included, and mostly respect correct ordering.
const score = scores[0] === 0 ? 1 : scores[0];
if (score > topScore) {
topScore = score;
}
}
if (topScore === Number.NEGATIVE_INFINITY) {
return undefined;
}
return topScore;
};
/** /**
* An interface that objects must extend in order to use the fuzzy sequence matcher * An interface that objects must extend in order to use the fuzzy sequence matcher
@@ -18,48 +66,18 @@ export interface ScorableTextItem {
strings: string[]; strings: string[];
} }
export type FuzzyFilterSort = <T extends ScorableTextItem>( type FuzzyFilterSort = <T extends ScorableTextItem>(
filter: string, filter: string,
items: T[] items: T[]
) => T[]; ) => T[];
export function fuzzyMatcher(search: string | null): (string) => boolean { export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) =>
const scorer = fuzzyScorer(search); items
return (value: string) => scorer([value]) !== Number.NEGATIVE_INFINITY;
}
export function fuzzyScorer(
search: string | null
): (values: string[]) => number {
const searchTerms = (search || "").match(/("[^"]+"|[^"\s]+)/g);
if (!searchTerms) {
return () => 0;
}
return (values) =>
searchTerms
.map((term) => {
const resultsForTerm = fuzzysort.go(term, values, {
allowTypo: true,
});
if (resultsForTerm.length > 0) {
return Math.max(...resultsForTerm.map((result) => result.score));
}
return Number.NEGATIVE_INFINITY;
})
.reduce((partial, current) => partial + current, 0);
}
export const fuzzySortFilterSort: FuzzyFilterSort = (filter, items) => {
const scorer = fuzzyScorer(filter);
return items
.map((item) => { .map((item) => {
item.score = scorer(item.strings); item.score = fuzzySequentialMatch(filter, item);
return item; return item;
}) })
.filter((item) => item.score !== undefined && item.score > -100000) .filter((item) => item.score !== undefined)
.sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) => .sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) =>
scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0 scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0
); );
};
export const defaultFuzzyFilterSort = fuzzySortFilterSort;

View File

@@ -7,26 +7,25 @@ import type {
SortableColumnContainer, SortableColumnContainer,
SortingDirection, SortingDirection,
} from "./ha-data-table"; } from "./ha-data-table";
import { fuzzyMatcher } from "../../common/string/filter/sequence-matching";
const filterData = ( const filterData = (
data: DataTableRowData[], data: DataTableRowData[],
columns: SortableColumnContainer, columns: SortableColumnContainer,
filter: string filter: string
) => { ) => {
const matcher = fuzzyMatcher(filter); filter = filter.toUpperCase();
return data.filter((row) => return data.filter((row) =>
Object.entries(columns).some((columnEntry) => { Object.entries(columns).some((columnEntry) => {
const [key, column] = columnEntry; const [key, column] = columnEntry;
if (column.filterable) { if (column.filterable) {
if ( if (
matcher( String(
String( column.filterKey
column.filterKey ? row[column.valueColumn || key][column.filterKey]
? row[column.valueColumn || key][column.filterKey] : row[column.valueColumn || key]
: row[column.valueColumn || key]
)
) )
.toUpperCase()
.includes(filter)
) { ) {
return true; return true;
} }

View File

@@ -5,6 +5,7 @@ import { fireEvent } from "../../common/dom/fire_event";
import { import {
DeviceAutomation, DeviceAutomation,
deviceAutomationsEqual, deviceAutomationsEqual,
sortDeviceAutomations,
} from "../../data/device_automation"; } from "../../data/device_automation";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-select"; import "../ha-select";
@@ -127,7 +128,9 @@ export abstract class HaDeviceAutomationPicker<
private async _updateDeviceInfo() { private async _updateDeviceInfo() {
this._automations = this.deviceId this._automations = this.deviceId
? await this._fetchDeviceAutomations(this.hass, this.deviceId) ? (await this._fetchDeviceAutomations(this.hass, this.deviceId)).sort(
sortDeviceAutomations
)
: // No device, clear the list of automations : // No device, clear the list of automations
[]; [];
@@ -161,8 +164,9 @@ export abstract class HaDeviceAutomationPicker<
if (this.value && deviceAutomationsEqual(automation, this.value)) { if (this.value && deviceAutomationsEqual(automation, this.value)) {
return; return;
} }
fireEvent(this, "change"); const value = { ...automation };
fireEvent(this, "value-changed", { value: automation }); delete value.metadata;
fireEvent(this, "value-changed", { value });
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@@ -15,7 +15,6 @@ import type { HaComboBox } from "../ha-combo-box";
import "../ha-icon-button"; import "../ha-icon-button";
import "../ha-svg-icon"; import "../ha-svg-icon";
import "./state-badge"; import "./state-badge";
import { defaultFuzzyFilterSort } from "../../common/string/filter/sequence-matching";
interface HassEntityWithCachedName extends HassEntity { interface HassEntityWithCachedName extends HassEntity {
friendly_name: string; friendly_name: string;
@@ -337,18 +336,11 @@ export class HaEntityPicker extends LitElement {
} }
private _filterChanged(ev: CustomEvent): void { private _filterChanged(ev: CustomEvent): void {
const filterString = ev.detail.value; const filterString = ev.detail.value.toLowerCase();
(this.comboBox as any).filteredItems = this._states.filter(
const sortableEntityStates = this._states.map((entityState) => ({ (entityState) =>
strings: [entityState.entity_id, computeStateName(entityState)], entityState.entity_id.toLowerCase().includes(filterString) ||
entityState: entityState, computeStateName(entityState).toLowerCase().includes(filterString)
}));
const sortedEntityStates = defaultFuzzyFilterSort(
filterString,
sortableEntityStates
);
(this.comboBox as any).filteredItems = sortedEntityStates.map(
(sortableItem) => sortableItem.entityState
); );
} }

View File

@@ -12,6 +12,8 @@ export class HaClickableListItem extends ListItemBase {
// property used only in css // property used only in css
@property({ type: Boolean, reflect: true }) public rtl = false; @property({ type: Boolean, reflect: true }) public rtl = false;
@property({ type: Boolean, reflect: true }) public openNewTab = false;
@query("a") private _anchor!: HTMLAnchorElement; @query("a") private _anchor!: HTMLAnchorElement;
public render() { public render() {
@@ -20,7 +22,12 @@ export class HaClickableListItem extends ListItemBase {
return html`${this.disableHref return html`${this.disableHref
? html`<a aria-role="option">${r}</a>` ? html`<a aria-role="option">${r}</a>`
: html`<a aria-role="option" href=${href}>${r}</a>`}`; : html`<a
aria-role="option"
target=${this.openNewTab ? "_blank" : ""}
href=${href}
>${r}</a
>`}`;
} }
firstUpdated() { firstUpdated() {
@@ -55,6 +62,7 @@ export class HaClickableListItem extends ListItemBase {
align-items: center; align-items: center;
padding-left: var(--mdc-list-side-padding, 20px); padding-left: var(--mdc-list-side-padding, 20px);
padding-right: var(--mdc-list-side-padding, 20px); padding-right: var(--mdc-list-side-padding, 20px);
overflow: hidden;
} }
`, `,
]; ];

View File

@@ -132,6 +132,11 @@ export class HaFormString extends LitElement implements HaFormElement {
--mdc-icon-button-size: 24px; --mdc-icon-button-size: 24px;
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
:host-context([style*="direction: rtl;"]) ha-icon-button {
right: auto;
left: 12px;
}
`; `;
} }
} }

View File

@@ -59,13 +59,6 @@ class HaNavigationList extends LitElement {
:host { :host {
--mdc-list-vertical-padding: 0; --mdc-list-vertical-padding: 0;
} }
a {
text-decoration: none;
color: var(--primary-text-color);
position: relative;
display: block;
outline: 0;
}
ha-svg-icon, ha-svg-icon,
ha-icon-next { ha-icon-next {
color: var(--secondary-text-color); color: var(--secondary-text-color);

View File

@@ -27,8 +27,8 @@ export class HaColorTempSelector extends LitElement {
pin pin
icon="hass:thermometer" icon="hass:thermometer"
.caption=${this.label || ""} .caption=${this.label || ""}
.min=${this.selector.color_temp.min_mireds ?? 153} .min=${this.selector.color_temp?.min_mireds ?? 153}
.max=${this.selector.color_temp.max_mireds ?? 500} .max=${this.selector.color_temp?.max_mireds ?? 500}
.value=${this.value} .value=${this.value}
.disabled=${this.disabled} .disabled=${this.disabled}
.helper=${this.helper} .helper=${this.helper}

View File

@@ -302,6 +302,10 @@ class DialogMediaManage extends LitElement {
--mdc-theme-primary: var(--mdc-theme-on-primary); --mdc-theme-primary: var(--mdc-theme-on-primary);
} }
mwc-list {
direction: ltr;
}
.danger { .danger {
--mdc-theme-primary: var(--error-color); --mdc-theme-primary: var(--error-color);
} }
@@ -310,6 +314,11 @@ class DialogMediaManage extends LitElement {
vertical-align: middle; vertical-align: middle;
} }
:host-context([style*="direction: rtl;"]) ha-svg-icon[slot="icon"] {
margin-left: 8px !important;
margin-right: 0px !important;
}
.refresh { .refresh {
display: flex; display: flex;
height: 200px; height: 200px;

View File

@@ -152,6 +152,7 @@ class DialogMediaPlayerBrowse extends LitElement {
ha-media-player-browse { ha-media-player-browse {
--media-browser-max-height: calc(100vh - 65px); --media-browser-max-height: calc(100vh - 65px);
height: calc(100vh - 65px); height: calc(100vh - 65px);
direction: ltr;
} }
@media (min-width: 800px) { @media (min-width: 800px) {

View File

@@ -59,6 +59,11 @@ class MediaManageButton extends LitElement {
ha-circular-progress[slot="icon"] { ha-circular-progress[slot="icon"] {
vertical-align: middle; vertical-align: middle;
} }
:host-context([style*="direction: rtl;"]) ha-svg-icon[slot="icon"] {
margin-left: 8px;
margin-right: 0px;
}
`; `;
} }

View File

@@ -1,10 +1,11 @@
import "@lit-labs/virtualizer";
import type { LitVirtualizer } from "@lit-labs/virtualizer";
import { grid } from "@lit-labs/virtualizer/layouts/grid";
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list"; import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiArrowUpRight, mdiPlay, mdiPlus } from "@mdi/js"; import { mdiArrowUpRight, mdiPlay, mdiPlus } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip"; import "@polymer/paper-tooltip/paper-tooltip";
import { grid } from "@lit-labs/virtualizer/layouts/grid";
import "@lit-labs/virtualizer";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@@ -21,10 +22,12 @@ import {
state, state,
} from "lit/decorators"; } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { until } from "lit/directives/until"; import { until } from "lit/directives/until";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { computeRTLDirection } from "../../common/util/compute_rtl"; import { computeRTLDirection } from "../../common/util/compute_rtl";
import { debounce } from "../../common/util/debounce"; import { debounce } from "../../common/util/debounce";
import { getSignedPath } from "../../data/auth";
import type { MediaPlayerItem } from "../../data/media-player"; import type { MediaPlayerItem } from "../../data/media-player";
import { import {
browseMediaPlayer, browseMediaPlayer,
@@ -39,6 +42,7 @@ import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer"; import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
import { haStyle } from "../../resources/styles"; import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url";
import { documentationUrl } from "../../util/documentation-url"; import { documentationUrl } from "../../util/documentation-url";
import "../entity/ha-entity-picker"; import "../entity/ha-entity-picker";
import "../ha-button-menu"; import "../ha-button-menu";
@@ -49,8 +53,6 @@ import "../ha-icon-button";
import "../ha-svg-icon"; import "../ha-svg-icon";
import "./ha-browse-media-tts"; import "./ha-browse-media-tts";
import type { TtsMediaPickedEvent } from "./ha-browse-media-tts"; import type { TtsMediaPickedEvent } from "./ha-browse-media-tts";
import { getSignedPath } from "../../data/auth";
import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url";
declare global { declare global {
interface HASSDomEvents { interface HASSDomEvents {
@@ -100,6 +102,10 @@ export class HaMediaPlayerBrowse extends LitElement {
@query(".content") private _content?: HTMLDivElement; @query(".content") private _content?: HTMLDivElement;
@query("lit-virtualizer") private _virtualizer?: LitVirtualizer;
private _observed = false;
private _headerOffsetHeight = 0; private _headerOffsetHeight = 0;
private _resizeObserver?: ResizeObserver; private _resizeObserver?: ResizeObserver;
@@ -280,6 +286,19 @@ export class HaMediaPlayerBrowse extends LitElement {
this._animateHeaderHeight(); this._animateHeaderHeight();
} else if (changedProps.has("_currentItem")) { } else if (changedProps.has("_currentItem")) {
this._setHeaderHeight(); this._setHeaderHeight();
// This fixes a race condition for resizing of the cards using the grid layout
if (this._observed) {
return;
}
// @ts-ignore
const virtualizer = this._virtualizer?._virtualizer;
if (virtualizer) {
this._observed = true;
setTimeout(() => virtualizer._observeMutations(), 0);
}
} }
} }
@@ -477,6 +496,9 @@ export class HaMediaPlayerBrowse extends LitElement {
<lit-virtualizer <lit-virtualizer
scroller scroller
.items=${children} .items=${children}
style=${styleMap({
height: `${children.length * 72 + 26}px`,
})}
.renderItem=${this._renderListItem} .renderItem=${this._renderListItem}
></lit-virtualizer> ></lit-virtualizer>
${currentItem.not_shown ${currentItem.not_shown
@@ -606,7 +628,6 @@ export class HaMediaPlayerBrowse extends LitElement {
</div> </div>
<span class="title">${child.title}</span> <span class="title">${child.title}</span>
</mwc-list-item> </mwc-list-item>
<li divider role="separator"></li>
`; `;
}; };

View File

@@ -119,6 +119,11 @@ class MediaUploadButton extends LitElement {
ha-circular-progress[slot="icon"] { ha-circular-progress[slot="icon"] {
vertical-align: middle; vertical-align: middle;
} }
:host-context([style*="direction: rtl;"]) ha-svg-icon[slot="icon"] {
margin-left: 8px;
margin-right: 0px;
}
`; `;
} }

View File

@@ -10,6 +10,8 @@ export class HaTimeline extends LitElement {
@property({ type: Boolean, reflect: true }) public raised = false; @property({ type: Boolean, reflect: true }) public raised = false;
@property({ reflect: true, type: Boolean }) notEnabled = false;
@property({ type: Boolean }) public lastItem = false; @property({ type: Boolean }) public lastItem = false;
@property({ type: String }) public icon?: string; @property({ type: String }) public icon?: string;
@@ -76,6 +78,9 @@ export class HaTimeline extends LitElement {
margin-right: 8px; margin-right: 8px;
width: 24px; width: 24px;
} }
:host([notEnabled]) ha-svg-icon {
opacity: 0.5;
}
ha-svg-icon { ha-svg-icon {
color: var( color: var(
--timeline-ball-color, --timeline-ball-color,

View File

@@ -114,6 +114,11 @@ export class HaTracePathDetails extends LitElement {
const { path, timestamp, result, error, changed_variables, ...rest } = const { path, timestamp, result, error, changed_variables, ...rest } =
trace as any; trace as any;
if (result?.enabled === false) {
return html`This node was disabled and skipped during execution so
no further trace information is available.`;
}
return html` return html`
${curPath === this.selected.path ${curPath === this.selected.path
? "" ? ""

View File

@@ -19,6 +19,8 @@ export class HatGraphNode extends LitElement {
@property({ reflect: true, type: Boolean }) disabled?: boolean; @property({ reflect: true, type: Boolean }) disabled?: boolean;
@property({ reflect: true, type: Boolean }) notEnabled = false;
@property({ reflect: true, type: Boolean }) graphStart?: boolean; @property({ reflect: true, type: Boolean }) graphStart?: boolean;
@property({ type: Boolean, attribute: "nofocus" }) noFocus = false; @property({ type: Boolean, attribute: "nofocus" }) noFocus = false;
@@ -114,8 +116,14 @@ export class HatGraphNode extends LitElement {
--stroke-clr: var(--hover-clr); --stroke-clr: var(--hover-clr);
--icon-clr: var(--default-icon-clr); --icon-clr: var(--default-icon-clr);
} }
:host([disabled]) circle { :host([notEnabled]) circle {
stroke: var(--disabled-clr); --stroke-clr: var(--disabled-clr);
}
:host([notEnabled][active]) circle {
--stroke-clr: var(--disabled-active-clr);
}
:host([notEnabled]:hover) circle {
--stroke-clr: var(--disabled-hover-clr);
} }
svg { svg {
width: 100%; width: 100%;

View File

@@ -1,7 +1,11 @@
import { import {
mdiAbTesting, mdiAbTesting,
mdiAlertOctagon,
mdiArrowDecision,
mdiArrowUp, mdiArrowUp,
mdiAsterisk, mdiAsterisk,
mdiCallMissed,
mdiCallReceived,
mdiCallSplit, mdiCallSplit,
mdiCheckboxBlankOutline, mdiCheckboxBlankOutline,
mdiCheckboxMarkedOutline, mdiCheckboxMarkedOutline,
@@ -9,10 +13,12 @@ import {
mdiChevronRight, mdiChevronRight,
mdiChevronUp, mdiChevronUp,
mdiClose, mdiClose,
mdiCloseOctagon,
mdiCodeBrackets, mdiCodeBrackets,
mdiDevices, mdiDevices,
mdiExclamation, mdiExclamation,
mdiRefresh, mdiRefresh,
mdiShuffleDisabled,
mdiTimerOutline, mdiTimerOutline,
mdiTrafficLight, mdiTrafficLight,
} from "@mdi/js"; } from "@mdi/js";
@@ -27,6 +33,9 @@ import {
DelayAction, DelayAction,
DeviceAction, DeviceAction,
EventAction, EventAction,
IfAction,
ManualScriptConfig,
ParallelAction,
RepeatAction, RepeatAction,
SceneAction, SceneAction,
ServiceAction, ServiceAction,
@@ -36,6 +45,8 @@ import {
import { import {
ChooseActionTraceStep, ChooseActionTraceStep,
ConditionTraceStep, ConditionTraceStep,
IfActionTraceStep,
StopActionTraceStep,
TraceExtended, TraceExtended,
} from "../../data/trace"; } from "../../data/trace";
import "../ha-icon-button"; import "../ha-icon-button";
@@ -85,6 +96,7 @@ export class HatScriptGraph extends LitElement {
@focus=${this.selectNode(config, path)} @focus=${this.selectNode(config, path)}
?active=${this.selected === path} ?active=${this.selected === path}
.iconPath=${mdiAsterisk} .iconPath=${mdiAsterisk}
.notEnabled=${config.enabled === false}
tabindex=${track ? "0" : "-1"} tabindex=${track ? "0" : "-1"}
></hat-graph-node> ></hat-graph-node>
`; `;
@@ -101,6 +113,9 @@ export class HatScriptGraph extends LitElement {
private typeRenderers = { private typeRenderers = {
condition: this.render_condition_node, condition: this.render_condition_node,
and: this.render_condition_node,
or: this.render_condition_node,
not: this.render_condition_node,
delay: this.render_delay_node, delay: this.render_delay_node,
event: this.render_event_node, event: this.render_event_node,
scene: this.render_scene_node, scene: this.render_scene_node,
@@ -110,23 +125,37 @@ export class HatScriptGraph extends LitElement {
repeat: this.render_repeat_node, repeat: this.render_repeat_node,
choose: this.render_choose_node, choose: this.render_choose_node,
device_id: this.render_device_node, device_id: this.render_device_node,
if: this.render_if_node,
stop: this.render_stop_node,
parallel: this.render_parallel_node,
other: this.render_other_node, other: this.render_other_node,
}; };
private render_action_node(node: Action, path: string, graphStart = false) { private render_action_node(
node: Action,
path: string,
graphStart = false,
disabled = false
) {
const type = const type =
Object.keys(this.typeRenderers).find((key) => key in node) || "other"; Object.keys(this.typeRenderers).find((key) => key in node) || "other";
this.renderedNodes[path] = { config: node, path }; this.renderedNodes[path] = { config: node, path };
if (this.trace && path in this.trace.trace) { if (this.trace && path in this.trace.trace) {
this.trackedNodes[path] = this.renderedNodes[path]; this.trackedNodes[path] = this.renderedNodes[path];
} }
return this.typeRenderers[type].bind(this)(node, path, graphStart); return this.typeRenderers[type].bind(this)(
node,
path,
graphStart,
disabled
);
} }
private render_choose_node( private render_choose_node(
config: ChooseAction, config: ChooseAction,
path: string, path: string,
graphStart = false graphStart = false,
disabled = false
) { ) {
const trace = this.trace.trace[path] as ChooseActionTraceStep[] | undefined; const trace = this.trace.trace[path] as ChooseActionTraceStep[] | undefined;
const trace_path = trace const trace_path = trace
@@ -143,12 +172,14 @@ export class HatScriptGraph extends LitElement {
@focus=${this.selectNode(config, path)} @focus=${this.selectNode(config, path)}
?track=${trace !== undefined} ?track=${trace !== undefined}
?active=${this.selected === path} ?active=${this.selected === path}
.notEnabled=${disabled || config.enabled === false}
> >
<hat-graph-node <hat-graph-node
.graphStart=${graphStart} .graphStart=${graphStart}
.iconPath=${mdiCallSplit} .iconPath=${mdiArrowDecision}
?track=${trace !== undefined} ?track=${trace !== undefined}
?active=${this.selected === path} ?active=${this.selected === path}
.notEnabled=${disabled || config.enabled === false}
slot="head" slot="head"
nofocus nofocus
></hat-graph-node> ></hat-graph-node>
@@ -171,12 +202,15 @@ export class HatScriptGraph extends LitElement {
@focus=${this.selectNode(config, branch_path)} @focus=${this.selectNode(config, branch_path)}
?track=${track_this} ?track=${track_this}
?active=${this.selected === branch_path} ?active=${this.selected === branch_path}
.notEnabled=${disabled || config.enabled === false}
></hat-graph-node> ></hat-graph-node>
${branch.sequence !== null ${branch.sequence !== null
? ensureArray(branch.sequence).map((action, j) => ? ensureArray(branch.sequence).map((action, j) =>
this.render_action_node( this.render_action_node(
action, action,
`${branch_path}/sequence/${j}` `${branch_path}/sequence/${j}`,
false,
disabled || config.enabled === false
) )
) )
: ""} : ""}
@@ -188,7 +222,12 @@ export class HatScriptGraph extends LitElement {
<hat-graph-spacer ?track=${track_default}></hat-graph-spacer> <hat-graph-spacer ?track=${track_default}></hat-graph-spacer>
${config.default !== null ${config.default !== null
? ensureArray(config.default)?.map((action, i) => ? ensureArray(config.default)?.map((action, i) =>
this.render_action_node(action, `${path}/default/${i}`) this.render_action_node(
action,
`${path}/default/${i}`,
false,
disabled || config.enabled === false
)
) )
: ""} : ""}
</div> </div>
@@ -196,10 +235,88 @@ export class HatScriptGraph extends LitElement {
`; `;
} }
private render_if_node(
config: IfAction,
path: string,
graphStart = false,
disabled = false
) {
const trace = this.trace.trace[path] as IfActionTraceStep[] | undefined;
let trackThen = false;
let trackElse = false;
for (const trc of trace || []) {
if (!trackThen && trc.result?.choice === "then") {
trackThen = true;
}
if ((!trackElse && trc.result?.choice === "else") || !trc.result) {
trackElse = true;
}
if (trackElse && trackThen) {
break;
}
}
return html`
<hat-graph-branch
tabindex=${trace === undefined ? "-1" : "0"}
@focus=${this.selectNode(config, path)}
?track=${trace !== undefined}
?active=${this.selected === path}
.notEnabled=${disabled || config.enabled === false}
>
<hat-graph-node
.graphStart=${graphStart}
.iconPath=${mdiCallSplit}
?track=${trace !== undefined}
?active=${this.selected === path}
.notEnabled=${disabled || config.enabled === false}
slot="head"
nofocus
></hat-graph-node>
${config.else
? html`<div class="graph-container" ?track=${trackElse}>
<hat-graph-node
.iconPath=${mdiCallMissed}
?track=${trackElse}
?active=${this.selected === path}
.notEnabled=${disabled || config.enabled === false}
nofocus
></hat-graph-node
>${ensureArray(config.else).map((action, j) =>
this.render_action_node(
action,
`${path}/else/${j}`,
false,
disabled || config.enabled === false
)
)}
</div>`
: html`<hat-graph-spacer ?track=${trackElse}></hat-graph-spacer>`}
<div class="graph-container" ?track=${trackThen}>
<hat-graph-node
.iconPath=${mdiCallReceived}
?track=${trackThen}
?active=${this.selected === path}
.notEnabled=${disabled || config.enabled === false}
nofocus
></hat-graph-node>
${ensureArray(config.then).map((action, j) =>
this.render_action_node(
action,
`${path}/then/${j}`,
false,
disabled || config.enabled === false
)
)}
</div>
</hat-graph-branch>
`;
}
private render_condition_node( private render_condition_node(
node: Condition, node: Condition,
path: string, path: string,
graphStart = false graphStart = false,
disabled = false
) { ) {
const trace = this.trace.trace[path] as ConditionTraceStep[] | undefined; const trace = this.trace.trace[path] as ConditionTraceStep[] | undefined;
let track = false; let track = false;
@@ -225,6 +342,7 @@ export class HatScriptGraph extends LitElement {
@focus=${this.selectNode(node, path)} @focus=${this.selectNode(node, path)}
?track=${track} ?track=${track}
?active=${this.selected === path} ?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
tabindex=${trace === undefined ? "-1" : "0"} tabindex=${trace === undefined ? "-1" : "0"}
short short
> >
@@ -233,6 +351,7 @@ export class HatScriptGraph extends LitElement {
slot="head" slot="head"
?track=${track} ?track=${track}
?active=${this.selected === path} ?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
.iconPath=${mdiAbTesting} .iconPath=${mdiAbTesting}
nofocus nofocus
></hat-graph-node> ></hat-graph-node>
@@ -247,6 +366,7 @@ export class HatScriptGraph extends LitElement {
nofocus nofocus
?track=${trackFailed} ?track=${trackFailed}
?active=${this.selected === path} ?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
></hat-graph-node> ></hat-graph-node>
</hat-graph-branch> </hat-graph-branch>
`; `;
@@ -255,7 +375,8 @@ export class HatScriptGraph extends LitElement {
private render_delay_node( private render_delay_node(
node: DelayAction, node: DelayAction,
path: string, path: string,
graphStart = false graphStart = false,
disabled = false
) { ) {
return html` return html`
<hat-graph-node <hat-graph-node
@@ -264,6 +385,7 @@ export class HatScriptGraph extends LitElement {
@focus=${this.selectNode(node, path)} @focus=${this.selectNode(node, path)}
?track=${path in this.trace.trace} ?track=${path in this.trace.trace}
?active=${this.selected === path} ?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"} tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node> ></hat-graph-node>
`; `;
@@ -272,7 +394,8 @@ export class HatScriptGraph extends LitElement {
private render_device_node( private render_device_node(
node: DeviceAction, node: DeviceAction,
path: string, path: string,
graphStart = false graphStart = false,
disabled = false
) { ) {
return html` return html`
<hat-graph-node <hat-graph-node
@@ -281,6 +404,7 @@ export class HatScriptGraph extends LitElement {
@focus=${this.selectNode(node, path)} @focus=${this.selectNode(node, path)}
?track=${path in this.trace.trace} ?track=${path in this.trace.trace}
?active=${this.selected === path} ?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"} tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node> ></hat-graph-node>
`; `;
@@ -289,7 +413,8 @@ export class HatScriptGraph extends LitElement {
private render_event_node( private render_event_node(
node: EventAction, node: EventAction,
path: string, path: string,
graphStart = false graphStart = false,
disabled = false
) { ) {
return html` return html`
<hat-graph-node <hat-graph-node
@@ -298,6 +423,7 @@ export class HatScriptGraph extends LitElement {
@focus=${this.selectNode(node, path)} @focus=${this.selectNode(node, path)}
?track=${path in this.trace.trace} ?track=${path in this.trace.trace}
?active=${this.selected === path} ?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"} tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node> ></hat-graph-node>
`; `;
@@ -306,7 +432,8 @@ export class HatScriptGraph extends LitElement {
private render_repeat_node( private render_repeat_node(
node: RepeatAction, node: RepeatAction,
path: string, path: string,
graphStart = false graphStart = false,
disabled = false
) { ) {
const trace: any = this.trace.trace[path]; const trace: any = this.trace.trace[path];
const repeats = this.trace?.trace[`${path}/repeat/sequence/0`]?.length; const repeats = this.trace?.trace[`${path}/repeat/sequence/0`]?.length;
@@ -316,12 +443,14 @@ export class HatScriptGraph extends LitElement {
@focus=${this.selectNode(node, path)} @focus=${this.selectNode(node, path)}
?track=${path in this.trace.trace} ?track=${path in this.trace.trace}
?active=${this.selected === path} ?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
> >
<hat-graph-node <hat-graph-node
.graphStart=${graphStart} .graphStart=${graphStart}
.iconPath=${mdiRefresh} .iconPath=${mdiRefresh}
?track=${path in this.trace.trace} ?track=${path in this.trace.trace}
?active=${this.selected === path} ?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
slot="head" slot="head"
nofocus nofocus
></hat-graph-node> ></hat-graph-node>
@@ -329,12 +458,18 @@ export class HatScriptGraph extends LitElement {
.iconPath=${mdiArrowUp} .iconPath=${mdiArrowUp}
?track=${repeats > 1} ?track=${repeats > 1}
?active=${this.selected === path} ?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
nofocus nofocus
.badge=${repeats > 1 ? repeats : undefined} .badge=${repeats > 1 ? repeats : undefined}
></hat-graph-node> ></hat-graph-node>
<div ?track=${trace}> <div ?track=${trace}>
${ensureArray(node.repeat.sequence).map((action, i) => ${ensureArray(node.repeat.sequence).map((action, i) =>
this.render_action_node(action, `${path}/repeat/sequence/${i}`) this.render_action_node(
action,
`${path}/repeat/sequence/${i}`,
false,
disabled || node.enabled === false
)
)} )}
</div> </div>
</hat-graph-branch> </hat-graph-branch>
@@ -344,7 +479,8 @@ export class HatScriptGraph extends LitElement {
private render_scene_node( private render_scene_node(
node: SceneAction, node: SceneAction,
path: string, path: string,
graphStart = false graphStart = false,
disabled = false
) { ) {
return html` return html`
<hat-graph-node <hat-graph-node
@@ -353,6 +489,7 @@ export class HatScriptGraph extends LitElement {
@focus=${this.selectNode(node, path)} @focus=${this.selectNode(node, path)}
?track=${path in this.trace.trace} ?track=${path in this.trace.trace}
?active=${this.selected === path} ?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"} tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node> ></hat-graph-node>
`; `;
@@ -361,7 +498,8 @@ export class HatScriptGraph extends LitElement {
private render_service_node( private render_service_node(
node: ServiceAction, node: ServiceAction,
path: string, path: string,
graphStart = false graphStart = false,
disabled = false
) { ) {
return html` return html`
<hat-graph-node <hat-graph-node
@@ -370,6 +508,7 @@ export class HatScriptGraph extends LitElement {
@focus=${this.selectNode(node, path)} @focus=${this.selectNode(node, path)}
?track=${path in this.trace.trace} ?track=${path in this.trace.trace}
?active=${this.selected === path} ?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"} tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node> ></hat-graph-node>
`; `;
@@ -378,7 +517,8 @@ export class HatScriptGraph extends LitElement {
private render_wait_node( private render_wait_node(
node: WaitAction | WaitForTriggerAction, node: WaitAction | WaitForTriggerAction,
path: string, path: string,
graphStart = false graphStart = false,
disabled = false
) { ) {
return html` return html`
<hat-graph-node <hat-graph-node
@@ -387,12 +527,87 @@ export class HatScriptGraph extends LitElement {
@focus=${this.selectNode(node, path)} @focus=${this.selectNode(node, path)}
?track=${path in this.trace.trace} ?track=${path in this.trace.trace}
?active=${this.selected === path} ?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"} tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node> ></hat-graph-node>
`; `;
} }
private render_other_node(node: Action, path: string, graphStart = false) { private render_parallel_node(
node: ParallelAction,
path: string,
graphStart = false,
disabled = false
) {
const trace: any = this.trace.trace[path];
return html`
<hat-graph-branch
tabindex=${trace === undefined ? "-1" : "0"}
@focus=${this.selectNode(node, path)}
?track=${path in this.trace.trace}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
>
<hat-graph-node
.graphStart=${graphStart}
.iconPath=${mdiShuffleDisabled}
?track=${path in this.trace.trace}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
slot="head"
nofocus
></hat-graph-node>
${ensureArray(node.parallel).map((action, i) =>
"sequence" in action
? html`<div ?track=${path in this.trace.trace}>
${ensureArray((action as ManualScriptConfig).sequence).map(
(sAction, j) =>
this.render_action_node(
sAction,
`${path}/parallel/${i}/sequence/${j}`,
false,
disabled || node.enabled === false
)
)}
</div>`
: this.render_action_node(
action,
`${path}/parallel/${i}/sequence/0`,
false,
disabled || node.enabled === false
)
)}
</hat-graph-branch>
`;
}
private render_stop_node(
node: Action,
path: string,
graphStart = false,
disabled = false
) {
const trace = this.trace.trace[path] as StopActionTraceStep[] | undefined;
return html`
<hat-graph-node
.graphStart=${graphStart}
.iconPath=${trace?.[0].result?.error
? mdiAlertOctagon
: mdiCloseOctagon}
@focus=${this.selectNode(node, path)}
?track=${path in this.trace.trace}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
></hat-graph-node>
`;
}
private render_other_node(
node: Action,
path: string,
graphStart = false,
disabled = false
) {
return html` return html`
<hat-graph-node <hat-graph-node
.graphStart=${graphStart} .graphStart=${graphStart}
@@ -400,6 +615,7 @@ export class HatScriptGraph extends LitElement {
@focus=${this.selectNode(node, path)} @focus=${this.selectNode(node, path)}
?track=${path in this.trace.trace} ?track=${path in this.trace.trace}
?active=${this.selected === path} ?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
></hat-graph-node> ></hat-graph-node>
`; `;
} }
@@ -538,6 +754,8 @@ export class HatScriptGraph extends LitElement {
--track-clr: var(--track-color, var(--accent-color)); --track-clr: var(--track-color, var(--accent-color));
--hover-clr: var(--hover-color, var(--primary-color)); --hover-clr: var(--hover-color, var(--primary-color));
--disabled-clr: var(--disabled-color, var(--disabled-text-color)); --disabled-clr: var(--disabled-color, var(--disabled-text-color));
--disabled-active-clr: rgba(var(--rgb-primary-color), 0.5);
--disabled-hover-clr: rgba(var(--rgb-primary-color), 0.7);
--default-trigger-color: 3, 169, 244; --default-trigger-color: 3, 169, 244;
--rgb-trigger-color: var(--trigger-color, var(--default-trigger-color)); --rgb-trigger-color: var(--trigger-color, var(--default-trigger-color));
--background-clr: var(--background-color, white); --background-clr: var(--background-color, white);

View File

@@ -25,12 +25,17 @@ import {
ChooseAction, ChooseAction,
ChooseActionChoice, ChooseActionChoice,
getActionType, getActionType,
IfAction,
ParallelAction,
RepeatAction,
} from "../../data/script"; } from "../../data/script";
import { describeAction } from "../../data/script_i18n"; import { describeAction } from "../../data/script_i18n";
import { import {
ActionTraceStep,
AutomationTraceExtended, AutomationTraceExtended,
ChooseActionTraceStep, ChooseActionTraceStep,
getDataFromPath, getDataFromPath,
IfActionTraceStep,
isTriggerPath, isTriggerPath,
TriggerTraceStep, TriggerTraceStep,
} from "../../data/trace"; } from "../../data/trace";
@@ -105,7 +110,7 @@ class LogbookRenderer {
} }
get hasNext() { get hasNext() {
return this.curIndex !== this.logbookEntries.length; return this.curIndex < this.logbookEntries.length;
} }
maybeRenderItem() { maybeRenderItem() {
@@ -201,7 +206,7 @@ class ActionRenderer {
} }
get hasNext() { get hasNext() {
return this.curIndex !== this.keys.length; return this.curIndex < this.keys.length;
} }
renderItem() { renderItem() {
@@ -214,15 +219,31 @@ class ActionRenderer {
private _renderItem( private _renderItem(
index: number, index: number,
actionType?: ReturnType<typeof getActionType> actionType?: ReturnType<typeof getActionType>,
renderAllIterations?: boolean
): number { ): number {
const value = this._getItem(index); const value = this._getItem(index);
if (isTriggerPath(value[0].path)) { if (renderAllIterations) {
return this._handleTrigger(index, value[0] as TriggerTraceStep); let i;
value.forEach((item) => {
i = this._renderIteration(index, item, actionType);
});
return i;
}
return this._renderIteration(index, value[0], actionType);
}
private _renderIteration(
index: number,
value: ActionTraceStep,
actionType?: ReturnType<typeof getActionType>
) {
if (isTriggerPath(value.path)) {
return this._handleTrigger(index, value as TriggerTraceStep);
} }
const timestamp = new Date(value[0].timestamp); const timestamp = new Date(value.timestamp);
// Render all logbook items that are in front of this item. // Render all logbook items that are in front of this item.
while ( while (
@@ -235,7 +256,7 @@ class ActionRenderer {
this.logbookRenderer.flush(); this.logbookRenderer.flush();
this.timeTracker.maybeRenderTime(timestamp); this.timeTracker.maybeRenderTime(timestamp);
const path = value[0].path; const path = value.path;
let data; let data;
try { try {
data = getDataFromPath(this.trace.config, path); data = getDataFromPath(this.trace.config, path);
@@ -263,7 +284,24 @@ class ActionRenderer {
return this._handleChoose(index); return this._handleChoose(index);
} }
this._renderEntry(path, describeAction(this.hass, data, actionType)); if (actionType === "repeat") {
return this._handleRepeat(index);
}
if (actionType === "if") {
return this._handleIf(index);
}
if (actionType === "parallel") {
return this._handleParallel(index);
}
this._renderEntry(
path,
describeAction(this.hass, data, actionType),
undefined,
data.enabled === false
);
let i = index + 1; let i = index + 1;
@@ -316,10 +354,16 @@ class ActionRenderer {
const chooseConfig = this._getDataFromPath( const chooseConfig = this._getDataFromPath(
this.keys[index] this.keys[index]
) as ChooseAction; ) as ChooseAction;
const disabled = chooseConfig.enabled === false;
const name = chooseConfig.alias || "Choose"; const name = chooseConfig.alias || "Choose";
if (defaultExecuted) { if (defaultExecuted) {
this._renderEntry(choosePath, `${name}: Default action executed`); this._renderEntry(
choosePath,
`${name}: Default action executed`,
undefined,
disabled
);
} else if (chooseTrace.result) { } else if (chooseTrace.result) {
const choiceNumeric = const choiceNumeric =
chooseTrace.result.choice !== "default" chooseTrace.result.choice !== "default"
@@ -331,9 +375,19 @@ class ActionRenderer {
const choiceName = choiceConfig const choiceName = choiceConfig
? `${choiceConfig.alias || `Option ${choiceNumeric}`} executed` ? `${choiceConfig.alias || `Option ${choiceNumeric}`} executed`
: `Error: ${chooseTrace.error}`; : `Error: ${chooseTrace.error}`;
this._renderEntry(choosePath, `${name}: ${choiceName}`); this._renderEntry(
choosePath,
`${name}: ${choiceName}`,
undefined,
disabled
);
} else { } else {
this._renderEntry(choosePath, `${name}: No action taken`); this._renderEntry(
choosePath,
`${name}: No action taken`,
undefined,
disabled
);
} }
let i; let i;
@@ -374,14 +428,130 @@ class ActionRenderer {
return i; return i;
} }
private _handleRepeat(index: number): number {
const repeatPath = this.keys[index];
const startLevel = repeatPath.split("/").length;
const repeatConfig = this._getDataFromPath(
this.keys[index]
) as RepeatAction;
const disabled = repeatConfig.enabled === false;
const name = repeatConfig.alias || describeAction(this.hass, repeatConfig);
this._renderEntry(repeatPath, name, undefined, disabled);
let i;
for (i = index + 1; i < this.keys.length; i++) {
const path = this.keys[i];
const parts = path.split("/");
// We're done if no more sequence in current level
if (parts.length <= startLevel) {
return i;
}
i = this._renderItem(i, getActionType(this._getDataFromPath(path)), true);
}
return i;
}
private _handleIf(index: number): number {
const ifPath = this.keys[index];
const startLevel = ifPath.split("/").length;
const ifTrace = this._getItem(index)[0] as IfActionTraceStep;
const ifConfig = this._getDataFromPath(this.keys[index]) as IfAction;
const disabled = ifConfig.enabled === false;
const name = ifConfig.alias || "If";
if (ifTrace.result?.choice) {
const choiceConfig = this._getDataFromPath(
`${this.keys[index]}/${ifTrace.result.choice}/`
) as any;
const choiceName = choiceConfig
? `${choiceConfig.alias || `${ifTrace.result.choice} action executed`}`
: `Error: ${ifTrace.error}`;
this._renderEntry(ifPath, `${name}: ${choiceName}`, undefined, disabled);
} else {
this._renderEntry(
ifPath,
`${name}: No action taken`,
undefined,
disabled
);
}
let i;
// Skip over conditions
for (i = index + 1; i < this.keys.length; i++) {
const path = this.keys[i];
const parts = this.keys[i].split("/");
// We're done if no more sequence in current level
if (parts.length <= startLevel) {
return i;
}
// We're going to skip all conditions
if (
parts[startLevel + 1] === "condition" ||
parts.length < startLevel + 2
) {
continue;
}
i = this._renderItem(i, getActionType(this._getDataFromPath(path)));
}
return i;
}
private _handleParallel(index: number): number {
const parallelPath = this.keys[index];
const startLevel = parallelPath.split("/").length;
const parallelConfig = this._getDataFromPath(
this.keys[index]
) as ParallelAction;
const disabled = parallelConfig.enabled === false;
const name = parallelConfig.alias || "Execute in parallel";
this._renderEntry(parallelPath, name, undefined, disabled);
let i;
for (i = index + 1; i < this.keys.length; i++) {
const path = this.keys[i];
const parts = path.split("/");
// We're done if no more sequence in current level
if (parts.length <= startLevel) {
return i;
}
i = this._renderItem(i, getActionType(this._getDataFromPath(path)));
}
return i;
}
private _renderEntry( private _renderEntry(
path: string, path: string,
description: string, description: string,
icon = mdiRecordCircleOutline icon = mdiRecordCircleOutline,
disabled = false
) { ) {
this.entries.push(html` this.entries.push(html`
<ha-timeline .icon=${icon} data-path=${path}> <ha-timeline .icon=${icon} data-path=${path} .notEnabled=${disabled}>
${description} ${description}${disabled
? html`<span class="disabled"> (disabled)</span>`
: ""}
</ha-timeline> </ha-timeline>
`); `);
} }

View File

@@ -65,6 +65,7 @@ export interface BaseTrigger {
platform: string; platform: string;
id?: string; id?: string;
variables?: Record<string, unknown>; variables?: Record<string, unknown>;
enabled?: boolean;
} }
export interface StateTrigger extends BaseTrigger { export interface StateTrigger extends BaseTrigger {
@@ -178,6 +179,7 @@ export type Trigger =
interface BaseCondition { interface BaseCondition {
condition: string; condition: string;
alias?: string; alias?: string;
enabled?: boolean;
} }
export interface LogicalCondition extends BaseCondition { export interface LogicalCondition extends BaseCondition {
@@ -235,6 +237,10 @@ export interface TriggerCondition extends BaseCondition {
type ShorthandBaseCondition = Omit<BaseCondition, "condition">; type ShorthandBaseCondition = Omit<BaseCondition, "condition">;
export interface ShorthandAndConditionList extends ShorthandBaseCondition {
condition: Condition[];
}
export interface ShorthandAndCondition extends ShorthandBaseCondition { export interface ShorthandAndCondition extends ShorthandBaseCondition {
and: Condition[]; and: Condition[];
} }
@@ -260,10 +266,33 @@ export type Condition =
export type ConditionWithShorthand = export type ConditionWithShorthand =
| Condition | Condition
| ShorthandAndConditionList
| ShorthandAndCondition | ShorthandAndCondition
| ShorthandOrCondition | ShorthandOrCondition
| ShorthandNotCondition; | ShorthandNotCondition;
export const expandConditionWithShorthand = (
cond: ConditionWithShorthand
): Condition => {
if ("condition" in cond && Array.isArray(cond.condition)) {
return {
condition: "and",
conditions: cond.condition,
};
}
for (const condition of ["and", "or", "not"]) {
if (condition in cond) {
return {
condition,
conditions: cond[condition],
} as Condition;
}
}
return cond as Condition;
};
export const triggerAutomationActions = ( export const triggerAutomationActions = (
hass: HomeAssistant, hass: HomeAssistant,
entityId: string entityId: string

View File

@@ -11,6 +11,8 @@ export interface DeviceAutomation {
type?: string; type?: string;
subtype?: string; subtype?: string;
event?: string; event?: string;
enabled?: boolean;
metadata?: { secondary: boolean };
} }
export interface DeviceAction extends DeviceAutomation { export interface DeviceAction extends DeviceAutomation {
@@ -179,3 +181,16 @@ export const localizeDeviceAutomationTrigger = (
(trigger.subtype ? `"${trigger.subtype}" ${trigger.type}` : trigger.type!) (trigger.subtype ? `"${trigger.subtype}" ${trigger.type}` : trigger.type!)
); );
}; };
export const sortDeviceAutomations = (
automationA: DeviceAutomation,
automationB: DeviceAutomation
) => {
if (automationA.metadata?.secondary && !automationB.metadata?.secondary) {
return 1;
}
if (!automationA.metadata?.secondary && automationB.metadata?.secondary) {
return -1;
}
return 0;
};

22
src/data/hardware.ts Normal file
View File

@@ -0,0 +1,22 @@
// Keep in sync with https://github.com/home-assistant/analytics.home-assistant.io/blob/dev/site/src/analytics-os-boards.ts#L6-L24
export const BOARD_NAMES: Record<string, string> = {
"odroid-n2": "Home Assistant Blue / ODROID-N2",
"odroid-xu4": "ODROID-XU4",
"odroid-c2": "ODROID-C2",
"odroid-c4": "ODROID-C4",
rpi: "Raspberry Pi",
rpi0: "Raspberry Pi Zero",
"rpi0-w": "Raspberry Pi Zero W",
rpi2: "Raspberry Pi 2",
rpi3: "Raspberry Pi 3 (32-bit)",
"rpi3-64": "Raspberry Pi 3",
rpi4: "Raspberry Pi 4 (32-bit)",
"rpi4-64": "Raspberry Pi 4",
tinker: "ASUS Tinker Board",
"khadas-vim3": "Khadas VIM3",
"generic-aarch64": "Generic AArch64",
ova: "Virtual Machine",
"generic-x86-64": "Generic x86-64",
"intel-nuc": "Intel NUC",
yellow: "Home Assistant Yellow",
};

View File

@@ -1,7 +1,7 @@
import { atLeastVersion } from "../../common/config/version"; import { atLeastVersion } from "../../common/config/version";
import { HomeAssistant, PanelInfo } from "../../types"; import { HomeAssistant, PanelInfo } from "../../types";
import { SupervisorArch } from "../supervisor/supervisor"; import { SupervisorArch } from "../supervisor/supervisor";
import { HassioAddonInfo, HassioAddonRepository } from "./addon"; import { HassioAddonInfo } from "./addon";
import { hassioApiResultExtractor, HassioResponse } from "./common"; import { hassioApiResultExtractor, HassioResponse } from "./common";
export type HassioHomeAssistantInfo = { export type HassioHomeAssistantInfo = {
@@ -23,7 +23,7 @@ export type HassioHomeAssistantInfo = {
export type HassioSupervisorInfo = { export type HassioSupervisorInfo = {
addons: HassioAddonInfo[]; addons: HassioAddonInfo[];
addons_repositories: HassioAddonRepository[]; addons_repositories: string[];
arch: SupervisorArch; arch: SupervisorArch;
channel: string; channel: string;
debug: boolean; debug: boolean;
@@ -179,7 +179,10 @@ export const fetchHassioInfo = async (
}; };
export const fetchHassioLogs = async (hass: HomeAssistant, provider: string) => export const fetchHassioLogs = async (hass: HomeAssistant, provider: string) =>
hass.callApi<string>("GET", `hassio/${provider}/logs`); hass.callApi<string>(
"GET",
`hassio/${provider.includes("_") ? `addons/${provider}` : provider}/logs`
);
export const setSupervisorOption = async ( export const setSupervisorOption = async (
hass: HomeAssistant, hass: HomeAssistant,

View File

@@ -13,11 +13,18 @@ import {
literal, literal,
is, is,
Describe, Describe,
boolean,
} from "superstruct"; } from "superstruct";
import { computeObjectId } from "../common/entity/compute_object_id"; import { computeObjectId } from "../common/entity/compute_object_id";
import { navigate } from "../common/navigate"; import { navigate } from "../common/navigate";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { Condition, Trigger } from "./automation"; import {
Condition,
ShorthandAndCondition,
ShorthandNotCondition,
ShorthandOrCondition,
Trigger,
} from "./automation";
import { BlueprintInput } from "./blueprint"; import { BlueprintInput } from "./blueprint";
export const MODES = ["single", "restart", "queued", "parallel"] as const; export const MODES = ["single", "restart", "queued", "parallel"] as const;
@@ -25,6 +32,7 @@ export const MODES_MAX = ["queued", "parallel"];
export const baseActionStruct = object({ export const baseActionStruct = object({
alias: optional(string()), alias: optional(string()),
enabled: optional(boolean()),
}); });
const targetStruct = object({ const targetStruct = object({
@@ -88,15 +96,18 @@ export interface BlueprintScriptConfig extends ManualScriptConfig {
use_blueprint: { path: string; input?: BlueprintInput }; use_blueprint: { path: string; input?: BlueprintInput };
} }
export interface EventAction { interface BaseAction {
alias?: string; alias?: string;
enabled?: boolean;
}
export interface EventAction extends BaseAction {
event: string; event: string;
event_data?: Record<string, any>; event_data?: Record<string, any>;
event_data_template?: Record<string, any>; event_data_template?: Record<string, any>;
} }
export interface ServiceAction { export interface ServiceAction extends BaseAction {
alias?: string;
service?: string; service?: string;
service_template?: string; service_template?: string;
entity_id?: string; entity_id?: string;
@@ -104,55 +115,48 @@ export interface ServiceAction {
data?: Record<string, unknown>; data?: Record<string, unknown>;
} }
export interface DeviceAction { export interface DeviceAction extends BaseAction {
alias?: string;
type: string; type: string;
device_id: string; device_id: string;
domain: string; domain: string;
entity_id: string; entity_id: string;
} }
export interface DelayActionParts { export interface DelayActionParts extends BaseAction {
milliseconds?: number; milliseconds?: number;
seconds?: number; seconds?: number;
minutes?: number; minutes?: number;
hours?: number; hours?: number;
days?: number; days?: number;
} }
export interface DelayAction { export interface DelayAction extends BaseAction {
alias?: string;
delay: number | Partial<DelayActionParts> | string; delay: number | Partial<DelayActionParts> | string;
} }
export interface ServiceSceneAction { export interface ServiceSceneAction extends BaseAction {
alias?: string;
service: "scene.turn_on"; service: "scene.turn_on";
target?: { entity_id?: string }; target?: { entity_id?: string };
entity_id?: string; entity_id?: string;
metadata: Record<string, unknown>; metadata: Record<string, unknown>;
} }
export interface LegacySceneAction { export interface LegacySceneAction extends BaseAction {
alias?: string;
scene: string; scene: string;
} }
export type SceneAction = ServiceSceneAction | LegacySceneAction; export type SceneAction = ServiceSceneAction | LegacySceneAction;
export interface WaitAction { export interface WaitAction extends BaseAction {
alias?: string;
wait_template: string; wait_template: string;
timeout?: number; timeout?: number;
continue_on_timeout?: boolean; continue_on_timeout?: boolean;
} }
export interface WaitForTriggerAction { export interface WaitForTriggerAction extends BaseAction {
alias?: string;
wait_for_trigger: Trigger | Trigger[]; wait_for_trigger: Trigger | Trigger[];
timeout?: number; timeout?: number;
continue_on_timeout?: boolean; continue_on_timeout?: boolean;
} }
export interface PlayMediaAction { export interface PlayMediaAction extends BaseAction {
alias?: string;
service: "media_player.play_media"; service: "media_player.play_media";
target?: { entity_id?: string }; target?: { entity_id?: string };
entity_id?: string; entity_id?: string;
@@ -160,13 +164,11 @@ export interface PlayMediaAction {
metadata: Record<string, unknown>; metadata: Record<string, unknown>;
} }
export interface RepeatAction { export interface RepeatAction extends BaseAction {
alias?: string; repeat: CountRepeat | WhileRepeat | UntilRepeat | ForEachRepeat;
repeat: CountRepeat | WhileRepeat | UntilRepeat;
} }
interface BaseRepeat { interface BaseRepeat extends BaseAction {
alias?: string;
sequence: Action | Action[]; sequence: Action | Action[];
} }
@@ -182,38 +184,40 @@ export interface UntilRepeat extends BaseRepeat {
until: Condition[]; until: Condition[];
} }
export interface ChooseActionChoice { export interface ForEachRepeat extends BaseRepeat {
alias?: string; for_each: string | any[];
}
export interface ChooseActionChoice extends BaseAction {
conditions: string | Condition[]; conditions: string | Condition[];
sequence: Action | Action[]; sequence: Action | Action[];
} }
export interface ChooseAction { export interface ChooseAction extends BaseAction {
alias?: string;
choose: ChooseActionChoice | ChooseActionChoice[] | null; choose: ChooseActionChoice | ChooseActionChoice[] | null;
default?: Action | Action[]; default?: Action | Action[];
} }
export interface IfAction { export interface IfAction extends BaseAction {
alias?: string;
if: string | Condition[]; if: string | Condition[];
then: Action | Action[]; then: Action | Action[];
else?: Action | Action[]; else?: Action | Action[];
} }
export interface VariablesAction { export interface VariablesAction extends BaseAction {
alias?: string;
variables: Record<string, unknown>; variables: Record<string, unknown>;
} }
export interface StopAction { export interface StopAction extends BaseAction {
alias?: string;
stop: string; stop: string;
error?: boolean; error?: boolean;
} }
interface UnknownAction { export interface ParallelAction extends BaseAction {
alias?: string; parallel: ManualScriptConfig | Action | (ManualScriptConfig | Action)[];
}
interface UnknownAction extends BaseAction {
[key: string]: unknown; [key: string]: unknown;
} }
@@ -222,6 +226,9 @@ export type Action =
| DeviceAction | DeviceAction
| ServiceAction | ServiceAction
| Condition | Condition
| ShorthandAndCondition
| ShorthandOrCondition
| ShorthandNotCondition
| DelayAction | DelayAction
| SceneAction | SceneAction
| WaitAction | WaitAction
@@ -232,6 +239,7 @@ export type Action =
| VariablesAction | VariablesAction
| PlayMediaAction | PlayMediaAction
| StopAction | StopAction
| ParallelAction
| UnknownAction; | UnknownAction;
export interface ActionTypes { export interface ActionTypes {
@@ -249,6 +257,7 @@ export interface ActionTypes {
service: ServiceAction; service: ServiceAction;
play_media: PlayMediaAction; play_media: PlayMediaAction;
stop: StopAction; stop: StopAction;
parallel: ParallelAction;
unknown: UnknownAction; unknown: UnknownAction;
} }
@@ -298,7 +307,7 @@ export const getActionType = (action: Action): ActionType => {
if ("wait_template" in action) { if ("wait_template" in action) {
return "wait_template"; return "wait_template";
} }
if ("condition" in action) { if (["condition", "and", "or", "not"].some((key) => key in action)) {
return "check_condition"; return "check_condition";
} }
if ("event" in action) { if ("event" in action) {
@@ -328,6 +337,9 @@ export const getActionType = (action: Action): ActionType => {
if ("stop" in action) { if ("stop" in action) {
return "stop"; return "stop";
} }
if ("parallel" in action) {
return "parallel";
}
if ("service" in action) { if ("service" in action) {
if ("metadata" in action) { if ("metadata" in action) {
if (is(action, activateSceneActionStruct)) { if (is(action, activateSceneActionStruct)) {

View File

@@ -8,12 +8,17 @@ import { describeCondition, describeTrigger } from "./automation_i18n";
import { import {
ActionType, ActionType,
ActionTypes, ActionTypes,
ChooseAction,
DelayAction, DelayAction,
DeviceAction, DeviceAction,
EventAction, EventAction,
getActionType, getActionType,
IfAction,
ParallelAction,
PlayMediaAction, PlayMediaAction,
RepeatAction,
SceneAction, SceneAction,
StopAction,
VariablesAction, VariablesAction,
WaitForTriggerAction, WaitForTriggerAction,
} from "./script"; } from "./script";
@@ -161,6 +166,81 @@ export const describeAction = <T extends ActionType>(
return `Test ${describeCondition(action as Condition)}`; return `Test ${describeCondition(action as Condition)}`;
} }
if (actionType === "stop") {
const config = action as StopAction;
return `Stopped${config.stop ? ` because: ${config.stop}` : ""}`;
}
if (actionType === "if") {
const config = action as IfAction;
return `If ${
typeof config.if === "string"
? config.if
: ensureArray(config.if)
.map((condition) => describeCondition(condition))
.join(", ")
} then ${ensureArray(config.then).map((thenAction) =>
describeAction(hass, thenAction)
)}${
config.else
? ` else ${ensureArray(config.else).map((elseAction) =>
describeAction(hass, elseAction)
)}`
: ""
}`;
}
if (actionType === "choose") {
const config = action as ChooseAction;
return config.choose
? `If ${ensureArray(config.choose)
.map(
(chooseAction) =>
`${
typeof chooseAction.conditions === "string"
? chooseAction.conditions
: ensureArray(chooseAction.conditions)
.map((condition) => describeCondition(condition))
.join(", ")
} then ${ensureArray(chooseAction.sequence)
.map((chooseSeq) => describeAction(hass, chooseSeq))
.join(", ")}`
)
.join(", else if ")}${
config.default
? `. If none match: ${ensureArray(config.default)
.map((dAction) => describeAction(hass, dAction))
.join(", ")}`
: ""
}`
: "Choose";
}
if (actionType === "repeat") {
const config = action as RepeatAction;
return `Repeat ${ensureArray(config.repeat.sequence).map((repeatAction) =>
describeAction(hass, repeatAction)
)} ${"count" in config.repeat ? `${config.repeat.count} times` : ""}${
"while" in config.repeat
? `while ${ensureArray(config.repeat.while)
.map((condition) => describeCondition(condition))
.join(", ")} is true`
: "until" in config.repeat
? `until ${ensureArray(config.repeat.until)
.map((condition) => describeCondition(condition))
.join(", ")} is true`
: "for_each" in config.repeat
? `for every item: ${ensureArray(config.repeat.for_each)
.map((item) => JSON.stringify(item))
.join(", ")}`
: ""
}`;
}
if (actionType === "check_condition") {
return `Test ${describeCondition(action as Condition)}`;
}
if (actionType === "device_action") { if (actionType === "device_action") {
const config = action as DeviceAction; const config = action as DeviceAction;
const stateObj = hass.states[config.entity_id as string]; const stateObj = hass.states[config.entity_id as string];
@@ -169,5 +249,12 @@ export const describeAction = <T extends ActionType>(
}`; }`;
} }
if (actionType === "parallel") {
const config = action as ParallelAction;
return `Run in parallel: ${ensureArray(config.parallel)
.map((pAction) => describeAction(hass, pAction))
.join(", ")}`;
}
return actionType; return actionType;
}; };

View File

@@ -44,6 +44,14 @@ export interface ChooseActionTraceStep extends BaseTraceStep {
result?: { choice: number | "default" }; result?: { choice: number | "default" };
} }
export interface IfActionTraceStep extends BaseTraceStep {
result?: { choice: "then" | "else" };
}
export interface StopActionTraceStep extends BaseTraceStep {
result?: { stop: string; error: boolean };
}
export interface ChooseChoiceActionTraceStep extends BaseTraceStep { export interface ChooseChoiceActionTraceStep extends BaseTraceStep {
result?: { result: boolean }; result?: { result: boolean };
} }
@@ -177,7 +185,11 @@ export const getDataFromPath = (
const asNumber = Number(raw); const asNumber = Number(raw);
if (isNaN(asNumber)) { if (isNaN(asNumber)) {
result = result[raw]; const tempResult = result[raw];
if (!tempResult && raw === "sequence") {
continue;
}
result = tempResult;
continue; continue;
} }

View File

@@ -2,8 +2,10 @@ import type {
HassEntities, HassEntities,
HassEntityAttributeBase, HassEntityAttributeBase,
HassEntityBase, HassEntityBase,
HassEvent,
} from "home-assistant-js-websocket"; } from "home-assistant-js-websocket";
import { BINARY_STATE_ON } from "../common/const"; import { BINARY_STATE_ON } from "../common/const";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDomain } from "../common/entity/compute_state_domain"; import { computeStateDomain } from "../common/entity/compute_state_domain";
import { supportsFeature } from "../common/entity/supports-feature"; import { supportsFeature } from "../common/entity/supports-feature";
import { caseInsensitiveStringCompare } from "../common/string/compare"; import { caseInsensitiveStringCompare } from "../common/string/compare";
@@ -110,15 +112,32 @@ export const checkForEntityUpdates = async (
return; return;
} }
let updated = 0;
const unsubscribeEvents = await hass.connection.subscribeEvents<HassEvent>(
(event) => {
if (computeDomain(event.data.entity_id) === "update") {
updated++;
showToast(element, {
message: hass.localize("ui.panel.config.updates.updates_refreshed", {
count: updated,
}),
});
}
},
"state_changed"
);
await hass.callService("homeassistant", "update_entity", { await hass.callService("homeassistant", "update_entity", {
entity_id: entities, entity_id: entities,
}); });
if (filterUpdateEntitiesWithInstall(hass.states).length) { // there is no reliable way to know if all the updates are done updating, so we just wait a bit for now...
showToast(element, { await new Promise((r) => setTimeout(r, 10000));
message: hass.localize("ui.panel.config.updates.updates_refreshed"),
}); unsubscribeEvents();
} else {
if (updated === 0) {
showToast(element, { showToast(element, {
message: hass.localize("ui.panel.config.updates.no_new_updates"), message: hass.localize("ui.panel.config.updates.no_new_updates"),
}); });

View File

@@ -312,6 +312,7 @@ class DataEntryFlowDialog extends LitElement {
.flowConfig=${this._params.flowConfig} .flowConfig=${this._params.flowConfig}
.step=${this._step} .step=${this._step}
.hass=${this.hass} .hass=${this.hass}
.domain=${this._step.handler}
></step-flow-abort> ></step-flow-abort>
` `
: this._step.type === "progress" : this._step.type === "progress"

View File

@@ -15,13 +15,11 @@ class StepFlowAbort extends LitElement {
@property({ attribute: false }) public step!: DataEntryFlowStepAbort; @property({ attribute: false }) public step!: DataEntryFlowStepAbort;
@property({ attribute: false }) public domain!: string;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<h2> <h2>${this.hass.localize(`component.${this.domain}.title`)}</h2>
${this.hass.localize(
"ui.panel.config.integrations.config_flow.aborted"
)}
</h2>
<div class="content"> <div class="content">
${this.flowConfig.renderAbortDescription(this.hass, this.step)} ${this.flowConfig.renderAbortDescription(this.hass, this.step)}
</div> </div>

View File

@@ -17,6 +17,7 @@ import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { canShowPage } from "../../common/config/can_show_page"; import { canShowPage } from "../../common/config/can_show_page";
import { componentsWithService } from "../../common/config/components_with_service"; import { componentsWithService } from "../../common/config/components_with_service";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain"; import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name"; import { computeStateName } from "../../common/entity/compute_state_name";
@@ -24,7 +25,7 @@ import { domainIcon } from "../../common/entity/domain_icon";
import { navigate } from "../../common/navigate"; import { navigate } from "../../common/navigate";
import { caseInsensitiveStringCompare } from "../../common/string/compare"; import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { import {
defaultFuzzyFilterSort, fuzzyFilterSort,
ScorableTextItem, ScorableTextItem,
} from "../../common/string/filter/sequence-matching"; } from "../../common/string/filter/sequence-matching";
import { debounce } from "../../common/util/debounce"; import { debounce } from "../../common/util/debounce";
@@ -33,6 +34,7 @@ import "../../components/ha-circular-progress";
import "../../components/ha-header-bar"; import "../../components/ha-header-bar";
import "../../components/ha-icon-button"; import "../../components/ha-icon-button";
import "../../components/ha-textfield"; import "../../components/ha-textfield";
import { fetchHassioSupervisorInfo } from "../../data/hassio/supervisor";
import { domainToName } from "../../data/integration"; import { domainToName } from "../../data/integration";
import { getPanelNameTranslationKey } from "../../data/panel"; import { getPanelNameTranslationKey } from "../../data/panel";
import { PageNavigation } from "../../layouts/hass-tabs-subpage"; import { PageNavigation } from "../../layouts/hass-tabs-subpage";
@@ -245,9 +247,10 @@ export class QuickBar extends LitElement {
`; `;
} }
private _initializeItemsIfNeeded() { private async _initializeItemsIfNeeded() {
if (this._commandMode) { if (this._commandMode) {
this._commandItems = this._commandItems || this._generateCommandItems(); this._commandItems =
this._commandItems || (await this._generateCommandItems());
} else { } else {
this._entityItems = this._entityItems || this._generateEntityItems(); this._entityItems = this._entityItems || this._generateEntityItems();
} }
@@ -485,11 +488,11 @@ export class QuickBar extends LitElement {
); );
} }
private _generateCommandItems(): CommandItem[] { private async _generateCommandItems(): Promise<CommandItem[]> {
return [ return [
...this._generateReloadCommands(), ...this._generateReloadCommands(),
...this._generateServerControlCommands(), ...this._generateServerControlCommands(),
...this._generateNavigationCommands(), ...(await this._generateNavigationCommands()),
].sort((a, b) => ].sort((a, b) =>
caseInsensitiveStringCompare(a.strings.join(" "), b.strings.join(" ")) caseInsensitiveStringCompare(a.strings.join(" "), b.strings.join(" "))
); );
@@ -578,11 +581,40 @@ export class QuickBar extends LitElement {
}); });
} }
private _generateNavigationCommands(): CommandItem[] { private async _generateNavigationCommands(): Promise<CommandItem[]> {
const panelItems = this._generateNavigationPanelCommands(); const panelItems = this._generateNavigationPanelCommands();
const sectionItems = this._generateNavigationConfigSectionCommands(); const sectionItems = this._generateNavigationConfigSectionCommands();
const supervisorItems: BaseNavigationCommand[] = [];
if (isComponentLoaded(this.hass, "hassio")) {
const supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
supervisorItems.push({
path: "/hassio/store",
primaryText: this.hass.localize(
"ui.dialogs.quick-bar.commands.navigation.addon_store"
),
});
supervisorItems.push({
path: "/hassio/dashboard",
primaryText: this.hass.localize(
"ui.dialogs.quick-bar.commands.navigation.addon_dashboard"
),
});
for (const addon of supervisorInfo.addons) {
supervisorItems.push({
path: `/hassio/addon/${addon.slug}`,
primaryText: this.hass.localize(
"ui.dialogs.quick-bar.commands.navigation.addon_info",
{ addon: addon.name }
),
});
}
}
return this._finalizeNavigationCommands([...panelItems, ...sectionItems]); return this._finalizeNavigationCommands([
...panelItems,
...sectionItems,
...supervisorItems,
]);
} }
private _generateNavigationPanelCommands(): BaseNavigationCommand[] { private _generateNavigationPanelCommands(): BaseNavigationCommand[] {
@@ -610,20 +642,14 @@ export class QuickBar extends LitElement {
if (!canShowPage(this.hass, page)) { if (!canShowPage(this.hass, page)) {
continue; continue;
} }
if (!page.component) {
continue;
}
const info = this._getNavigationInfoFromConfig(page); const info = this._getNavigationInfoFromConfig(page);
if (!info) { if (!info) {
continue; continue;
} }
// Add to list, but only if we do not already have an entry for the same path and component // Add to list, but only if we do not already have an entry for the same path and component
if ( if (items.some((e) => e.path === info.path)) {
items.some(
(e) => e.path === info.path && e.component === info.component
)
) {
continue; continue;
} }
@@ -637,14 +663,19 @@ export class QuickBar extends LitElement {
private _getNavigationInfoFromConfig( private _getNavigationInfoFromConfig(
page: PageNavigation page: PageNavigation
): NavigationInfo | undefined { ): NavigationInfo | undefined {
if (!page.component) { const path = page.path.substring(1);
return undefined;
}
const caption = this.hass.localize(
`ui.dialogs.quick-bar.commands.navigation.${page.component}`
);
if (page.translationKey && caption) { let name = path.substring(path.indexOf("/") + 1);
name = name.indexOf("/") > -1 ? name.substring(0, name.indexOf("/")) : name;
const caption =
(name &&
this.hass.localize(
`ui.dialogs.quick-bar.commands.navigation.${name}`
)) ||
(page.translationKey && this.hass.localize(page.translationKey));
if (caption) {
return { ...page, primaryText: caption }; return { ...page, primaryText: caption };
} }
@@ -694,7 +725,7 @@ export class QuickBar extends LitElement {
private _filterItems = memoizeOne( private _filterItems = memoizeOne(
(items: QuickBarItem[], filter: string): QuickBarItem[] => (items: QuickBarItem[], filter: string): QuickBarItem[] =>
defaultFuzzyFilterSort<QuickBarItem>(filter.trimLeft(), items) fuzzyFilterSort<QuickBarItem>(filter.trimLeft(), items)
); );
static get styles() { static get styles() {

View File

@@ -99,6 +99,7 @@ class HassSubpage extends LitElement {
ha-icon-button-arrow-prev, ha-icon-button-arrow-prev,
::slotted([slot="toolbar-icon"]) { ::slotted([slot="toolbar-icon"]) {
pointer-events: auto; pointer-events: auto;
color: var(--sidebar-icon-color);
} }
.main-title { .main-title {

View File

@@ -11,14 +11,7 @@ import listPlugin from "@fullcalendar/list";
// @ts-ignore // @ts-ignore
import listStyle from "@fullcalendar/list/main.css"; import listStyle from "@fullcalendar/list/main.css";
import "@material/mwc-button"; import "@material/mwc-button";
import { import { mdiViewAgenda, mdiViewDay, mdiViewModule, mdiViewWeek } from "@mdi/js";
mdiChevronLeft,
mdiChevronRight,
mdiViewAgenda,
mdiViewDay,
mdiViewModule,
mdiViewWeek,
} from "@mdi/js";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@@ -33,7 +26,6 @@ import memoize from "memoize-one";
import { useAmPm } from "../../common/datetime/use_am_pm"; import { useAmPm } from "../../common/datetime/use_am_pm";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-button-toggle-group"; import "../../components/ha-button-toggle-group";
import "../../components/ha-icon-button";
import "../../components/ha-icon-button-prev"; import "../../components/ha-icon-button-prev";
import "../../components/ha-icon-button-next"; import "../../components/ha-icon-button-next";
import { haStyle } from "../../resources/styles"; import { haStyle } from "../../resources/styles";
@@ -152,20 +144,18 @@ export class HAFullCalendar extends LitElement {
<div class="controls"> <div class="controls">
<h1>${this.calendar.view.title}</h1> <h1>${this.calendar.view.title}</h1>
<div> <div>
<ha-icon-button <ha-icon-button-prev
.label=${this.hass.localize("ui.common.previous")} .label=${this.hass.localize("ui.common.previous")}
.path=${mdiChevronLeft}
class="prev" class="prev"
@click=${this._handlePrev} @click=${this._handlePrev}
> >
</ha-icon-button> </ha-icon-button-prev>
<ha-icon-button <ha-icon-button-next
.label=${this.hass.localize("ui.common.next")} .label=${this.hass.localize("ui.common.next")}
.path=${mdiChevronRight}
class="next" class="next"
@click=${this._handleNext} @click=${this._handleNext}
> >
</ha-icon-button> </ha-icon-button-next>
</div> </div>
</div> </div>
<div class="controls"> <div class="controls">

View File

@@ -259,6 +259,7 @@ class HaConfigAreaPage extends LitElement {
<ha-svg-icon .path=${mdiImagePlus} slot="icon"></ha-svg-icon> <ha-svg-icon .path=${mdiImagePlus} slot="icon"></ha-svg-icon>
</mwc-button>`} </mwc-button>`}
<ha-card <ha-card
outlined
.header=${this.hass.localize("ui.panel.config.devices.caption")} .header=${this.hass.localize("ui.panel.config.devices.caption")}
>${devices.length >${devices.length
? devices.map( ? devices.map(
@@ -281,6 +282,7 @@ class HaConfigAreaPage extends LitElement {
`} `}
</ha-card> </ha-card>
<ha-card <ha-card
outlined
.header=${this.hass.localize( .header=${this.hass.localize(
"ui.panel.config.areas.editor.linked_entities_caption" "ui.panel.config.areas.editor.linked_entities_caption"
)} )}
@@ -314,6 +316,7 @@ class HaConfigAreaPage extends LitElement {
${isComponentLoaded(this.hass, "automation") ${isComponentLoaded(this.hass, "automation")
? html` ? html`
<ha-card <ha-card
outlined
.header=${this.hass.localize( .header=${this.hass.localize(
"ui.panel.config.devices.automation.automations_heading" "ui.panel.config.devices.automation.automations_heading"
)} )}
@@ -361,6 +364,7 @@ class HaConfigAreaPage extends LitElement {
${isComponentLoaded(this.hass, "scene") ${isComponentLoaded(this.hass, "scene")
? html` ? html`
<ha-card <ha-card
outlined
.header=${this.hass.localize( .header=${this.hass.localize(
"ui.panel.config.devices.scene.scenes_heading" "ui.panel.config.devices.scene.scenes_heading"
)} )}
@@ -400,6 +404,7 @@ class HaConfigAreaPage extends LitElement {
${isComponentLoaded(this.hass, "script") ${isComponentLoaded(this.hass, "script")
? html` ? html`
<ha-card <ha-card
outlined
.header=${this.hass.localize( .header=${this.hass.localize(
"ui.panel.config.devices.script.scripts_heading" "ui.panel.config.devices.script.scripts_heading"
)} )}

View File

@@ -33,6 +33,7 @@ import "./types/ha-automation-action-delay";
import "./types/ha-automation-action-device_id"; import "./types/ha-automation-action-device_id";
import "./types/ha-automation-action-event"; import "./types/ha-automation-action-event";
import "./types/ha-automation-action-if"; import "./types/ha-automation-action-if";
import "./types/ha-automation-action-parallel";
import "./types/ha-automation-action-play_media"; import "./types/ha-automation-action-play_media";
import "./types/ha-automation-action-repeat"; import "./types/ha-automation-action-repeat";
import "./types/ha-automation-action-service"; import "./types/ha-automation-action-service";
@@ -54,6 +55,7 @@ const OPTIONS = [
"if", "if",
"device_id", "device_id",
"stop", "stop",
"parallel",
]; ];
const getType = (action: Action | undefined) => { const getType = (action: Action | undefined) => {
@@ -63,6 +65,9 @@ const getType = (action: Action | undefined) => {
if ("service" in action || "scene" in action) { if ("service" in action || "scene" in action) {
return getActionType(action); return getActionType(action);
} }
if (["and", "or", "not"].some((key) => key in action)) {
return "condition";
}
return OPTIONS.find((option) => option in action); return OPTIONS.find((option) => option in action);
}; };
@@ -159,63 +164,83 @@ export default class HaAutomationActionRow extends LitElement {
const yamlMode = this._yamlMode; const yamlMode = this._yamlMode;
return html` return html`
<ha-card> <ha-card outlined>
<div class="card-content"> ${this.action.enabled === false
<div class="card-menu"> ? html`<div class="disabled-bar">
${this.index !== 0 ${this.hass.localize(
? html` "ui.panel.config.automation.editor.actions.disabled"
<ha-icon-button )}
.label=${this.hass.localize( </div>`
"ui.panel.config.automation.editor.move_up" : ""}
)} <div class="card-menu">
.path=${mdiArrowUp} ${this.index !== 0
@click=${this._moveUp} ? html`
></ha-icon-button> <ha-icon-button
` .label=${this.hass.localize(
: ""} "ui.panel.config.automation.editor.move_up"
${this.index !== this.totalActions - 1 )}
? html` .path=${mdiArrowUp}
<ha-icon-button @click=${this._moveUp}
.label=${this.hass.localize( ></ha-icon-button>
"ui.panel.config.automation.editor.move_down" `
)} : ""}
.path=${mdiArrowDown} ${this.index !== this.totalActions - 1
@click=${this._moveDown} ? html`
></ha-icon-button> <ha-icon-button
` .label=${this.hass.localize(
: ""} "ui.panel.config.automation.editor.move_down"
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}> )}
<ha-icon-button .path=${mdiArrowDown}
slot="trigger" @click=${this._moveDown}
.label=${this.hass.localize("ui.common.menu")} ></ha-icon-button>
.path=${mdiDotsVertical} `
></ha-icon-button> : ""}
<mwc-list-item> <ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
${this.hass.localize( <ha-icon-button
"ui.panel.config.automation.editor.actions.run_action" slot="trigger"
)} .label=${this.hass.localize("ui.common.menu")}
</mwc-list-item> .path=${mdiDotsVertical}
<mwc-list-item .disabled=${!this._uiModeAvailable}> ></ha-icon-button>
${yamlMode <mwc-list-item>
? this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.edit_ui" "ui.panel.config.automation.editor.actions.run_action"
) )}
: this.hass.localize( </mwc-list-item>
"ui.panel.config.automation.editor.edit_yaml" <mwc-list-item .disabled=${!this._uiModeAvailable}>
)} ${yamlMode
</mwc-list-item> ? this.hass.localize(
<mwc-list-item> "ui.panel.config.automation.editor.edit_ui"
${this.hass.localize( )
"ui.panel.config.automation.editor.actions.duplicate" : this.hass.localize(
)} "ui.panel.config.automation.editor.edit_yaml"
</mwc-list-item> )}
<mwc-list-item class="warning"> </mwc-list-item>
${this.hass.localize( <mwc-list-item>
"ui.panel.config.automation.editor.actions.delete" ${this.hass.localize(
)} "ui.panel.config.automation.editor.actions.duplicate"
</mwc-list-item> )}
</ha-button-menu> </mwc-list-item>
</div> <mwc-list-item>
${this.action.enabled === false
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.actions.disable"
)}
</mwc-list-item>
<mwc-list-item class="warning">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
</mwc-list-item>
</ha-button-menu>
</div>
<div
class="card-content ${this.action.enabled === false
? "disabled"
: ""}"
>
${this._warnings ${this._warnings
? html`<ha-alert ? html`<ha-alert
alert-type="warning" alert-type="warning"
@@ -314,11 +339,23 @@ export default class HaAutomationActionRow extends LitElement {
fireEvent(this, "duplicate"); fireEvent(this, "duplicate");
break; break;
case 3: case 3:
this._onDisable();
break;
case 4:
this._onDelete(); this._onDelete();
break; break;
} }
} }
private _onDisable() {
const enabled = !(this.action.enabled ?? true);
const value = { ...this.action, enabled };
fireEvent(this, "value-changed", { value });
if (this._yamlMode) {
this._yamlEditor?.setValue(value);
}
}
private async _runAction() { private async _runAction() {
const validated = await validateConfig(this.hass, { const validated = await validateConfig(this.hass, {
action: this.action, action: this.action,
@@ -408,11 +445,27 @@ export default class HaAutomationActionRow extends LitElement {
return [ return [
haStyle, haStyle,
css` css`
.disabled {
opacity: 0.5;
pointer-events: none;
}
.card-content {
padding-top: 16px;
margin-top: 0;
}
.disabled-bar {
background: var(--divider-color, #e0e0e0);
text-align: center;
border-top-right-radius: var(--ha-card-border-radius);
border-top-left-radius: var(--ha-card-border-radius);
}
.card-menu { .card-menu {
position: absolute; float: right;
right: 16px;
z-index: 3; z-index: 3;
margin: 4px;
--mdc-theme-text-primary-on-background: var(--primary-text-color); --mdc-theme-text-primary-on-background: var(--primary-text-color);
display: flex;
align-items: center;
} }
:host-context([style*="direction: rtl;"]) .card-menu { :host-context([style*="direction: rtl;"]) .card-menu {
right: initial; right: initial;

View File

@@ -1,3 +1,4 @@
import deepClone from "deep-clone-simple";
import "@material/mwc-button"; import "@material/mwc-button";
import { css, CSSResultGroup, html, LitElement } from "lit"; import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
@@ -32,7 +33,7 @@ export default class HaAutomationAction extends LitElement {
></ha-automation-action-row> ></ha-automation-action-row>
` `
)} )}
<ha-card> <ha-card outlined>
<div class="card-actions add-card"> <div class="card-actions add-card">
<mwc-button @click=${this._addAction}> <mwc-button @click=${this._addAction}>
${this.hass.localize( ${this.hass.localize(
@@ -83,7 +84,7 @@ export default class HaAutomationAction extends LitElement {
ev.stopPropagation(); ev.stopPropagation();
const index = (ev.target as any).index; const index = (ev.target as any).index;
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: this.actions.concat(this.actions[index]), value: this.actions.concat(deepClone(this.actions[index])),
}); });
} }

View File

@@ -69,7 +69,7 @@ export class HaChooseAction extends LitElement implements ActionElement {
</div> </div>
</ha-card>` </ha-card>`
)} )}
<ha-card> <ha-card outlined>
<div class="card-actions add-card"> <div class="card-actions add-card">
<mwc-button @click=${this._addOption}> <mwc-button @click=${this._addOption}>
${this.hass.localize( ${this.hass.localize(

View File

@@ -0,0 +1,56 @@
import { CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { Action, ParallelAction } from "../../../../../data/script";
import { HaDeviceAction } from "./ha-automation-action-device_id";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import "../ha-automation-action";
import "../../../../../components/ha-textfield";
import type { ActionElement } from "../ha-automation-action-row";
@customElement("ha-automation-action-parallel")
export class HaParallelAction extends LitElement implements ActionElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public action!: ParallelAction;
public static get defaultConfig() {
return {
parallel: [HaDeviceAction.defaultConfig],
};
}
protected render() {
const action = this.action;
return html`
<ha-automation-action
.actions=${action.parallel}
@value-changed=${this._actionsChanged}
.hass=${this.hass}
></ha-automation-action>
`;
}
private _actionsChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = ev.detail.value as Action[];
fireEvent(this, "value-changed", {
value: {
...this.action,
parallel: value,
},
});
}
static get styles(): CSSResultGroup {
return haStyle;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-action-parallel": HaParallelAction;
}
}

View File

@@ -33,7 +33,7 @@ export class HaWaitAction extends LitElement implements ActionElement {
@property({ attribute: false }) public action!: WaitAction; @property({ attribute: false }) public action!: WaitAction;
public static get defaultConfig() { public static get defaultConfig() {
return { wait_template: "" }; return { wait_template: "", continue_on_timeout: true };
} }
protected render() { protected render() {

View File

@@ -75,7 +75,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
"ui.panel.config.automation.editor.introduction" "ui.panel.config.automation.editor.introduction"
)} )}
</span> </span>
<ha-card> <ha-card outlined>
<div class="card-content"> <div class="card-content">
<ha-textfield <ha-textfield
.label=${this.hass.localize( .label=${this.hass.localize(
@@ -145,6 +145,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
</ha-config-section> </ha-config-section>
<ha-card <ha-card
outlined
class="blueprint" class="blueprint"
.header=${this.hass.localize( .header=${this.hass.localize(
"ui.panel.config.automation.editor.blueprint.header" "ui.panel.config.automation.editor.blueprint.header"

View File

@@ -5,11 +5,11 @@ import { dynamicElement } from "../../../../common/dom/dynamic-element-directive
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { stringCompare } from "../../../../common/string/compare"; import { stringCompare } from "../../../../common/string/compare";
import type { LocalizeFunc } from "../../../../common/translations/localize"; import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-card";
import "../../../../components/ha-select"; import "../../../../components/ha-select";
import type { HaSelect } from "../../../../components/ha-select"; import type { HaSelect } from "../../../../components/ha-select";
import "../../../../components/ha-yaml-editor"; import "../../../../components/ha-yaml-editor";
import type { Condition } from "../../../../data/automation"; import type { Condition } from "../../../../data/automation";
import { expandConditionWithShorthand } from "../../../../data/automation";
import { haStyle } from "../../../../resources/styles"; import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import "./types/ha-automation-condition-and"; import "./types/ha-automation-condition-and";
@@ -42,10 +42,14 @@ const OPTIONS = [
export default class HaAutomationConditionEditor extends LitElement { export default class HaAutomationConditionEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public condition!: Condition; @property() condition!: Condition;
@property() public yamlMode = false; @property() public yamlMode = false;
private _processedCondition = memoizeOne((condition) =>
expandConditionWithShorthand(condition)
);
private _processedTypes = memoizeOne( private _processedTypes = memoizeOne(
(localize: LocalizeFunc): [string, string][] => (localize: LocalizeFunc): [string, string][] =>
OPTIONS.map( OPTIONS.map(
@@ -60,7 +64,8 @@ export default class HaAutomationConditionEditor extends LitElement {
); );
protected render() { protected render() {
const selected = OPTIONS.indexOf(this.condition.condition); const condition = this._processedCondition(this.condition);
const selected = OPTIONS.indexOf(condition.condition);
const yamlMode = this.yamlMode || selected === -1; const yamlMode = this.yamlMode || selected === -1;
return html` return html`
${yamlMode ${yamlMode
@@ -70,7 +75,7 @@ export default class HaAutomationConditionEditor extends LitElement {
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.conditions.unsupported_condition", "ui.panel.config.automation.editor.conditions.unsupported_condition",
"condition", "condition",
this.condition.condition condition.condition
)} )}
` `
: ""} : ""}
@@ -90,7 +95,7 @@ export default class HaAutomationConditionEditor extends LitElement {
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type_select" "ui.panel.config.automation.editor.conditions.type_select"
)} )}
.value=${this.condition.condition} .value=${condition.condition}
naturalMenuWidth naturalMenuWidth
@selected=${this._typeChanged} @selected=${this._typeChanged}
> >
@@ -103,8 +108,8 @@ export default class HaAutomationConditionEditor extends LitElement {
<div> <div>
${dynamicElement( ${dynamicElement(
`ha-automation-condition-${this.condition.condition}`, `ha-automation-condition-${condition.condition}`,
{ hass: this.hass, condition: this.condition } { hass: this.hass, condition: condition }
)} )}
</div> </div>
`} `}
@@ -124,7 +129,7 @@ export default class HaAutomationConditionEditor extends LitElement {
defaultConfig: Omit<Condition, "condition">; defaultConfig: Omit<Condition, "condition">;
}; };
if (type !== this.condition.condition) { if (type !== this._processedCondition(this.condition).condition) {
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: { value: {
condition: type, condition: type,

View File

@@ -2,7 +2,7 @@ import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js"; import { mdiDotsVertical } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement } from "lit"; import { css, CSSResultGroup, html, LitElement } 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 { fireEvent } from "../../../../common/dom/fire_event";
import { handleStructError } from "../../../../common/structs/handle-errors"; import { handleStructError } from "../../../../common/structs/handle-errors";
import "../../../../components/ha-button-menu"; import "../../../../components/ha-button-menu";
@@ -19,6 +19,7 @@ import { haStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
import "./ha-automation-condition-editor"; import "./ha-automation-condition-editor";
import { validateConfig } from "../../../../data/config"; import { validateConfig } from "../../../../data/config";
import { HaYamlEditor } from "../../../../components/ha-yaml-editor";
export interface ConditionElement extends LitElement { export interface ConditionElement extends LitElement {
condition: Condition; condition: Condition;
@@ -59,47 +60,69 @@ export default class HaAutomationConditionRow extends LitElement {
@state() private _warnings?: string[]; @state() private _warnings?: string[];
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
protected render() { protected render() {
if (!this.condition) { if (!this.condition) {
return html``; return html``;
} }
return html` return html`
<ha-card> <ha-card outlined>
<div class="card-content"> ${this.condition.enabled === false
<div class="card-menu"> ? html`<div class="disabled-bar">
<ha-progress-button @click=${this._testCondition}>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.conditions.test" "ui.panel.config.automation.editor.actions.disabled"
)} )}
</ha-progress-button> </div>`
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}> : ""}
<ha-icon-button <div class="card-menu">
slot="trigger" <ha-progress-button @click=${this._testCondition}>
.label=${this.hass.localize("ui.common.menu")} ${this.hass.localize(
.path=${mdiDotsVertical} "ui.panel.config.automation.editor.conditions.test"
> )}
</ha-icon-button> </ha-progress-button>
<mwc-list-item> <ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
${this._yamlMode <ha-icon-button
? this.hass.localize( slot="trigger"
"ui.panel.config.automation.editor.edit_ui" .label=${this.hass.localize("ui.common.menu")}
) .path=${mdiDotsVertical}
: this.hass.localize( >
"ui.panel.config.automation.editor.edit_yaml" </ha-icon-button>
)} <mwc-list-item>
</mwc-list-item> ${this._yamlMode
<mwc-list-item> ? this.hass.localize(
${this.hass.localize( "ui.panel.config.automation.editor.edit_ui"
"ui.panel.config.automation.editor.actions.duplicate" )
)} : this.hass.localize(
</mwc-list-item> "ui.panel.config.automation.editor.edit_yaml"
<mwc-list-item class="warning"> )}
${this.hass.localize( </mwc-list-item>
"ui.panel.config.automation.editor.actions.delete" <mwc-list-item>
)} ${this.hass.localize(
</mwc-list-item> "ui.panel.config.automation.editor.actions.duplicate"
</ha-button-menu> )}
</div> </mwc-list-item>
<mwc-list-item>
${this.condition.enabled === false
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.actions.disable"
)}
</mwc-list-item>
<mwc-list-item class="warning">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
</mwc-list-item>
</ha-button-menu>
</div>
<div
class="card-content ${this.condition.enabled === false
? "disabled"
: ""}"
>
${this._warnings ${this._warnings
? html`<ha-alert ? html`<ha-alert
alert-type="warning" alert-type="warning"
@@ -153,11 +176,23 @@ export default class HaAutomationConditionRow extends LitElement {
fireEvent(this, "duplicate"); fireEvent(this, "duplicate");
break; break;
case 2: case 2:
this._onDisable();
break;
case 3:
this._onDelete(); this._onDelete();
break; break;
} }
} }
private _onDisable() {
const enabled = !(this.condition.enabled ?? true);
const value = { ...this.condition, enabled };
fireEvent(this, "value-changed", { value });
if (this._yamlMode) {
this._yamlEditor?.setValue(value);
}
}
private _onDelete() { private _onDelete() {
showConfirmationDialog(this, { showConfirmationDialog(this, {
text: this.hass.localize( text: this.hass.localize(
@@ -238,9 +273,24 @@ export default class HaAutomationConditionRow extends LitElement {
return [ return [
haStyle, haStyle,
css` css`
.disabled {
opacity: 0.5;
pointer-events: none;
}
.card-content {
padding-top: 16px;
margin-top: 0;
}
.disabled-bar {
background: var(--divider-color, #e0e0e0);
text-align: center;
border-top-right-radius: var(--ha-card-border-radius);
border-top-left-radius: var(--ha-card-border-radius);
}
.card-menu { .card-menu {
float: right; float: right;
z-index: 3; z-index: 3;
margin: 4px;
--mdc-theme-text-primary-on-background: var(--primary-text-color); --mdc-theme-text-primary-on-background: var(--primary-text-color);
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -1,3 +1,4 @@
import deepClone from "deep-clone-simple";
import "@material/mwc-button"; import "@material/mwc-button";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
@@ -56,7 +57,7 @@ export default class HaAutomationCondition extends LitElement {
></ha-automation-condition-row> ></ha-automation-condition-row>
` `
)} )}
<ha-card> <ha-card outlined>
<div class="card-actions add-card"> <div class="card-actions add-card">
<mwc-button @click=${this._addCondition}> <mwc-button @click=${this._addCondition}>
${this.hass.localize( ${this.hass.localize(
@@ -96,7 +97,7 @@ export default class HaAutomationCondition extends LitElement {
ev.stopPropagation(); ev.stopPropagation();
const index = (ev.target as any).index; const index = (ev.target as any).index;
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: this.conditions.concat(this.conditions[index]), value: this.conditions.concat(deepClone(this.conditions[index])),
}); });
} }

View File

@@ -3,7 +3,6 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-blueprint-picker"; import "../../../components/ha-blueprint-picker";
import "../../../components/ha-card";
import "../../../components/ha-circular-progress"; import "../../../components/ha-circular-progress";
import { createCloseHeading } from "../../../components/ha-dialog"; import { createCloseHeading } from "../../../components/ha-dialog";
import { showAutomationEditor } from "../../../data/automation"; import { showAutomationEditor } from "../../../data/automation";

View File

@@ -239,8 +239,8 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
? html` ? html`
${!this.narrow ${!this.narrow
? html` ? html`
<ha-card <ha-card outlined>
><div class="card-header"> <div class="card-header">
${this._config.alias} ${this._config.alias}
</div> </div>
${stateObj ${stateObj
@@ -275,8 +275,8 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
.defaultValue=${this._preprocessYaml()} .defaultValue=${this._preprocessYaml()}
@value-changed=${this._yamlChanged} @value-changed=${this._yamlChanged}
></ha-yaml-editor> ></ha-yaml-editor>
<ha-card <ha-card outlined>
><div class="card-actions"> <div class="card-actions">
<mwc-button @click=${this._copyYaml}> <mwc-button @click=${this._copyYaml}>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.copy_to_clipboard" "ui.panel.config.automation.editor.copy_to_clipboard"

View File

@@ -47,7 +47,7 @@ export class HaManualAutomationEditor extends LitElement {
"ui.panel.config.automation.editor.introduction" "ui.panel.config.automation.editor.introduction"
)} )}
</span> </span>
<ha-card> <ha-card outlined>
<div class="card-content"> <div class="card-content">
<ha-textfield <ha-textfield
.label=${this.hass.localize( .label=${this.hass.localize(

View File

@@ -1,8 +1,9 @@
import { object, optional, number, string } from "superstruct"; import { object, optional, number, string, boolean } from "superstruct";
export const baseTriggerStruct = object({ export const baseTriggerStruct = object({
platform: string(), platform: string(),
id: optional(string()), id: optional(string()),
enabled: optional(boolean()),
}); });
export const forDictStruct = object({ export const forDictStruct = object({

View File

@@ -3,7 +3,7 @@ import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js"; import { mdiDotsVertical } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive"; import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
@@ -16,7 +16,7 @@ import "../../../../components/ha-alert";
import "../../../../components/ha-button-menu"; import "../../../../components/ha-button-menu";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import "../../../../components/ha-icon-button"; import "../../../../components/ha-icon-button";
import "../../../../components/ha-yaml-editor"; import { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import "../../../../components/ha-select"; import "../../../../components/ha-select";
import type { HaSelect } from "../../../../components/ha-select"; import type { HaSelect } from "../../../../components/ha-select";
import "../../../../components/ha-textfield"; import "../../../../components/ha-textfield";
@@ -104,6 +104,8 @@ export default class HaAutomationTriggerRow extends LitElement {
@state() private _triggerColor = false; @state() private _triggerColor = false;
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
private _triggerUnsub?: Promise<UnsubscribeFunc>; private _triggerUnsub?: Promise<UnsubscribeFunc>;
private _processedTypes = memoizeOne( private _processedTypes = memoizeOne(
@@ -125,41 +127,61 @@ export default class HaAutomationTriggerRow extends LitElement {
const showId = "id" in this.trigger || this._requestShowId; const showId = "id" in this.trigger || this._requestShowId;
return html` return html`
<ha-card> <ha-card outlined>
<div class="card-content"> ${this.trigger.enabled === false
<div class="card-menu"> ? html`<div class="disabled-bar">
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}> ${this.hass.localize(
<ha-icon-button "ui.panel.config.automation.editor.actions.disabled"
slot="trigger" )}
.label=${this.hass.localize("ui.common.menu")} </div>`
.path=${mdiDotsVertical} : ""}
></ha-icon-button> <div class="card-menu">
<mwc-list-item> <ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
${this.hass.localize( <ha-icon-button
"ui.panel.config.automation.editor.triggers.edit_id" slot="trigger"
)} .label=${this.hass.localize("ui.common.menu")}
</mwc-list-item> .path=${mdiDotsVertical}
<mwc-list-item .disabled=${selected === -1}> ></ha-icon-button>
${yamlMode <mwc-list-item>
? this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.edit_ui" "ui.panel.config.automation.editor.triggers.edit_id"
) )}
: this.hass.localize( </mwc-list-item>
"ui.panel.config.automation.editor.edit_yaml" <mwc-list-item .disabled=${selected === -1}>
)} ${yamlMode
</mwc-list-item> ? this.hass.localize(
<mwc-list-item> "ui.panel.config.automation.editor.edit_ui"
${this.hass.localize( )
"ui.panel.config.automation.editor.actions.duplicate" : this.hass.localize(
)} "ui.panel.config.automation.editor.edit_yaml"
</mwc-list-item> )}
<mwc-list-item class="warning"> </mwc-list-item>
${this.hass.localize( <mwc-list-item>
"ui.panel.config.automation.editor.actions.delete" ${this.hass.localize(
)} "ui.panel.config.automation.editor.actions.duplicate"
</mwc-list-item> )}
</ha-button-menu> </mwc-list-item>
</div> <mwc-list-item>
${this.trigger.enabled === false
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.actions.disable"
)}
</mwc-list-item>
<mwc-list-item class="warning">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
</mwc-list-item>
</ha-button-menu>
</div>
<div
class="card-content ${this.trigger.enabled === false
? "disabled"
: ""}"
>
${this._warnings ${this._warnings
? html`<ha-alert ? html`<ha-alert
alert-type="warning" alert-type="warning"
@@ -214,7 +236,6 @@ export default class HaAutomationTriggerRow extends LitElement {
` `
)} )}
</ha-select> </ha-select>
${showId ${showId
? html` ? html`
<ha-textfield <ha-textfield
@@ -250,7 +271,7 @@ export default class HaAutomationTriggerRow extends LitElement {
`; `;
} }
protected override updated(changedProps: PropertyValues): void { protected override updated(changedProps: PropertyValues<this>): void {
super.updated(changedProps); super.updated(changedProps);
if (changedProps.has("trigger")) { if (changedProps.has("trigger")) {
this._subscribeTrigger(); this._subscribeTrigger();
@@ -347,6 +368,9 @@ export default class HaAutomationTriggerRow extends LitElement {
fireEvent(this, "duplicate"); fireEvent(this, "duplicate");
break; break;
case 3: case 3:
this._onDisable();
break;
case 4:
this._onDelete(); this._onDelete();
break; break;
} }
@@ -365,6 +389,15 @@ export default class HaAutomationTriggerRow extends LitElement {
}); });
} }
private _onDisable() {
const enabled = !(this.trigger.enabled ?? true);
const value = { ...this.trigger, enabled };
fireEvent(this, "value-changed", { value });
if (this._yamlMode) {
this._yamlEditor?.setValue(value);
}
}
private _typeChanged(ev: CustomEvent) { private _typeChanged(ev: CustomEvent) {
const type = (ev.target as HaSelect).value; const type = (ev.target as HaSelect).value;
@@ -439,10 +472,27 @@ export default class HaAutomationTriggerRow extends LitElement {
return [ return [
haStyle, haStyle,
css` css`
.disabled {
opacity: 0.5;
pointer-events: none;
}
.card-content {
padding-top: 16px;
margin-top: 0;
}
.disabled-bar {
background: var(--divider-color, #e0e0e0);
text-align: center;
border-top-right-radius: var(--ha-card-border-radius);
border-top-left-radius: var(--ha-card-border-radius);
}
.card-menu { .card-menu {
float: right; float: right;
z-index: 3; z-index: 3;
margin: 4px;
--mdc-theme-text-primary-on-background: var(--primary-text-color); --mdc-theme-text-primary-on-background: var(--primary-text-color);
display: flex;
align-items: center;
} }
:host-context([style*="direction: rtl;"]) .card-menu { :host-context([style*="direction: rtl;"]) .card-menu {
float: left; float: left;

View File

@@ -1,3 +1,4 @@
import deepClone from "deep-clone-simple";
import "@material/mwc-button"; import "@material/mwc-button";
import { css, CSSResultGroup, html, LitElement } from "lit"; import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
@@ -27,7 +28,7 @@ export default class HaAutomationTrigger extends LitElement {
></ha-automation-trigger-row> ></ha-automation-trigger-row>
` `
)} )}
<ha-card> <ha-card outlined>
<div class="card-actions add-card"> <div class="card-actions add-card">
<mwc-button @click=${this._addTrigger}> <mwc-button @click=${this._addTrigger}>
${this.hass.localize( ${this.hass.localize(
@@ -67,7 +68,7 @@ export default class HaAutomationTrigger extends LitElement {
ev.stopPropagation(); ev.stopPropagation();
const index = (ev.target as any).index; const index = (ev.target as any).index;
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: this.triggers.concat(this.triggers[index]), value: this.triggers.concat(deepClone(this.triggers[index])),
}); });
} }

View File

@@ -1,26 +1,28 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "@material/mwc-list/mwc-list-item";
import type { ActionDetail } from "@material/mwc-list"; import type { ActionDetail } from "@material/mwc-list";
import "@polymer/paper-item/paper-item-body"; import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js"; import { mdiDotsVertical } from "@mdi/js";
import { LitElement, css, html, PropertyValues } from "lit"; import "@polymer/paper-item/paper-item-body";
import { css, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { formatDateTime } from "../../../../common/datetime/format_date_time"; import { formatDateTime } from "../../../../common/datetime/format_date_time";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { computeRTLDirection } from "../../../../common/util/compute_rtl"; import { computeRTLDirection } from "../../../../common/util/compute_rtl";
import { debounce } from "../../../../common/util/debounce";
import "../../../../components/buttons/ha-call-api-button"; import "../../../../components/buttons/ha-call-api-button";
import "../../../../components/ha-card";
import "../../../../components/ha-alert"; import "../../../../components/ha-alert";
import "../../../../components/ha-button-menu"; import "../../../../components/ha-button-menu";
import "../../../../components/ha-card";
import "../../../../components/ha-icon-button"; import "../../../../components/ha-icon-button";
import { debounce } from "../../../../common/util/debounce";
import { import {
cloudLogout, cloudLogout,
CloudStatusLoggedIn, CloudStatusLoggedIn,
fetchCloudSubscriptionInfo, fetchCloudSubscriptionInfo,
SubscriptionInfo, SubscriptionInfo,
} from "../../../../data/cloud"; } from "../../../../data/cloud";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
import "../../../../layouts/hass-subpage"; import "../../../../layouts/hass-subpage";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
import "../../ha-config-section"; import "../../ha-config-section";
import "./cloud-alexa-pref"; import "./cloud-alexa-pref";
@@ -28,8 +30,6 @@ import "./cloud-google-pref";
import "./cloud-remote-pref"; import "./cloud-remote-pref";
import "./cloud-tts-pref"; import "./cloud-tts-pref";
import "./cloud-webhooks"; import "./cloud-webhooks";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
@customElement("cloud-account") @customElement("cloud-account")
export class CloudAccount extends SubscribeMixin(LitElement) { export class CloudAccount extends SubscribeMixin(LitElement) {
@@ -81,6 +81,7 @@ export class CloudAccount extends SubscribeMixin(LitElement) {
</div> </div>
<ha-card <ha-card
outlined
.header=${this.hass.localize( .header=${this.hass.localize(
"ui.panel.config.cloud.account.nabu_casa_account" "ui.panel.config.cloud.account.nabu_casa_account"
)} )}
@@ -210,6 +211,7 @@ export class CloudAccount extends SubscribeMixin(LitElement) {
<cloud-webhooks <cloud-webhooks
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow}
.cloudStatus=${this.cloudStatus} .cloudStatus=${this.cloudStatus}
dir=${this._rtlDirection} dir=${this._rtlDirection}
></cloud-webhooks> ></cloud-webhooks>

View File

@@ -26,6 +26,7 @@ export class CloudAlexaPref extends LitElement {
return html` return html`
<ha-card <ha-card
outlined
header=${this.hass!.localize( header=${this.hass!.localize(
"ui.panel.config.cloud.account.alexa.title" "ui.panel.config.cloud.account.alexa.title"
)} )}

View File

@@ -31,6 +31,7 @@ export class CloudGooglePref extends LitElement {
return html` return html`
<ha-card <ha-card
outlined
header=${this.hass.localize( header=${this.hass.localize(
"ui.panel.config.cloud.account.google.title" "ui.panel.config.cloud.account.google.title"
)} )}

View File

@@ -34,6 +34,7 @@ export class CloudRemotePref extends LitElement {
if (!remote_certificate) { if (!remote_certificate) {
return html` return html`
<ha-card <ha-card
outlined
header=${this.hass.localize( header=${this.hass.localize(
"ui.panel.config.cloud.account.remote.title" "ui.panel.config.cloud.account.remote.title"
)} )}
@@ -49,6 +50,7 @@ export class CloudRemotePref extends LitElement {
return html` return html`
<ha-card <ha-card
outlined
header=${this.hass.localize( header=${this.hass.localize(
"ui.panel.config.cloud.account.remote.title" "ui.panel.config.cloud.account.remote.title"
)} )}

View File

@@ -44,6 +44,7 @@ export class CloudTTSPref extends LitElement {
return html` return html`
<ha-card <ha-card
outlined
header=${this.hass.localize("ui.panel.config.cloud.account.tts.title")} header=${this.hass.localize("ui.panel.config.cloud.account.tts.title")}
> >
<div class="card-content"> <div class="card-content">

View File

@@ -40,6 +40,7 @@ export class CloudWebhooks extends LitElement {
protected render() { protected render() {
return html` return html`
<ha-card <ha-card
outlined
header=${this.hass!.localize( header=${this.hass!.localize(
"ui.panel.config.cloud.account.webhooks.title" "ui.panel.config.cloud.account.webhooks.title"
)} )}

View File

@@ -153,7 +153,7 @@ class CloudAlexa extends SubscribeMixin(LitElement) {
></ha-icon-button>`; ></ha-icon-button>`;
target.push(html` target.push(html`
<ha-card> <ha-card outlined>
<div class="card-content"> <div class="card-content">
<div class="top-line"> <div class="top-line">
<state-info <state-info

View File

@@ -36,6 +36,7 @@ export class CloudForgotPassword extends LitElement {
> >
<div class="content"> <div class="content">
<ha-card <ha-card
outlined
.header=${this.hass.localize( .header=${this.hass.localize(
"ui.panel.config.cloud.forgot_password.subtitle" "ui.panel.config.cloud.forgot_password.subtitle"
)} )}

View File

@@ -159,7 +159,7 @@ class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
></ha-icon-button>`; ></ha-icon-button>`;
target.push(html` target.push(html`
<ha-card> <ha-card outlined>
<div class="card-content"> <div class="card-content">
<div class="top-line"> <div class="top-line">
<state-info <state-info

View File

@@ -99,6 +99,7 @@ export class CloudLogin extends LitElement {
: ""} : ""}
<ha-card <ha-card
outlined
.header=${this.hass.localize( .header=${this.hass.localize(
"ui.panel.config.cloud.login.sign_in" "ui.panel.config.cloud.login.sign_in"
)} )}
@@ -157,7 +158,7 @@ export class CloudLogin extends LitElement {
</div> </div>
</ha-card> </ha-card>
<ha-card> <ha-card outlined>
<paper-item @click=${this._handleRegister}> <paper-item @click=${this._handleRegister}>
<paper-item-body two-line> <paper-item-body two-line>
${this.hass.localize( ${this.hass.localize(

View File

@@ -121,6 +121,7 @@ export class CloudRegister extends LitElement {
</ul> </ul>
</div> </div>
<ha-card <ha-card
outlined
.header=${this.hass.localize( .header=${this.hass.localize(
"ui.panel.config.cloud.register.create_account" "ui.panel.config.cloud.register.create_account"
)} )}

View File

@@ -110,7 +110,9 @@ class ConfigAnalytics extends LitElement {
ha-settings-row { ha-settings-row {
padding: 0; padding: 0;
} }
p {
margin-top: 0;
}
.card-actions { .card-actions {
display: flex; display: flex;
flex-direction: row-reverse; flex-direction: row-reverse;

View File

@@ -1,15 +1,23 @@
import "@material/mwc-list/mwc-list-item";
import timezones from "google-timezones-json"; import timezones from "google-timezones-json";
import { css, html, LitElement, TemplateResult } from "lit"; import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { UNIT_C } from "../../../common/const"; import { UNIT_C } from "../../../common/const";
import { stopPropagation } from "../../../common/dom/stop_propagation"; import { stopPropagation } from "../../../common/dom/stop_propagation";
import { navigate } from "../../../common/navigate"; import { navigate } from "../../../common/navigate";
import { HaProgressButton } from "../../../components/buttons/ha-progress-button"; import "../../../components/buttons/ha-progress-button";
import type { HaProgressButton } from "../../../components/buttons/ha-progress-button";
import { currencies } from "../../../components/currency-datalist"; import { currencies } from "../../../components/currency-datalist";
import "../../../components/ha-card";
import "../../../components/ha-formfield"; import "../../../components/ha-formfield";
import "../../../components/ha-radio"; import "../../../components/ha-radio";
import type { HaRadio } from "../../../components/ha-radio"; import type { HaRadio } from "../../../components/ha-radio";
import "../../../components/ha-select";
import "../../../components/ha-settings-row"; import "../../../components/ha-settings-row";
import "../../../components/ha-textfield";
import "../../../components/map/ha-locations-editor";
import type { MarkerLocation } from "../../../components/map/ha-locations-editor";
import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core"; import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core";
import { SYMBOL_TO_ISO } from "../../../data/currency"; import { SYMBOL_TO_ISO } from "../../../data/currency";
import "../../../layouts/hass-subpage"; import "../../../layouts/hass-subpage";
@@ -34,6 +42,8 @@ class HaConfigSectionGeneral extends LitElement {
@state() private _timeZone?: string; @state() private _timeZone?: string;
@state() private _location?: [number, number];
protected render(): TemplateResult { protected render(): TemplateResult {
const canEdit = ["storage", "default"].includes( const canEdit = ["storage", "default"].includes(
this.hass.config.config_source this.hass.config.config_source
@@ -47,7 +57,7 @@ class HaConfigSectionGeneral extends LitElement {
.header=${this.hass.localize("ui.panel.config.core.caption")} .header=${this.hass.localize("ui.panel.config.core.caption")}
> >
<div class="content"> <div class="content">
<ha-card> <ha-card outlined>
<div class="card-content"> <div class="card-content">
${!canEdit ${!canEdit
? html` ? html`
@@ -177,27 +187,42 @@ class HaConfigSectionGeneral extends LitElement {
href="https://en.wikipedia.org/wiki/ISO_4217#Active_codes" href="https://en.wikipedia.org/wiki/ISO_4217#Active_codes"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="find-value"
>${this.hass.localize( >${this.hass.localize(
"ui.panel.config.core.section.core.core_config.find_currency_value" "ui.panel.config.core.section.core.core_config.find_currency_value"
)}</a )}</a
> >
</div> </div>
</div> </div>
<ha-settings-row> ${this.narrow
<div slot="heading"> ? html`
${this.hass.localize( <ha-locations-editor
"ui.panel.config.core.section.core.core_config.edit_location" .hass=${this.hass}
)} .locations=${this._markerLocation(
</div> this.hass.config.latitude,
<div slot="description" class="secondary"> this.hass.config.longitude,
${this.hass.localize( this._location
"ui.panel.config.core.section.core.core_config.edit_location_description" )}
)} @location-updated=${this._locationChanged}
</div> ></ha-locations-editor>
<mwc-button @click=${this._editLocation} `
>${this.hass.localize("ui.common.edit")}</mwc-button : html`
> <ha-settings-row>
</ha-settings-row> <div slot="heading">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.edit_location"
)}
</div>
<div slot="description" class="secondary">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.edit_location_description"
)}
</div>
<mwc-button @click=${this._editLocation}
>${this.hass.localize("ui.common.edit")}</mwc-button
>
</ha-settings-row>
`}
<div class="card-actions"> <div class="card-actions">
<ha-progress-button @click=${this._updateEntry}> <ha-progress-button @click=${this._updateEntry}>
${this.hass!.localize("ui.panel.config.zone.detail.update")} ${this.hass!.localize("ui.panel.config.zone.detail.update")}
@@ -237,7 +262,11 @@ class HaConfigSectionGeneral extends LitElement {
this._unitSystem = (ev.target as HaRadio).value as "metric" | "imperial"; this._unitSystem = (ev.target as HaRadio).value as "metric" | "imperial";
} }
private async _updateEntry(ev) { private _locationChanged(ev: CustomEvent) {
this._location = ev.detail.location;
}
private async _updateEntry(ev: CustomEvent) {
const button = ev.target as HaProgressButton; const button = ev.target as HaProgressButton;
if (button.progress) { if (button.progress) {
return; return;
@@ -261,6 +290,21 @@ class HaConfigSectionGeneral extends LitElement {
} }
} }
private _markerLocation = memoizeOne(
(
lat: number,
lng: number,
location?: [number, number]
): MarkerLocation[] => [
{
id: "location",
latitude: location ? location[0] : lat,
longitude: location ? location[1] : lng,
location_editable: true,
},
]
);
private _editLocation() { private _editLocation() {
navigate("/config/zone"); navigate("/config/zone");
} }
@@ -274,7 +318,7 @@ class HaConfigSectionGeneral extends LitElement {
margin: 0 auto; margin: 0 auto;
} }
ha-card { ha-card {
max-width: 500px; max-width: 600px;
margin: 0 auto; margin: 0 auto;
height: 100%; height: 100%;
justify-content: space-between; justify-content: space-between;
@@ -302,6 +346,15 @@ class HaConfigSectionGeneral extends LitElement {
ha-select { ha-select {
display: block; display: block;
} }
a.find-value {
margin-top: 8px;
display: inline-block;
}
ha-locations-editor {
display: block;
height: 400px;
padding: 16px;
}
`, `,
]; ];
} }

View File

@@ -109,7 +109,11 @@ class HaConfigSectionUpdates extends LitElement {
></ha-config-updates> ></ha-config-updates>
` `
: html` : html`
${this.hass.localize("ui.panel.config.updates.no_updates")} <div class="no-updates">
${this.hass.localize(
"ui.panel.config.updates.no_updates"
)}
</div>
`} `}
</div> </div>
</ha-card> </ha-card>
@@ -196,6 +200,10 @@ class HaConfigSectionUpdates extends LitElement {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
flex-direction: column; flex-direction: column;
padding: 0;
}
.no-updates {
padding: 16px; padding: 16px;
} }
`; `;

View File

@@ -1,12 +1,25 @@
import { ActionDetail } from "@material/mwc-list";
import { mdiDotsVertical } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { canShowPage } from "../../../common/config/can_show_page"; import { canShowPage } from "../../../common/config/can_show_page";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { relativeTime } from "../../../common/datetime/relative_time";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-navigation-list"; import "../../../components/ha-navigation-list";
import { CloudStatus } from "../../../data/cloud"; import "../../../components/ha-tip";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import { BackupContent, fetchBackupInfo } from "../../../data/backup";
import { CloudStatus, fetchCloudStatus } from "../../../data/cloud";
import { BOARD_NAMES } from "../../../data/hardware";
import { fetchHassioBackups, HassioBackup } from "../../../data/hassio/backup";
import {
fetchHassioHassOsInfo,
fetchHassioHostInfo,
HassioHassOSInfo,
HassioHostInfo,
} from "../../../data/hassio/host";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-subpage"; import "../../../layouts/hass-subpage";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
@@ -26,71 +39,192 @@ class HaConfigSystemNavigation extends LitElement {
@property({ type: Boolean }) public showAdvanced!: boolean; @property({ type: Boolean }) public showAdvanced!: boolean;
@state() private _latestBackupDate?: string;
@state() private _boardName?: string;
@state() private _storageInfo?: { used: number; free: number; total: number };
@state() private _externalAccess = false;
protected render(): TemplateResult { protected render(): TemplateResult {
const pages = configSections.general const pages = configSections.general
.filter((page) => canShowPage(this.hass, page)) .filter((page) => canShowPage(this.hass, page))
.map((page) => ({ .map((page) => {
...page, let description = "";
name: page.translationKey
? this.hass.localize(page.translationKey) switch (page.translationKey) {
: page.name, case "backup":
})); description = this._latestBackupDate
? this.hass.localize(
"ui.panel.config.backup.description",
"relative_time",
relativeTime(
new Date(this._latestBackupDate),
this.hass.locale
)
)
: this.hass.localize(
"ui.panel.config.backup.description_no_backup"
);
break;
case "network":
description = this.hass.localize(
"ui.panel.config.network.description",
"state",
this._externalAccess
? this.hass.localize("ui.panel.config.network.enabled")
: this.hass.localize("ui.panel.config.network.disabled")
);
break;
case "storage":
description = this._storageInfo
? this.hass.localize(
"ui.panel.config.storage.description",
"percent_used",
`${Math.round(
(this._storageInfo.used / this._storageInfo.total) * 100
)}%`,
"free_space",
`${this._storageInfo.free} GB`
)
: "";
break;
case "hardware":
description =
this._boardName ||
this.hass.localize("ui.panel.config.hardware.description");
break;
default:
description = this.hass.localize(
`ui.panel.config.${page.translationKey}.description`
);
break;
}
return {
...page,
name: page.translationKey
? this.hass.localize(
`ui.panel.config.${page.translationKey}.caption`
)
: page.name,
description,
};
});
return html` return html`
<hass-subpage <hass-subpage
back-path="/config" back-path="/config"
.header=${this.hass.localize("ui.panel.config.dashboard.system.main")} .header=${this.hass.localize("ui.panel.config.dashboard.system.main")}
> >
<ha-button-menu <mwc-button
corner="BOTTOM_START"
slot="toolbar-icon" slot="toolbar-icon"
@action=${this._handleAction} .label=${this.hass.localize(
> "ui.panel.config.system_dashboard.restart_homeassistant_short"
<ha-icon-button )}
slot="trigger" @click=${this._restart}
.label=${this.hass.localize("ui.common.overflow_menu")} ></mwc-button>
.path=${mdiDotsVertical}
></ha-icon-button>
<mwc-list-item>
${this.hass.localize(
"ui.panel.config.system_dashboard.restart_homeassistant"
)}
</mwc-list-item>
</ha-button-menu>
<ha-config-section <ha-config-section
.narrow=${this.narrow} .narrow=${this.narrow}
.isWide=${this.isWide} .isWide=${this.isWide}
full-width full-width
> >
<ha-card outlined> <ha-card outlined>
${this.narrow
? html`<div class="title">
${this.hass.localize("ui.panel.config.dashboard.system.main")}
</div>`
: ""}
<ha-navigation-list <ha-navigation-list
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.pages=${pages} .pages=${pages}
hasSecondary
></ha-navigation-list> ></ha-navigation-list>
</ha-card> </ha-card>
${this.hass.userData?.showAdvanced
? html`<ha-tip>
Looking for YAML Configuration? It has moved to
<a href="/developer-tools/yaml">Developer Tools</a>
</ha-tip>`
: ""}
</ha-config-section> </ha-config-section>
</hass-subpage> </hass-subpage>
`; `;
} }
private _handleAction(ev: CustomEvent<ActionDetail>) { protected firstUpdated(_changedProperties): void {
switch (ev.detail.index) { super.firstUpdated(_changedProperties);
case 0:
showConfirmationDialog(this, { this._fetchNetworkStatus();
text: this.hass.localize( const isHassioLoaded = isComponentLoaded(this.hass, "hassio");
"ui.panel.config.system_dashboard.confirm_restart" this._fetchBackupInfo(isHassioLoaded);
), if (isHassioLoaded) {
confirm: () => { this._fetchHardwareInfo();
this.hass.callService("homeassistant", "restart"); this._fetchStorageInfo();
}, }
}
private _restart() {
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.system_dashboard.confirm_restart_title"
),
text: this.hass.localize(
"ui.panel.config.system_dashboard.confirm_restart_text"
),
confirmText: this.hass.localize(
"ui.panel.config.system_dashboard.restart_homeassistant_short"
),
confirm: () => {
this.hass.callService("homeassistant", "restart").catch((reason) => {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.system_dashboard.restart_error"
),
text: reason.message,
});
}); });
break; },
});
}
private async _fetchBackupInfo(isHassioLoaded: boolean) {
const backups: BackupContent[] | HassioBackup[] = isHassioLoaded
? await fetchHassioBackups(this.hass)
: await fetchBackupInfo(this.hass).then(
(backupData) => backupData.backups
);
if (backups.length > 0) {
this._latestBackupDate = (backups as any[]).reduce((a, b) =>
a.date > b.date ? a : b
).date;
}
}
private async _fetchHardwareInfo() {
const osData: HassioHassOSInfo = await fetchHassioHassOsInfo(this.hass);
if (osData.board) {
this._boardName = BOARD_NAMES[osData.board];
}
}
private async _fetchStorageInfo() {
const hostInfo: HassioHostInfo = await fetchHassioHostInfo(this.hass);
this._storageInfo = {
used: hostInfo.disk_used,
free: hostInfo.disk_free,
total: hostInfo.disk_total,
};
}
private async _fetchNetworkStatus() {
if (isComponentLoaded(this.hass, "cloud")) {
fetchCloudStatus(this.hass).then((cloudStatus) => {
if (cloudStatus.logged_in) {
this._externalAccess = true;
}
});
} else {
this._externalAccess = this.hass.config.external_url !== null;
} }
} }
@@ -125,18 +259,22 @@ class HaConfigSystemNavigation extends LitElement {
padding-bottom: 0; padding-bottom: 0;
} }
:host([narrow]) ha-card { @media all and (max-width: 600px) {
border-radius: 0; ha-card {
box-shadow: unset; border-width: 1px 0;
} border-radius: 0;
box-shadow: unset;
:host([narrow]) ha-config-section { }
margin-top: -42px; ha-config-section {
margin-top: -42px;
}
} }
ha-navigation-list { ha-navigation-list {
--navigation-list-item-title-font-size: 16px; --navigation-list-item-title-font-size: 16px;
--navigation-list-item-padding: 4px; }
ha-tip {
margin-bottom: max(env(safe-area-inset-bottom), 8px);
} }
`, `,
]; ];

View File

@@ -39,9 +39,9 @@ import { configSections } from "../ha-panel-config";
import "./ha-config-navigation"; import "./ha-config-navigation";
import "./ha-config-updates"; import "./ha-config-updates";
const randomTip = (hass: HomeAssistant) => { const randomTip = (hass: HomeAssistant, narrow: boolean) => {
const weighted: string[] = []; const weighted: string[] = [];
const tips = [ let tips = [
{ {
content: hass.localize( content: hass.localize(
"ui.panel.config.tips.join", "ui.panel.config.tips.join",
@@ -84,11 +84,16 @@ const randomTip = (hass: HomeAssistant) => {
</span>` </span>`
), ),
weight: 2, weight: 2,
narrow: true,
}, },
{ content: hass.localize("ui.tips.key_c_hint"), weight: 1 }, { content: hass.localize("ui.tips.key_c_hint"), weight: 1, narrow: false },
{ content: hass.localize("ui.tips.key_m_hint"), weight: 1 }, { content: hass.localize("ui.tips.key_m_hint"), weight: 1, narrow: false },
]; ];
if (narrow) {
tips = tips.filter((tip) => tip.narrow);
}
tips.forEach((tip) => { tips.forEach((tip) => {
for (let i = 0; i < tip.weight; i++) { for (let i = 0; i < tip.weight; i++) {
weighted.push(tip.content); weighted.push(tip.content);
@@ -190,11 +195,6 @@ class HaConfigDashboard extends LitElement {
</ha-card>` </ha-card>`
: ""} : ""}
<ha-card outlined> <ha-card outlined>
${this.narrow && canInstallUpdates.length
? html`<div class="title">
${this.hass.localize("panel.config")}
</div>`
: ""}
<ha-config-navigation <ha-config-navigation
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
@@ -215,7 +215,7 @@ class HaConfigDashboard extends LitElement {
super.updated(changedProps); super.updated(changedProps);
if (!this._tip && changedProps.has("hass")) { if (!this._tip && changedProps.has("hass")) {
this._tip = randomTip(this.hass); this._tip = randomTip(this.hass, this.narrow);
} }
} }
@@ -277,13 +277,16 @@ class HaConfigDashboard extends LitElement {
padding: 16px; padding: 16px;
padding-bottom: 0; padding-bottom: 0;
} }
:host([narrow]) ha-card {
border-radius: 0;
box-shadow: unset;
}
:host([narrow]) ha-config-section { @media all and (max-width: 600px) {
margin-top: -42px; ha-card {
border-width: 1px 0;
border-radius: 0;
box-shadow: unset;
}
ha-config-section {
margin-top: -42px;
}
} }
ha-tip { ha-tip {

View File

@@ -6,7 +6,7 @@ import { canShowPage } from "../../../common/config/can_show_page";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-icon-next"; import "../../../components/ha-icon-next";
import "../../../components/ha-navigation-list"; import "../../../components/ha-navigation-list";
import type { CloudStatus, CloudStatusLoggedIn } from "../../../data/cloud"; import type { CloudStatus } from "../../../data/cloud";
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage"; import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
@@ -37,9 +37,7 @@ class HaConfigNavigation extends LitElement {
? page.info.logged_in ? page.info.logged_in
? ` ? `
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.cloud.description_login", "ui.panel.config.cloud.description_login"
"email",
(page.info as CloudStatusLoggedIn).email
)} )}
` `
: ` : `

View File

@@ -9,6 +9,7 @@ import "../../../components/ha-alert";
import "../../../components/ha-icon-next"; import "../../../components/ha-icon-next";
import type { UpdateEntity } from "../../../data/update"; import type { UpdateEntity } from "../../../data/update";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import "../../../components/ha-circular-progress";
@customElement("ha-config-updates") @customElement("ha-config-updates")
class HaConfigUpdates extends LitElement { class HaConfigUpdates extends LitElement {
@@ -51,7 +52,18 @@ class HaConfigUpdates extends LitElement {
.title=${entity.attributes.title || .title=${entity.attributes.title ||
entity.attributes.friendly_name} entity.attributes.friendly_name}
.stateObj=${entity} .stateObj=${entity}
class=${this.narrow && entity.attributes.in_progress
? "updating"
: ""}
></state-badge> ></state-badge>
${this.narrow && entity.attributes.in_progress
? html`<ha-circular-progress
active
size="small"
slot="graphic"
class="absolute"
></ha-circular-progress>`
: ""}
<span <span
>${entity.attributes.title || >${entity.attributes.title ||
entity.attributes.friendly_name}</span entity.attributes.friendly_name}</span
@@ -67,7 +79,13 @@ class HaConfigUpdates extends LitElement {
: ""} : ""}
</span> </span>
${!this.narrow ${!this.narrow
? html`<ha-icon-next slot="meta"></ha-icon-next>` ? entity.attributes.in_progress
? html`<ha-circular-progress
active
size="small"
slot="meta"
></ha-circular-progress>`
: html`<ha-icon-next slot="meta"></ha-icon-next>`
: ""} : ""}
</mwc-list-item> </mwc-list-item>
` `
@@ -121,6 +139,12 @@ class HaConfigUpdates extends LitElement {
cursor: pointer; cursor: pointer;
font-size: 16px; font-size: 16px;
} }
ha-circular-progress.absolute {
position: absolute;
}
state-badge.updating {
opacity: 0.5;
}
`, `,
]; ];
} }

View File

@@ -1,5 +1,4 @@
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import "../../../../components/ha-card";
import { import {
DeviceAction, DeviceAction,
localizeDeviceAutomationAction, localizeDeviceAutomationAction,

View File

@@ -1,7 +1,6 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, html, LitElement, TemplateResult } from "lit";
import { property } from "lit/decorators"; import { property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-card";
import "../../../../components/ha-chip"; import "../../../../components/ha-chip";
import "../../../../components/ha-chip-set"; import "../../../../components/ha-chip-set";
import { showAutomationEditor } from "../../../../data/automation"; import { showAutomationEditor } from "../../../../data/automation";
@@ -10,6 +9,7 @@ import {
DeviceAutomation, DeviceAutomation,
} from "../../../../data/device_automation"; } from "../../../../data/device_automation";
import { showScriptEditor } from "../../../../data/script"; import { showScriptEditor } from "../../../../data/script";
import { buttonLinkStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
declare global { declare global {
@@ -29,6 +29,8 @@ export abstract class HaDeviceAutomationCard<
@property() public automations: T[] = []; @property() public automations: T[] = [];
@state() public _showSecondary = false;
protected headerKey = ""; protected headerKey = "";
protected type = ""; protected type = "";
@@ -60,28 +62,47 @@ export abstract class HaDeviceAutomationCard<
if (this.automations.length === 0) { if (this.automations.length === 0) {
return html``; return html``;
} }
const automations = this._showSecondary
? this.automations
: this.automations.filter(
(automation) => automation.metadata?.secondary === false
);
return html` return html`
<h3>${this.hass.localize(this.headerKey)}</h3> <h3>${this.hass.localize(this.headerKey)}</h3>
<div class="content"> <div class="content">
<ha-chip-set> <ha-chip-set>
${this.automations.map( ${automations.map(
(automation, idx) => (automation, idx) =>
html` html`
<ha-chip .index=${idx} @click=${this._handleAutomationClicked}> <ha-chip
.index=${idx}
@click=${this._handleAutomationClicked}
class=${automation.metadata?.secondary ? "secondary" : ""}
>
${this._localizeDeviceAutomation(this.hass, automation)} ${this._localizeDeviceAutomation(this.hass, automation)}
</ha-chip> </ha-chip>
` `
)} )}
</ha-chip-set> </ha-chip-set>
${!this._showSecondary && automations.length < this.automations.length
? html`<button class="link" @click=${this._toggleSecondary}>
Show ${this.automations.length - automations.length} more...
</button>`
: ""}
</div> </div>
`; `;
} }
private _toggleSecondary() {
this._showSecondary = !this._showSecondary;
}
private _handleAutomationClicked(ev: CustomEvent) { private _handleAutomationClicked(ev: CustomEvent) {
const automation = this.automations[(ev.currentTarget as any).index]; const automation = { ...this.automations[(ev.currentTarget as any).index] };
if (!automation) { if (!automation) {
return; return;
} }
delete automation.metadata;
if (this.script) { if (this.script) {
showScriptEditor({ sequence: [automation as DeviceAction] }); showScriptEditor({ sequence: [automation as DeviceAction] });
fireEvent(this, "entry-selected"); fireEvent(this, "entry-selected");
@@ -93,11 +114,18 @@ export abstract class HaDeviceAutomationCard<
fireEvent(this, "entry-selected"); fireEvent(this, "entry-selected");
} }
static get styles(): CSSResultGroup { static styles = [
return css` buttonLinkStyle,
css`
h3 { h3 {
color: var(--primary-text-color); color: var(--primary-text-color);
} }
`; .secondary {
} --ha-chip-background-color: rgba(var(--rgb-primary-text-color), 0.07);
}
button.link {
color: var(--primary-color);
}
`,
];
} }

View File

@@ -10,6 +10,7 @@ import {
fetchDeviceActions, fetchDeviceActions,
fetchDeviceConditions, fetchDeviceConditions,
fetchDeviceTriggers, fetchDeviceTriggers,
sortDeviceAutomations,
} from "../../../../data/device_automation"; } from "../../../../data/device_automation";
import { haStyleDialog } from "../../../../resources/styles"; import { haStyleDialog } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
@@ -63,16 +64,16 @@ export class DialogDeviceAutomation extends LitElement {
const { device, script } = this._params; const { device, script } = this._params;
fetchDeviceActions(this.hass, device.id).then((actions) => { fetchDeviceActions(this.hass, device.id).then((actions) => {
this._actions = actions; this._actions = actions.sort(sortDeviceAutomations);
}); });
if (script) { if (script) {
return; return;
} }
fetchDeviceTriggers(this.hass, device.id).then((triggers) => { fetchDeviceTriggers(this.hass, device.id).then((triggers) => {
this._triggers = triggers; this._triggers = triggers.sort(sortDeviceAutomations);
}); });
fetchDeviceConditions(this.hass, device.id).then((conditions) => { fetchDeviceConditions(this.hass, device.id).then((conditions) => {
this._conditions = conditions; this._conditions = conditions.sort(sortDeviceAutomations);
}); });
} }

View File

@@ -1,5 +1,4 @@
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import "../../../../components/ha-card";
import { import {
DeviceCondition, DeviceCondition,
localizeDeviceAutomationCondition, localizeDeviceAutomationCondition,

View File

@@ -62,7 +62,7 @@ export class HaDeviceEntitiesCard extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.entities.length) { if (!this.entities.length) {
return html` return html`
<ha-card .header=${this.header}> <ha-card outlined .header=${this.header}>
<div class="empty card-content"> <div class="empty card-content">
${this.hass.localize("ui.panel.config.devices.entities.none")} ${this.hass.localize("ui.panel.config.devices.entities.none")}
</div> </div>
@@ -89,7 +89,7 @@ export class HaDeviceEntitiesCard extends LitElement {
}); });
return html` return html`
<ha-card .header=${this.header}> <ha-card outlined .header=${this.header}>
<div id="entities" @hass-more-info=${this._overrideMoreInfo}> <div id="entities" @hass-more-info=${this._overrideMoreInfo}>
${shownEntities.map((entry) => ${shownEntities.map((entry) =>
this.hass.states[entry.entity_id] this.hass.states[entry.entity_id]

View File

@@ -1,5 +1,6 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "../../../../components/ha-card";
import { AreaRegistryEntry } from "../../../../data/area_registry"; import { AreaRegistryEntry } from "../../../../data/area_registry";
import { import {
computeDeviceName, computeDeviceName,
@@ -24,6 +25,7 @@ export class HaDeviceCard extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<ha-card <ha-card
outlined
.header=${this.hass.localize( .header=${this.hass.localize(
"ui.panel.config.devices.device_info", "ui.panel.config.devices.device_info",
"type", "type",
@@ -145,3 +147,9 @@ export class HaDeviceCard extends LitElement {
]; ];
} }
} }
declare global {
interface HTMLElementTagNameMap {
"ha-device-info-card": HaDeviceCard;
}
}

View File

@@ -579,7 +579,7 @@ export class HaConfigDevicePage extends LitElement {
${ ${
isComponentLoaded(this.hass, "automation") isComponentLoaded(this.hass, "automation")
? html` ? html`
<ha-card> <ha-card outlined>
<h1 class="card-header"> <h1 class="card-header">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.devices.automation.automations_heading" "ui.panel.config.devices.automation.automations_heading"
@@ -673,7 +673,7 @@ export class HaConfigDevicePage extends LitElement {
${ ${
isComponentLoaded(this.hass, "scene") && entities.length isComponentLoaded(this.hass, "scene") && entities.length
? html` ? html`
<ha-card> <ha-card outlined>
<h1 class="card-header"> <h1 class="card-header">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.devices.scene.scenes_heading" "ui.panel.config.devices.scene.scenes_heading"
@@ -771,7 +771,7 @@ export class HaConfigDevicePage extends LitElement {
${ ${
isComponentLoaded(this.hass, "script") isComponentLoaded(this.hass, "script")
? html` ? html`
<ha-card> <ha-card outlined>
<h1 class="card-header"> <h1 class="card-header">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.devices.script.scripts_heading" "ui.panel.config.devices.script.scripts_heading"

View File

@@ -51,7 +51,7 @@ export class EnergyBatterySettings extends LitElement {
}); });
return html` return html`
<ha-card> <ha-card outlined>
<h1 class="card-header"> <h1 class="card-header">
<ha-svg-icon .path=${mdiBatteryHigh}></ha-svg-icon> <ha-svg-icon .path=${mdiBatteryHigh}></ha-svg-icon>
${this.hass.localize("ui.panel.config.energy.battery.title")} ${this.hass.localize("ui.panel.config.energy.battery.title")}

View File

@@ -36,7 +36,7 @@ export class EnergyDeviceSettings extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<ha-card> <ha-card outlined>
<h1 class="card-header"> <h1 class="card-header">
<ha-svg-icon .path=${mdiDevices}></ha-svg-icon> <ha-svg-icon .path=${mdiDevices}></ha-svg-icon>
${this.hass.localize( ${this.hass.localize(

View File

@@ -51,7 +51,7 @@ export class EnergyGasSettings extends LitElement {
}); });
return html` return html`
<ha-card> <ha-card outlined>
<h1 class="card-header"> <h1 class="card-header">
<ha-svg-icon .path=${mdiFire}></ha-svg-icon> <ha-svg-icon .path=${mdiFire}></ha-svg-icon>
${this.hass.localize("ui.panel.config.energy.gas.title")} ${this.hass.localize("ui.panel.config.energy.gas.title")}

View File

@@ -80,7 +80,7 @@ export class EnergyGridSettings extends LitElement {
} }
return html` return html`
<ha-card> <ha-card outlined>
<h1 class="card-header"> <h1 class="card-header">
<ha-svg-icon .path=${mdiTransmissionTower}></ha-svg-icon> <ha-svg-icon .path=${mdiTransmissionTower}></ha-svg-icon>
${this.hass.localize("ui.panel.config.energy.grid.title")} ${this.hass.localize("ui.panel.config.energy.grid.title")}

View File

@@ -54,7 +54,7 @@ export class EnergySolarSettings extends LitElement {
}); });
return html` return html`
<ha-card> <ha-card outlined>
<h1 class="card-header"> <h1 class="card-header">
<ha-svg-icon .path=${mdiSolarPower}></ha-svg-icon> <ha-svg-icon .path=${mdiSolarPower}></ha-svg-icon>
${this.hass.localize("ui.panel.config.energy.solar.title")} ${this.hass.localize("ui.panel.config.energy.solar.title")}

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