Compare commits

...

170 Commits

Author SHA1 Message Date
Zack 44a52075ec Remove entity_matches_only 2022-05-11 10:20:28 -05:00
Yosi Levy c9c3be71cc Merge pull request #12620 from yosilevy/RTL-no-host-context
Replace host-context with css properties - after session with Bram
2022-05-11 16:30:21 +03:00
Bram Kragten f1b965dcc5 Update ha-fab.ts 2022-05-11 15:19:03 +02:00
Bram Kragten a08a23a93d Use FabBase 2022-05-11 14:25:43 +02:00
Bram Kragten 2040a49458 Update var name 2022-05-11 14:21:02 +02:00
Bram Kragten df94f4f907 Merge branch 'dev' into RTL-no-host-context 2022-05-11 14:18:26 +02:00
Bram Kragten 96d375cb84 Use / 2022-05-11 14:16:44 +02:00
Yosi Levy 7a9c2f56c5 Rtl menu fix (#12561)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-05-11 11:01:45 +02:00
J. Nick Koston 5ec7193e5c Show script traces in logbook (#12643) 2022-05-10 23:32:09 +02:00
Zack Barett d89e4337f2 Hide Cloud URL - Add Copy Icon (#12655)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-05-10 19:37:31 +00:00
Zack Barett 2e192d5021 Update Translations to create helper (#12656) 2022-05-10 21:25:03 +02:00
Yosi Levy 7db28c0156 Update following review 2022-05-10 19:31:23 +03:00
Yosi Levy f09c842981 Update src/state/translations-mixin.ts
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-05-10 18:25:28 +03:00
Yosi Levy b295bbd706 RTL settings clickable list item fix (#12595) 2022-05-10 16:57:18 +02:00
Patrick ZAJDA 8d3132fefc Add label for Fix issue column header in statistics developer tools (#12597)
Signed-off-by: Patrick ZAJDA <patrick@zajda.fr>
2022-05-09 17:14:59 +02:00
Allen Porter 00c5d3dbbb Add configuration panel for Application Credentials (#12344)
Co-authored-by: Zack Barett <zackbarett@hey.com>
Co-authored-by: Zack <zackbarett@hey.com>
2022-05-09 17:03:59 +02:00
Zack Barett ca37aff47d Move YAML to first tab of Developer Tools (#12589) 2022-05-09 08:07:17 -05:00
Joakim Sørensen 9ed069ef6a Get full core logs from core (#12639) 2022-05-09 08:07:01 -05:00
Philip Allgaier 6faa3eb848 Remove "Lovelace" from Github issue templates (#12614)
* Remove "Lovelace" from Github issue templates

* Changes from review
2022-05-09 09:47:39 +02:00
Yosi Levy 6c73ae5bf7 Replace host-context with css properties 2022-05-07 06:39:39 +03:00
Zack Barett ce77ddf365 Revert #10991 (#12618) 2022-05-07 02:48:57 +00:00
Steve Repsher cf05fbaa9d Fix enter key support for generic dialog box (#12600) 2022-05-06 13:32:44 +02:00
Joakim Sørensen 552c474feb Fix setting _externalAccess (#12584) 2022-05-04 08:17:09 -05: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
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
Yosi Levy 3149ffbf19 RTL fix for log buttons (#12474) 2022-04-27 12:26:19 -05:00
Philip Allgaier 4cd8b76d7e Safeguard against non-existant area in device handling (#12475) 2022-04-27 12:25:13 -05:00
Joakim Sørensen 4b644d8bc5 Add supervisor redirects to m keyboard shortcut (#12466) 2022-04-27 13:36:47 +00:00
Joakim Sørensen 307cd5ad8c Use startsWith for m shortcut for partial match (#12464) 2022-04-27 08:10:38 -05:00
Joakim Sørensen ebc807a6a4 Add hass-quick-bar-trigger event to trigger quickbar from supervisor (#12467) 2022-04-27 08:08:45 -05:00
Philip Allgaier 66adecdfc9 Make helper option button more user friendly (#12468) 2022-04-27 08:07:57 -05:00
Philip Allgaier 2cc6432a0f Use correct label for update config menu (#12465) 2022-04-27 06:37:50 -05:00
Paulus Schoutsen a2c0c0474a Bumped version to 20220427.0 2022-04-26 22:13:16 -07:00
Zack Barett 27884b9a54 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
2022-04-26 22:12:44 -07:00
Paulus Schoutsen 293df61872 Add a tip for my shortcut (#12462) 2022-04-27 05:01:40 +00:00
Paulus Schoutsen f82dada3e5 Fix icon alignment in nav list (#12463) 2022-04-26 21:58:26 -07:00
Paulus Schoutsen e5824c4794 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>
2022-04-27 04:58:18 +00:00
Paulus Schoutsen 186550229c Tweak menu descriptions (#12460) 2022-04-27 04:53:42 +00:00
Zack Barett 7877dd8e6b 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>
2022-04-26 21:53:29 -07:00
Zack Barett b03abc249b Fix Updates Page Toast - Move to overflow (#12453) 2022-04-26 21:52:22 -07:00
Paulus Schoutsen fda03918b9 Move the analytics link (#12459) 2022-04-27 04:40:01 +00:00
Zack Barett 6747375a1b Move Provider Selection to Menu on top header (#12443) 2022-04-26 23:27:15 -05:00
Zack Barett 53b6e31881 Update Configuration badge color to be accent color to match (#12455) 2022-04-26 21:12:09 -07:00
Zack Barett fa004de2d1 Fix more info input number #12396 (#12456) 2022-04-26 21:11:54 -07:00
Zack Barett 3605f7b70f Fix when creating new area in picker #11392 (#12457) 2022-04-26 21:11:38 -07:00
Zack Barett 5348c54c91 Update the hint for key C (#12458) 2022-04-26 21:11:18 -07:00
Zack Barett 684e4421bc Fix for backup overflow (#12454) 2022-04-26 21:10:35 -07:00
Zack Barett 28f5611df5 Small edits on config menu (#12440) 2022-04-26 21:07:53 -07:00
Johann Vanackere 8da73d49d7 Terms based entities search (#10991) 2022-04-26 19:39:58 -05:00
Joakim Sørensen 049ddd5f84 Add "m" keyboard shortcut to get to the create my link page (#12451) 2022-04-27 00:11:09 +02:00
Bram Kragten 8ae2d4e93a Fix integration page on mobile (#12447) 2022-04-26 14:38:59 -05:00
Philip Allgaier 824bb9ba35 Add title to backups config page (#12442) 2022-04-26 21:04:32 +02:00
Philip Allgaier d550b1a18e Fix content display for ha-network after #12438 (#12445)
* Fix content display for `ha-network` after #12438

* Add var default
2022-04-26 20:41:19 +02:00
Bram Kragten dea6c0e761 Add header to supervisor backups page (#12444) 2022-04-26 17:53:32 +00:00
Philip Allgaier 9caee357c0 Fix incorrect text if no backups are found (#12441) 2022-04-26 12:32:04 -05:00
Bram Kragten 35d892c418 Set border radius in config to 8px (#12437) 2022-04-26 11:50:36 -05:00
Bram Kragten 9572a2a46b Dont show tabs when less than 2 (#12439) 2022-04-26 15:39:50 +00:00
Bram Kragten 8996361b26 Fix settings row width (#12438) 2022-04-26 15:17:00 +00:00
Joakim Sørensen 02ee731602 Add join/leave beta to updates panel (#12436) 2022-04-26 16:39:37 +02:00
Joakim Sørensen bb1e6bf35b Fix backup back path (#12435) 2022-04-26 15:29:56 +02:00
Joakim Sørensen c1b65285c1 Redirect hassio system my links to new locations (#12429) 2022-04-26 13:15:29 +02:00
Bram Kragten 8b8d6e5fa3 Resources lovelace should just go back (#12432) 2022-04-26 11:12:14 +00:00
Joakim Sørensen c34fe184e8 Fix log syntax highlight when fetching logs from supervisor (#12430) 2022-04-26 06:09:39 -05:00
Joakim Sørensen 7363838f86 Move unsupported and unhealthy alerts (#12431) 2022-04-26 12:24:55 +02:00
Jaroslav Hanslík 3081425ccd Typo in en.json (#12428) 2022-04-26 12:20:26 +02:00
Joakim Sørensen 95d494a54c Guard against non OS installation (#12427) 2022-04-26 12:18:43 +02:00
J. Nick Koston 145e5d7bc6 Format sensors with state class duration (#12426) 2022-04-26 02:07:11 +00:00
Zack Barett 876fd9e85a Bumped version to 20220425.0 (#12425) 2022-04-25 18:25:07 -05:00
Zack Barett e8c30cabca Show usage stats in System Health (#12424) 2022-04-25 23:24:58 +00:00
Bram Kragten 490f84a7b1 link to updates page (#12423) 2022-04-25 15:56:34 -05:00
Artem Sorokin ca28178b86 Fix title and description for menu step in options flow (#12420) 2022-04-25 20:26:05 +00:00
Zack Barett 2fceb0aeee Allow for checking for updates (#12422) 2022-04-25 22:15:26 +02:00
Bram Kragten 86f39d1d43 Add supervisor, OS version info to about page (#12421)
* Add supervisor, OS version info to about page

* description

* description
2022-04-25 22:14:32 +02:00
Zack Barett 1faf60444d Move Data Disk Moving to Storage (#12416) 2022-04-25 20:49:44 +02:00
Netzwerkfehler e927091d21 Better gauge segment coloring (#11570) 2022-04-25 13:43:53 -05:00
Bram Kragten cff2f856b3 Don't show tabs in supervisor (#12417) 2022-04-25 13:37:48 -05:00
Bram Kragten a743e3bbba Show what updates are skipped (#12418) 2022-04-25 18:37:24 +00:00
Zack Barett f8a52d250e Move System Health to a page (#12412) 2022-04-25 20:26:53 +02:00
Zack Barett b70a523bdf Backup Page - Will load which is available (#12414) 2022-04-25 19:54:11 +02:00
Zack Barett 8f2ed747e6 Configuration Menu Cleanup items (#12413) 2022-04-25 19:53:02 +02:00
Zack Barett 5deccefb15 Allow Showing Skipped Updates on Updates Page (#12415) 2022-04-25 19:50:30 +02:00
Zack Barett 3f04abfa9d Add Supervisor logs to core page (#12410) 2022-04-25 15:35:03 +00:00
Zack Barett 8e55c83996 Add Hardware Page to Configuration System Menu (#12405) 2022-04-25 17:30:53 +02:00
Bram Kragten dee59486ba Add supervisor hostname config (#12407) 2022-04-25 10:27:38 -05:00
Bram Kragten 77ef509aea Fix zones (#12409) 2022-04-25 15:13:31 +00:00
Bram Kragten bfa7bccfa6 Add supervisor network interface settings (#12403) 2022-04-25 16:21:03 +02:00
Thomas Lovén a8c365edc8 Fix broken cards being able to crash entire view (#11440) 2022-04-25 14:37:32 +02:00
Bram Kragten 94953ddf6c Hide supervisor only config, fix backup config page (#12401) 2022-04-25 07:09:23 -05:00
Zack Barett 6b67546daf Virtualize Media Player Grid (#11898) 2022-04-25 12:32:50 +02:00
Paulus Schoutsen 3e188d1f87 Add shorthand condition to the gallery (#12400) 2022-04-25 10:00:28 +02:00
Zack Barett f69eb15a90 Config Menu: Addressing Comments in #12377 (#12399) 2022-04-24 20:25:47 -07:00
Paulus Schoutsen dfe348187f Bumped version to 20220424.0 2022-04-24 15:26:42 -07:00
Zack Barett 9706c56c5c Configuration Menu Updates 3 (#12377) 2022-04-24 15:26:01 -07:00
Raman Gupta 3677c5be2c Update zwavejs controller model (#12390) 2022-04-24 14:55:31 -07:00
Zack Barett bd339fa963 Fix Dashboard URLs (#12394) 2022-04-24 14:55:04 -07:00
Yosi Levy 28f1b6bdf4 Force LTR on time & number inputs (#12393) 2022-04-23 19:48:17 -05:00
Zack Barett c5aac3b81d Add Empty list item for None (#12356) 2022-04-23 10:11:44 -05:00
yangqian 70836597e9 Show vacuum state in more-info dialog for StateVacuumEntity (#12391) 2022-04-23 06:32:59 +00:00
Allen Porter 958a1de2fd Add calendar event end trigger to automation editor (#12389) 2022-04-23 00:19:23 -05:00
Allen Porter 36d30266e3 Add automation editor for calendar trigger (#12343) 2022-04-22 16:53:45 -05:00
Yosi Levy 558ab9761d RTL reading orders and alignments in system log (#12388) 2022-04-22 20:19:38 +00:00
Eric Stern 269ef370e4 Accept new value when hitting ENTER to close a prompt dialog (#12360)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-04-22 14:23:34 -05:00
Kuba Wolanin ba2958ecd2 zwave_js: Add title tag to config box heading (#12387) 2022-04-22 13:17:42 -05:00
203 changed files with 8012 additions and 3161 deletions
+2 -2
View File
@@ -1,4 +1,4 @@
name: Report a bug with the UI, Frontend or Lovelace
name: Report a bug with the UI / Dashboards
description: Report an issue related to the Home Assistant frontend.
labels: bug
body:
@@ -9,7 +9,7 @@ body:
If you have a feature or enhancement request for the frontend, please [start an discussion][fr] instead of creating an issue.
**Please not not report issues for custom Lovelace cards.**
**Please not not report issues for custom cards.**
[fr]: https://github.com/home-assistant/frontend/discussions
[releases]: https://github.com/home-assistant/home-assistant/releases
+4 -4
View File
@@ -1,17 +1,17 @@
blank_issues_enabled: false
contact_links:
- name: Request a feature for the UI, Frontend or Lovelace
- name: Request a feature for the UI / Dashboards
url: https://github.com/home-assistant/frontend/discussions/category_choices
about: Request an new feature for the Home Assistant frontend.
- name: Report a bug that is NOT related to the UI, Frontend or Lovelace
- name: Report a bug that is NOT related to the UI / Dashboards
url: https://github.com/home-assistant/core/issues
about: This is the issue tracker for our frontend. Please report other issues with the backend repository.
about: This is the issue tracker for our frontend. Please report other issues in the backend ("core") repository.
- name: Report incorrect or missing information on our website
url: https://github.com/home-assistant/home-assistant.io/issues
about: Our documentation has its own issue tracker. Please report issues with the website there.
- name: I have a question or need support
url: https://www.home-assistant.io/help
about: We use GitHub for tracking bugs, check our website for resources on getting help.
about: We use GitHub for tracking bugs. Check our website for resources on getting help.
- name: I'm unsure where to go
url: https://www.home-assistant.io/join-chat
about: If you are unsure where to go, then joining our chat is recommended; Just ask!
+4 -2
View File
@@ -3,10 +3,10 @@ const webpack = require("webpack");
const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
const paths = require("./paths.js");
const bundle = require("./bundle.js");
const log = require("fancy-log");
const WebpackBar = require("webpackbar");
const paths = require("./paths.js");
const bundle = require("./bundle.js");
class LogStartCompilePlugin {
ignoredFirst = false;
@@ -138,6 +138,8 @@ const createWebpackConfig = ({
"lit/directives/cache$": "lit/directives/cache.js",
"lit/directives/repeat$": "lit/directives/repeat.js",
"lit/polyfill-support$": "lit/polyfill-support.js",
"@lit-labs/virtualizer/layouts/grid":
"@lit-labs/virtualizer/layouts/grid.js",
},
},
output: {
@@ -62,6 +62,45 @@ const ACTIONS = [
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")
@@ -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 { Action } from "../../../../src/data/script";
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[] }[] = [
{ name: "Event", actions: [HaEventAction.defaultConfig] },
@@ -28,11 +32,15 @@ const SCHEMAS: { name: string; actions: Action[] }[] = [
{ name: "Condition", actions: [HaConditionAction.defaultConfig] },
{ name: "Delay", actions: [HaDelayAction.defaultConfig] },
{ name: "Scene", actions: [HaSceneAction.defaultConfig] },
{ name: "Play media", actions: [HaPlayMediaAction.defaultConfig] },
{ name: "Wait", actions: [HaWaitAction.defaultConfig] },
{ name: "WaitForTrigger", actions: [HaWaitForTriggerAction.defaultConfig] },
{ name: "Repeat", actions: [HaRepeatAction.defaultConfig] },
{ name: "If-Then", actions: [HaIfAction.defaultConfig] },
{ name: "Choose", actions: [HaChooseAction.defaultConfig] },
{ name: "Variables", actions: [{ variables: { hello: "1" } }] },
{ name: "Parallel", actions: [HaParallelAction.defaultConfig] },
{ name: "Stop", actions: [HaStopAction.defaultConfig] },
];
@customElement("demo-automation-editor-action")
@@ -86,6 +94,6 @@ class DemoHaAutomationEditorAction extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"demo-ha-automation-editor-action": DemoHaAutomationEditorAction;
"demo-automation-editor-action": DemoHaAutomationEditorAction;
}
}
@@ -8,7 +8,7 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
import type { Condition } from "../../../../src/data/automation";
import type { ConditionWithShorthand } from "../../../../src/data/automation";
import "../../../../src/panels/config/automation/condition/ha-automation-condition";
import { HaDeviceCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-device";
import { HaLogicalCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-logical";
@@ -20,7 +20,7 @@ import { HaTimeCondition } from "../../../../src/panels/config/automation/condit
import { HaTriggerCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-trigger";
import { HaZoneCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-zone";
const SCHEMAS: { name: string; conditions: Condition[] }[] = [
const SCHEMAS: { name: string; conditions: ConditionWithShorthand[] }[] = [
{
name: "State",
conditions: [{ condition: "state", ...HaStateCondition.defaultConfig }],
@@ -69,6 +69,14 @@ const SCHEMAS: { name: string; conditions: Condition[] }[] = [
name: "Trigger",
conditions: [{ condition: "trigger", ...HaTriggerCondition.defaultConfig }],
},
{
name: "Shorthand",
conditions: [
{ and: HaLogicalCondition.defaultConfig.conditions },
{ or: HaLogicalCondition.defaultConfig.conditions },
{ not: HaLogicalCondition.defaultConfig.conditions },
],
},
];
@customElement("demo-automation-editor-condition")
@@ -68,6 +68,7 @@ class HassioAddonRepositoryEl extends LitElement {
${addons.map(
(addon) => html`
<ha-card
outlined
.addon=${addon}
class=${addon.available ? "" : "not_available"}
@click=${this._addonTapped}
@@ -50,6 +50,7 @@ class HassioAddonAudio extends LitElement {
protected render(): TemplateResult {
return html`
<ha-card
outlined
.header=${this.supervisor.localize("addon.configuration.audio.header")}
>
<div class="card-content">
@@ -162,7 +162,7 @@ class HassioAddonConfig extends LitElement {
);
return html`
<h1>${this.addon.name}</h1>
<ha-card>
<ha-card outlined>
<div class="header">
<h2>
${this.supervisor.localize("addon.configuration.options.header")}
@@ -58,6 +58,7 @@ class HassioAddonNetwork extends LitElement {
return html`
<ha-card
outlined
.header=${this.supervisor.localize(
"addon.configuration.network.header"
)}
@@ -38,7 +38,7 @@ class HassioAddonDocumentationDashboard extends LitElement {
}
return html`
<div class="content">
<ha-card>
<ha-card outlined>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
@@ -17,7 +17,9 @@ import {
HassioAddonDetails,
} from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { setSupervisorOption } from "../../../src/data/hassio/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-loading-screen";
import "../../../src/layouts/hass-tabs-subpage";
@@ -166,6 +168,42 @@ class HassioAddonDashboard extends LitElement {
protected async firstUpdated(): Promise<void> {
if (this.route.path === "") {
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) {
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
const validAddon = addonsInfo.addons.some(
@@ -166,7 +166,7 @@ class HassioAddonInfo extends LitElement {
`
: ""}
<ha-card>
<ha-card outlined>
<div class="card-content">
<div class="addon-header">
${!this.narrow ? this.addon.name : ""}
@@ -649,7 +649,7 @@ class HassioAddonInfo extends LitElement {
${this.addon.long_description
? html`
<ha-card>
<ha-card outlined>
<div class="card-content">
<ha-markdown
.content=${this.addon.long_description}
@@ -2,6 +2,7 @@ import "@material/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-ansi-to-html";
import "../../../../src/components/ha-card";
import {
fetchHassioAddonLogs,
@@ -11,7 +12,6 @@ import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import "../../components/hassio-ansi-to-html";
import { hassioStyle } from "../../resources/hassio-style";
@customElement("hassio-addon-logs")
@@ -34,15 +34,15 @@ class HassioAddonLogs extends LitElement {
protected render(): TemplateResult {
return html`
<h1>${this.addon.name}</h1>
<ha-card>
<ha-card outlined>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<div class="card-content">
${this._content
? html`<hassio-ansi-to-html
? html`<ha-ansi-to-html
.content=${this._content}
></hassio-ansi-to-html>`
></ha-ansi-to-html>`
: ""}
</div>
<div class="card-actions">
+13 -3
View File
@@ -1,7 +1,7 @@
import "@material/mwc-button";
import { ActionDetail } from "@material/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js";
import { mdiBackupRestore, mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js";
import {
css,
CSSResultGroup,
@@ -166,7 +166,15 @@ export class HassioBackups extends LitElement {
}
return html`
<hass-tabs-subpage-data-table
.tabs=${supervisorTabs(this.hass)}
.tabs=${atLeastVersion(this.hass.config.version, 2022, 5)
? [
{
translationKey: "panel.backups",
path: `/hassio/backups`,
iconPath: mdiBackupRestore,
},
]
: supervisorTabs(this.hass)}
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.searchLabel=${this.supervisor.localize("search")}
@@ -182,7 +190,9 @@ export class HassioBackups extends LitElement {
selectable
hasFab
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
back-path="/config"
back-path=${atLeastVersion(this.hass.config.version, 2022, 5)
? "/config/system"
: "/config"}
supervisor
>
<ha-button-menu
+6 -2
View File
@@ -26,7 +26,7 @@ class HassioAddons extends LitElement {
<div class="card-group">
${!this.supervisor.supervisor.addons?.length
? html`
<ha-card>
<ha-card outlined>
<div class="card-content">
<button class="link" @click=${this._openStore}>
${this.supervisor.localize("dashboard.no_addons")}
@@ -38,7 +38,11 @@ class HassioAddons extends LitElement {
.sort((a, b) => caseInsensitiveStringCompare(a.name, b.name))
.map(
(addon) => html`
<ha-card .addon=${addon} @click=${this._addonTapped}>
<ha-card
outlined
.addon=${addon}
@click=${this._addonTapped}
>
<div class="card-content">
<hassio-card-content
.hass=${this.hass}
+32
View File
@@ -10,6 +10,7 @@ import { HomeAssistant, Route } from "../../../src/types";
import { supervisorTabs } from "../hassio-tabs";
import "./hassio-addons";
import "./hassio-update";
import "../../../src/layouts/hass-subpage";
@customElement("hassio-dashboard")
class HassioDashboard extends LitElement {
@@ -22,6 +23,31 @@ class HassioDashboard extends LitElement {
@property({ attribute: false }) public route!: Route;
protected render(): TemplateResult {
if (atLeastVersion(this.hass.config.version, 2022, 5)) {
return html`<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.header=${this.supervisor.localize("panel.addons")}
>
<hassio-addons
.hass=${this.hass}
.supervisor=${this.supervisor}
></hassio-addons>
<a href="/hassio/store">
<ha-fab
.label=${this.supervisor.localize("panel.store")}
extended
class="non-tabs"
>
<ha-svg-icon
slot="icon"
.path=${mdiStorePlus}
></ha-svg-icon> </ha-fab
></a>
</hass-subpage>`;
}
return html`
<hass-tabs-subpage
.hass=${this.hass}
@@ -74,6 +100,12 @@ class HassioDashboard extends LitElement {
.content {
margin: 0 auto;
}
ha-fab.non-tabs {
position: fixed;
right: calc(16px + env(safe-area-inset-right));
bottom: calc(16px + env(safe-area-inset-bottom));
z-index: 1;
}
`,
];
}
+1 -1
View File
@@ -85,7 +85,7 @@ export class HassioUpdate extends LitElement {
return html``;
}
return html`
<ha-card>
<ha-card outlined>
<div class="card-content">
<div class="icon">
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
+13 -1
View File
@@ -3,8 +3,8 @@ import { customElement, property } from "lit/decorators";
import { atLeastVersion } from "../../src/common/config/version";
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
import { fireEvent } from "../../src/common/dom/fire_event";
import { isNavigationClick } from "../../src/common/dom/is-navigation-click";
import { mainWindow } from "../../src/common/dom/get_main_window";
import { isNavigationClick } from "../../src/common/dom/is-navigation-click";
import { navigate } from "../../src/common/navigate";
import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
import { Supervisor } from "../../src/data/supervisor/supervisor";
@@ -73,6 +73,18 @@ export class HassioMain extends SupervisorBaseElement {
});
});
// Forward keydown events to the main window for quickbar access
document.body.addEventListener("keydown", (ev: KeyboardEvent) => {
if (ev.altKey || ev.ctrlKey || ev.shiftKey || ev.metaKey) {
// Ignore if modifier keys are pressed
return;
}
// @ts-ignore
fireEvent(mainWindow, "hass-quick-bar-trigger", ev, {
bubbles: false,
});
});
makeDialogManager(this, this.shadowRoot!);
}
+12 -1
View File
@@ -15,7 +15,7 @@ import {
} from "../../src/panels/my/ha-panel-my";
import { HomeAssistant, Route } from "../../src/types";
const REDIRECTS: Redirects = {
export const REDIRECTS: Redirects = {
supervisor: {
redirect: "/hassio/dashboard",
},
@@ -42,6 +42,9 @@ const REDIRECTS: Redirects = {
params: {
addon: "string",
},
optional_params: {
repository_url: "url",
},
},
supervisor_ingress: {
redirect: "/hassio/ingress",
@@ -124,6 +127,14 @@ class HassioMyRedirect extends LitElement {
}
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)}`;
}
+24 -21
View File
@@ -8,24 +8,27 @@ import { atLeastVersion } from "../../src/common/config/version";
import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage";
import { HomeAssistant } from "../../src/types";
export const supervisorTabs = (hass: HomeAssistant): PageNavigation[] => [
{
translationKey: atLeastVersion(hass.config.version, 2021, 12)
? "panel.addons"
: "panel.dashboard",
path: `/hassio/dashboard`,
iconPath: atLeastVersion(hass.config.version, 2021, 12)
? mdiPuzzle
: mdiViewDashboard,
},
{
translationKey: "panel.backups",
path: `/hassio/backups`,
iconPath: mdiBackupRestore,
},
{
translationKey: "panel.system",
path: `/hassio/system`,
iconPath: mdiCogs,
},
];
export const supervisorTabs = (hass: HomeAssistant): PageNavigation[] =>
atLeastVersion(hass.config.version, 2022, 5)
? []
: [
{
translationKey: atLeastVersion(hass.config.version, 2021, 12)
? "panel.addons"
: "panel.dashboard",
path: `/hassio/dashboard`,
iconPath: atLeastVersion(hass.config.version, 2021, 12)
? mdiPuzzle
: mdiViewDashboard,
},
{
translationKey: "panel.backups",
path: `/hassio/backups`,
iconPath: mdiBackupRestore,
},
{
translationKey: "panel.system",
path: `/hassio/system`,
iconPath: mdiCogs,
},
];
+1 -1
View File
@@ -48,7 +48,7 @@ class HassioCoreInfo extends LitElement {
];
return html`
<ha-card header="Core">
<ha-card header="Core" outlined>
<div class="card-content">
<div>
<ha-settings-row>
+1 -1
View File
@@ -66,7 +66,7 @@ class HassioHostInfo extends LitElement {
},
];
return html`
<ha-card header="Host">
<ha-card header="Host" outlined>
<div class="card-content">
<div>
${this.supervisor.host.features.includes("hostname")
+5 -6
View File
@@ -23,6 +23,10 @@ import {
showAlertDialog,
showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import {
UNHEALTHY_REASON_URL,
UNSUPPORTED_REASON_URL,
} from "../../../src/panels/config/system-health/ha-config-system-health";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { bytesToString } from "../../../src/util/bytes-to-string";
@@ -30,11 +34,6 @@ import { documentationUrl } from "../../../src/util/documentation-url";
import "../components/supervisor-metric";
import { hassioStyle } from "../resources/hassio-style";
const UNSUPPORTED_REASON_URL = {};
const UNHEALTHY_REASON_URL = {
privileged: "/more-info/unsupported/privileged",
};
@customElement("hassio-supervisor-info")
class HassioSupervisorInfo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -58,7 +57,7 @@ class HassioSupervisorInfo extends LitElement {
},
];
return html`
<ha-card header="Supervisor">
<ha-card header="Supervisor" outlined>
<div class="card-content">
<div>
<ha-settings-row>
+4 -4
View File
@@ -1,3 +1,4 @@
import "../../../src/components/ha-ansi-to-html";
import "@material/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -11,7 +12,6 @@ import { Supervisor } from "../../../src/data/supervisor/supervisor";
import "../../../src/layouts/hass-loading-screen";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import "../components/hassio-ansi-to-html";
import { hassioStyle } from "../resources/hassio-style";
interface LogProvider {
@@ -65,7 +65,7 @@ class HassioSupervisorLog extends LitElement {
protected render(): TemplateResult | void {
return html`
<ha-card>
<ha-card outlined>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
@@ -89,8 +89,8 @@ class HassioSupervisorLog extends LitElement {
<div class="card-content" id="content">
${this._content
? html`<hassio-ansi-to-html .content=${this._content}>
</hassio-ansi-to-html>`
? html`<ha-ansi-to-html .content=${this._content}>
</ha-ansi-to-html>`
: html`<hass-loading-screen no-toolbar></hass-loading-screen>`}
</div>
<div class="card-actions">
@@ -128,6 +128,7 @@ class UpdateAvailableCard extends LitElement {
return html`
<ha-card
outlined
.header=${this.supervisor.localize("update_available.update_name", {
name: this._name,
})}
+1 -1
View File
@@ -1,6 +1,6 @@
[metadata]
name = home-assistant-frontend
version = 20220420.0
version = 20220504.0
author = The Home Assistant Authors
author_email = hello@home-assistant.io
license = Apache-2.0
+16
View File
@@ -0,0 +1,16 @@
import secondsToDuration from "./seconds_to_duration";
const DAY_IN_SECONDS = 86400;
const HOUR_IN_SECONDS = 3600;
const MINUTE_IN_SECONDS = 60;
export const UNIT_TO_SECOND_CONVERT = {
s: 1,
min: MINUTE_IN_SECONDS,
h: HOUR_IN_SECONDS,
d: DAY_IN_SECONDS,
};
export const formatDuration = (duration: string, units: string): string =>
secondsToDuration(parseFloat(duration) * UNIT_TO_SECOND_CONVERT[units]) ||
"0";
@@ -13,6 +13,7 @@ import { formatNumber, isNumericState } from "../number/format_number";
import { LocalizeFunc } from "../translations/localize";
import { computeStateDomain } from "./compute_state_domain";
import { supportsFeature } from "./supports-feature";
import { formatDuration, UNIT_TO_SECOND_CONVERT } from "../datetime/duration";
export const computeStateDisplay = (
localize: LocalizeFunc,
@@ -28,6 +29,21 @@ export const computeStateDisplay = (
// Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber`
if (isNumericState(stateObj)) {
// state is duration
if (
stateObj.attributes.device_class === "duration" &&
stateObj.attributes.unit_of_measurement &&
UNIT_TO_SECOND_CONVERT[stateObj.attributes.unit_of_measurement]
) {
try {
return formatDuration(
compareState,
stateObj.attributes.unit_of_measurement
);
} catch (_err) {
// fallback to default
}
}
if (stateObj.attributes.device_class === "monetary") {
try {
return formatNumber(compareState, locale, {
+1 -1
View File
@@ -1,4 +1,4 @@
export const promiseTimeout = (ms: number, promise: Promise<any>) => {
export const promiseTimeout = (ms: number, promise: Promise<any> | any) => {
const timeout = new Promise((_resolve, reject) => {
setTimeout(() => {
reject(`Timed out in ${ms} ms.`);
+18
View File
@@ -0,0 +1,18 @@
import { HomeAssistant } from "../../types";
export const subscribePollingCollection = (
hass: HomeAssistant,
updateData: (hass: HomeAssistant) => void,
interval: number
) => {
let timeout;
const fetchData = async () => {
try {
await updateData(hass);
} finally {
timeout = setTimeout(() => fetchData(), interval);
}
};
fetchData();
return () => clearTimeout(timeout);
};
+161 -159
View File
@@ -1,165 +1,167 @@
export const currencies = [
"AED",
"AFN",
"ALL",
"AMD",
"ANG",
"AOA",
"ARS",
"AUD",
"AWG",
"AZN",
"BAM",
"BBD",
"BDT",
"BGN",
"BHD",
"BIF",
"BMD",
"BND",
"BOB",
"BRL",
"BSD",
"BTN",
"BWP",
"BYN",
"BYR",
"BZD",
"CAD",
"CDF",
"CHF",
"CLP",
"CNY",
"COP",
"CRC",
"CUP",
"CVE",
"CZK",
"DJF",
"DKK",
"DOP",
"DZD",
"EGP",
"ERN",
"ETB",
"EUR",
"FJD",
"FKP",
"GBP",
"GEL",
"GHS",
"GIP",
"GMD",
"GNF",
"GTQ",
"GYD",
"HKD",
"HNL",
"HRK",
"HTG",
"HUF",
"IDR",
"ILS",
"INR",
"IQD",
"IRR",
"ISK",
"JMD",
"JOD",
"JPY",
"KES",
"KGS",
"KHR",
"KMF",
"KPW",
"KRW",
"KWD",
"KYD",
"KZT",
"LAK",
"LBP",
"LKR",
"LRD",
"LSL",
"LTL",
"LYD",
"MAD",
"MDL",
"MGA",
"MKD",
"MMK",
"MNT",
"MOP",
"MRO",
"MUR",
"MVR",
"MWK",
"MXN",
"MYR",
"MZN",
"NAD",
"NGN",
"NIO",
"NOK",
"NPR",
"NZD",
"OMR",
"PAB",
"PEN",
"PGK",
"PHP",
"PKR",
"PLN",
"PYG",
"QAR",
"RON",
"RSD",
"RUB",
"RWF",
"SAR",
"SBD",
"SCR",
"SDG",
"SEK",
"SGD",
"SHP",
"SLL",
"SOS",
"SRD",
"SSP",
"STD",
"SYP",
"SZL",
"THB",
"TJS",
"TMT",
"TND",
"TOP",
"TRY",
"TTD",
"TWD",
"TZS",
"UAH",
"UGX",
"USD",
"UYU",
"UZS",
"VEF",
"VND",
"VUV",
"WST",
"XAF",
"XCD",
"XOF",
"XPF",
"YER",
"ZAR",
"ZMK",
"ZWL",
];
export const createCurrencyListEl = () => {
const list = document.createElement("datalist");
list.id = "currencies";
for (const currency of [
"AED",
"AFN",
"ALL",
"AMD",
"ANG",
"AOA",
"ARS",
"AUD",
"AWG",
"AZN",
"BAM",
"BBD",
"BDT",
"BGN",
"BHD",
"BIF",
"BMD",
"BND",
"BOB",
"BRL",
"BSD",
"BTN",
"BWP",
"BYN",
"BYR",
"BZD",
"CAD",
"CDF",
"CHF",
"CLP",
"CNY",
"COP",
"CRC",
"CUP",
"CVE",
"CZK",
"DJF",
"DKK",
"DOP",
"DZD",
"EGP",
"ERN",
"ETB",
"EUR",
"FJD",
"FKP",
"GBP",
"GEL",
"GHS",
"GIP",
"GMD",
"GNF",
"GTQ",
"GYD",
"HKD",
"HNL",
"HRK",
"HTG",
"HUF",
"IDR",
"ILS",
"INR",
"IQD",
"IRR",
"ISK",
"JMD",
"JOD",
"JPY",
"KES",
"KGS",
"KHR",
"KMF",
"KPW",
"KRW",
"KWD",
"KYD",
"KZT",
"LAK",
"LBP",
"LKR",
"LRD",
"LSL",
"LTL",
"LYD",
"MAD",
"MDL",
"MGA",
"MKD",
"MMK",
"MNT",
"MOP",
"MRO",
"MUR",
"MVR",
"MWK",
"MXN",
"MYR",
"MZN",
"NAD",
"NGN",
"NIO",
"NOK",
"NPR",
"NZD",
"OMR",
"PAB",
"PEN",
"PGK",
"PHP",
"PKR",
"PLN",
"PYG",
"QAR",
"RON",
"RSD",
"RUB",
"RWF",
"SAR",
"SBD",
"SCR",
"SDG",
"SEK",
"SGD",
"SHP",
"SLL",
"SOS",
"SRD",
"SSP",
"STD",
"SYP",
"SZL",
"THB",
"TJS",
"TMT",
"TND",
"TOP",
"TRY",
"TTD",
"TWD",
"TZS",
"UAH",
"UGX",
"USD",
"UYU",
"UZS",
"VEF",
"VND",
"VUV",
"WST",
"XAF",
"XCD",
"XOF",
"XPF",
"YER",
"ZAR",
"ZMK",
"ZWL",
]) {
for (const currency of currencies) {
const option = document.createElement("option");
option.value = currency;
option.innerHTML = currency;
+2 -2
View File
@@ -269,8 +269,8 @@ export class HaDataTable extends LitElement {
@change=${this._handleHeaderRowCheckboxClick}
.indeterminate=${this._checkedRows.length &&
this._checkedRows.length !== this._checkableRowsCount}
.checked=${this._checkedRows.length ===
this._checkableRowsCount}
.checked=${this._checkedRows.length &&
this._checkedRows.length === this._checkableRowsCount}
>
</ha-checkbox>
</div>
@@ -5,6 +5,7 @@ import { fireEvent } from "../../common/dom/fire_event";
import {
DeviceAutomation,
deviceAutomationsEqual,
sortDeviceAutomations,
} from "../../data/device_automation";
import { HomeAssistant } from "../../types";
import "../ha-select";
@@ -127,7 +128,9 @@ export abstract class HaDeviceAutomationPicker<
private async _updateDeviceInfo() {
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
[];
@@ -161,8 +164,9 @@ export abstract class HaDeviceAutomationPicker<
if (this.value && deviceAutomationsEqual(automation, this.value)) {
return;
}
fireEvent(this, "change");
fireEvent(this, "value-changed", { value: automation });
const value = { ...automation };
delete value.metadata;
fireEvent(this, "value-changed", { value });
}
static get styles(): CSSResultGroup {
+4 -3
View File
@@ -198,9 +198,10 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
this.hass,
deviceEntityLookup[device.id]
),
area: device.area_id
? areaLookup[device.area_id].name
: this.hass.localize("ui.components.device-picker.no_area"),
area:
device.area_id && areaLookup[device.area_id]
? areaLookup[device.area_id].name
: this.hass.localize("ui.components.device-picker.no_area"),
}));
if (!outputDevices.length) {
return [
@@ -10,8 +10,8 @@ interface State {
backgroundColor: null | string;
}
@customElement("hassio-ansi-to-html")
class HassioAnsiToHtml extends LitElement {
@customElement("ha-ansi-to-html")
class HaAnsiToHtml extends LitElement {
@property() public content!: string;
protected render(): TemplateResult | void {
@@ -241,6 +241,6 @@ class HassioAnsiToHtml extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"hassio-ansi-to-html": HassioAnsiToHtml;
"ha-ansi-to-html": HaAnsiToHtml;
}
}
+1 -1
View File
@@ -409,7 +409,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
name,
});
this._areas = [...this._areas!, area];
(this.comboBox as any).items = this._getAreas(
(this.comboBox as any).filteredItems = this._getAreas(
this._areas!,
this._devices!,
this._entities!,
+1
View File
@@ -310,6 +310,7 @@ export class HaBaseTimeInput extends LitElement {
border-radius: var(--mdc-shape-small, 4px) var(--mdc-shape-small, 4px) 0 0;
overflow: hidden;
position: relative;
direction: ltr;
}
ha-textfield {
width: 40px;
+15
View File
@@ -50,6 +50,21 @@ export class HaButtonMenu extends LitElement {
`;
}
protected firstUpdated(changedProps): void {
super.firstUpdated(changedProps);
if (document.dir === "rtl") {
this.updateComplete.then(() => {
this.querySelectorAll("mwc-list-item").forEach((item) => {
const style = document.createElement("style");
style.innerHTML =
"span.material-icons:first-of-type { margin-left: var(--mdc-list-item-graphic-margin, 32px) !important; margin-right: 0px !important;}";
item!.shadowRoot!.appendChild(style);
});
});
}
}
private _handleClick(): void {
if (this.disabled) {
return;
+19 -5
View File
@@ -12,6 +12,8 @@ export class HaClickableListItem extends ListItemBase {
// property used only in css
@property({ type: Boolean, reflect: true }) public rtl = false;
@property({ type: Boolean, reflect: true }) public openNewTab = false;
@query("a") private _anchor!: HTMLAnchorElement;
public render() {
@@ -20,7 +22,12 @@ export class HaClickableListItem extends ListItemBase {
return html`${this.disableHref
? 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() {
@@ -40,10 +47,6 @@ export class HaClickableListItem extends ListItemBase {
padding-left: 0px;
padding-right: 0px;
}
:host([rtl]) span {
margin-left: var(--mdc-list-item-graphic-margin, 20px) !important;
margin-right: 0px !important;
}
:host([graphic="avatar"]:not([twoLine])),
:host([graphic="icon"]:not([twoLine])) {
height: 48px;
@@ -55,6 +58,17 @@ export class HaClickableListItem extends ListItemBase {
align-items: center;
padding-left: var(--mdc-list-side-padding, 20px);
padding-right: var(--mdc-list-side-padding, 20px);
overflow: hidden;
}
:host-context([style*="direction: rtl;"])
span.material-icons:first-of-type {
margin-left: var(--mdc-list-item-graphic-margin, 16px) !important;
margin-right: 0px !important;
}
:host-context([style*="direction: rtl;"])
span.material-icons:last-of-type {
margin-left: 0px !important;
margin-right: auto !important;
}
`,
];
+10 -9
View File
@@ -1,24 +1,25 @@
import { Fab } from "@material/mwc-fab";
import { FabBase } from "@material/mwc-fab/mwc-fab-base";
import { styles } from "@material/mwc-fab/mwc-fab.css";
import { customElement } from "lit/decorators";
import { css } from "lit";
@customElement("ha-fab")
export class HaFab extends Fab {
export class HaFab extends FabBase {
protected firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)");
}
static override styles = Fab.styles.concat([
static override styles = [
styles,
css`
:host-context([style*="direction: rtl;"])
.mdc-fab--extended
.mdc-fab__icon {
margin-left: 12px !important;
margin-right: calc(12px - 20px) !important;
:host .mdc-fab--extended .mdc-fab__icon {
margin-inline-start: -8px;
margin-inline-end: 12px;
direction: var(--direction);
}
`,
]);
];
}
declare global {
+5
View File
@@ -132,6 +132,11 @@ export class HaFormString extends LitElement implements HaFormElement {
--mdc-icon-button-size: 24px;
color: var(--secondary-text-color);
}
:host-context([style*="direction: rtl;"]) ha-icon-button {
right: auto;
left: 12px;
}
`;
}
}
+79
View File
@@ -0,0 +1,79 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { roundWithOneDecimal } from "../util/calculate";
import "./ha-bar";
import "./ha-settings-row";
@customElement("ha-metric")
class HaMetric extends LitElement {
@property({ type: Number }) public value!: number;
@property({ type: String }) public heading!: string;
@property({ type: String }) public tooltip?: string;
protected render(): TemplateResult {
const roundedValue = roundWithOneDecimal(this.value);
return html`
<ha-settings-row>
<span slot="heading"> ${this.heading} </span>
<div slot="description" .title=${this.tooltip ?? ""}>
<span class="value"> ${roundedValue} % </span>
<ha-bar
class=${classMap({
"target-warning": roundedValue > 50,
"target-critical": roundedValue > 85,
})}
.value=${this.value}
></ha-bar>
</div>
</ha-settings-row>
`;
}
static get styles(): CSSResultGroup {
return css`
ha-settings-row {
padding: 0;
height: 54px;
width: 100%;
}
ha-settings-row > div[slot="description"] {
white-space: normal;
color: var(--secondary-text-color);
display: flex;
justify-content: space-between;
}
ha-bar {
--ha-bar-primary-color: var(
--metric-bar-ok-color,
var(--success-color)
);
}
.target-warning {
--ha-bar-primary-color: var(
--metric-bar-warning-color,
var(--warning-color)
);
}
.target-critical {
--ha-bar-primary-color: var(
--metric-bar-critical-color,
var(--error-color)
);
}
.value {
width: 48px;
padding-right: 4px;
flex-shrink: 0;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-metric": HaMetric;
}
}
+6 -8
View File
@@ -4,9 +4,9 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
import type { HomeAssistant } from "../types";
import "./ha-clickable-list-item";
import "./ha-icon-next";
import "./ha-svg-icon";
import "./ha-clickable-list-item";
@customElement("ha-navigation-list")
class HaNavigationList extends LitElement {
@@ -56,18 +56,15 @@ class HaNavigationList extends LitElement {
}
static styles: CSSResultGroup = css`
a {
text-decoration: none;
color: var(--primary-text-color);
position: relative;
display: block;
outline: 0;
:host {
--mdc-list-vertical-padding: 0;
}
ha-svg-icon,
ha-icon-next {
color: var(--secondary-text-color);
height: 24px;
width: 24px;
display: block;
}
ha-svg-icon {
padding: 8px;
@@ -78,9 +75,10 @@ class HaNavigationList extends LitElement {
.icon-background ha-svg-icon {
color: #fff;
}
mwc-list-item {
ha-clickable-list-item {
cursor: pointer;
font-size: var(--navigation-list-item-title-font-size);
padding: var(--navigation-list-item-padding) 0;
}
`;
}
+3
View File
@@ -163,6 +163,9 @@ export class HaNetwork extends LitElement {
ha-settings-row {
padding: 0;
--paper-time-input-justify-content: flex-end;
--settings-row-content-display: contents;
--settings-row-prefix-display: contents;
}
span[slot="heading"],
@@ -27,8 +27,8 @@ export class HaColorTempSelector extends LitElement {
pin
icon="hass:thermometer"
.caption=${this.label || ""}
.min=${this.selector.color_temp.min_mireds ?? 153}
.max=${this.selector.color_temp.max_mireds ?? 500}
.min=${this.selector.color_temp?.min_mireds ?? 153}
.max=${this.selector.color_temp?.max_mireds ?? 500}
.value=${this.value}
.disabled=${this.disabled}
.helper=${this.helper}
@@ -1,4 +1,4 @@
import { html, LitElement } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
@@ -76,6 +76,13 @@ export class HaLocationSelector extends LitElement {
const radius = ev.detail.radius;
fireEvent(this, "value-changed", { value: { ...this.value, radius } });
}
static styles = css`
:host {
display: block;
height: 400px;
}
`;
}
declare global {
@@ -107,6 +107,7 @@ export class HaNumberSelector extends LitElement {
display: flex;
justify-content: space-between;
align-items: center;
direction: ltr;
}
ha-slider {
flex: 1;
+1
View File
@@ -472,6 +472,7 @@ export class HaServiceControl extends LitElement {
ha-settings-row {
--paper-time-input-justify-content: flex-end;
--settings-row-content-width: 100%;
--settings-row-prefix-display: contents;
border-top: var(
--service-control-items-border-top,
1px solid var(--divider-color)
+2 -2
View File
@@ -47,7 +47,7 @@ export class HaSettingsRow extends LitElement {
display: contents;
}
:host(:not([narrow])) .content {
display: flex;
display: var(--settings-row-content-display, flex);
justify-content: flex-end;
flex: 1;
padding: 16px 0;
@@ -68,7 +68,7 @@ export class HaSettingsRow extends LitElement {
white-space: normal;
}
.prefix-wrap {
display: contents;
display: var(--settings-row-prefix-display);
}
:host([narrow]) .prefix-wrap {
display: flex;
-3
View File
@@ -1051,9 +1051,6 @@ class HaSidebar extends LitElement {
padding: 0px 6px;
color: var(--text-accent-color, var(--text-primary-color));
}
.configuration-badge {
background-color: var(--primary-color);
}
ha-svg-icon + .notification-badge,
ha-svg-icon + .configuration-badge {
position: absolute;
@@ -302,6 +302,10 @@ class DialogMediaManage extends LitElement {
--mdc-theme-primary: var(--mdc-theme-on-primary);
}
mwc-list {
direction: ltr;
}
.danger {
--mdc-theme-primary: var(--error-color);
}
@@ -310,6 +314,11 @@ class DialogMediaManage extends LitElement {
vertical-align: middle;
}
:host-context([style*="direction: rtl;"]) ha-svg-icon[slot="icon"] {
margin-left: 8px !important;
margin-right: 0px !important;
}
.refresh {
display: flex;
height: 200px;
@@ -151,6 +151,8 @@ class DialogMediaPlayerBrowse extends LitElement {
ha-media-player-browse {
--media-browser-max-height: calc(100vh - 65px);
height: calc(100vh - 65px);
direction: ltr;
}
@media (min-width: 800px) {
@@ -163,6 +165,7 @@ class DialogMediaPlayerBrowse extends LitElement {
ha-media-player-browse {
position: initial;
--media-browser-max-height: 100vh - 137px;
height: 100vh - 137px;
width: 700px;
}
}
@@ -59,6 +59,11 @@ class MediaManageButton extends LitElement {
ha-circular-progress[slot="icon"] {
vertical-align: middle;
}
:host-context([style*="direction: rtl;"]) ha-svg-icon[slot="icon"] {
margin-left: 8px;
margin-right: 0px;
}
`;
}
@@ -1,3 +1,6 @@
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-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
@@ -16,12 +19,11 @@ import {
eventOptions,
property,
query,
queryAll,
state,
} from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import { until } from "lit/directives/until";
import { fireEvent } from "../../common/dom/fire_event";
import { computeRTLDirection } from "../../common/util/compute_rtl";
import { debounce } from "../../common/util/debounce";
@@ -45,7 +47,6 @@ import { documentationUrl } from "../../util/documentation-url";
import "../entity/ha-entity-picker";
import "../ha-button-menu";
import "../ha-card";
import type { HaCard } from "../ha-card";
import "../ha-circular-progress";
import "../ha-fab";
import "../ha-icon-button";
@@ -101,7 +102,9 @@ export class HaMediaPlayerBrowse extends LitElement {
@query(".content") private _content?: HTMLDivElement;
@queryAll(".lazythumbnail") private _thumbnails?: HaCard[];
@query("lit-virtualizer") private _virtualizer?: LitVirtualizer;
private _observed = false;
private _headerOffsetHeight = 0;
@@ -148,326 +151,6 @@ export class HaMediaPlayerBrowse extends LitElement {
}
}
protected render(): TemplateResult {
if (this._error) {
return html`
<div class="container">${this._renderError(this._error)}</div>
`;
}
if (!this._currentItem) {
return html`<ha-circular-progress active></ha-circular-progress>`;
}
const currentItem = this._currentItem;
const subtitle = this.hass.localize(
`ui.components.media-browser.class.${currentItem.media_class}`
);
const children = currentItem.children || [];
const mediaClass = MediaClassBrowserSettings[currentItem.media_class];
const childrenMediaClass = currentItem.children_media_class
? MediaClassBrowserSettings[currentItem.children_media_class]
: MediaClassBrowserSettings.directory;
return html`
${
currentItem.can_play
? html` <div
class="header ${classMap({
"no-img": !currentItem.thumbnail,
"no-dialog": !this.dialog,
})}"
@transitionend=${this._setHeaderHeight}
>
<div class="header-content">
${currentItem.thumbnail
? html`
<div
class="img"
style=${styleMap({
backgroundImage: currentItem.thumbnail
? `url(${currentItem.thumbnail})`
: "none",
})}
>
${this._narrow && currentItem?.can_play
? html`
<ha-fab
mini
.item=${currentItem}
@click=${this._actionClicked}
>
<ha-svg-icon
slot="icon"
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play"
? mdiPlay
: mdiPlus}
></ha-svg-icon>
${this.hass.localize(
`ui.components.media-browser.${this.action}`
)}
</ha-fab>
`
: ""}
</div>
`
: html``}
<div class="header-info">
<div class="breadcrumb">
<h1 class="title">${currentItem.title}</h1>
${subtitle
? html` <h2 class="subtitle">${subtitle}</h2> `
: ""}
</div>
${currentItem.can_play &&
(!currentItem.thumbnail || !this._narrow)
? html`
<mwc-button
raised
.item=${currentItem}
@click=${this._actionClicked}
>
<ha-svg-icon
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play"
? mdiPlay
: mdiPlus}
></ha-svg-icon>
${this.hass.localize(
`ui.components.media-browser.${this.action}`
)}
</mwc-button>
`
: ""}
</div>
</div>
</div>`
: ""
}
<div
class="content"
@scroll=${this._scroll}
@touchmove=${this._scroll}
>
${
this._error
? html`
<div class="container">
${this._renderError(this._error)}
</div>
`
: isTTSMediaSource(currentItem.media_content_id)
? html`
<ha-browse-media-tts
.item=${currentItem}
.hass=${this.hass}
.action=${this.action}
@tts-picked=${this._ttsPicked}
></ha-browse-media-tts>
`
: !children.length && !currentItem.not_shown
? html`
<div class="container no-items">
${currentItem.media_content_id ===
"media-source://media_source/local/."
? html`
<div class="highlight-add-button">
<span>
<ha-svg-icon
.path=${mdiArrowUpRight}
></ha-svg-icon>
</span>
<span>
${this.hass.localize(
"ui.components.media-browser.file_management.highlight_button"
)}
</span>
</div>
`
: this.hass.localize(
"ui.components.media-browser.no_items"
)}
</div>
`
: childrenMediaClass.layout === "grid"
? html`
<div
class="children ${classMap({
portrait:
childrenMediaClass.thumbnail_ratio === "portrait",
})}"
>
${children.map(
(child) => html`
<div
class="child"
.item=${child}
@click=${this._childClicked}
>
<ha-card outlined>
<div class="thumbnail">
${child.thumbnail
? html`
<div
class="${["app", "directory"].includes(
child.media_class
)
? "centered-image"
: ""} image lazythumbnail"
data-src=${child.thumbnail}
></div>
`
: html`
<div class="icon-holder image">
<ha-svg-icon
class="folder"
.path=${MediaClassBrowserSettings[
child.media_class === "directory"
? child.children_media_class ||
child.media_class
: child.media_class
].icon}
></ha-svg-icon>
</div>
`}
${child.can_play
? html`
<ha-icon-button
class="play ${classMap({
can_expand: child.can_expand,
})}"
.item=${child}
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play"
? mdiPlay
: mdiPlus}
@click=${this._actionClicked}
></ha-icon-button>
`
: ""}
</div>
<div class="title">
${child.title}
<paper-tooltip
fitToVisibleBounds
position="top"
offset="4"
>${child.title}</paper-tooltip
>
</div>
</ha-card>
</div>
`
)}
${currentItem.not_shown
? html`
<div class="grid not-shown">
<div class="title">
${this.hass.localize(
"ui.components.media-browser.not_shown",
{ count: currentItem.not_shown }
)}
</div>
</div>
`
: ""}
</div>
`
: html`
<mwc-list>
${children.map(
(child) => html`
<mwc-list-item
@click=${this._childClicked}
.item=${child}
.graphic=${mediaClass.show_list_images
? "medium"
: "avatar"}
dir=${computeRTLDirection(this.hass)}
>
<div
class=${classMap({
graphic: true,
lazythumbnail:
mediaClass.show_list_images === true,
})}
data-src=${ifDefined(
mediaClass.show_list_images && child.thumbnail
? child.thumbnail
: undefined
)}
slot="graphic"
>
<ha-icon-button
class="play ${classMap({
show:
!mediaClass.show_list_images ||
!child.thumbnail,
})}"
.item=${child}
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play"
? mdiPlay
: mdiPlus}
@click=${this._actionClicked}
></ha-icon-button>
</div>
<span class="title">${child.title}</span>
</mwc-list-item>
<li divider role="separator"></li>
`
)}
${currentItem.not_shown
? html`
<mwc-list-item
noninteractive
class="not-shown"
.graphic=${mediaClass.show_list_images
? "medium"
: "avatar"}
dir=${computeRTLDirection(this.hass)}
>
<span class="title">
${this.hass.localize(
"ui.components.media-browser.not_shown",
{ count: currentItem.not_shown }
)}
</span>
</mwc-list-item>
`
: ""}
</mwc-list>
`
}
</div>
</div>
</div>
`;
}
protected firstUpdated(): void {
this._measureCard();
this._attachResizeObserver();
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
if (changedProps.size > 1 || !changedProps.has("hass")) {
return true;
}
const oldHass = changedProps.get("hass") as this["hass"];
return oldHass === undefined || oldHass.localize !== this.hass.localize;
}
public willUpdate(changedProps: PropertyValues<this>): void {
super.willUpdate(changedProps);
@@ -583,6 +266,19 @@ export class HaMediaPlayerBrowse extends LitElement {
}
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
if (changedProps.size > 1 || !changedProps.has("hass")) {
return true;
}
const oldHass = changedProps.get("hass") as this["hass"];
return oldHass === undefined || oldHass.localize !== this.hass.localize;
}
protected firstUpdated(): void {
this._measureCard();
this._attachResizeObserver();
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
@@ -590,16 +286,383 @@ export class HaMediaPlayerBrowse extends LitElement {
this._animateHeaderHeight();
} else if (changedProps.has("_currentItem")) {
this._setHeaderHeight();
this._attachIntersectionObserver();
// 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);
}
}
}
private _actionClicked(ev: MouseEvent): void {
protected render(): TemplateResult {
if (this._error) {
return html`
<div class="container">${this._renderError(this._error)}</div>
`;
}
if (!this._currentItem) {
return html`<ha-circular-progress active></ha-circular-progress>`;
}
const currentItem = this._currentItem;
const subtitle = this.hass.localize(
`ui.components.media-browser.class.${currentItem.media_class}`
);
const children = currentItem.children || [];
const mediaClass = MediaClassBrowserSettings[currentItem.media_class];
const childrenMediaClass = currentItem.children_media_class
? MediaClassBrowserSettings[currentItem.children_media_class]
: MediaClassBrowserSettings.directory;
const backgroundImage = currentItem.thumbnail
? this._getSignedThumbnail(currentItem.thumbnail).then(
(value) => `url(${value})`
)
: "none";
return html`
${
currentItem.can_play
? html`
<div
class="header ${classMap({
"no-img": !currentItem.thumbnail,
"no-dialog": !this.dialog,
})}"
@transitionend=${this._setHeaderHeight}
>
<div class="header-content">
${currentItem.thumbnail
? html`
<div
class="img"
style="background-image: ${until(
backgroundImage,
""
)}"
>
${this._narrow && currentItem?.can_play
? html`
<ha-fab
mini
.item=${currentItem}
@click=${this._actionClicked}
>
<ha-svg-icon
slot="icon"
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play"
? mdiPlay
: mdiPlus}
></ha-svg-icon>
${this.hass.localize(
`ui.components.media-browser.${this.action}`
)}
</ha-fab>
`
: ""}
</div>
`
: html``}
<div class="header-info">
<div class="breadcrumb">
<h1 class="title">${currentItem.title}</h1>
${subtitle
? html` <h2 class="subtitle">${subtitle}</h2> `
: ""}
</div>
${currentItem.can_play &&
(!currentItem.thumbnail || !this._narrow)
? html`
<mwc-button
raised
.item=${currentItem}
@click=${this._actionClicked}
>
<ha-svg-icon
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play"
? mdiPlay
: mdiPlus}
></ha-svg-icon>
${this.hass.localize(
`ui.components.media-browser.${this.action}`
)}
</mwc-button>
`
: ""}
</div>
</div>
</div>
`
: ""
}
<div
class="content"
@scroll=${this._scroll}
@touchmove=${this._scroll}
>
${
this._error
? html`
<div class="container">
${this._renderError(this._error)}
</div>
`
: isTTSMediaSource(currentItem.media_content_id)
? html`
<ha-browse-media-tts
.item=${currentItem}
.hass=${this.hass}
.action=${this.action}
@tts-picked=${this._ttsPicked}
></ha-browse-media-tts>
`
: !children.length && !currentItem.not_shown
? html`
<div class="container no-items">
${currentItem.media_content_id ===
"media-source://media_source/local/."
? html`
<div class="highlight-add-button">
<span>
<ha-svg-icon
.path=${mdiArrowUpRight}
></ha-svg-icon>
</span>
<span>
${this.hass.localize(
"ui.components.media-browser.file_management.highlight_button"
)}
</span>
</div>
`
: this.hass.localize(
"ui.components.media-browser.no_items"
)}
</div>
`
: childrenMediaClass.layout === "grid"
? html`
<lit-virtualizer
scroller
.layout=${grid({
itemSize: {
width: "175px",
height: "225px",
},
gap: "16px",
flex: { preserve: "aspect-ratio" },
justify: "space-evenly",
direction: "vertical",
})}
.items=${children}
.renderItem=${this._renderGridItem}
class="children ${classMap({
portrait:
childrenMediaClass.thumbnail_ratio === "portrait",
not_shown: !!currentItem.not_shown,
})}"
></lit-virtualizer>
${currentItem.not_shown
? html`
<div class="grid not-shown">
<div class="title">
${this.hass.localize(
"ui.components.media-browser.not_shown",
{ count: currentItem.not_shown }
)}
</div>
</div>
`
: ""}
`
: html`
<mwc-list>
<lit-virtualizer
scroller
.items=${children}
style=${styleMap({
height: `${children.length * 72 + 26}px`,
})}
.renderItem=${this._renderListItem}
></lit-virtualizer>
${currentItem.not_shown
? html`
<mwc-list-item
noninteractive
class="not-shown"
.graphic=${mediaClass.show_list_images
? "medium"
: "avatar"}
dir=${computeRTLDirection(this.hass)}
>
<span class="title">
${this.hass.localize(
"ui.components.media-browser.not_shown",
{ count: currentItem.not_shown }
)}
</span>
</mwc-list-item>
`
: ""}
</mwc-list>
`
}
</div>
</div>
</div>
`;
}
private _renderGridItem = (child: MediaPlayerItem): TemplateResult => {
const backgroundImage = child.thumbnail
? this._getSignedThumbnail(child.thumbnail).then(
(value) => `url(${value})`
)
: "none";
return html`
<div class="child" .item=${child} @click=${this._childClicked}>
<ha-card outlined>
<div class="thumbnail">
${child.thumbnail
? html`
<div
class="${["app", "directory"].includes(child.media_class)
? "centered-image"
: ""} image"
style="background-image: ${until(backgroundImage, "")}"
></div>
`
: html`
<div class="icon-holder image">
<ha-svg-icon
class="folder"
.path=${MediaClassBrowserSettings[
child.media_class === "directory"
? child.children_media_class || child.media_class
: child.media_class
].icon}
></ha-svg-icon>
</div>
`}
${child.can_play
? html`
<ha-icon-button
class="play ${classMap({
can_expand: child.can_expand,
})}"
.item=${child}
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play" ? mdiPlay : mdiPlus}
@click=${this._actionClicked}
></ha-icon-button>
`
: ""}
</div>
<div class="title">
${child.title}
<paper-tooltip fitToVisibleBounds position="top" offset="4"
>${child.title}</paper-tooltip
>
</div>
</ha-card>
</div>
`;
};
private _renderListItem = (child: MediaPlayerItem): TemplateResult => {
const currentItem = this._currentItem;
const mediaClass = MediaClassBrowserSettings[currentItem!.media_class];
const backgroundImage =
mediaClass.show_list_images && child.thumbnail
? this._getSignedThumbnail(child.thumbnail).then(
(value) => `url(${value})`
)
: "none";
return html`
<mwc-list-item
@click=${this._childClicked}
.item=${child}
.graphic=${mediaClass.show_list_images ? "medium" : "avatar"}
dir=${computeRTLDirection(this.hass)}
>
<div
class=${classMap({
graphic: true,
thumbnail: mediaClass.show_list_images === true,
})}
style="background-image: ${until(backgroundImage, "")}"
slot="graphic"
>
<ha-icon-button
class="play ${classMap({
show: !mediaClass.show_list_images || !child.thumbnail,
})}"
.item=${child}
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play" ? mdiPlay : mdiPlus}
@click=${this._actionClicked}
></ha-icon-button>
</div>
<span class="title">${child.title}</span>
</mwc-list-item>
`;
};
private async _getSignedThumbnail(
thumbnailUrl: string | undefined
): Promise<string> {
if (!thumbnailUrl) {
return "";
}
if (thumbnailUrl.startsWith("/")) {
// Thumbnails served by local API require authentication
return (await getSignedPath(this.hass, thumbnailUrl)).path;
}
if (thumbnailUrl.startsWith("https://brands.home-assistant.io")) {
// The backend is not aware of the theme used by the users,
// so we rewrite the URL to show a proper icon
thumbnailUrl = brandsUrl({
domain: extractDomainFromBrandUrl(thumbnailUrl),
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
});
}
return thumbnailUrl;
}
private _actionClicked = (ev: MouseEvent): void => {
ev.stopPropagation();
const item = (ev.currentTarget as any).item;
this._runAction(item);
}
};
private _runAction(item: MediaPlayerItem): void {
fireEvent(this, "media-picked", { item, navigateIds: this.navigateIds });
@@ -615,7 +678,7 @@ export class HaMediaPlayerBrowse extends LitElement {
});
}
private async _childClicked(ev: MouseEvent): Promise<void> {
private _childClicked = async (ev: MouseEvent): Promise<void> => {
const target = ev.currentTarget as any;
const item: MediaPlayerItem = target.item;
@@ -631,7 +694,7 @@ export class HaMediaPlayerBrowse extends LitElement {
fireEvent(this, "media-browsed", {
ids: [...this.navigateIds, item],
});
}
};
private async _fetchData(
entityId: string,
@@ -658,55 +721,6 @@ export class HaMediaPlayerBrowse extends LitElement {
this._resizeObserver.observe(this);
}
/**
* Load thumbnails for images on demand as they become visible.
*/
private async _attachIntersectionObserver(): Promise<void> {
if (!("IntersectionObserver" in window) || !this._thumbnails) {
return;
}
if (!this._intersectionObserver) {
this._intersectionObserver = new IntersectionObserver(
async (entries, observer) => {
await Promise.all(
entries.map(async (entry) => {
if (!entry.isIntersecting) {
return;
}
const thumbnailCard = entry.target as HTMLElement;
let thumbnailUrl = thumbnailCard.dataset.src;
if (!thumbnailUrl) {
return;
}
if (thumbnailUrl.startsWith("/")) {
// Thumbnails served by local API require authentication
const signedPath = await getSignedPath(this.hass, thumbnailUrl);
thumbnailUrl = signedPath.path;
} else if (
thumbnailUrl.startsWith("https://brands.home-assistant.io")
) {
// The backend is not aware of the theme used by the users,
// so we rewrite the URL to show a proper icon
thumbnailUrl = brandsUrl({
domain: extractDomainFromBrandUrl(thumbnailUrl),
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
});
}
thumbnailCard.style.backgroundImage = `url(${thumbnailUrl})`;
observer.unobserve(thumbnailCard); // loaded, so no need to observe anymore
})
);
}
);
}
const observer = this._intersectionObserver!;
for (const thumbnailCard of this._thumbnails) {
observer.observe(thumbnailCard);
}
}
private _closeDialogAction(): void {
fireEvent(this, "close-dialog");
}
@@ -841,6 +855,7 @@ export class HaMediaPlayerBrowse extends LitElement {
.content {
overflow-y: auto;
box-sizing: border-box;
height: 100%;
}
/* HEADER */
@@ -926,6 +941,7 @@ export class HaMediaPlayerBrowse extends LitElement {
.not-shown {
font-style: italic;
color: var(--secondary-text-color);
padding: 8px 16px 8px;
}
.grid.not-shown {
@@ -951,7 +967,11 @@ export class HaMediaPlayerBrowse extends LitElement {
border-bottom-color: var(--divider-color);
}
.children {
mwc-list-item {
width: 100%;
}
div.children {
display: grid;
grid-template-columns: repeat(
auto-fit,
@@ -988,7 +1008,7 @@ export class HaMediaPlayerBrowse extends LitElement {
padding-bottom: 100%;
}
.portrait.children ha-card .thumbnail {
.portrait ha-card .thumbnail {
padding-bottom: 150%;
}
@@ -1062,10 +1082,6 @@ export class HaMediaPlayerBrowse extends LitElement {
color: var(--primary-color);
}
ha-card:hover .lazythumbnail {
opacity: 0.5;
}
.child .title {
font-size: 16px;
padding-top: 16px;
@@ -1127,7 +1143,7 @@ export class HaMediaPlayerBrowse extends LitElement {
padding: 0 24px;
}
:host([narrow]) .children {
:host([narrow]) div.children {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) !important;
}
@@ -1232,6 +1248,16 @@ export class HaMediaPlayerBrowse extends LitElement {
--mdc-fab-box-shadow: none;
--mdc-theme-secondary: rgba(var(--rgb-primary-color), 0.5);
}
lit-virtualizer {
height: 100%;
overflow: overlay !important;
contain: size layout !important;
}
lit-virtualizer.not_shown {
height: calc(100% - 36px);
}
`,
];
}
@@ -119,6 +119,11 @@ class MediaUploadButton extends LitElement {
ha-circular-progress[slot="icon"] {
vertical-align: middle;
}
:host-context([style*="direction: rtl;"]) ha-svg-icon[slot="icon"] {
margin-left: 8px;
margin-right: 0px;
}
`;
}
+5
View File
@@ -10,6 +10,8 @@ export class HaTimeline extends LitElement {
@property({ type: Boolean, reflect: true }) public raised = false;
@property({ reflect: true, type: Boolean }) notEnabled = false;
@property({ type: Boolean }) public lastItem = false;
@property({ type: String }) public icon?: string;
@@ -76,6 +78,9 @@ export class HaTimeline extends LitElement {
margin-right: 8px;
width: 24px;
}
:host([notEnabled]) ha-svg-icon {
opacity: 0.5;
}
ha-svg-icon {
color: var(
--timeline-ball-color,
@@ -114,6 +114,11 @@ export class HaTracePathDetails extends LitElement {
const { path, timestamp, result, error, changed_variables, ...rest } =
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`
${curPath === this.selected.path
? ""
+10 -2
View File
@@ -19,6 +19,8 @@ export class HatGraphNode extends LitElement {
@property({ reflect: true, type: Boolean }) disabled?: boolean;
@property({ reflect: true, type: Boolean }) notEnabled = false;
@property({ reflect: true, type: Boolean }) graphStart?: boolean;
@property({ type: Boolean, attribute: "nofocus" }) noFocus = false;
@@ -114,8 +116,14 @@ export class HatGraphNode extends LitElement {
--stroke-clr: var(--hover-clr);
--icon-clr: var(--default-icon-clr);
}
:host([disabled]) circle {
stroke: var(--disabled-clr);
:host([notEnabled]) circle {
--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 {
width: 100%;
+234 -16
View File
@@ -1,7 +1,11 @@
import {
mdiAbTesting,
mdiAlertOctagon,
mdiArrowDecision,
mdiArrowUp,
mdiAsterisk,
mdiCallMissed,
mdiCallReceived,
mdiCallSplit,
mdiCheckboxBlankOutline,
mdiCheckboxMarkedOutline,
@@ -9,10 +13,12 @@ import {
mdiChevronRight,
mdiChevronUp,
mdiClose,
mdiCloseOctagon,
mdiCodeBrackets,
mdiDevices,
mdiExclamation,
mdiRefresh,
mdiShuffleDisabled,
mdiTimerOutline,
mdiTrafficLight,
} from "@mdi/js";
@@ -27,6 +33,9 @@ import {
DelayAction,
DeviceAction,
EventAction,
IfAction,
ManualScriptConfig,
ParallelAction,
RepeatAction,
SceneAction,
ServiceAction,
@@ -36,6 +45,8 @@ import {
import {
ChooseActionTraceStep,
ConditionTraceStep,
IfActionTraceStep,
StopActionTraceStep,
TraceExtended,
} from "../../data/trace";
import "../ha-icon-button";
@@ -85,6 +96,7 @@ export class HatScriptGraph extends LitElement {
@focus=${this.selectNode(config, path)}
?active=${this.selected === path}
.iconPath=${mdiAsterisk}
.notEnabled=${config.enabled === false}
tabindex=${track ? "0" : "-1"}
></hat-graph-node>
`;
@@ -101,6 +113,9 @@ export class HatScriptGraph extends LitElement {
private typeRenderers = {
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,
event: this.render_event_node,
scene: this.render_scene_node,
@@ -110,23 +125,37 @@ export class HatScriptGraph extends LitElement {
repeat: this.render_repeat_node,
choose: this.render_choose_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,
};
private render_action_node(node: Action, path: string, graphStart = false) {
private render_action_node(
node: Action,
path: string,
graphStart = false,
disabled = false
) {
const type =
Object.keys(this.typeRenderers).find((key) => key in node) || "other";
this.renderedNodes[path] = { config: node, path };
if (this.trace && path in this.trace.trace) {
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(
config: ChooseAction,
path: string,
graphStart = false
graphStart = false,
disabled = false
) {
const trace = this.trace.trace[path] as ChooseActionTraceStep[] | undefined;
const trace_path = trace
@@ -143,12 +172,14 @@ export class HatScriptGraph extends LitElement {
@focus=${this.selectNode(config, path)}
?track=${trace !== undefined}
?active=${this.selected === path}
.notEnabled=${disabled || config.enabled === false}
>
<hat-graph-node
.graphStart=${graphStart}
.iconPath=${mdiCallSplit}
.iconPath=${mdiArrowDecision}
?track=${trace !== undefined}
?active=${this.selected === path}
.notEnabled=${disabled || config.enabled === false}
slot="head"
nofocus
></hat-graph-node>
@@ -171,12 +202,15 @@ export class HatScriptGraph extends LitElement {
@focus=${this.selectNode(config, branch_path)}
?track=${track_this}
?active=${this.selected === branch_path}
.notEnabled=${disabled || config.enabled === false}
></hat-graph-node>
${branch.sequence !== null
? ensureArray(branch.sequence).map((action, j) =>
this.render_action_node(
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>
${config.default !== null
? 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>
@@ -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(
node: Condition,
path: string,
graphStart = false
graphStart = false,
disabled = false
) {
const trace = this.trace.trace[path] as ConditionTraceStep[] | undefined;
let track = false;
@@ -225,6 +342,7 @@ export class HatScriptGraph extends LitElement {
@focus=${this.selectNode(node, path)}
?track=${track}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
tabindex=${trace === undefined ? "-1" : "0"}
short
>
@@ -233,6 +351,7 @@ export class HatScriptGraph extends LitElement {
slot="head"
?track=${track}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
.iconPath=${mdiAbTesting}
nofocus
></hat-graph-node>
@@ -247,6 +366,7 @@ export class HatScriptGraph extends LitElement {
nofocus
?track=${trackFailed}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
></hat-graph-node>
</hat-graph-branch>
`;
@@ -255,7 +375,8 @@ export class HatScriptGraph extends LitElement {
private render_delay_node(
node: DelayAction,
path: string,
graphStart = false
graphStart = false,
disabled = false
) {
return html`
<hat-graph-node
@@ -264,6 +385,7 @@ export class HatScriptGraph extends LitElement {
@focus=${this.selectNode(node, path)}
?track=${path in this.trace.trace}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node>
`;
@@ -272,7 +394,8 @@ export class HatScriptGraph extends LitElement {
private render_device_node(
node: DeviceAction,
path: string,
graphStart = false
graphStart = false,
disabled = false
) {
return html`
<hat-graph-node
@@ -281,6 +404,7 @@ export class HatScriptGraph extends LitElement {
@focus=${this.selectNode(node, path)}
?track=${path in this.trace.trace}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node>
`;
@@ -289,7 +413,8 @@ export class HatScriptGraph extends LitElement {
private render_event_node(
node: EventAction,
path: string,
graphStart = false
graphStart = false,
disabled = false
) {
return html`
<hat-graph-node
@@ -298,6 +423,7 @@ export class HatScriptGraph extends LitElement {
@focus=${this.selectNode(node, path)}
?track=${path in this.trace.trace}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node>
`;
@@ -306,7 +432,8 @@ export class HatScriptGraph extends LitElement {
private render_repeat_node(
node: RepeatAction,
path: string,
graphStart = false
graphStart = false,
disabled = false
) {
const trace: any = this.trace.trace[path];
const repeats = this.trace?.trace[`${path}/repeat/sequence/0`]?.length;
@@ -316,12 +443,14 @@ export class HatScriptGraph extends LitElement {
@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=${mdiRefresh}
?track=${path in this.trace.trace}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
slot="head"
nofocus
></hat-graph-node>
@@ -329,12 +458,18 @@ export class HatScriptGraph extends LitElement {
.iconPath=${mdiArrowUp}
?track=${repeats > 1}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
nofocus
.badge=${repeats > 1 ? repeats : undefined}
></hat-graph-node>
<div ?track=${trace}>
${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>
</hat-graph-branch>
@@ -344,7 +479,8 @@ export class HatScriptGraph extends LitElement {
private render_scene_node(
node: SceneAction,
path: string,
graphStart = false
graphStart = false,
disabled = false
) {
return html`
<hat-graph-node
@@ -353,6 +489,7 @@ export class HatScriptGraph extends LitElement {
@focus=${this.selectNode(node, path)}
?track=${path in this.trace.trace}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node>
`;
@@ -361,7 +498,8 @@ export class HatScriptGraph extends LitElement {
private render_service_node(
node: ServiceAction,
path: string,
graphStart = false
graphStart = false,
disabled = false
) {
return html`
<hat-graph-node
@@ -370,6 +508,7 @@ export class HatScriptGraph extends LitElement {
@focus=${this.selectNode(node, path)}
?track=${path in this.trace.trace}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node>
`;
@@ -378,7 +517,8 @@ export class HatScriptGraph extends LitElement {
private render_wait_node(
node: WaitAction | WaitForTriggerAction,
path: string,
graphStart = false
graphStart = false,
disabled = false
) {
return html`
<hat-graph-node
@@ -387,12 +527,87 @@ export class HatScriptGraph extends LitElement {
@focus=${this.selectNode(node, path)}
?track=${path in this.trace.trace}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></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`
<hat-graph-node
.graphStart=${graphStart}
@@ -400,6 +615,7 @@ export class HatScriptGraph extends LitElement {
@focus=${this.selectNode(node, path)}
?track=${path in this.trace.trace}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
></hat-graph-node>
`;
}
@@ -538,6 +754,8 @@ export class HatScriptGraph extends LitElement {
--track-clr: var(--track-color, var(--accent-color));
--hover-clr: var(--hover-color, var(--primary-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;
--rgb-trigger-color: var(--trigger-color, var(--default-trigger-color));
--background-clr: var(--background-color, white);
+184 -14
View File
@@ -25,12 +25,17 @@ import {
ChooseAction,
ChooseActionChoice,
getActionType,
IfAction,
ParallelAction,
RepeatAction,
} from "../../data/script";
import { describeAction } from "../../data/script_i18n";
import {
ActionTraceStep,
AutomationTraceExtended,
ChooseActionTraceStep,
getDataFromPath,
IfActionTraceStep,
isTriggerPath,
TriggerTraceStep,
} from "../../data/trace";
@@ -105,7 +110,7 @@ class LogbookRenderer {
}
get hasNext() {
return this.curIndex !== this.logbookEntries.length;
return this.curIndex < this.logbookEntries.length;
}
maybeRenderItem() {
@@ -201,7 +206,7 @@ class ActionRenderer {
}
get hasNext() {
return this.curIndex !== this.keys.length;
return this.curIndex < this.keys.length;
}
renderItem() {
@@ -214,15 +219,31 @@ class ActionRenderer {
private _renderItem(
index: number,
actionType?: ReturnType<typeof getActionType>
actionType?: ReturnType<typeof getActionType>,
renderAllIterations?: boolean
): number {
const value = this._getItem(index);
if (isTriggerPath(value[0].path)) {
return this._handleTrigger(index, value[0] as TriggerTraceStep);
if (renderAllIterations) {
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.
while (
@@ -235,7 +256,7 @@ class ActionRenderer {
this.logbookRenderer.flush();
this.timeTracker.maybeRenderTime(timestamp);
const path = value[0].path;
const path = value.path;
let data;
try {
data = getDataFromPath(this.trace.config, path);
@@ -263,7 +284,24 @@ class ActionRenderer {
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;
@@ -316,10 +354,16 @@ class ActionRenderer {
const chooseConfig = this._getDataFromPath(
this.keys[index]
) as ChooseAction;
const disabled = chooseConfig.enabled === false;
const name = chooseConfig.alias || "Choose";
if (defaultExecuted) {
this._renderEntry(choosePath, `${name}: Default action executed`);
this._renderEntry(
choosePath,
`${name}: Default action executed`,
undefined,
disabled
);
} else if (chooseTrace.result) {
const choiceNumeric =
chooseTrace.result.choice !== "default"
@@ -331,9 +375,19 @@ class ActionRenderer {
const choiceName = choiceConfig
? `${choiceConfig.alias || `Option ${choiceNumeric}`} executed`
: `Error: ${chooseTrace.error}`;
this._renderEntry(choosePath, `${name}: ${choiceName}`);
this._renderEntry(
choosePath,
`${name}: ${choiceName}`,
undefined,
disabled
);
} else {
this._renderEntry(choosePath, `${name}: No action taken`);
this._renderEntry(
choosePath,
`${name}: No action taken`,
undefined,
disabled
);
}
let i;
@@ -374,14 +428,130 @@ class ActionRenderer {
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(
path: string,
description: string,
icon = mdiRecordCircleOutline
icon = mdiRecordCircleOutline,
disabled = false
) {
this.entries.push(html`
<ha-timeline .icon=${icon} data-path=${path}>
${description}
<ha-timeline .icon=${icon} data-path=${path} .notEnabled=${disabled}>
${description}${disabled
? html`<span class="disabled"> (disabled)</span>`
: ""}
</ha-timeline>
`);
}
+44
View File
@@ -0,0 +1,44 @@
import { HomeAssistant } from "../types";
export interface ApplicationCredentialsConfig {
domains: string[];
}
export interface ApplicationCredential {
id: string;
domain: string;
client_id: string;
client_secret: string;
}
export const fetchApplicationCredentialsConfig = async (hass: HomeAssistant) =>
hass.callWS<ApplicationCredentialsConfig>({
type: "application_credentials/config",
});
export const fetchApplicationCredentials = async (hass: HomeAssistant) =>
hass.callWS<ApplicationCredential[]>({
type: "application_credentials/list",
});
export const createApplicationCredential = async (
hass: HomeAssistant,
domain: string,
clientId: string,
clientSecret: string
) =>
hass.callWS<ApplicationCredential>({
type: "application_credentials/create",
domain,
client_id: clientId,
client_secret: clientSecret,
});
export const deleteApplicationCredential = async (
hass: HomeAssistant,
applicationCredentialsId: string
) =>
hass.callWS<void>({
type: "application_credentials/delete",
application_credentials_id: applicationCredentialsId,
});
+57 -1
View File
@@ -65,6 +65,7 @@ export interface BaseTrigger {
platform: string;
id?: string;
variables?: Record<string, unknown>;
enabled?: boolean;
}
export interface StateTrigger extends BaseTrigger {
@@ -152,6 +153,12 @@ export interface EventTrigger extends BaseTrigger {
context?: ContextConstraint;
}
export interface CalendarTrigger extends BaseTrigger {
platform: "calendar";
event: "start" | "end";
entity_id: string;
}
export type Trigger =
| StateTrigger
| MqttTrigger
@@ -166,11 +173,13 @@ export type Trigger =
| TimeTrigger
| TemplateTrigger
| EventTrigger
| DeviceTrigger;
| DeviceTrigger
| CalendarTrigger;
interface BaseCondition {
condition: string;
alias?: string;
enabled?: boolean;
}
export interface LogicalCondition extends BaseCondition {
@@ -226,6 +235,24 @@ export interface TriggerCondition extends BaseCondition {
id: string;
}
type ShorthandBaseCondition = Omit<BaseCondition, "condition">;
export interface ShorthandAndConditionList extends ShorthandBaseCondition {
condition: Condition[];
}
export interface ShorthandAndCondition extends ShorthandBaseCondition {
and: Condition[];
}
export interface ShorthandOrCondition extends ShorthandBaseCondition {
or: Condition[];
}
export interface ShorthandNotCondition extends ShorthandBaseCondition {
not: Condition[];
}
export type Condition =
| StateCondition
| NumericStateCondition
@@ -237,6 +264,35 @@ export type Condition =
| LogicalCondition
| TriggerCondition;
export type ConditionWithShorthand =
| Condition
| ShorthandAndConditionList
| ShorthandAndCondition
| ShorthandOrCondition
| 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 = (
hass: HomeAssistant,
entityId: string
+15
View File
@@ -11,6 +11,8 @@ export interface DeviceAutomation {
type?: string;
subtype?: string;
event?: string;
enabled?: boolean;
metadata?: { secondary: boolean };
}
export interface DeviceAction extends DeviceAutomation {
@@ -179,3 +181,16 @@ export const localizeDeviceAutomationTrigger = (
(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;
};
+5
View File
@@ -1,4 +1,9 @@
import { HomeAssistant } from "../types";
export interface LogProvider {
key: string;
name: string;
}
export const fetchErrorLog = (hass: HomeAssistant) =>
hass.callApi<string>("GET", "error_log");
+22
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",
};
+6 -3
View File
@@ -1,7 +1,7 @@
import { atLeastVersion } from "../../common/config/version";
import { HomeAssistant, PanelInfo } from "../../types";
import { SupervisorArch } from "../supervisor/supervisor";
import { HassioAddonInfo, HassioAddonRepository } from "./addon";
import { HassioAddonInfo } from "./addon";
import { hassioApiResultExtractor, HassioResponse } from "./common";
export type HassioHomeAssistantInfo = {
@@ -23,7 +23,7 @@ export type HassioHomeAssistantInfo = {
export type HassioSupervisorInfo = {
addons: HassioAddonInfo[];
addons_repositories: HassioAddonRepository[];
addons_repositories: string[];
arch: SupervisorArch;
channel: string;
debug: boolean;
@@ -179,7 +179,10 @@ export const fetchHassioInfo = async (
};
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 (
hass: HomeAssistant,
+4 -18
View File
@@ -46,7 +46,6 @@ export const getLogbookDataForContext = async (
startDate,
undefined,
undefined,
undefined,
contextId
)
);
@@ -56,20 +55,13 @@ export const getLogbookData = async (
hass: HomeAssistant,
startDate: string,
endDate: string,
entityId?: string,
entity_matches_only?: boolean
entityId?: string
): Promise<LogbookEntry[]> => {
const localize = await hass.loadBackendTranslation("device_class");
return addLogbookMessage(
hass,
localize,
await getLogbookDataCache(
hass,
startDate,
endDate,
entityId,
entity_matches_only
)
await getLogbookDataCache(hass, startDate, endDate, entityId)
);
};
@@ -97,8 +89,7 @@ export const getLogbookDataCache = async (
hass: HomeAssistant,
startDate: string,
endDate: string,
entityId?: string,
entity_matches_only?: boolean
entityId?: string
) => {
const ALL_ENTITIES = "*";
@@ -125,8 +116,7 @@ export const getLogbookDataCache = async (
hass,
startDate,
endDate,
entityId !== ALL_ENTITIES ? entityId : undefined,
entity_matches_only
entityId !== ALL_ENTITIES ? entityId : undefined
).then((entries) => entries.reverse());
return DATA_CACHE[cacheKey][entityId];
};
@@ -136,7 +126,6 @@ const getLogbookDataFromServer = async (
startDate: string,
endDate?: string,
entityId?: string,
entitymatchesOnly?: boolean,
contextId?: string
) => {
const params = new URLSearchParams();
@@ -147,9 +136,6 @@ const getLogbookDataFromServer = async (
if (entityId) {
params.append("entity", entityId);
}
if (entitymatchesOnly) {
params.append("entity_matches_only", "");
}
if (contextId) {
params.append("context_id", contextId);
}
+49 -37
View File
@@ -13,11 +13,18 @@ import {
literal,
is,
Describe,
boolean,
} from "superstruct";
import { computeObjectId } from "../common/entity/compute_object_id";
import { navigate } from "../common/navigate";
import { HomeAssistant } from "../types";
import { Condition, Trigger } from "./automation";
import {
Condition,
ShorthandAndCondition,
ShorthandNotCondition,
ShorthandOrCondition,
Trigger,
} from "./automation";
import { BlueprintInput } from "./blueprint";
export const MODES = ["single", "restart", "queued", "parallel"] as const;
@@ -25,6 +32,7 @@ export const MODES_MAX = ["queued", "parallel"];
export const baseActionStruct = object({
alias: optional(string()),
enabled: optional(boolean()),
});
const targetStruct = object({
@@ -88,15 +96,18 @@ export interface BlueprintScriptConfig extends ManualScriptConfig {
use_blueprint: { path: string; input?: BlueprintInput };
}
export interface EventAction {
interface BaseAction {
alias?: string;
enabled?: boolean;
}
export interface EventAction extends BaseAction {
event: string;
event_data?: Record<string, any>;
event_data_template?: Record<string, any>;
}
export interface ServiceAction {
alias?: string;
export interface ServiceAction extends BaseAction {
service?: string;
service_template?: string;
entity_id?: string;
@@ -104,55 +115,48 @@ export interface ServiceAction {
data?: Record<string, unknown>;
}
export interface DeviceAction {
alias?: string;
export interface DeviceAction extends BaseAction {
type: string;
device_id: string;
domain: string;
entity_id: string;
}
export interface DelayActionParts {
export interface DelayActionParts extends BaseAction {
milliseconds?: number;
seconds?: number;
minutes?: number;
hours?: number;
days?: number;
}
export interface DelayAction {
alias?: string;
export interface DelayAction extends BaseAction {
delay: number | Partial<DelayActionParts> | string;
}
export interface ServiceSceneAction {
alias?: string;
export interface ServiceSceneAction extends BaseAction {
service: "scene.turn_on";
target?: { entity_id?: string };
entity_id?: string;
metadata: Record<string, unknown>;
}
export interface LegacySceneAction {
alias?: string;
export interface LegacySceneAction extends BaseAction {
scene: string;
}
export type SceneAction = ServiceSceneAction | LegacySceneAction;
export interface WaitAction {
alias?: string;
export interface WaitAction extends BaseAction {
wait_template: string;
timeout?: number;
continue_on_timeout?: boolean;
}
export interface WaitForTriggerAction {
alias?: string;
export interface WaitForTriggerAction extends BaseAction {
wait_for_trigger: Trigger | Trigger[];
timeout?: number;
continue_on_timeout?: boolean;
}
export interface PlayMediaAction {
alias?: string;
export interface PlayMediaAction extends BaseAction {
service: "media_player.play_media";
target?: { entity_id?: string };
entity_id?: string;
@@ -160,13 +164,11 @@ export interface PlayMediaAction {
metadata: Record<string, unknown>;
}
export interface RepeatAction {
alias?: string;
repeat: CountRepeat | WhileRepeat | UntilRepeat;
export interface RepeatAction extends BaseAction {
repeat: CountRepeat | WhileRepeat | UntilRepeat | ForEachRepeat;
}
interface BaseRepeat {
alias?: string;
interface BaseRepeat extends BaseAction {
sequence: Action | Action[];
}
@@ -182,38 +184,40 @@ export interface UntilRepeat extends BaseRepeat {
until: Condition[];
}
export interface ChooseActionChoice {
alias?: string;
export interface ForEachRepeat extends BaseRepeat {
for_each: string | any[];
}
export interface ChooseActionChoice extends BaseAction {
conditions: string | Condition[];
sequence: Action | Action[];
}
export interface ChooseAction {
alias?: string;
export interface ChooseAction extends BaseAction {
choose: ChooseActionChoice | ChooseActionChoice[] | null;
default?: Action | Action[];
}
export interface IfAction {
alias?: string;
export interface IfAction extends BaseAction {
if: string | Condition[];
then: Action | Action[];
else?: Action | Action[];
}
export interface VariablesAction {
alias?: string;
export interface VariablesAction extends BaseAction {
variables: Record<string, unknown>;
}
export interface StopAction {
alias?: string;
export interface StopAction extends BaseAction {
stop: string;
error?: boolean;
}
interface UnknownAction {
alias?: string;
export interface ParallelAction extends BaseAction {
parallel: ManualScriptConfig | Action | (ManualScriptConfig | Action)[];
}
interface UnknownAction extends BaseAction {
[key: string]: unknown;
}
@@ -222,6 +226,9 @@ export type Action =
| DeviceAction
| ServiceAction
| Condition
| ShorthandAndCondition
| ShorthandOrCondition
| ShorthandNotCondition
| DelayAction
| SceneAction
| WaitAction
@@ -232,6 +239,7 @@ export type Action =
| VariablesAction
| PlayMediaAction
| StopAction
| ParallelAction
| UnknownAction;
export interface ActionTypes {
@@ -249,6 +257,7 @@ export interface ActionTypes {
service: ServiceAction;
play_media: PlayMediaAction;
stop: StopAction;
parallel: ParallelAction;
unknown: UnknownAction;
}
@@ -298,7 +307,7 @@ export const getActionType = (action: Action): ActionType => {
if ("wait_template" in action) {
return "wait_template";
}
if ("condition" in action) {
if (["condition", "and", "or", "not"].some((key) => key in action)) {
return "check_condition";
}
if ("event" in action) {
@@ -328,6 +337,9 @@ export const getActionType = (action: Action): ActionType => {
if ("stop" in action) {
return "stop";
}
if ("parallel" in action) {
return "parallel";
}
if ("service" in action) {
if ("metadata" in action) {
if (is(action, activateSceneActionStruct)) {
+87
View File
@@ -8,12 +8,17 @@ import { describeCondition, describeTrigger } from "./automation_i18n";
import {
ActionType,
ActionTypes,
ChooseAction,
DelayAction,
DeviceAction,
EventAction,
getActionType,
IfAction,
ParallelAction,
PlayMediaAction,
RepeatAction,
SceneAction,
StopAction,
VariablesAction,
WaitForTriggerAction,
} from "./script";
@@ -161,6 +166,81 @@ export const describeAction = <T extends ActionType>(
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") {
const config = action as DeviceAction;
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;
};
+13 -1
View File
@@ -44,6 +44,14 @@ export interface ChooseActionTraceStep extends BaseTraceStep {
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 {
result?: { result: boolean };
}
@@ -177,7 +185,11 @@ export const getDataFromPath = (
const asNumber = Number(raw);
if (isNaN(asNumber)) {
result = result[raw];
const tempResult = result[raw];
if (!tempResult && raw === "sequence") {
continue;
}
result = tempResult;
continue;
}
+102 -2
View File
@@ -1,10 +1,17 @@
import type {
HassEntities,
HassEntityAttributeBase,
HassEntityBase,
HassEvent,
} from "home-assistant-js-websocket";
import { BINARY_STATE_ON } from "../common/const";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { supportsFeature } from "../common/entity/supports-feature";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import { HomeAssistant } from "../types";
import { showToast } from "../util/toast";
export const UPDATE_SUPPORT_INSTALL = 1;
export const UPDATE_SUPPORT_SPECIFIC_VERSION = 2;
@@ -31,8 +38,12 @@ export const updateUsesProgress = (entity: UpdateEntity): boolean =>
supportsFeature(entity, UPDATE_SUPPORT_PROGRESS) &&
typeof entity.attributes.in_progress === "number";
export const updateCanInstall = (entity: UpdateEntity): boolean =>
entity.state === BINARY_STATE_ON &&
export const updateCanInstall = (
entity: UpdateEntity,
showSkipped = false
): boolean =>
(entity.state === BINARY_STATE_ON ||
(showSkipped && Boolean(entity.attributes.skipped_version))) &&
supportsFeature(entity, UPDATE_SUPPORT_INSTALL);
export const updateIsInstalling = (entity: UpdateEntity): boolean =>
@@ -43,3 +54,92 @@ export const updateReleaseNotes = (hass: HomeAssistant, entityId: string) =>
type: "update/release_notes",
entity_id: entityId,
});
export const filterUpdateEntities = (entities: HassEntities) =>
(
Object.values(entities).filter(
(entity) => computeStateDomain(entity) === "update"
) as UpdateEntity[]
).sort((a, b) => {
if (a.attributes.title === "Home Assistant Core") {
return -3;
}
if (b.attributes.title === "Home Assistant Core") {
return 3;
}
if (a.attributes.title === "Home Assistant Operating System") {
return -2;
}
if (b.attributes.title === "Home Assistant Operating System") {
return 2;
}
if (a.attributes.title === "Home Assistant Supervisor") {
return -1;
}
if (b.attributes.title === "Home Assistant Supervisor") {
return 1;
}
return caseInsensitiveStringCompare(
a.attributes.title || a.attributes.friendly_name || "",
b.attributes.title || b.attributes.friendly_name || ""
);
});
export const filterUpdateEntitiesWithInstall = (
entities: HassEntities,
showSkipped = false
) =>
filterUpdateEntities(entities).filter((entity) =>
updateCanInstall(entity, showSkipped)
);
export const checkForEntityUpdates = async (
element: HTMLElement,
hass: HomeAssistant
) => {
const entities = filterUpdateEntities(hass.states).map(
(entity) => entity.entity_id
);
if (!entities.length) {
showAlertDialog(element, {
title: hass.localize("ui.panel.config.updates.no_update_entities.title"),
text: hass.localize(
"ui.panel.config.updates.no_update_entities.description"
),
warning: true,
});
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", {
entity_id: entities,
});
// there is no reliable way to know if all the updates are done updating, so we just wait a bit for now...
await new Promise((r) => setTimeout(r, 10000));
unsubscribeEvents();
if (updated === 0) {
showToast(element, {
message: hass.localize("ui.panel.config.updates.no_new_updates"),
});
}
};
+2 -2
View File
@@ -127,7 +127,7 @@ export interface ZWaveJSClient {
export interface ZWaveJSController {
home_id: number;
library_version: string;
sdk_version: string;
type: number;
own_node_id: number;
is_secondary: boolean;
@@ -136,7 +136,7 @@ export interface ZWaveJSController {
was_real_primary: boolean;
is_static_update_controller: boolean;
is_slave: boolean;
serial_api_version: string;
firmware_version: string;
manufacturer_id: number;
product_id: number;
product_type: number;
@@ -312,6 +312,7 @@ class DataEntryFlowDialog extends LitElement {
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
.domain=${this._step.handler}
></step-flow-abort>
`
: this._step.type === "progress"
@@ -146,14 +146,14 @@ export const showOptionsFlowDialog = (
renderMenuHeader(hass, step) {
return (
hass.localize(
`component.${step.handler}.option.step.${step.step_id}.title`
) || hass.localize(`component.${step.handler}.title`)
`component.${configEntry.domain}.options.step.${step.step_id}.title`
) || hass.localize(`component.${configEntry.domain}.title`)
);
},
renderMenuDescription(hass, step) {
const description = hass.localize(
`component.${step.handler}.option.step.${step.step_id}.description`,
`component.${configEntry.domain}.options.step.${step.step_id}.description`,
step.description_placeholders
);
return description
@@ -169,7 +169,7 @@ export const showOptionsFlowDialog = (
renderMenuOption(hass, step, option) {
return hass.localize(
`component.${step.handler}.options.step.${step.step_id}.menu_options.${option}`,
`component.${configEntry.domain}.options.step.${step.step_id}.menu_options.${option}`,
step.description_placeholders
);
},
+3 -5
View File
@@ -15,13 +15,11 @@ class StepFlowAbort extends LitElement {
@property({ attribute: false }) public step!: DataEntryFlowStepAbort;
@property({ attribute: false }) public domain!: string;
protected render(): TemplateResult {
return html`
<h2>
${this.hass.localize(
"ui.panel.config.integrations.config_flow.aborted"
)}
</h2>
<h2>${this.hass.localize(`component.${this.domain}.title`)}</h2>
<div class="content">
${this.flowConfig.renderAbortDescription(this.hass, this.step)}
</div>
+6 -20
View File
@@ -1,12 +1,13 @@
import "@material/mwc-button/mwc-button";
import { mdiAlertOutline } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-dialog";
import "../../components/ha-svg-icon";
import "../../components/ha-switch";
import "../../components/ha-textfield";
import { HaTextField } from "../../components/ha-textfield";
import { haStyleDialog } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import { DialogBoxParams } from "./show-dialog-box";
@@ -17,13 +18,10 @@ class DialogBox extends LitElement {
@state() private _params?: DialogBoxParams;
@state() private _value?: string;
@query("ha-textfield") private _textField?: HaTextField;
public async showDialog(params: DialogBoxParams): Promise<void> {
this._params = params;
if (params.prompt) {
this._value = params.defaultValue;
}
}
public closeDialog(): boolean {
@@ -75,9 +73,7 @@ class DialogBox extends LitElement {
? html`
<ha-textfield
dialogInitialFocus
.value=${this._value || ""}
@keyup=${this._handleKeyUp}
@change=${this._valueChanged}
value=${ifDefined(this._params.defaultValue)}
.label=${this._params.inputLabel
? this._params.inputLabel
: ""}
@@ -109,10 +105,6 @@ class DialogBox extends LitElement {
`;
}
private _valueChanged(ev) {
this._value = ev.target.value;
}
private _dismiss(): void {
if (this._params?.cancel) {
this._params.cancel();
@@ -120,15 +112,9 @@ class DialogBox extends LitElement {
this._close();
}
private _handleKeyUp(ev: KeyboardEvent) {
if (ev.keyCode === 13) {
this._confirm();
}
}
private _confirm(): void {
if (this._params!.confirm) {
this._params!.confirm(this._value);
this._params!.confirm(this._textField?.value);
}
this._close();
}
@@ -119,7 +119,15 @@ class MoreInfoVacuum extends LitElement {
"ui.dialogs.more_info_control.vacuum.status"
)}:
</span>
<span><strong>${stateObj.attributes.status}</strong></span>
<span>
<strong>
${stateObj.attributes.status ||
this.hass.localize(
`component.vacuum.state._.${stateObj.state}`
) ||
stateObj.state}
</strong>
</span>
</div>
`
: ""}
+52 -21
View File
@@ -17,6 +17,7 @@ import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { canShowPage } from "../../common/config/can_show_page";
import { componentsWithService } from "../../common/config/components_with_service";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
@@ -33,6 +34,7 @@ import "../../components/ha-circular-progress";
import "../../components/ha-header-bar";
import "../../components/ha-icon-button";
import "../../components/ha-textfield";
import { fetchHassioSupervisorInfo } from "../../data/hassio/supervisor";
import { domainToName } from "../../data/integration";
import { getPanelNameTranslationKey } from "../../data/panel";
import { PageNavigation } from "../../layouts/hass-tabs-subpage";
@@ -245,9 +247,10 @@ export class QuickBar extends LitElement {
`;
}
private _initializeItemsIfNeeded() {
private async _initializeItemsIfNeeded() {
if (this._commandMode) {
this._commandItems = this._commandItems || this._generateCommandItems();
this._commandItems =
this._commandItems || (await this._generateCommandItems());
} else {
this._entityItems = this._entityItems || this._generateEntityItems();
}
@@ -485,11 +488,11 @@ export class QuickBar extends LitElement {
);
}
private _generateCommandItems(): CommandItem[] {
private async _generateCommandItems(): Promise<CommandItem[]> {
return [
...this._generateReloadCommands(),
...this._generateServerControlCommands(),
...this._generateNavigationCommands(),
...(await this._generateNavigationCommands()),
].sort((a, b) =>
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 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[] {
@@ -610,20 +642,14 @@ export class QuickBar extends LitElement {
if (!canShowPage(this.hass, page)) {
continue;
}
if (!page.component) {
continue;
}
const info = this._getNavigationInfoFromConfig(page);
if (!info) {
continue;
}
// Add to list, but only if we do not already have an entry for the same path and component
if (
items.some(
(e) => e.path === info.path && e.component === info.component
)
) {
if (items.some((e) => e.path === info.path)) {
continue;
}
@@ -637,14 +663,19 @@ export class QuickBar extends LitElement {
private _getNavigationInfoFromConfig(
page: PageNavigation
): NavigationInfo | undefined {
if (!page.component) {
return undefined;
}
const caption = this.hass.localize(
`ui.dialogs.quick-bar.commands.navigation.${page.component}`
);
const path = page.path.substring(1);
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 };
}
+1
View File
@@ -99,6 +99,7 @@ class HassSubpage extends LitElement {
ha-icon-button-arrow-prev,
::slotted([slot="toolbar-icon"]) {
pointer-events: auto;
color: var(--sidebar-icon-color);
}
.main-title {
+4 -1
View File
@@ -130,7 +130,7 @@ export class HaTabsSubpageDataTable extends LitElement {
* Array of tabs to show on the page.
* @type {Array}
*/
@property() public tabs!: PageNavigation[];
@property() public tabs: PageNavigation[] = [];
/**
* Force hides the filter menu.
@@ -283,6 +283,9 @@ export class HaTabsSubpageDataTable extends LitElement {
height: calc(100vh - 1px - var(--header-height));
display: block;
}
:host([narrow]) hass-tabs-subpage {
--main-title-margin: 0;
}
.table-header {
display: flex;
align-items: center;
+16 -3
View File
@@ -82,6 +82,16 @@ class HassTabsSubpage extends LitElement {
(!page.advancedOnly || showAdvanced)
);
if (shownTabs.length < 2) {
if (shownTabs.length === 1) {
const page = shownTabs[0];
return [
page.translationKey ? localizeFunc(page.translationKey) : page.name,
];
}
return [""];
}
return shownTabs.map(
(page) =>
html`
@@ -134,7 +144,7 @@ class HassTabsSubpage extends LitElement {
this.narrow,
this.localizeFunc || this.hass.localize
);
const showTabs = tabs.length > 1 || !this.narrow;
const showTabs = tabs.length > 1;
return html`
<div class="toolbar">
${this.mainPage || (!this.backPath && history.state?.root)
@@ -159,8 +169,10 @@ class HassTabsSubpage extends LitElement {
@click=${this._backTapped}
></ha-icon-button-arrow-prev>
`}
${this.narrow
? html`<div class="main-title"><slot name="header"></slot></div>`
${this.narrow || !showTabs
? html`<div class="main-title">
<slot name="header">${!showTabs ? tabs[0] : ""}</slot>
</div>`
: ""}
${showTabs
? html`
@@ -283,6 +295,7 @@ class HassTabsSubpage extends LitElement {
max-height: var(--header-height);
line-height: 20px;
color: var(--sidebar-text-color);
margin: var(--main-title-margin, 0 0 0 24px);
}
.content {
+2 -2
View File
@@ -1,11 +1,11 @@
import "@material/mwc-button/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import "../../hassio/src/components/hassio-ansi-to-html";
import { showBackupUploadDialog } from "../../hassio/src/dialogs/backup/show-dialog-backup-upload";
import { showHassioBackupDialog } from "../../hassio/src/dialogs/backup/show-dialog-hassio-backup";
import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-card";
import "../components/ha-ansi-to-html";
import { fetchInstallationType } from "../data/onboarding";
import { makeDialogManager } from "../dialogs/make-dialog-manager";
import { ProvideHassLitMixin } from "../mixins/provide-hass-lit-mixin";
@@ -86,7 +86,7 @@ class OnboardingRestoreBackup extends ProvideHassLitMixin(LitElement) {
padding: 4px;
margin-top: 8px;
}
hassio-ansi-to-html {
ha-ansi-to-html {
display: block;
line-height: 22px;
padding: 0 8px;
+5 -15
View File
@@ -11,14 +11,7 @@ import listPlugin from "@fullcalendar/list";
// @ts-ignore
import listStyle from "@fullcalendar/list/main.css";
import "@material/mwc-button";
import {
mdiChevronLeft,
mdiChevronRight,
mdiViewAgenda,
mdiViewDay,
mdiViewModule,
mdiViewWeek,
} from "@mdi/js";
import { mdiViewAgenda, mdiViewDay, mdiViewModule, mdiViewWeek } from "@mdi/js";
import {
css,
CSSResultGroup,
@@ -33,7 +26,6 @@ import memoize from "memoize-one";
import { useAmPm } from "../../common/datetime/use_am_pm";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-button-toggle-group";
import "../../components/ha-icon-button";
import "../../components/ha-icon-button-prev";
import "../../components/ha-icon-button-next";
import { haStyle } from "../../resources/styles";
@@ -152,20 +144,18 @@ export class HAFullCalendar extends LitElement {
<div class="controls">
<h1>${this.calendar.view.title}</h1>
<div>
<ha-icon-button
<ha-icon-button-prev
.label=${this.hass.localize("ui.common.previous")}
.path=${mdiChevronLeft}
class="prev"
@click=${this._handlePrev}
>
</ha-icon-button>
<ha-icon-button
</ha-icon-button-prev>
<ha-icon-button-next
.label=${this.hass.localize("ui.common.next")}
.path=${mdiChevronRight}
class="next"
@click=${this._handleNext}
>
</ha-icon-button>
</ha-icon-button-next>
</div>
</div>
<div class="controls">
@@ -0,0 +1,224 @@
import "@material/mwc-button";
import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ComboBoxLitRenderer } from "lit-vaadin-helpers";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-circular-progress";
import "../../../components/ha-combo-box";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-textfield";
import {
fetchApplicationCredentialsConfig,
createApplicationCredential,
ApplicationCredential,
} from "../../../data/application_credential";
import { domainToName } from "../../../data/integration";
import { PolymerChangedEvent } from "../../../polymer-types";
import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { AddApplicationCredentialDialogParams } from "./show-dialog-add-application-credential";
interface Domain {
id: string;
name: string;
}
const rowRenderer: ComboBoxLitRenderer<Domain> = (item) => html`<mwc-list-item>
<span>${item.name}</span>
</mwc-list-item>`;
@customElement("dialog-add-application-credential")
export class DialogAddApplicationCredential extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _loading = false;
// Error message when can't talk to server etc
@state() private _error?: string;
@state() private _params?: AddApplicationCredentialDialogParams;
@state() private _domain?: string;
@state() private _clientId?: string;
@state() private _clientSecret?: string;
@state() private _domains?: Domain[];
public showDialog(params: AddApplicationCredentialDialogParams) {
this._params = params;
this._domain = "";
this._clientId = "";
this._clientSecret = "";
this._error = undefined;
this._loading = false;
this._fetchConfig();
}
private async _fetchConfig() {
const config = await fetchApplicationCredentialsConfig(this.hass);
this._domains = config.domains.map((domain) => ({
id: domain,
name: domainToName(this.hass.localize, domain),
}));
}
protected render(): TemplateResult {
if (!this._params || !this._domains) {
return html``;
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this.hass.localize(
"ui.panel.config.application_credentials.editor.caption"
)
)}
>
<div>
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
<ha-combo-box
name="domain"
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.application_credentials.editor.domain"
)}
.value=${this._domain}
.renderer=${rowRenderer}
.items=${this._domains}
item-id-path="id"
item-value-path="id"
item-label-path="name"
required
@value-changed=${this._handleDomainPicked}
></ha-combo-box>
<ha-textfield
class="clientId"
name="clientId"
.label=${this.hass.localize(
"ui.panel.config.application_credentials.editor.client_id"
)}
.value=${this._clientId}
required
@input=${this._handleValueChanged}
error-message=${this.hass.localize("ui.common.error_required")}
dialogInitialFocus
></ha-textfield>
<ha-textfield
.label=${this.hass.localize(
"ui.panel.config.application_credentials.editor.client_secret"
)}
type="password"
name="clientSecret"
.value=${this._clientSecret}
required
@input=${this._handleValueChanged}
error-message=${this.hass.localize("ui.common.error_required")}
></ha-textfield>
</div>
${this._loading
? html`
<div slot="primaryAction" class="submit-spinner">
<ha-circular-progress active></ha-circular-progress>
</div>
`
: html`
<mwc-button
slot="primaryAction"
.disabled=${!this._domain ||
!this._clientId ||
!this._clientSecret}
@click=${this._createApplicationCredential}
>
${this.hass.localize(
"ui.panel.config.application_credentials.editor.create"
)}
</mwc-button>
`}
</ha-dialog>
`;
}
public closeDialog() {
this._params = undefined;
this._domains = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
private async _handleDomainPicked(ev: PolymerChangedEvent<string>) {
const target = ev.target as any;
if (target.selectedItem) {
this._domain = target.selectedItem.id;
}
}
private _handleValueChanged(ev: CustomEvent) {
this._error = undefined;
const name = (ev.target as any).name;
const value = (ev.target as any).value;
this[`_${name}`] = value;
}
private async _createApplicationCredential(ev) {
ev.preventDefault();
if (!this._domain || !this._clientId || !this._clientSecret) {
return;
}
this._loading = true;
this._error = "";
let applicationCredential: ApplicationCredential;
try {
applicationCredential = await createApplicationCredential(
this.hass,
this._domain,
this._clientId,
this._clientSecret
);
} catch (err: any) {
this._loading = false;
this._error = err.message;
return;
}
this._params!.applicationCredentialAddedCallback(applicationCredential);
this.closeDialog();
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-max-width: 500px;
--dialog-z-index: 10;
}
.row {
display: flex;
padding: 8px 0;
}
ha-combo-box {
display: block;
margin-bottom: 24px;
}
ha-textfield {
display: block;
margin-bottom: 24px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-add-application-credential": DialogAddApplicationCredential;
}
}
@@ -0,0 +1,259 @@
import { mdiDelete, mdiPlus } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { LocalizeFunc } from "../../../common/translations/localize";
import {
DataTableColumnContainer,
SelectionChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-icon";
import "../../../components/ha-fab";
import "../../../components/ha-help-tooltip";
import "../../../components/ha-svg-icon";
import {
ApplicationCredential,
deleteApplicationCredential,
fetchApplicationCredentials,
} from "../../../data/application_credential";
import { domainToName } from "../../../data/integration";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-tabs-subpage-data-table";
import type { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-data-table";
import { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config";
import { showAddApplicationCredentialDialog } from "./show-dialog-add-application-credential";
@customElement("ha-config-application-credentials")
export class HaConfigApplicationCredentials extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() public _applicationCredentials: ApplicationCredential[] = [];
@property() public isWide!: boolean;
@property() public narrow!: boolean;
@property() public route!: Route;
@state() private _selected: string[] = [];
@query("hass-tabs-subpage-data-table", true)
private _dataTable!: HaTabsSubpageDataTable;
private _columns = memoizeOne(
(narrow: boolean, localize: LocalizeFunc): DataTableColumnContainer => {
const columns: DataTableColumnContainer<ApplicationCredential> = {
clientId: {
title: localize(
"ui.panel.config.application_credentials.picker.headers.client_id"
),
width: "25%",
direction: "asc",
grows: true,
template: (_, entry: ApplicationCredential) =>
html`${entry.client_id}`,
},
application: {
title: localize(
"ui.panel.config.application_credentials.picker.headers.application"
),
sortable: true,
width: "20%",
direction: "asc",
hidden: narrow,
template: (_, entry) => html`${domainToName(localize, entry.domain)}`,
},
};
return columns;
}
);
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
this._loadTranslations();
this._fetchApplicationCredentials();
}
protected render() {
return html`
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
backPath="/config"
.tabs=${configSections.devices}
.columns=${this._columns(this.narrow, this.hass.localize)}
.data=${this._applicationCredentials}
hasFab
selectable
@selection-changed=${this._handleSelectionChanged}
>
${this._selected.length
? html`
<div
class=${classMap({
"header-toolbar": this.narrow,
"table-header": !this.narrow,
})}
slot="header"
>
<p class="selected-txt">
${this.hass.localize(
"ui.panel.config.application_credentials.picker.selected",
"number",
this._selected.length
)}
</p>
<div class="header-btns">
${!this.narrow
? html`
<mwc-button
@click=${this._removeSelected}
class="warning"
>${this.hass.localize(
"ui.panel.config.application_credentials.picker.remove_selected.button"
)}</mwc-button
>
`
: html`
<ha-icon-button
class="warning"
id="remove-btn"
@click=${this._removeSelected}
.path=${mdiDelete}
.label=${this.hass.localize("ui.common.remove")}
></ha-icon-button>
<ha-help-tooltip
.label=${this.hass.localize(
"ui.panel.config.application_credentials.picker.remove_selected.button"
)}
>
</ha-help-tooltip>
`}
</div>
</div>
`
: html``}
<ha-fab
slot="fab"
.label=${this.hass.localize(
"ui.panel.config.application_credentials.picker.add_application_credential"
)}
extended
@click=${this._addApplicationCredential}
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
</hass-tabs-subpage-data-table>
`;
}
private _handleSelectionChanged(
ev: HASSDomEvent<SelectionChangedEvent>
): void {
this._selected = ev.detail.value;
}
private _removeSelected() {
showConfirmationDialog(this, {
title: this.hass.localize(
`ui.panel.config.application_credentials.picker.remove_selected.confirm_title`,
"number",
this._selected.length
),
text: this.hass.localize(
"ui.panel.config.application_credentials.picker.remove_selected.confirm_text"
),
confirmText: this.hass.localize("ui.common.remove"),
dismissText: this.hass.localize("ui.common.cancel"),
confirm: async () => {
await Promise.all(
this._selected.map(async (applicationCredential) => {
await deleteApplicationCredential(this.hass, applicationCredential);
})
);
this._dataTable.clearSelection();
this._fetchApplicationCredentials();
},
});
}
private async _loadTranslations() {
await this.hass.loadBackendTranslation("title", undefined, true);
}
private async _fetchApplicationCredentials() {
this._applicationCredentials = await fetchApplicationCredentials(this.hass);
}
private _addApplicationCredential() {
showAddApplicationCredentialDialog(this, {
applicationCredentialAddedCallback: async (
applicationCredential: ApplicationCredential
) => {
if (applicationCredential) {
this._applicationCredentials = [
...this._applicationCredentials,
applicationCredential,
];
}
},
});
}
static get styles(): CSSResultGroup {
return css`
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
height: 56px;
background-color: var(--mdc-text-field-fill-color, whitesmoke);
border-bottom: 1px solid
var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));
box-sizing: border-box;
}
.header-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
color: var(--secondary-text-color);
position: relative;
top: -4px;
}
.selected-txt {
font-weight: bold;
padding-left: 16px;
}
.table-header .selected-txt {
margin-top: 20px;
}
.header-toolbar .selected-txt {
font-size: 16px;
}
.header-toolbar .header-btns {
margin-right: -12px;
}
.header-btns {
display: flex;
}
.header-btns > mwc-button,
.header-btns > ha-icon-button {
margin: 8px;
}
ha-button-menu {
margin-left: 8px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-application-credentials": HaConfigApplicationCredentials;
}
}
@@ -0,0 +1,22 @@
import { fireEvent } from "../../../common/dom/fire_event";
import { ApplicationCredential } from "../../../data/application_credential";
export interface AddApplicationCredentialDialogParams {
applicationCredentialAddedCallback: (
applicationCredential: ApplicationCredential
) => void;
}
export const loadAddApplicationCredentialDialog = () =>
import("./dialog-add-application-credential");
export const showAddApplicationCredentialDialog = (
element: HTMLElement,
dialogParams: AddApplicationCredentialDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-add-application-credential",
dialogImport: loadAddApplicationCredentialDialog,
dialogParams,
});
};
@@ -259,6 +259,7 @@ class HaConfigAreaPage extends LitElement {
<ha-svg-icon .path=${mdiImagePlus} slot="icon"></ha-svg-icon>
</mwc-button>`}
<ha-card
outlined
.header=${this.hass.localize("ui.panel.config.devices.caption")}
>${devices.length
? devices.map(
@@ -281,6 +282,7 @@ class HaConfigAreaPage extends LitElement {
`}
</ha-card>
<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.areas.editor.linked_entities_caption"
)}
@@ -314,6 +316,7 @@ class HaConfigAreaPage extends LitElement {
${isComponentLoaded(this.hass, "automation")
? html`
<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.devices.automation.automations_heading"
)}
@@ -361,6 +364,7 @@ class HaConfigAreaPage extends LitElement {
${isComponentLoaded(this.hass, "scene")
? html`
<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.devices.scene.scenes_heading"
)}
@@ -400,6 +404,7 @@ class HaConfigAreaPage extends LitElement {
${isComponentLoaded(this.hass, "script")
? html`
<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.devices.script.scripts_heading"
)}
@@ -33,6 +33,7 @@ import "./types/ha-automation-action-delay";
import "./types/ha-automation-action-device_id";
import "./types/ha-automation-action-event";
import "./types/ha-automation-action-if";
import "./types/ha-automation-action-parallel";
import "./types/ha-automation-action-play_media";
import "./types/ha-automation-action-repeat";
import "./types/ha-automation-action-service";
@@ -54,6 +55,7 @@ const OPTIONS = [
"if",
"device_id",
"stop",
"parallel",
];
const getType = (action: Action | undefined) => {
@@ -63,6 +65,9 @@ const getType = (action: Action | undefined) => {
if ("service" in action || "scene" in action) {
return getActionType(action);
}
if (["and", "or", "not"].some((key) => key in action)) {
return "condition";
}
return OPTIONS.find((option) => option in action);
};
@@ -159,63 +164,83 @@ export default class HaAutomationActionRow extends LitElement {
const yamlMode = this._yamlMode;
return html`
<ha-card>
<div class="card-content">
<div class="card-menu">
${this.index !== 0
? html`
<ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.automation.editor.move_up"
)}
.path=${mdiArrowUp}
@click=${this._moveUp}
></ha-icon-button>
`
: ""}
${this.index !== this.totalActions - 1
? html`
<ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.automation.editor.move_down"
)}
.path=${mdiArrowDown}
@click=${this._moveDown}
></ha-icon-button>
`
: ""}
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<mwc-list-item>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.run_action"
)}
</mwc-list-item>
<mwc-list-item .disabled=${!this._uiModeAvailable}>
${yamlMode
? this.hass.localize(
"ui.panel.config.automation.editor.edit_ui"
)
: this.hass.localize(
"ui.panel.config.automation.editor.edit_yaml"
)}
</mwc-list-item>
<mwc-list-item>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
</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>
<ha-card outlined>
${this.action.enabled === false
? html`<div class="disabled-bar">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.disabled"
)}
</div>`
: ""}
<div class="card-menu">
${this.index !== 0
? html`
<ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.automation.editor.move_up"
)}
.path=${mdiArrowUp}
@click=${this._moveUp}
></ha-icon-button>
`
: ""}
${this.index !== this.totalActions - 1
? html`
<ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.automation.editor.move_down"
)}
.path=${mdiArrowDown}
@click=${this._moveDown}
></ha-icon-button>
`
: ""}
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<mwc-list-item>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.run_action"
)}
</mwc-list-item>
<mwc-list-item .disabled=${!this._uiModeAvailable}>
${yamlMode
? this.hass.localize(
"ui.panel.config.automation.editor.edit_ui"
)
: this.hass.localize(
"ui.panel.config.automation.editor.edit_yaml"
)}
</mwc-list-item>
<mwc-list-item>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
</mwc-list-item>
<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
? html`<ha-alert
alert-type="warning"
@@ -314,11 +339,23 @@ export default class HaAutomationActionRow extends LitElement {
fireEvent(this, "duplicate");
break;
case 3:
this._onDisable();
break;
case 4:
this._onDelete();
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() {
const validated = await validateConfig(this.hass, {
action: this.action,
@@ -408,11 +445,27 @@ export default class HaAutomationActionRow extends LitElement {
return [
haStyle,
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 {
position: absolute;
right: 16px;
float: right;
z-index: 3;
margin: 4px;
--mdc-theme-text-primary-on-background: var(--primary-text-color);
display: flex;
align-items: center;
}
:host-context([style*="direction: rtl;"]) .card-menu {
right: initial;
@@ -1,3 +1,4 @@
import deepClone from "deep-clone-simple";
import "@material/mwc-button";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
@@ -32,7 +33,7 @@ export default class HaAutomationAction extends LitElement {
></ha-automation-action-row>
`
)}
<ha-card>
<ha-card outlined>
<div class="card-actions add-card">
<mwc-button @click=${this._addAction}>
${this.hass.localize(
@@ -83,7 +84,7 @@ export default class HaAutomationAction extends LitElement {
ev.stopPropagation();
const index = (ev.target as any).index;
fireEvent(this, "value-changed", {
value: this.actions.concat(this.actions[index]),
value: this.actions.concat(deepClone(this.actions[index])),
});
}
@@ -69,7 +69,7 @@ export class HaChooseAction extends LitElement implements ActionElement {
</div>
</ha-card>`
)}
<ha-card>
<ha-card outlined>
<div class="card-actions add-card">
<mwc-button @click=${this._addOption}>
${this.hass.localize(
@@ -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;
}
}
@@ -33,7 +33,7 @@ export class HaWaitAction extends LitElement implements ActionElement {
@property({ attribute: false }) public action!: WaitAction;
public static get defaultConfig() {
return { wait_template: "" };
return { wait_template: "", continue_on_timeout: true };
}
protected render() {
@@ -75,7 +75,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
"ui.panel.config.automation.editor.introduction"
)}
</span>
<ha-card>
<ha-card outlined>
<div class="card-content">
<ha-textfield
.label=${this.hass.localize(
@@ -145,6 +145,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
</ha-config-section>
<ha-card
outlined
class="blueprint"
.header=${this.hass.localize(
"ui.panel.config.automation.editor.blueprint.header"
@@ -332,6 +333,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
ha-settings-row {
--paper-time-input-justify-content: flex-end;
--settings-row-content-width: 100%;
--settings-row-prefix-display: contents;
border-top: 1px solid var(--divider-color);
}
`,
@@ -5,11 +5,11 @@ import { dynamicElement } from "../../../../common/dom/dynamic-element-directive
import { fireEvent } from "../../../../common/dom/fire_event";
import { stringCompare } from "../../../../common/string/compare";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-card";
import "../../../../components/ha-select";
import type { HaSelect } from "../../../../components/ha-select";
import "../../../../components/ha-yaml-editor";
import type { Condition } from "../../../../data/automation";
import { expandConditionWithShorthand } from "../../../../data/automation";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import "./types/ha-automation-condition-and";
@@ -42,10 +42,14 @@ const OPTIONS = [
export default class HaAutomationConditionEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public condition!: Condition;
@property() condition!: Condition;
@property() public yamlMode = false;
private _processedCondition = memoizeOne((condition) =>
expandConditionWithShorthand(condition)
);
private _processedTypes = memoizeOne(
(localize: LocalizeFunc): [string, string][] =>
OPTIONS.map(
@@ -60,7 +64,8 @@ export default class HaAutomationConditionEditor extends LitElement {
);
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;
return html`
${yamlMode
@@ -70,7 +75,7 @@ export default class HaAutomationConditionEditor extends LitElement {
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.unsupported_condition",
"condition",
this.condition.condition
condition.condition
)}
`
: ""}
@@ -90,7 +95,7 @@ export default class HaAutomationConditionEditor extends LitElement {
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type_select"
)}
.value=${this.condition.condition}
.value=${condition.condition}
naturalMenuWidth
@selected=${this._typeChanged}
>
@@ -103,8 +108,8 @@ export default class HaAutomationConditionEditor extends LitElement {
<div>
${dynamicElement(
`ha-automation-condition-${this.condition.condition}`,
{ hass: this.hass, condition: this.condition }
`ha-automation-condition-${condition.condition}`,
{ hass: this.hass, condition: condition }
)}
</div>
`}
@@ -124,7 +129,7 @@ export default class HaAutomationConditionEditor extends LitElement {
defaultConfig: Omit<Condition, "condition">;
};
if (type !== this.condition.condition) {
if (type !== this._processedCondition(this.condition).condition) {
fireEvent(this, "value-changed", {
value: {
condition: type,
@@ -2,7 +2,7 @@ import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js";
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 { handleStructError } from "../../../../common/structs/handle-errors";
import "../../../../components/ha-button-menu";
@@ -19,6 +19,7 @@ import { haStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import "./ha-automation-condition-editor";
import { validateConfig } from "../../../../data/config";
import { HaYamlEditor } from "../../../../components/ha-yaml-editor";
export interface ConditionElement extends LitElement {
condition: Condition;
@@ -59,47 +60,69 @@ export default class HaAutomationConditionRow extends LitElement {
@state() private _warnings?: string[];
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
protected render() {
if (!this.condition) {
return html``;
}
return html`
<ha-card>
<div class="card-content">
<div class="card-menu">
<ha-progress-button @click=${this._testCondition}>
<ha-card outlined>
${this.condition.enabled === false
? html`<div class="disabled-bar">
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.test"
"ui.panel.config.automation.editor.actions.disabled"
)}
</ha-progress-button>
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
>
</ha-icon-button>
<mwc-list-item>
${this._yamlMode
? this.hass.localize(
"ui.panel.config.automation.editor.edit_ui"
)
: this.hass.localize(
"ui.panel.config.automation.editor.edit_yaml"
)}
</mwc-list-item>
<mwc-list-item>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
</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>`
: ""}
<div class="card-menu">
<ha-progress-button @click=${this._testCondition}>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.test"
)}
</ha-progress-button>
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
>
</ha-icon-button>
<mwc-list-item>
${this._yamlMode
? this.hass.localize(
"ui.panel.config.automation.editor.edit_ui"
)
: this.hass.localize(
"ui.panel.config.automation.editor.edit_yaml"
)}
</mwc-list-item>
<mwc-list-item>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
</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
? html`<ha-alert
alert-type="warning"
@@ -153,11 +176,23 @@ export default class HaAutomationConditionRow extends LitElement {
fireEvent(this, "duplicate");
break;
case 2:
this._onDisable();
break;
case 3:
this._onDelete();
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() {
showConfirmationDialog(this, {
text: this.hass.localize(
@@ -238,9 +273,24 @@ export default class HaAutomationConditionRow extends LitElement {
return [
haStyle,
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 {
float: right;
z-index: 3;
margin: 4px;
--mdc-theme-text-primary-on-background: var(--primary-text-color);
display: flex;
align-items: center;
@@ -1,3 +1,4 @@
import deepClone from "deep-clone-simple";
import "@material/mwc-button";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
@@ -56,7 +57,7 @@ export default class HaAutomationCondition extends LitElement {
></ha-automation-condition-row>
`
)}
<ha-card>
<ha-card outlined>
<div class="card-actions add-card">
<mwc-button @click=${this._addCondition}>
${this.hass.localize(
@@ -96,7 +97,7 @@ export default class HaAutomationCondition extends LitElement {
ev.stopPropagation();
const index = (ev.target as any).index;
fireEvent(this, "value-changed", {
value: this.conditions.concat(this.conditions[index]),
value: this.conditions.concat(deepClone(this.conditions[index])),
});
}
@@ -3,7 +3,6 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-blueprint-picker";
import "../../../components/ha-card";
import "../../../components/ha-circular-progress";
import { createCloseHeading } from "../../../components/ha-dialog";
import { showAutomationEditor } from "../../../data/automation";
@@ -239,8 +239,8 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
? html`
${!this.narrow
? html`
<ha-card
><div class="card-header">
<ha-card outlined>
<div class="card-header">
${this._config.alias}
</div>
${stateObj
@@ -275,8 +275,8 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
.defaultValue=${this._preprocessYaml()}
@value-changed=${this._yamlChanged}
></ha-yaml-editor>
<ha-card
><div class="card-actions">
<ha-card outlined>
<div class="card-actions">
<mwc-button @click=${this._copyYaml}>
${this.hass.localize(
"ui.panel.config.automation.editor.copy_to_clipboard"
@@ -47,7 +47,7 @@ export class HaManualAutomationEditor extends LitElement {
"ui.panel.config.automation.editor.introduction"
)}
</span>
<ha-card>
<ha-card outlined>
<div class="card-content">
<ha-textfield
.label=${this.hass.localize(

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