Compare commits

...

62 Commits

Author SHA1 Message Date
Paulus Schoutsen
02af4c2156 Bump Master to 20220503.0 (#12567)
* Use selectors for add-on configurations (#12234)

* replace ToggleSwitch with new LightSwitch (#12218)

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

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

* Use selectors for add-on network configuration

* Show container port as UOM if advanced user

* adjust

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

* Lineup sidebar badges

* Exclude hidden entities from area card

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

* Adjust import

* Guard for partial translations (#12296)

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

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

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

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

* Update cloud text (#12305)

* Select default mode if none set (#12306)

* Decode view path URL (#12310)

* Always render title field (#12319)

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

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

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

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

* RTL calendar fix - arrows fix and views fix

* Removed path attributes

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

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

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

* Add Template selector (#12348)

* Add basic frontend support for siren (#12345)

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

* Bump HAWS to 7.0.3 (#12358)

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

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

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

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

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

* Add stop script/automation action (#12299)

* Getting started on Configuration Changes (#12309)

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

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

* Bumped version to 20220420.0 (#12369)

* Use template selector in wait_template (#12366)

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

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

* Fix for monetary entities (#12378)

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

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

* RTL fixes (#12367)

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

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

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

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

* Add automation editor for calendar trigger (#12343)

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

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

* Add Empty list item for None (#12356)

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

* Fix Dashboard URLs (#12394)

* Update zwavejs controller model (#12390)

* Configuration Menu Updates 3 (#12377)

* Bumped version to 20220424.0

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

* Add shorthand condition to the gallery (#12400)

* Virtualize Media Player Grid (#11898)

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

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

* Add supervisor network interface settings (#12403)

* Fix zones (#12409)

* Add supervisor hostname config (#12407)

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

* Add Supervisor logs to core page (#12410)

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

* Configuration Menu Cleanup items (#12413)

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

* Move System Health to a page (#12412)

* Show what updates are skipped (#12418)

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

* Better gauge segment coloring (#11570)

* Move Data Disk Moving to Storage (#12416)

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

* Add supervisor, OS version info to about page

* description

* description

* Allow for checking for updates (#12422)

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

* link to updates page (#12423)

* Show usage stats in System Health (#12424)

* Bumped version to 20220425.0 (#12425)

* Format sensors with state class duration (#12426)

* Guard against non OS installation (#12427)

* Typo in en.json (#12428)

* Move unsupported and unhealthy alerts (#12431)

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

* Resources lovelace should just go back (#12432)

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

* Fix backup back path (#12435)

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

* Fix settings row width (#12438)

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

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

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

* Add header to supervisor backups page (#12444)

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

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

* Add var default

* Add title to backups config page (#12442)

* Fix integration page on mobile (#12447)

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

* Terms based entities search (#10991)

* Small edits on config menu (#12440)

* Fix for backup overflow (#12454)

* Update the hint for key C (#12458)

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

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

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

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

* Move the analytics link (#12459)

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

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

* Move Zones Edit to General config + add general

* Update src/translations/en.json

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

* add paper tooltip back for yaml

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

* Tweak menu descriptions (#12460)

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

* Fix my link for config dashboard and profile

* add server control redirect

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

* Fix icon alignment in nav list (#12463)

* Add a tip for my shortcut (#12462)

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

* Move Restart to Overflow and yaml config advanced

* Move around YAML Config page

* Move to developer tools

* Make card actions

* Update Translations

* Bumped version to 20220427.0

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

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

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

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

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

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

* RTL fix for log buttons (#12474)

* Fix YAML Config Invalid button (#12476)

* Small config fixes (#12472)

* Visual tweaks to YAML validation results (#12479)

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

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

* Fix Restarting Home Assistant (#12480)

* Fix Restarting Home ASsistant

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

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

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

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

* reviews

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

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

* Media panel fix (#12485)

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

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

* Address minor comments about config menu (#12492)

* Hide and sort secondary device automations (#12496)

* Evaluate condition shorthands in editors (#12473)

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

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

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

* Add more visual indication of disabled

* review

* margin

* Dont make overflow transparent

* Change color of bar

* Add parallel automation/script action (#12491)

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

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

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

* Bumped version to 20220428.0 (#12501)

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

* form-string password fix (#12507)

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

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

* Media browser RTL fixes (#12506)

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

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

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

* Add condition shorthand to action types (#12514)

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

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

* Add actions to design gallery (#12518)

* Add actions to design gallery

* Update describe-action.ts

* Move integrations to System Health (#12504)

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

* Bumped version to 20220429.0 (#12521)

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

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

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

* Calendar-card fix (#12532)

* Handle condition shorthands in trace graphs (#12533)

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

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

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

* Prevent color temp selector mired exception (#12536)

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

* Add descriptions for actions (#12541)

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

* Add repeat to trace timeline (#12547)

* Change name to Settings (#12548)

* Add trace timeline for if (#12543)

* Fix script graph parallel (#12545)

* Handle if in repeat (#12544)

* Add parallel action to trace timeline (#12549)

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

* Indicate things are disabled in trace graph

* Update hat-script-graph.ts

* Bumped version to 20220502.0

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

* Fix Webhook Overflow (#12551)

* Search in Overflow on Mobile (#12552)

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

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

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

* Add supervisor redirects to quickbar (#12557)

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

* Fix searching in hassio logs (#12560)

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

* Add new system menu descriptions (#12564)

* Add missing outlined to supervisor panel (#12565)

* Bumped version to 20220503.0 (#12566)

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

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

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

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

* Add more visual indication of disabled

* review

* margin

* Dont make overflow transparent

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

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

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

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

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

* reviews

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-04-27 15:55:04 -07:00
Zack Barett
d02cd122a9 Merge pull request #12233 from home-assistant/dev
Co-authored-by: Joakim Sørensen <ludeeus@ludeeus.dev>
Co-authored-by: Philip Allgaier <mail@spacegaier.de>
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-04-05 18:02:02 -05:00
Joakim Sørensen
8e962fdecb Use installed_version for update entities (#12194) 2022-04-01 19:29:26 +02:00
Bram Kragten
1f65193a97 Merge pull request #12193 from home-assistant/dev 2022-04-01 18:56:12 +02:00
Bram Kragten
24484d0e74 20220330.0 (#12165)
* Add a docs icon to the config flow dialog

* Use same help icon everywhere

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

* Supervisor mobile click accessibility (#11915)

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

* Fix datepicker triangle (#11920)

* Always show tab labels (#11919)

* Remove zwave and ozw panels (#11911)

Remove zwave and ozw panels

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

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

* Show triggered vars on click (#11924)

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

* Convert inputs (#11907)

* Convert inputs

* Update dialog-thingtalk.ts

* imports

* Remove some additional old zwave code (#11941)

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

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

* Fix humidifier more info mode dropdown (#11964)

* Make min width of select configurable (#11965)

* Fix for Statistics Editor (#11942)

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

* A11y expansion panel (#11967)

* Convert file upload to mdc (#11906)

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

* Add systemd_resolved unsupported reason (#11971)

* replace default switch icon 

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

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

* Allow selecting multiple entities (#11986)

* Fix theme setting (#11977)

* Update Style of Design Page (#11982)

* change icon to mimic physical device

and follow comments

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

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

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

* Fix zwave_js 'add/remove device' disabled bug

* revert extra change

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

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

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

* Fix Dashboard Editing (#12011)

* Fix For Selecting Device Class (#12010)

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

* Script ID update with Alias (#12008)

* HAWS 6.1 (#12016)

* Bumped version to 20220301.1

* Bumped version to 20220301.2

* Bumped version to 20220312.0

* Add shade to device class overrides (#11874)

* Fix: Changing Blueprint Automation Name (#12036)

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

* Add all cover device classes (#12042)

* Rename Lovelace Dashboard to just Dashboard (#12044)

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

* Add Color Temp Selector (#12041)

* Utilize Hide Hidden Entities

* Reviews

* add to demo

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

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

* Add HA to public folder and show in markdown

* Update Translations

* Disabled by

* remove 1

* Add Description of chosen

* Add icons and buttons

* Add Color RGB Selector (#12039)

* Add Date Selector

* Add ha-form context (#12062)

* test condition (#11925)

* Revamp URL form (#12060)

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

* Add translation

* add to basic editor and update advanced style

* clean up

* Entity Status

* Add Devices Picker (#12056)

* Remvoe redunency

* Bumped version to 20220316.0

* Bump HAWS to 7.0.0 (#12067)

* Create new Logo page

* Add files via upload

* Ignore diagnostics not found exceptions (#12066)

* Bump HAWS to 7.0.1

* Update lock

* Add Date Time Selector (#12070)

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

* Bumped version to 20220317.0 (#12074)

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

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

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

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

* Update logo.markdown

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

* Update required version of MDI to 6.6.95

* Upload release assets (#11566)

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

* Update styles for hui-editor

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

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

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

* Change Netlify preview URL (#12095)

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

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

* Stack Action Inputs in the Button Editor

* update style

* Update for other editors

* Add support for update entities (#12059)

* Add support for update entities

* Apply suggestions from code review

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

* Add to gallery

* implement xx%

* Adjustments for skipped

* Add progress bar

* Add UPDATE_SUPPORT_INSTALL

* Allow skipping without install support

* Add version to service call if supported

* Adjust changelog link

* Use Installing

* adjustments

* Use unavailable

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

* Add support for integration type (#12077)

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

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

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

* change from hidden to not shown (#12097)

* Add statistic adjust dialog (#12101)

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

* Fix Duration Selector Default (#12098)

* Fix Duration Default

* USe initial form data function

* Bumped version to 20220322.0 (#12102)

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

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

* Remove `setup.py` (#11593)

* Fix selecting 0 with number selector

* Update lock file with MDI updates

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

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

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

* Only show docs link when showing a form

* Exclude restored automations from dashboard (#12113)

* Support descriptions in flow menu steps (#12108)

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

* Sort selectors (#12120)

* Update type for backend (#12122)

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

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

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

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

* Allow rendering helper text from strings.json

* Persistent helpers

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

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

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

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

* Add Day to duration selector (#12125)

* Add variables to automation trigger type

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

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

* Add Area Multiple Selector option (#12138)

* break theme picker out of lovelace (#12140)

* Allow binary sensor device class updates (#12124)

* Add selector initial values (#12142)

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

* Bumped version to 20220329.0 (#12152)

* Add entity source API (#12149)

* Update adjust statistic dialog (#12118)

* Update text for adjust statistic dialog

* Change everything

* Import type

* Max show 5

* Revert back the API change

* Hide adjust button if no sum

* Adjustments

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

* Render optional

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

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

* Fetch release notes for update entities that provides it

* lint

* Add support for new timer properties (#11940)

* Fix theme settings on design page (#12154)

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

* Allow device_tracker entities to use state_color (#12127)

* Automation description text overflow (#12040)

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

* Update Pickers and selectors with required (#12151)

* Update Pickers and selectors with required

* Use native * for device and entity

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

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

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

* Add options to selectors gallery (#12156)

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

* List Selector (#12099)

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

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

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

* Add panel to Backup integration (#11671)

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

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

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

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

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

* Bumped version to 20220330.0 (#12164)

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

View File

@@ -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")

View File

@@ -20,6 +20,10 @@ import { HaWaitForTriggerAction } from "../../../../src/panels/config/automation
import { HaWaitAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_template";
import { 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;
}
}

View File

@@ -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}

View File

@@ -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">

View File

@@ -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")}

View File

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

View File

@@ -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>`
: ""}

View File

@@ -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(

View File

@@ -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}

View File

@@ -34,7 +34,7 @@ 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>`
: ""}

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}

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>

View File

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

View File

@@ -42,6 +42,9 @@ export 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)}`;
}

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>

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")

View File

@@ -57,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>

View File

@@ -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>`
: ""}

View File

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

View File

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

View File

@@ -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 {

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() {
@@ -55,6 +62,7 @@ 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;
}
`,
];

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;
}
`;
}
}

View File

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

View File

@@ -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}

View File

@@ -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;

View File

@@ -152,6 +152,7 @@ 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) {

View File

@@ -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;
}
`;
}

View File

@@ -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;
}
`;
}

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,

View File

@@ -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
? ""

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%;

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);

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>
`);
}

View File

@@ -65,6 +65,7 @@ export interface BaseTrigger {
platform: string;
id?: string;
variables?: Record<string, unknown>;
enabled?: boolean;
}
export interface StateTrigger extends BaseTrigger {
@@ -178,6 +179,7 @@ export type Trigger =
interface BaseCondition {
condition: string;
alias?: string;
enabled?: boolean;
}
export interface LogicalCondition extends BaseCondition {
@@ -235,6 +237,10 @@ export interface TriggerCondition extends BaseCondition {
type ShorthandBaseCondition = Omit<BaseCondition, "condition">;
export interface ShorthandAndConditionList extends ShorthandBaseCondition {
condition: Condition[];
}
export interface ShorthandAndCondition extends ShorthandBaseCondition {
and: Condition[];
}
@@ -260,10 +266,33 @@ export type Condition =
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

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;
};

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

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

View File

@@ -1,7 +1,7 @@
import { atLeastVersion } from "../../common/config/version";
import { 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,

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)) {

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;
};

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;
}

View File

@@ -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"

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>

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 };
}

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">

View File

@@ -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"
)}

View File

@@ -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;

View File

@@ -32,7 +32,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(

View File

@@ -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(

View File

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

View File

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

View File

@@ -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"

View File

@@ -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,

View File

@@ -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;

View File

@@ -56,7 +56,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(

View File

@@ -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";

View File

@@ -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"

View File

@@ -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(

View File

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

View File

@@ -3,7 +3,7 @@ import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
@@ -16,7 +16,7 @@ import "../../../../components/ha-alert";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-card";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-yaml-editor";
import { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import "../../../../components/ha-select";
import type { HaSelect } from "../../../../components/ha-select";
import "../../../../components/ha-textfield";
@@ -104,6 +104,8 @@ export default class HaAutomationTriggerRow extends LitElement {
@state() private _triggerColor = false;
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
private _triggerUnsub?: Promise<UnsubscribeFunc>;
private _processedTypes = memoizeOne(
@@ -125,41 +127,61 @@ export default class HaAutomationTriggerRow extends LitElement {
const showId = "id" in this.trigger || this._requestShowId;
return html`
<ha-card>
<div class="card-content">
<div class="card-menu">
<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.triggers.edit_id"
)}
</mwc-list-item>
<mwc-list-item .disabled=${selected === -1}>
${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.trigger.enabled === false
? html`<div class="disabled-bar">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.disabled"
)}
</div>`
: ""}
<div class="card-menu">
<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.triggers.edit_id"
)}
</mwc-list-item>
<mwc-list-item .disabled=${selected === -1}>
${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.trigger.enabled === false
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.actions.disable"
)}
</mwc-list-item>
<mwc-list-item class="warning">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
</mwc-list-item>
</ha-button-menu>
</div>
<div
class="card-content ${this.trigger.enabled === false
? "disabled"
: ""}"
>
${this._warnings
? html`<ha-alert
alert-type="warning"
@@ -214,7 +236,6 @@ export default class HaAutomationTriggerRow extends LitElement {
`
)}
</ha-select>
${showId
? html`
<ha-textfield
@@ -250,7 +271,7 @@ export default class HaAutomationTriggerRow extends LitElement {
`;
}
protected override updated(changedProps: PropertyValues): void {
protected override updated(changedProps: PropertyValues<this>): void {
super.updated(changedProps);
if (changedProps.has("trigger")) {
this._subscribeTrigger();
@@ -347,6 +368,9 @@ export default class HaAutomationTriggerRow extends LitElement {
fireEvent(this, "duplicate");
break;
case 3:
this._onDisable();
break;
case 4:
this._onDelete();
break;
}
@@ -365,6 +389,15 @@ export default class HaAutomationTriggerRow extends LitElement {
});
}
private _onDisable() {
const enabled = !(this.trigger.enabled ?? true);
const value = { ...this.trigger, enabled };
fireEvent(this, "value-changed", { value });
if (this._yamlMode) {
this._yamlEditor?.setValue(value);
}
}
private _typeChanged(ev: CustomEvent) {
const type = (ev.target as HaSelect).value;
@@ -439,10 +472,27 @@ export default class HaAutomationTriggerRow 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;
}
:host-context([style*="direction: rtl;"]) .card-menu {
float: left;

View File

@@ -27,7 +27,7 @@ export default class HaAutomationTrigger extends LitElement {
></ha-automation-trigger-row>
`
)}
<ha-card>
<ha-card outlined>
<div class="card-actions add-card">
<mwc-button @click=${this._addTrigger}>
${this.hass.localize(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -187,6 +187,7 @@ class HaConfigSectionGeneral extends LitElement {
href="https://en.wikipedia.org/wiki/ISO_4217#Active_codes"
target="_blank"
rel="noopener noreferrer"
class="find-value"
>${this.hass.localize(
"ui.panel.config.core.section.core.core_config.find_currency_value"
)}</a
@@ -345,6 +346,10 @@ class HaConfigSectionGeneral extends LitElement {
ha-select {
display: block;
}
a.find-value {
margin-top: 8px;
display: inline-block;
}
ha-locations-editor {
display: block;
height: 400px;

View File

@@ -196,7 +196,7 @@ class HaConfigSectionUpdates extends LitElement {
display: flex;
justify-content: space-between;
flex-direction: column;
padding: 16px;
padding: 0;
}
`;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -93,7 +93,7 @@ export const configSections: { [name: string]: PageNavigation[] } = {
path: "/config/person",
translationKey: "people",
iconPath: mdiAccount,
iconColor: "#832EA6",
iconColor: "#5A87FA",
components: ["person", "users"],
},
{
@@ -224,14 +224,14 @@ export const configSections: { [name: string]: PageNavigation[] } = {
path: "/config/person",
translationKey: "ui.panel.config.person.caption",
iconPath: mdiAccount,
iconColor: "#E48629",
iconColor: "#5A87FA",
},
{
component: "users",
path: "/config/users",
translationKey: "ui.panel.config.users.caption",
iconPath: mdiBadgeAccountHorizontal,
iconColor: "#E48629",
iconColor: "#5A87FA",
core: true,
advancedOnly: true,
},
@@ -254,74 +254,74 @@ export const configSections: { [name: string]: PageNavigation[] } = {
},
],
general: [
{
path: "/config/general",
translationKey: "core",
iconPath: mdiCog,
iconColor: "#653249",
core: true,
},
{
path: "/config/updates",
translationKey: "ui.panel.config.updates.caption",
translationKey: "updates",
iconPath: mdiUpdate,
iconColor: "#3B808E",
},
{
component: "logs",
path: "/config/logs",
translationKey: "ui.panel.config.logs.caption",
translationKey: "logs",
iconPath: mdiMathLog,
iconColor: "#C65326",
core: true,
},
{
path: "/config/backup",
translationKey: "ui.panel.config.backup.caption",
translationKey: "backup",
iconPath: mdiBackupRestore,
iconColor: "#0D47A1",
component: "backup",
},
{
path: "/hassio/backups",
translationKey: "ui.panel.config.backup.caption",
translationKey: "backup",
iconPath: mdiBackupRestore,
iconColor: "#0D47A1",
component: "hassio",
},
{
path: "/config/analytics",
translationKey: "ui.panel.config.analytics.caption",
translationKey: "analytics",
iconPath: mdiShape,
iconColor: "#f1c447",
},
{
path: "/config/network",
translationKey: "ui.panel.config.network.caption",
translationKey: "network",
iconPath: mdiNetwork,
iconColor: "#B1345C",
},
{
path: "/config/storage",
translationKey: "ui.panel.config.storage.caption",
translationKey: "storage",
iconPath: mdiDatabase,
iconColor: "#518C43",
component: "hassio",
},
{
path: "/config/hardware",
translationKey: "ui.panel.config.hardware.caption",
translationKey: "hardware",
iconPath: mdiMemory,
iconColor: "#301A8E",
component: "hassio",
},
{
path: "/config/system_health",
translationKey: "ui.panel.config.system_health.caption",
translationKey: "system_health",
iconPath: mdiHeart,
iconColor: "#507FfE",
components: ["system_health", "hassio"],
},
{
path: "/config/general",
translationKey: "ui.panel.config.core.caption",
iconPath: mdiCog,
iconColor: "#653249",
core: true,
},
],
about: [
{

View File

@@ -8,6 +8,7 @@ import "../../../components/ha-alert";
import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-settings-row";
import { BOARD_NAMES } from "../../../data/hardware";
import {
extractApiErrorMessage,
ignoreSupervisorError,
@@ -56,6 +57,18 @@ class HaConfigHardware extends LitElement {
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.hardware.caption")}
>
<ha-button-menu corner="BOTTOM_START" slot="toolbar-icon">
<ha-icon-button
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>
<mwc-list-item @click=${this._openHardware}
>${this.hass.localize(
"ui.panel.config.hardware.available_hardware.title"
)}</mwc-list-item
>
</ha-button-menu>
${this._error
? html`
<ha-alert alert-type="error"
@@ -63,65 +76,57 @@ class HaConfigHardware extends LitElement {
>
`
: ""}
${this._OSData && this._hostData
${this._OSData || this._hostData
? html`
<div class="content">
<ha-card outlined>
<div class="card-content">
<ha-settings-row>
<span slot="heading"
>${this.hass.localize(
"ui.panel.config.hardware.board"
)}</span
>
<div slot="description">
<span class="value">${this._OSData.board}</span>
</div>
</ha-settings-row>
</div>
<div class="card-actions">
<div class="buttons">
${this._hostData.features.includes("reboot")
? html`
<ha-progress-button
class="warning"
@click=${this._hostReboot}
${this._OSData?.board
? html`
<div class="card-content">
<ha-settings-row>
<span slot="heading"
>${BOARD_NAMES[this._OSData.board] ||
this.hass.localize(
"ui.panel.config.hardware.board"
)}</span
>
${this.hass.localize(
"ui.panel.config.hardware.reboot_host"
)}
</ha-progress-button>
`
: ""}
${this._hostData.features.includes("shutdown")
? html`
<ha-progress-button
class="warning"
@click=${this._hostShutdown}
>
${this.hass.localize(
"ui.panel.config.hardware.shutdown_host"
)}
</ha-progress-button>
`
: ""}
</div>
<ha-button-menu corner="BOTTOM_START">
<ha-icon-button
.label=${this.hass.localize("common.menu")}
.path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>
<mwc-list-item
.action=${"hardware"}
@click=${this._openHardware}
>
${this.hass.localize(
"ui.panel.config.hardware.available_hardware.title"
)}
</mwc-list-item>
</ha-button-menu>
</div>
<div slot="description">
<span class="value">${this._OSData.board}</span>
</div>
</ha-settings-row>
</div>
`
: ""}
${this._hostData
? html`
<div class="card-actions">
${this._hostData.features.includes("reboot")
? html`
<ha-progress-button
class="warning"
@click=${this._hostReboot}
>
${this.hass.localize(
"ui.panel.config.hardware.reboot_host"
)}
</ha-progress-button>
`
: ""}
${this._hostData.features.includes("shutdown")
? html`
<ha-progress-button
class="warning"
@click=${this._hostShutdown}
>
${this.hass.localize(
"ui.panel.config.hardware.shutdown_host"
)}
</ha-progress-button>
`
: ""}
</div>
`
: ""}
</ha-card>
</div>
`
@@ -241,10 +246,6 @@ class HaConfigHardware extends LitElement {
justify-content: space-between;
align-items: center;
}
.buttons {
display: flex;
align-items: center;
}
`,
];
}

View File

@@ -3,17 +3,16 @@ import { property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/ha-logo-svg";
import {
fetchHassioHostInfo,
fetchHassioHassOsInfo,
fetchHassioHostInfo,
HassioHassOSInfo,
HassioHostInfo,
} from "../../../data/hassio/host";
import { HassioInfo, fetchHassioInfo } from "../../../data/hassio/supervisor";
import { fetchHassioInfo, HassioInfo } from "../../../data/hassio/supervisor";
import "../../../layouts/hass-subpage";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import "./integrations-card";
const JS_TYPE = __BUILD__;
const JS_VERSION = __VERSION__;
@@ -21,13 +20,13 @@ const JS_VERSION = __VERSION__;
class HaConfigInfo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public narrow!: boolean;
@property({ type: Boolean }) public narrow!: boolean;
@property() public isWide!: boolean;
@property({ type: Boolean }) public isWide!: boolean;
@property() public showAdvanced!: boolean;
@property({ type: Boolean }) public showAdvanced!: boolean;
@property() public route!: Route;
@property({ attribute: false }) public route!: Route;
@state() private _hostInfo?: HassioHostInfo;
@@ -61,18 +60,22 @@ class HaConfigInfo extends LitElement {
</ha-logo-svg>
</a>
<br />
<h2>Home Assistant Core ${hass.connection.haVersion}</h2>
<h3>Home Assistant Core ${hass.connection.haVersion}</h3>
${this._hassioInfo
? html`<h2>
Home Assistant Supervisor ${this._hassioInfo.supervisor}
</h2>`
? html`
<h3>
Home Assistant Supervisor ${this._hassioInfo.supervisor}
</h3>
`
: ""}
${this._osInfo?.version
? html`<h2>Home Assistant OS ${this._osInfo.version}</h2>`
? html`<h3>Home Assistant OS ${this._osInfo.version}</h3>`
: ""}
${this._hostInfo
? html`<h4>Kernel version ${this._hostInfo.kernel}</h4>
<h4>Agent version ${this._hostInfo.agent_version}</h4>`
? html`
<h4>Kernel version ${this._hostInfo.kernel}</h4>
<h4>Agent version ${this._hostInfo.agent_version}</h4>
`
: ""}
<p>
${this.hass.localize(
@@ -153,12 +156,6 @@ class HaConfigInfo extends LitElement {
: ""}
</p>
</div>
<div>
<integrations-card
.hass=${this.hass}
.narrow=${this.narrow}
></integrations-card>
</div>
</hass-subpage>
`;
}
@@ -217,18 +214,15 @@ class HaConfigInfo extends LitElement {
.about a {
color: var(--primary-color);
}
integrations-card {
display: block;
max-width: 600px;
margin: 0 auto;
padding-bottom: 16px;
}
ha-logo-svg {
padding: 12px;
height: 180px;
width: 180px;
}
h4 {
font-weight: 400;
}
`,
];
}

View File

@@ -1,210 +0,0 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import "../../../components/ha-card";
import {
domainToName,
fetchIntegrationManifests,
fetchIntegrationSetups,
integrationIssuesUrl,
IntegrationManifest,
IntegrationSetup,
} from "../../../data/integration";
import { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
import { documentationUrl } from "../../../util/documentation-url";
@customElement("integrations-card")
class IntegrationsCard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@state() private _manifests?: {
[domain: string]: IntegrationManifest;
};
@state() private _setups?: {
[domain: string]: IntegrationSetup;
};
private _sortedIntegrations = memoizeOne((components: string[]) =>
Array.from(
new Set(
components.map((comp) =>
comp.includes(".") ? comp.split(".")[1] : comp
)
)
).sort()
);
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._fetchManifests();
this._fetchSetups();
}
protected render(): TemplateResult {
return html`
<ha-card
.header=${this.hass.localize("ui.panel.config.info.integrations")}
>
<table class="card-content">
<thead>
<tr>
<th></th>
${!this.narrow
? html`<th></th>
<th></th>
<th></th>`
: ""}
<th>${this.hass.localize("ui.panel.config.info.setup_time")}</th>
</tr>
</thead>
<tbody>
${this._sortedIntegrations(this.hass!.config.components).map(
(domain) => {
const manifest = this._manifests && this._manifests[domain];
const docLink = manifest
? html`<a
href=${manifest.is_built_in
? documentationUrl(
this.hass,
`/integrations/${manifest.domain}`
)
: manifest.documentation}
target="_blank"
rel="noreferrer"
>${this.hass.localize(
"ui.panel.config.info.documentation"
)}</a
>`
: "";
const issueLink =
manifest && (manifest.is_built_in || manifest.issue_tracker)
? html`
<a
href=${integrationIssuesUrl(domain, manifest)}
target="_blank"
rel="noreferrer"
>${this.hass.localize(
"ui.panel.config.info.issues"
)}</a
>
`
: "";
const setupSeconds =
this._setups?.[domain]?.seconds?.toFixed(2);
return html`
<tr>
<td>
<img
loading="lazy"
src=${brandsUrl({
domain: domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
referrerpolicy="no-referrer"
/>
</td>
<td class="name">
${domainToName(
this.hass.localize,
domain,
manifest
)}<br />
<span class="domain">${domain}</span>
${this.narrow
? html`<div class="mobile-row">
<div>${docLink} ${issueLink}</div>
${setupSeconds ? html`${setupSeconds} s` : ""}
</div>`
: ""}
</td>
${this.narrow
? ""
: html`
<td>${docLink}</td>
<td>${issueLink}</td>
<td class="setup">
${setupSeconds ? html`${setupSeconds} s` : ""}
</td>
`}
</tr>
`;
}
)}
</tbody>
</table>
</ha-card>
`;
}
private async _fetchManifests() {
const manifests = {};
for (const manifest of await fetchIntegrationManifests(this.hass)) {
manifests[manifest.domain] = manifest;
}
this._manifests = manifests;
}
private async _fetchSetups() {
const setups = {};
for (const setup of await fetchIntegrationSetups(this.hass)) {
setups[setup.domain] = setup;
}
this._setups = setups;
}
static get styles(): CSSResultGroup {
return css`
table {
width: 100%;
}
td,
th {
padding: 0 8px;
}
td:first-child {
padding-left: 0;
}
td.name {
padding: 8px;
}
td.setup {
text-align: right;
white-space: nowrap;
direction: ltr;
}
th {
text-align: right;
}
.domain {
color: var(--secondary-text-color);
}
.mobile-row {
display: flex;
justify-content: space-between;
}
.mobile-row a:not(:last-of-type) {
margin-right: 4px;
}
img {
display: block;
max-height: 40px;
max-width: 40px;
}
a {
color: var(--primary-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"integrations-card": IntegrationsCard;
}
}

View File

@@ -21,6 +21,7 @@ import { fetchErrorLog } from "../../../data/error_log";
import { extractApiErrorMessage } from "../../../data/hassio/common";
import { fetchHassioLogs } from "../../../data/hassio/supervisor";
import { HomeAssistant } from "../../../types";
import { debounce } from "../../../common/util/debounce";
@customElement("error-log-card")
class ErrorLogCard extends LitElement {
@@ -76,6 +77,12 @@ class ErrorLogCard extends LitElement {
`;
}
private _debounceSearch = debounce(
() => (this._isLogLoaded ? this._refreshLogs() : this._debounceSearch()),
150,
false
);
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
@@ -93,11 +100,15 @@ class ErrorLogCard extends LitElement {
}
if (
(changedProps.has("filter") && this._isLogLoaded) ||
(changedProps.has("show") && this.show) ||
(changedProps.has("provider") && this.show)
) {
this._refreshLogs();
return;
}
if (changedProps.has("filter")) {
this._debounceSearch();
}
}
@@ -116,6 +127,18 @@ class ErrorLogCard extends LitElement {
if (isComponentLoaded(this.hass, "hassio")) {
try {
log = await fetchHassioLogs(this.hass, this.provider);
if (this.filter) {
log = log
.split("\n")
.filter((entry) =>
entry.toLowerCase().includes(this.filter.toLowerCase())
)
.join("\n");
}
if (!log) {
this._logHTML = this.hass.localize("ui.panel.config.logs.no_errors");
return;
}
this._logHTML = html`<ha-ansi-to-html .content=${log}>
</ha-ansi-to-html>`;
this._isLogLoaded = true;
@@ -136,31 +159,33 @@ class ErrorLogCard extends LitElement {
this._isLogLoaded = true;
this._logHTML = log
? log
.split("\n")
.filter((entry) => {
if (this.filter) {
return entry.toLowerCase().includes(this.filter.toLowerCase());
}
return entry;
})
.map((entry) => {
if (entry.includes("INFO"))
return html`<div class="info">${entry}</div>`;
const split = log && log.split("\n");
if (entry.includes("WARNING"))
return html`<div class="warning">${entry}</div>`;
this._logHTML = split
? (this.filter
? split.filter((entry) => {
if (this.filter) {
return entry.toLowerCase().includes(this.filter.toLowerCase());
}
return entry;
})
: split
).map((entry) => {
if (entry.includes("INFO"))
return html`<div class="info">${entry}</div>`;
if (
entry.includes("ERROR") ||
entry.includes("FATAL") ||
entry.includes("CRITICAL")
)
return html`<div class="error">${entry}</div>`;
if (entry.includes("WARNING"))
return html`<div class="warning">${entry}</div>`;
return html`<div>${entry}</div>`;
})
if (
entry.includes("ERROR") ||
entry.includes("FATAL") ||
entry.includes("CRITICAL")
)
return html`<div class="error">${entry}</div>`;
return html`<div>${entry}</div>`;
})
: this.hass.localize("ui.panel.config.logs.no_errors");
}

View File

@@ -6,6 +6,7 @@ import { extractSearchParam } from "../../../common/url/search-params";
import "../../../components/ha-button-menu";
import "../../../components/search-input";
import { LogProvider } from "../../../data/error_log";
import { fetchHassioSupervisorInfo } from "../../../data/hassio/supervisor";
import "../../../layouts/hass-subpage";
import "../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../resources/styles";
@@ -59,6 +60,8 @@ export class HaConfigLogs extends LitElement {
@state() private _selectedLogProvider = "core";
@state() private _logProviders = logProviders;
public connectedCallback() {
super.connectedCallback();
if (this.systemLog && this.systemLog.loaded) {
@@ -66,6 +69,13 @@ export class HaConfigLogs extends LitElement {
}
}
protected firstUpdated(changedProps): void {
super.firstUpdated(changedProps);
if (isComponentLoaded(this.hass, "hassio")) {
this._getInstalledAddons();
}
}
private async _filterChanged(ev) {
this._filter = ev.detail.value;
}
@@ -107,7 +117,7 @@ export class HaConfigLogs extends LitElement {
<ha-button-menu corner="BOTTOM_START" slot="toolbar-icon">
<mwc-button
slot="trigger"
.label=${logProviders.find(
.label=${this._logProviders.find(
(p) => p.key === this._selectedLogProvider
)!.name}
>
@@ -116,7 +126,7 @@ export class HaConfigLogs extends LitElement {
.path=${mdiChevronDown}
></ha-svg-icon>
</mwc-button>
${logProviders.map(
${this._logProviders.map(
(provider) => html`
<mwc-list-item
?selected=${provider.key === this._selectedLogProvider}
@@ -155,6 +165,21 @@ export class HaConfigLogs extends LitElement {
this._selectedLogProvider = (ev.currentTarget as any).provider;
}
private async _getInstalledAddons() {
try {
const supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
this._logProviders = [
...this._logProviders,
...supervisorInfo.addons.map((addon) => ({
key: addon.slug,
name: addon.name,
})),
];
} catch (err) {
// Ignore, nothing the user can do anyway
}
}
static get styles(): CSSResultGroup {
return [
haStyle,

View File

@@ -252,6 +252,8 @@ class ConfigUrlForm extends LitElement {
this._cloudStatus = cloudStatus;
if (cloudStatus.logged_in) {
this._showCustomExternalUrl = this._externalUrlValue !== null;
} else {
this._showCustomExternalUrl = true;
}
});
} else {

View File

@@ -111,6 +111,9 @@ export class HassioHostname extends LitElement {
justify-content: space-between;
align-items: center;
}
ha-settings-row {
border-top: none;
}
`;
}

View File

@@ -88,7 +88,7 @@ class HaConfigPerson extends LitElement {
</a>
</span>
<ha-card class="storage">
<ha-card outlined class="storage">
${this._storageItems.map(
(entry) => html`
<paper-icon-item @click=${this._openEditEntry} .entry=${entry}>
@@ -117,7 +117,7 @@ class HaConfigPerson extends LitElement {
</ha-card>
${this._configItems.length > 0
? html`
<ha-card header="Configuration.yaml persons">
<ha-card outlined header="Configuration.yaml persons">
${this._configItems.map(
(entry) => html`
<paper-icon-item>

View File

@@ -287,7 +287,7 @@ export class HaSceneEditor extends SubscribeMixin(
"ui.panel.config.scene.editor.introduction"
)}
</div>
<ha-card>
<ha-card outlined>
<div class="card-content">
<ha-textfield
.value=${this._config.name}
@@ -335,7 +335,7 @@ export class HaSceneEditor extends SubscribeMixin(
${devices.map(
(device) =>
html`
<ha-card>
<ha-card outlined>
<h1 class="card-header">
${device.name}
<ha-icon-button
@@ -373,6 +373,7 @@ export class HaSceneEditor extends SubscribeMixin(
)}
<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.scene.editor.devices.add"
)}
@@ -405,6 +406,7 @@ export class HaSceneEditor extends SubscribeMixin(
${entities.length
? html`
<ha-card
outlined
class="entities"
.header=${this.hass.localize(
"ui.panel.config.scene.editor.entities.without_device"
@@ -445,6 +447,7 @@ export class HaSceneEditor extends SubscribeMixin(
: ""}
<ha-card
outlined
header=${this.hass.localize(
"ui.panel.config.scene.editor.entities.add"
)}

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