Compare commits

...

315 Commits

Author SHA1 Message Date
Joakim Sørensen f69bce534a Update src/dialogs/analytics/dialog-analytics-optin.ts
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-04-27 16:30:52 +02:00
Joakim Sørensen 575f58bd88 Update src/dialogs/analytics/dialog-analytics-optin.ts
Co-authored-by: Charles Garwood <cgarwood@gmail.com>
2021-04-27 15:28:54 +02:00
Ludeeus 35535628fc reword 2021-04-27 11:33:39 +00:00
Ludeeus 8e018c9cfe add anonymized word 2021-04-27 11:23:53 +00:00
Ludeeus 5ae268b792 add analyticsLearnMore 2021-04-27 11:09:02 +00:00
Ludeeus 329732ac30 change button wording 2021-04-27 11:08:24 +00:00
Ludeeus 7f88bab552 Add analytics dialog 2021-04-27 11:06:25 +00:00
GitHub Action 9f3bb7f4d6 Translation update 2021-04-27 00:48:52 +00:00
Charles Garwood 73bb346c00 Show feedback for setting Z-Wave JS config parameters (#8956) 2021-04-27 01:20:23 +02:00
Philip Allgaier 33703a3b53 Add link to integration docs from service control (#8290)
* Add link to integration help to dev tool services

* Adjust to new service control

* Update src/translations/en.json

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

* Make icon less noticable + correct translation

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-04-27 00:28:22 +02:00
Charles Garwood b7a4f97eca Add opt-in toggle for zwave-js telemetry to config panel (#8958) 2021-04-27 00:19:48 +02:00
Bram Kragten dd4efe0f51 Apply dark style on init when prefers-color-scheme: dark (#8997) 2021-04-26 14:54:47 -07:00
Bram Kragten 7e0522c3b3 Don't do migration of service data in public prop (#8949)
Fixes #8879
2021-04-26 14:52:18 -07:00
Franck Nijhof e682abfb75 Tweak inputs for GitHub issue form (#8999) 2021-04-26 23:48:21 +02:00
Paulus Schoutsen 24e202a3d7 Use translations for config entry reason (#8981) 2021-04-26 17:50:23 +02:00
David F. Mulcahey ac9a881ab5 Fix ZHA network visualization page navigation (#8994)
* Fix ZHA visualization page navigation

* Update src/panels/config/integrations/integration-panels/zha/zha-network-visualization-page.ts

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

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-04-26 10:48:11 -04:00
Joakim Sørensen 4d287a1f83 Use top.history in dialogs and navigate (#8995) 2021-04-26 16:41:30 +02:00
Paulus Schoutsen b8d6b1ebdd Fetch manifests for discovered flows (#8987) 2021-04-26 07:33:00 -07:00
David F. Mulcahey 8ca1b9320d Initial custom configuration for ZHA (#8737) 2021-04-26 16:25:02 +02:00
Philip Allgaier cba3992d2b Make "Events" dev tools use screen space better (#7449) 2021-04-26 12:09:50 +02:00
Paulus Schoutsen 96d6e337be Document last step (#8979)
Co-authored-by: Philip Allgaier <mail@spacegaier.de>
2021-04-26 12:02:56 +02:00
GitHub Action 959f7ae046 Translation update 2021-04-25 00:48:30 +00:00
GitHub Action 9572a58764 Translation update 2021-04-24 00:48:31 +00:00
Paulus Schoutsen 393ae9e5dc Bumped version to 20210423.0 2021-04-23 15:23:32 -07:00
Paulus Schoutsen 63e10314bd Sketch out strategies (#8959)
Co-authored-by: Zack Arnett <arnett.zackary@gmail.com>
2021-04-23 09:36:45 -07:00
Paulus Schoutsen b599417a37 Improve rendering status text on integration cards (#8973) 2021-04-23 09:30:17 +02:00
Philip Allgaier 899eab4e5c Ensure 0 does not get formatted to empty string (#8971) 2021-04-23 09:29:03 +02:00
Paulus Schoutsen 3f21c87a3d Allow config entries to show the reason (#8974) 2021-04-23 09:25:09 +02:00
GitHub Action c296a60bab Translation update 2021-04-23 00:48:28 +00:00
Paulus Schoutsen 5f78f18cb4 Fix rendering of a choose without any action taken (#8952) 2021-04-22 21:01:09 +02:00
Paulus Schoutsen 0b8d356865 Clean up HUI-VIEW (#8967) 2021-04-22 09:46:15 -07:00
Bram Kragten e8d1318a5b Bump codemirror (#8953)
Fixes #8557
2021-04-21 19:22:56 +02:00
GitHub Action 07ce07c4a5 Translation update 2021-04-21 00:48:45 +00:00
Franck Nijhof a07220f383 Update GitHub issue form (#8954) 2021-04-20 12:37:59 +02:00
J. Nick Koston f21ed24a49 Make error optional in connection lost service check (#8937) 2021-04-20 10:58:39 +02:00
GitHub Action e3c38b93f4 Translation update 2021-04-20 00:48:22 +00:00
Aaron Godfrey b398727413 Allow falsey values for attribute value in a picture-elements card element. (#8943) 2021-04-19 18:51:55 +02:00
uvjustin 9bc2ab29a1 Version bump hls.js to v1.0.1 (#8951)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-04-19 17:25:19 +02:00
GitHub Action 51f1ff26f1 Translation update 2021-04-19 00:48:51 +00:00
Bram Kragten 97d5e6512d Fix link to config panels (#8936) 2021-04-17 22:12:07 -07:00
GitHub Action b76c67fc9b Translation update 2021-04-18 00:48:53 +00:00
Paulus Schoutsen b96a70cd55 Make the integration header banner smaller (#8935) 2021-04-16 23:51:48 -07:00
Paulus Schoutsen 982ab93cdb Do not vertically align integration icon (#8934) 2021-04-16 23:16:20 -07:00
Paulus Schoutsen c7f4e1152d Pass manifest to config flow card (#8933) 2021-04-16 23:02:29 -07:00
J. Nick Koston 519988326b Do not throw warnings when a service calls disconnects the websocket (#8932) 2021-04-16 20:59:10 -07:00
GitHub Action b518f4b03c Translation update 2021-04-17 00:48:32 +00:00
Paulus Schoutsen 5493fdfcb7 Bumped version to 20210416.0 2021-04-16 12:27:18 -07:00
Paulus Schoutsen 179767e9f8 Align layout of all cards (#8931)
* Align layout of all cards

* Make ignore card have normal button
2021-04-16 12:27:01 -07:00
Paulus Schoutsen 25b3bb1285 Fixes for integration cards (#8930) 2021-04-16 20:22:22 +02:00
Bram Kragten 841c8ab1f1 Update script editor (#8919) 2021-04-16 08:57:07 -07:00
Philip Allgaier 1ce17e2847 Remove non effective CSS for CM6 search panel input (#8921) 2021-04-16 16:29:36 +02:00
Philip Allgaier a09b206b0e Added missing <ul> to beta join dialog (#8927) 2021-04-16 16:06:52 +02:00
Carlos Garcia Saura bb4617c53b Correct two swapped supervisor beta join action/confirm texts (#8922) 2021-04-16 14:54:39 +02:00
Philip Allgaier cfd18bfb74 Corrected "not loaded" state string (#8925) 2021-04-16 14:45:40 +02:00
Philip Allgaier e225d6f546 Correct wording from "component" to "integration" on new integration page (#8924) 2021-04-16 14:41:38 +02:00
Paulus Schoutsen 60fe48d355 Show config entry state on card (#8911) 2021-04-16 13:16:59 +02:00
GitHub Action 2dcd0d2b0a Translation update 2021-04-16 00:48:38 +00:00
Bram Kragten 8e11aa9130 Fix activate scene button + allow removing icon (#8916) 2021-04-15 13:02:09 +02:00
Philip Allgaier f6e223c18d Use const everywhere for "group.default_view" (#8918) 2021-04-15 09:54:32 +02:00
Bram Kragten 9d29b55bee Add z-index to add user dialog (#8917) 2021-04-15 09:46:19 +02:00
GitHub Action 92aa8580db Translation update 2021-04-15 00:48:36 +00:00
Donnie 538028a003 Refactor sequence matching to accept item rather than word array (#8866)
* Refactor sequence matching to require an item rather than array of words to filter against

* change 'words' to 'strings'. Add tsdoc description for ScorableTextItem

* Replace type checking with 'as' to clean up code
2021-04-14 15:29:10 -07:00
Carlos Garcia Saura c53575a74f Set standard name for Cancel button, to align translations (#8914) 2021-04-14 23:09:31 +02:00
Bram Kragten 193016a46a Fix time selector + base am/pm on user language (#8908)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-04-14 21:51:29 +02:00
Bram Kragten aaa50b4d1d Don't add toast to history (#8915) 2021-04-14 12:01:42 -07:00
Bram Kragten a43120320e Bump typescript to 4.2.4 (#8876) 2021-04-14 12:00:24 -07:00
Paulus Schoutsen b8bb0c038d Highlight if log comes from custom component (#8912)
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
2021-04-14 11:59:00 -07:00
GitHub Action dc79fc2919 Translation update 2021-04-14 00:48:24 +00:00
Philip Allgaier 30787fef60 Hide new light color mode attributes in more-info (#8895) 2021-04-13 20:23:58 +02:00
J. Nick Koston 445ae156ef Unsubscribe when dismissing during wrap up (#8909) 2021-04-13 20:18:37 +02:00
Jakub Dąbrowski 62a0cfb0f6 Fix computing cards (#8894)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-04-13 16:25:48 +02:00
LJU 96bc3ef99a Improve spelling (#8901) 2021-04-13 15:49:53 +02:00
GitHub Action 1d3b95d24f Translation update 2021-04-13 00:49:02 +00:00
Bram Kragten 56fe4b07f3 Show toast with call service error (#8904) 2021-04-12 17:10:25 -07:00
Jakub Dąbrowski ea60f7005b Fix saving entities of the device in scene editor (#8884) 2021-04-12 23:04:35 +02:00
Philip Allgaier 9eb59062aa Increase supervisor metric value span width to account for blank (#8885) 2021-04-12 23:02:09 +02:00
Bram Kragten d00927c31f Update codemirror (#8903) 2021-04-12 22:04:58 +02:00
Charles Garwood c03017208d Remove link/text about ZHA/Z-Wave config panels moving to integration page (#8867) 2021-04-12 20:17:31 +02:00
GitHub Action 73f945458a Translation update 2021-04-12 00:48:46 +00:00
GitHub Action db12234611 Translation update 2021-04-11 00:48:30 +00:00
GitHub Action ed1cd4632f Translation update 2021-04-10 00:48:37 +00:00
Paulus Schoutsen 9833accc79 Fix failed conditions reason (#8870) 2021-04-08 23:01:12 -07:00
GitHub Action d46123771a Translation update 2021-04-09 00:48:50 +00:00
Charles Garwood 87fe84b1ac Add units to Z-Wave JS Node Config inputs (#8869)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-04-08 16:32:47 -07:00
Bram Kragten 21140f437e Update value of date input (#8865) 2021-04-08 16:31:46 -07:00
Paulus Schoutsen ba9e410393 Pass narrow (#8864) 2021-04-08 22:59:24 +02:00
Paulus Schoutsen 587fb2a170 Add logbook note (#8843)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-04-08 20:52:37 +02:00
Bram Kragten 7d801ff84c Handle choose being null (#8859)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-04-08 20:48:49 +02:00
Bram Kragten d69accd9a5 Add dev import buttons for debugging traces (#8860) 2021-04-08 11:32:31 -07:00
J. Nick Koston 1127750c5e Show which integrations are being setup at startup (#8834)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-04-08 07:30:47 -10:00
Bram Kragten 7758bd89c1 Check if logbook component loaded when fetching trace (#8861) 2021-04-08 09:04:08 -07:00
Philip Allgaier de7264327a Do not use "media_play_pause" but atomic services instead (#8845) 2021-04-08 16:47:04 +02:00
Philip Allgaier c3f0932794 Use number format setting for attribute rows (#8844) 2021-04-08 10:52:10 +02:00
Philip Allgaier 367907e037 Mention unique ID requirement in trace button tooltip (#8853) 2021-04-08 09:47:25 +02:00
Donnie 2d15bd651e Fix spinner regression and remove unnecessary twoline config (#8847) 2021-04-07 21:18:55 -07:00
GitHub Action 4b1d7863f8 Translation update 2021-04-08 00:48:34 +00:00
Paulus Schoutsen e425d768dd Remove owner guard from analytics (#8842) 2021-04-07 18:41:39 +02:00
Bram Kragten 9075146b47 Bumped version to 20210407.1 2021-04-07 16:21:07 +02:00
Bram Kragten 26c4591baa Keep root state when replacing, fix subpages for menu button on mobile (#8837) 2021-04-07 12:25:17 +02:00
Bram Kragten 2aac8c55e7 Guard for trace component not loaded (#8838) 2021-04-07 12:21:19 +02:00
Bram Kragten 9d6e07ff96 Bumped version to 20210407.0 2021-04-07 10:07:35 +02:00
Bram Kragten 8f58eee6af Update hui-timer-entity-row.ts 2021-04-07 10:07:17 +02:00
Paulus Schoutsen 8dd3d78f21 Tweak the analytics screens (#8833) 2021-04-07 03:48:10 +02:00
GitHub Action 48161fd02f Translation update 2021-04-07 00:48:35 +00:00
Joakim Sørensen b61410826d Add AppArmor reason (#8829) 2021-04-07 02:05:27 +02:00
Jaroslav Hanslík 2f0188b280 Fixed generic entity row for climate entities (#8369) 2021-04-06 22:31:47 +02:00
Bram Kragten 3a4fffdb0b Remove dynamic height/width calcs on graph nodes (#8832) 2021-04-06 22:31:22 +02:00
Bram Kragten 109910d18f Add spacer for default of choose (#8827) 2021-04-06 09:32:30 -07:00
Paulus Schoutsen 8874aaabe9 Bumped version to 20210406.0 2021-04-06 16:13:04 +00:00
Bram Kragten cafbea9c42 Update quick bar (#8823) 2021-04-06 09:05:29 -07:00
Bram Kragten 4843ee80a7 Use checkmark only in chosen choose (#8824) 2021-04-06 09:03:04 -07:00
Bram Kragten 4511c8f30c Don't show back button when no history (#8822)
* Don't show back button when no history

* Update src/translations/en.json

Co-authored-by: Philip Allgaier <mail@spacegaier.de>

Co-authored-by: Philip Allgaier <mail@spacegaier.de>
2021-04-06 08:59:08 -07:00
Thomas Lovén 4cf1e52ac0 Select one branch at a time in choose script graphs (#8812) 2021-04-06 14:38:42 +02:00
Bram Kragten b501b7f47c Change date picker (#8821) 2021-04-06 05:36:12 -07:00
Philip Allgaier cc275f9877 Prevent unwanted line breaks in picture-glance tooltips (#8819) 2021-04-06 14:19:24 +02:00
Philip Allgaier 7aae55cde7 Allow manually entering entity IDs in service target entity picker (#8820) 2021-04-06 14:18:55 +02:00
GitHub Action 85eaa219c6 Translation update 2021-04-06 00:48:51 +00:00
Donnie 7d5ecb8ba4 Update fuzzy scorer from VSCode (#8793) 2021-04-05 12:15:09 -07:00
GitHub Action 1fd142d337 Translation update 2021-04-05 00:48:55 +00:00
Josh McCarty d75c6aecbe Format input number (#8811) 2021-04-04 20:47:08 +02:00
Bram Kragten dffe0f656d Update styling trace tabs (#8807) 2021-04-03 23:19:06 -07:00
GitHub Action 890639436b Translation update 2021-04-04 00:48:42 +00:00
Charles Garwood 99f66d7c5d Fix zwave_js config panel manual entry inputs (#8806) 2021-04-03 16:08:41 +02:00
GitHub Action 05faa52425 Translation update 2021-04-03 00:48:30 +00:00
Bram Kragten 8f6ec03446 Bumped version to 20210402.1 2021-04-02 20:48:57 +02:00
Bram Kragten c56b4fade3 Add filtering by related entity + fixes (#8801) 2021-04-02 20:35:28 +02:00
Bram Kragten 61aaaabcb5 Add close button to import blueprint dialog (#8802) 2021-04-02 20:19:35 +02:00
Bram Kragten d57cf93580 Fix disabled icon button color (#8800)
Fixes #8797
2021-04-02 20:18:56 +02:00
Paulus Schoutsen 82ad5c103d Handle configurations that don't wrap their action sequences in arrays (#8798) 2021-04-02 14:45:34 +02:00
GitHub Action a0b5bc5456 Translation update 2021-04-02 00:48:47 +00:00
Bram Kragten 05ea3b8187 Make version number based on UTC time (#8796) 2021-04-01 15:33:11 -07:00
Bram Kragten 8301dffb21 Bumped version to 20210402.0 2021-04-02 00:14:02 +02:00
Bram Kragten 01be5243de Option flows dont have result (#8787) 2021-04-01 15:11:15 -07:00
Paulus Schoutsen 334196799a Improve keyboard nav (#8794) 2021-04-02 00:10:17 +02:00
Bram Kragten c11bbcf442 Add blueprint config to trace (#8751)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-04-01 23:37:46 +02:00
Bram Kragten 8e3a7576ea Align has template functions (#8784)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-04-01 13:49:59 -07:00
Bram Kragten deca6f03ba Improve ensureArray and use it in tracing (#8785)
* Improve ensureArray and use it in tracing

* Fix typing

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-04-01 13:33:47 -07:00
Paulus Schoutsen 401064d3c8 Render script execution state (#8789) 2021-04-01 11:29:08 -07:00
Paulus Schoutsen b6f59d3c98 Fix the automation picker icons (#8790) 2021-04-01 11:28:56 -07:00
Bram Kragten 1fb3663398 Add sortable last trigger column to automation and script overview (#8783) 2021-04-01 09:02:58 -07:00
Paulus Schoutsen 5c1604e959 Fix showing choose actions if default path chosen and other things (#8779) 2021-04-01 10:28:37 +02:00
GitHub Action 17b1f3e465 Translation update 2021-04-01 00:48:39 +00:00
Bram Kragten 9a68bdeec1 Handle errors in trace (#8775) 2021-03-31 09:35:30 -07:00
Bram Kragten 9b947ef734 Add top level logbook entries tab (#8776) 2021-03-31 09:15:06 -07:00
Bram Kragten d8153ac8fc Merge branch 'master' into dev 2021-03-31 17:02:45 +02:00
Bram Kragten 27d9f82f7d Bumped version to 20210331.0 2021-03-31 16:59:29 +02:00
Bram Kragten 5b55bcd879 Use logbook for trace logbook items (#8773) 2021-03-31 16:57:48 +02:00
Bram Kragten 5cfd28881b Update cloud-google-pref.ts 2021-03-31 16:53:18 +02:00
Philip Allgaier bc54a42e01 Fix typo in analytics info text (#8772) 2021-03-31 15:55:29 +02:00
Bram Kragten 03f9964c59 Fix max being undefined in automation (#8771) 2021-03-31 15:36:09 +02:00
Philip Allgaier f159219d2c Add Lovelace edit mode URL param (#8574)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-03-31 15:10:55 +02:00
Thomas Lovén e714f32737 Refactoring automation trace graphs (#8763)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-03-31 15:09:00 +02:00
Philip Allgaier 20858db96d Use service name in action confirmation popup (#8493) 2021-03-31 14:12:06 +02:00
dependabot[bot] 89b82bb778 Bump y18n from 3.2.1 to 3.2.2 (#8765)
Bumps [y18n](https://github.com/yargs/y18n) from 3.2.1 to 3.2.2.
- [Release notes](https://github.com/yargs/y18n/releases)
- [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yargs/y18n/commits)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-31 11:16:55 +02:00
David F. Mulcahey 2c886d739f Add feedback dialog for ZHA device reconfiguration (#8742)
* Add feedback dialog for ZHA device reconfiguration

* review comments

* fix scope
2021-03-31 11:16:35 +02:00
GitHub Action 1ccf4e49bc Translation update 2021-03-31 00:48:38 +00:00
Joakim Sørensen 7d63e3e088 Hide system generated integration options (#8764) 2021-03-30 23:09:08 +02:00
Joakim Sørensen 828523f281 Adjust documentation link (#8762) 2021-03-30 21:52:15 +02:00
Philip Allgaier afe3831f25 Make log levels translatable (#8761) 2021-03-30 20:19:50 +02:00
Philip Allgaier 3888c56f1a Add padding when loading traces or none found (#8760) 2021-03-30 20:16:06 +02:00
Bram Kragten 6f07966ef8 Update en.json 2021-03-30 10:48:04 +02:00
GitHub Action 09eafe8abd Translation update 2021-03-30 00:48:42 +00:00
Paulus Schoutsen 6719a42e27 Bumped version to 20210330.0 2021-03-30 00:15:58 +00:00
Bram Kragten 4b98a70ee8 Fix rendering when selecting all in datatable (#8749)
Fixes #8619
2021-03-29 17:15:08 -07:00
Bram Kragten db3f5447ca Add filtering by devices/areas to scripts (#8748) 2021-03-29 17:14:02 -07:00
Bram Kragten fed63f645d Add filtering by devices/areas to scenes (#8747) 2021-03-29 17:09:50 -07:00
Bram Kragten e7315bb570 Align filtering of integrations with other pages (#8746) 2021-03-29 17:07:11 -07:00
Bram Kragten cd2404f26a Add link between trace and editor (#8750) 2021-03-29 17:05:51 -07:00
Paulus Schoutsen b866166425 Adjust for latest trace API (#8755) 2021-03-29 17:01:39 -07:00
Bram Kragten 46580376dd Make trace somewhat useable on mobile (#8752) 2021-03-29 16:59:31 -07:00
Thomas Lovén b8bfb44aec Rename layout to view_layout (#8714) 2021-03-29 14:28:58 -07:00
Bram Kragten a153f572d0 Add screenshot for manifest (#8753) 2021-03-29 13:12:55 -07:00
Bram Kragten 66c30a59e7 Check if Google Assistant is linked (#8613)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-03-29 19:27:45 +02:00
Bram Kragten 10b8efc5cb Add filtering to automaton overview (#8736)
* Add filtering to automaton overview

* Update ha-automation-picker.ts

* Update ha-combo-box.ts

* imports

* Rename component

* localize + comments
2021-03-29 14:33:48 +02:00
Bram Kragten c65d414b7b Show if config entry is not loaded (#8717) 2021-03-29 11:33:41 +02:00
Bram Kragten 7f7d89c745 Hopefully fix some back button issues (#8708) 2021-03-29 11:29:43 +02:00
Joakim Sørensen 742028b691 Add analytics integration (#8695)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Philip Allgaier <mail@spacegaier.de>
2021-03-29 09:47:04 +02:00
GitHub Action 62f685bac2 Translation update 2021-03-29 00:48:39 +00:00
Paulus Schoutsen 0b3333e88c Bumped version to 20210328.0 2021-03-28 22:28:20 +00:00
Paulus Schoutsen c341a99b83 Add more trace visualization (#8724)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-03-28 20:31:59 +02:00
Josh McCarty f43c420d59 Create number formatting options on the profile panel (#7925) 2021-03-28 18:32:48 +02:00
Bram Kragten 0393970a80 Generalize filtering in datatable sub page (#8734) 2021-03-28 18:29:55 +02:00
Paulus Schoutsen 1865e0661f Add child_id to call service result in trace (#8728) 2021-03-27 20:06:02 -07:00
GitHub Action c07b1194b3 Translation update 2021-03-28 00:48:51 +00:00
Philip Allgaier bf802628b9 Add margin to text next to chip (#8726)
* Add margin to text next to chip

* Increase margin to 8px
2021-03-27 12:59:30 +01:00
GitHub Action 36020373cd Translation update 2021-03-27 00:48:37 +00:00
Bram Kragten 43e73d69de Handle edit mode for panel mode cards better (#8687) 2021-03-26 15:46:15 +01:00
Bram Kragten 47a3f649d2 Prevent cloud tts update prefs on page load (#8707) 2021-03-26 15:34:19 +01:00
Joakim Sørensen 5c63f8e52a Fix USB sync endpoint (#8725) 2021-03-26 14:14:54 +01:00
GitHub Action 01c553ef13 Translation update 2021-03-26 00:48:26 +00:00
Paulus Schoutsen f229e4e12a Make iterating ha-timeline efficient (#8721) 2021-03-25 14:59:21 -07:00
Paulus Schoutsen 40cf4c8d32 Add trace details foundation (#8716)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-03-25 11:58:29 -07:00
J. Nick Koston ee38c419de Ensure platform only integrations are displayed on the config info page (#8698)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-03-25 08:15:40 -10:00
Donnie 10baa34c18 Fix regression where command item text lost capitalization rules (#8719) 2021-03-25 18:20:20 +01:00
Donnie 343b67fa7f Move margin styling from ha-chip to ha-chip-set (#8718) 2021-03-25 09:03:47 -07:00
Donnie 6de8b4e35f Quick Bar: Use command category labels instead of icons (#7692)
* Change commands to use category labels instead of icons. Fixes several translation issues.

* Hydrate with latest dev and resolve conflicts

* Replace custom pill element with material chips

* Add category icons. Fix dark mode text colors

* Update src/components/ha-chip-set.ts

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

* Update src/dialogs/quick-bar/ha-quick-bar.ts

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

* Update src/dialogs/quick-bar/ha-quick-bar.ts

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

* Update src/components/ha-chip.ts

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

* Update src/components/ha-chip.ts

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

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-03-25 08:21:48 -07:00
Bram Kragten 57e535c2c8 Fix changing view type (#8688) 2021-03-25 14:21:38 +01:00
Bram Kragten af5b22a265 Fix picture glance editor + small opti in hui image (#8692) 2021-03-25 14:17:32 +01:00
Bram Kragten 77972c961b Service control: Don't fire value changed for optional empty input (#8705) 2021-03-25 14:15:57 +01:00
Bram Kragten a3efa5676b Log less in service worker (#8691) 2021-03-25 14:14:49 +01:00
Bram Kragten 014dbc2a86 Remove paper-material-styles (#8706)
Fix #8702
2021-03-25 13:12:31 +01:00
Bram Kragten 226a2941d6 Service dev tools: Disable UI mode when using templates (#8711) 2021-03-25 13:11:12 +01:00
Paulus Schoutsen c269c8fd3f Fix ordering of logbook entries inside choose sequence with multiple … (#8715) 2021-03-25 10:54:20 +01:00
GitHub Action d8fc3c1ebf Translation update 2021-03-25 00:36:40 +00:00
Charles Garwood a5c6ffd1b9 Fix Z-Wave JS Node Config Panel handling null values (#8710)
* attempt fix for null values

* cleanup
2021-03-24 14:02:45 -04:00
Thomas Lovén 9aaaaae175 Fix race condition in map card (#8697) 2021-03-24 09:49:28 +01:00
Philip Allgaier 7d39b69540 Ensure dev-tool-states is consistently case-insensitive (#8696) 2021-03-24 09:48:04 +01:00
GitHub Action 09bad14c3d Translation update 2021-03-24 01:30:48 +00:00
Paulus Schoutsen 369c9dc6e2 Bumped version to 20210324.0 2021-03-24 00:22:11 +00:00
Paulus Schoutsen 9676d2cee7 Add compatibility with latest trace API (#8700) 2021-03-23 17:21:57 -07:00
Paulus Schoutsen 5156c67226 Refactor trace rendering (#8693)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-03-23 17:06:59 +01:00
GitHub Action 4d48fc3d85 Translation update 2021-03-23 01:28:19 +00:00
Paulus Schoutsen 20da329a21 Fix types 2021-03-22 22:43:08 +00:00
Charles Garwood 4b664cc142 Add node config panel for Z-Wave JS (#8440) 2021-03-22 23:25:42 +01:00
Paulus Schoutsen c9b620fdb2 Update basic trace in gallery 2021-03-22 19:26:59 +00:00
twodice 25c886d401 Add an absolute height to cast receiver to fix height inheritance (#8667)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-03-22 13:11:01 +01:00
Joakim Sørensen 740805356f Add content trust reasons (#8674) 2021-03-22 12:25:59 +01:00
Philip Allgaier ce5fb57577 Add missing extra-fields translations (#8681) 2021-03-22 12:24:38 +01:00
GitHub Action 3e20d2b454 Translation update 2021-03-22 01:28:57 +00:00
GitHub Action a9e8186491 Translation update 2021-03-21 01:30:26 +00:00
GitHub Action 3bb909b026 Translation update 2021-03-20 01:26:31 +00:00
GitHub Action b921d91aeb Translation update 2021-03-19 01:27:47 +00:00
GitHub Action 05790954c6 Translation update 2021-03-18 01:26:33 +00:00
Marc Mueller 13014c1351 Update metadata license string (#8431) 2021-03-17 14:25:58 +01:00
Mick Vleeshouwer e34c63b830 Add word wrap (#8654) 2021-03-17 12:22:14 +01:00
GitHub Action 943100d758 Translation update 2021-03-17 01:27:02 +00:00
Paulus Schoutsen 55f40d66f2 Bumped version to 20210316.0 2021-03-16 23:07:01 +00:00
Paulus Schoutsen 593e5ac79c Link to traces from logbook entries (#8659)
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
2021-03-16 16:03:54 -07:00
Paulus Schoutsen ef31bce5ee Allow linking to trace (#8658) 2021-03-16 13:26:48 -07:00
Paulus Schoutsen 3c75eb96f1 Adjust traces to latest API (#8656) 2021-03-16 10:43:30 -07:00
Paulus Schoutsen f34dfde925 Fix spaces in changelog link (#8652) 2021-03-16 12:39:16 +01:00
Erik Montnemery e3b72fe0aa Use execute_script call in services developer tool (#8657) 2021-03-16 12:38:49 +01:00
GitHub Action 60de74a375 Translation update 2021-03-16 01:24:49 +00:00
Paulus Schoutsen 55e58f8d35 Make time a label without icon (#8649) 2021-03-15 09:39:29 +01:00
Paulus Schoutsen a465254418 Add new trace (#8633) 2021-03-15 09:38:23 +01:00
GitHub Action 5d27a138cf Translation update 2021-03-15 01:26:05 +00:00
Paulus Schoutsen 22f4b036df Bumped version to 20210314.0 2021-03-14 23:46:14 +00:00
Paulus Schoutsen 03f694922d Add timeline entry when a long period of time passes (#8638) 2021-03-14 16:45:49 -07:00
Paulus Schoutsen a841e287e5 Add basic action descriptions in traces (#8639) 2021-03-14 15:05:13 +01:00
Paulus Schoutsen 5d2afdd825 Add motion light trace (#8637) 2021-03-14 15:03:50 +01:00
Paulus Schoutsen 67240e2339 Group multiple logbook entries in traces (#8634) 2021-03-14 15:03:09 +01:00
Bram Kragten f84a8eccfa FIx accessibility of data tables (#8611)
According to #6487
2021-03-14 14:54:46 +01:00
GitHub Action 68a058e4f1 Translation update 2021-03-14 01:28:21 +00:00
Paulus Schoutsen d678b42ece Bumped version to 20210313.0 2021-03-13 04:40:35 +00:00
Paulus Schoutsen 2cf63cda08 Add download button 2021-03-13 04:35:47 +00:00
Paulus Schoutsen 7bd4eeb0df Trace foundation (#8608) 2021-03-12 20:13:06 -08:00
GitHub Action dc3ee7c779 Translation update 2021-03-13 01:24:27 +00:00
Thomas Lovén e8cc97a8e5 Enable turning off edit mode in panel views (#8625) 2021-03-12 16:26:18 +01:00
Philip Allgaier 3b837e1d54 Consistent spelling of "PIN" (#8618) 2021-03-12 09:43:00 +01:00
GitHub Action bb6c2050bc Translation update 2021-03-12 01:25:56 +00:00
GitHub Action 082d4f9691 Translation update 2021-03-11 01:25:12 +00:00
Joakim Sørensen 153d68a9cd Custom error page when failing to load Supervisor panel (#8465) 2021-03-10 14:11:03 +01:00
Bram Kragten 0404faa856 Update serviceworker with catch handler (#8601) 2021-03-09 20:33:11 -08:00
GitHub Action afbc2d6b8f Translation update 2021-03-10 01:24:36 +00:00
Bram Kragten 89ecc8bd2f Change preload to modulepreload (#8600) 2021-03-09 11:39:08 -08:00
Philip Allgaier 7f21a2b319 Properly align date time input fields around suffix separator (#8462) 2021-03-09 20:38:26 +01:00
Mick Vleeshouwer e2f07f6723 Add support for DEVICE_CLASS_CO and CO2 (#8602) 2021-03-09 19:32:06 +01:00
dependabot[bot] a475e143b7 Bump elliptic from 6.5.3 to 6.5.4 (#8603)
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.3 to 6.5.4.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.3...v6.5.4)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-09 19:31:27 +01:00
Paulus Schoutsen e50fd80b2e Add particles to onboarding (#8567) 2021-03-09 15:30:29 +01:00
Bram Kragten 68ea1abc05 Fallback to yaml for service data if unknown key is in data (#8595) 2021-03-09 15:26:26 +01:00
Bram Kragten 2e76b306c4 Add guard for when default url is not available (#8593) 2021-03-09 11:56:26 +01:00
Bram Kragten ca3cac4ed3 Fix missing areas in area picker (#8594) 2021-03-09 11:23:31 +01:00
Bram Kragten 41852460e1 Improve code mirror comments check (#8585) 2021-03-09 11:23:02 +01:00
Bram Kragten 9ec4e083d9 Change layout of automation yaml editor (#8560) 2021-03-09 11:21:59 +01:00
GitHub Action 9560a1c4a7 Translation update 2021-03-09 01:24:35 +00:00
Milan Meulemans 4f5a47ace7 Fix typo (#8587) 2021-03-08 13:23:30 +01:00
David F. Mulcahey 01c4d662f2 ZHA UI enhancements (#8573)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-03-08 13:23:14 +01:00
Bram Kragten 9bdda77e89 Fix demo states translation (#8586) 2021-03-08 13:04:58 +01:00
Bram Kragten 194024edb9 Fix demo states translation (#8586) 2021-03-08 13:02:28 +01:00
Ģirts bef0d3a6a1 Remove margin from button card icon if icon is all that is set (#8584) 2021-03-08 12:47:19 +01:00
Bram Kragten 47a024b795 Update translations 2021-03-08 09:46:13 +01:00
GitHub Action 39847f9c9d Translation update 2021-03-08 01:25:07 +00:00
Bram Kragten fa7bd28c92 Bumped version to 20210302.6 2021-03-07 23:20:37 +01:00
Philip Allgaier 279f78e4a8 Ensure dev-tools state attribute checkbox state gets stored (#8579) 2021-03-07 23:19:13 +01:00
Bram Kragten 8ec3cbdb33 Fix codemirror cursor color (#8571) 2021-03-07 23:18:55 +01:00
Philip Allgaier 7449f7e73f Handle delay templates properly + error handling tweaks (#8578)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-03-07 23:17:05 +01:00
Bram Kragten d680fde759 Don't allow UI editor for service calls with templates (#8581) 2021-03-07 23:16:49 +01:00
Philip Allgaier f24f21ca91 Handle delay templates properly + error handling tweaks (#8578)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-03-07 23:15:53 +01:00
Philip Allgaier c8ea37eec0 Consistent blank before "%" (#8366) 2021-03-07 23:05:20 +01:00
tkdrob b71f452795 Fix spelling (#8582) 2021-03-07 22:33:02 +01:00
Bram Kragten 7ea1ece169 Don't allow UI editor for service calls with templates (#8581) 2021-03-07 21:02:44 +01:00
Philip Allgaier aece3a37c0 Ensure dev-tools state attribute checkbox state gets stored (#8579) 2021-03-07 16:40:54 +01:00
GitHub Action 705871f8dc Translation update 2021-03-07 01:26:43 +00:00
Bram Kragten 4a11975349 Fix codemirror cursor color (#8571) 2021-03-06 23:11:57 +01:00
Philip Allgaier 4d3d27f2c4 Move log item level position + color in detail popup header (#8270) 2021-03-06 18:58:29 +01:00
Paulus Schoutsen d784a30d42 Allow sharing blueprints (#8565) 2021-03-06 14:19:56 +01:00
Paulus Schoutsen 35f776284b Better place where device edit button is on desktop (#8566) 2021-03-06 14:18:42 +01:00
Ville Skyttä f659a6fe37 Grammar and spelling fixes (#8568)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-03-06 14:17:30 +01:00
GitHub Action ad53c99fc4 Translation update 2021-03-06 01:22:48 +00:00
Philip Allgaier fa0172d00c Fix a few translation typos (#8563) 2021-03-05 22:24:44 +01:00
Bram Kragten 845411b48c Fix codemirror active line (#8558)
fixes #8556
2021-03-05 15:01:22 +01:00
Joakim Sørensen d715867b09 More consistant ignoring errors (#8553) 2021-03-05 10:40:49 +01:00
GitHub Action 0ca2cdfbed Translation update 2021-03-05 01:23:51 +00:00
Bram Kragten 0d1c72386e Bump codemirror to 0.18 (#8546) 2021-03-04 16:43:34 +01:00
Joakim Sørensen c91779dffe Add supervisor_add_addon_repository redirect (#8545) 2021-03-04 16:31:32 +01:00
Joakim Sørensen 3853cc9214 Check if addon is valid before navigating (#8538)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-03-04 11:51:35 +01:00
Joakim Sørensen a66b3f6b80 Fix managing custom addon repositories (#8536) 2021-03-04 10:29:00 +01:00
Joakim Sørensen c97ec32343 Fix missing name in full snapshot (#8535) 2021-03-04 10:25:51 +01:00
Josh McCarty 2abba7e445 Alarm numeric inputmode (#8521) 2021-03-04 10:08:16 +01:00
Tierney Cyren f887c27ad1 fix: move @types modules from deps to devDeps (#8539) 2021-03-04 10:05:28 +01:00
Joakim Sørensen 6ee8d74899 Remove duplicate save (#8532) 2021-03-04 10:03:09 +01:00
GitHub Action f196c72563 Translation update 2021-03-04 01:22:38 +00:00
Joakim Sørensen 419e564441 Use correct version (#8530) 2021-03-03 16:09:57 +01:00
Joakim Sørensen de97b54c95 Fix localize keys for supervisor update dialog (#8529) 2021-03-03 16:01:30 +01:00
Philip Allgaier 07001f7b5c Fix add-on toggles description translation keys (#8528) 2021-03-03 15:33:52 +01:00
Joakim Sørensen bee17fce64 Fix second load in firefox and localize init (#8525) 2021-03-03 15:06:36 +01:00
Bram Kragten 718904a853 Add max height to yaml editor (#8527) 2021-03-03 14:31:39 +01:00
Bram Kragten 72af4a69d6 Bump codemirror (#8524) 2021-03-03 12:25:51 +01:00
Joakim Sørensen fe50f4229c Fix missing localize on old core versions (#8522) 2021-03-03 11:05:04 +01:00
J. Nick Koston ca4de877c1 Add remote more info card (#8506)
Co-authored-by: Philip Allgaier <philip.allgaier@gmx.de>
2021-03-02 17:57:49 -10:00
GitHub Action 1dfecf9618 Translation update 2021-03-03 01:23:10 +00:00
Bram Kragten 0a3505ed89 Dont show config changes when user saved it (#8520) 2021-03-02 21:43:45 +01:00
Joakim Sørensen 33cbf7eabe Fix localize action (#8519) 2021-03-02 20:45:35 +01:00
Joakim Sørensen 935d97ce1a Fix reload of addon after update (#8518) 2021-03-02 17:05:11 +01:00
Joakim Sørensen 9f73f0ca8d Merge update dialogs (#8516) 2021-03-02 16:39:54 +01:00
390 changed files with 33769 additions and 6618 deletions
+2 -1
View File
@@ -84,7 +84,8 @@
"@typescript-eslint/no-unused-vars": 0,
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/explicit-module-boundary-types": 0,
"@typescript-eslint/no-shadow": ["error"]
"@typescript-eslint/no-shadow": ["error"],
"lit/attribute-value-entities": 0
},
"plugins": ["disable", "import", "lit", "prettier", "@typescript-eslint"],
"processor": "disable/disable"
+9 -26
View File
@@ -1,8 +1,6 @@
name: Report a bug with the UI, Frontend or Lovelace
about: Report an issue related to the Home Assistant frontend.
description: Report an issue related to the Home Assistant frontend.
labels: bug
title: ""
issue_body: true
body:
- type: markdown
attributes:
@@ -97,11 +95,7 @@ body:
If your issue is about how an entity is shown in the UI, please add the
state and attributes for all situations. You can find this information
at Developer Tools -> States.
value: |
```yaml
# Paste your state here.
```
render: txt
- type: textarea
attributes:
label: Problem-relevant frontend configuration
@@ -110,29 +104,18 @@ body:
configuration of the used cards. Fill this out even if it seems
unimportant to you. Please be sure to remove personal information like
passwords, private URLs and other credentials.
value: |
```yaml
# Paste your YAML here.
```
render: yaml
- type: textarea
attributes:
label: Javascript errors shown in your browser console/inspector
description: >
If you come across any Javascript or other error logs, e.g., in your
browser console/inspector please provide them.
value: |
```txt
# Paste your logs here.
```
- type: markdown
render: txt
- type: textarea
attributes:
value: |
## Additional information
- type: markdown
attributes:
value: |
label: Additional information
description: >
If you have any additional information for us, use the field below.
Please note, you can attach screenshots or screen recordings here,
by dragging and dropping files in the field below.
Please note, you can attach screenshots or screen recordings here, by
dragging and dropping files in the field below.
+1 -1
View File
@@ -7,7 +7,7 @@ on:
branches:
- dev
paths:
- translations/en.json
- src/translations/en.json
env:
NODE_VERSION: 12
+1 -1
View File
@@ -100,7 +100,7 @@ class HcLayout extends LitElement {
display: block;
margin: 0;
}
.hero {
border-radius: 4px 4px 0 0;
}
+3 -1
View File
@@ -35,11 +35,12 @@ class HcLovelace extends LitElement {
}
const lovelace: Lovelace = {
config: this.lovelaceConfig,
rawConfig: this.lovelaceConfig,
editMode: false,
urlPath: this.urlPath!,
enableFullEditMode: () => undefined,
mode: "storage",
language: "en",
locale: this.hass.locale,
saveConfig: async () => undefined,
deleteConfig: async () => undefined,
setEditMode: () => undefined,
@@ -94,6 +95,7 @@ class HcLovelace extends LitElement {
return css`
:host {
min-height: 100vh;
height: 0;
display: flex;
flex-direction: column;
box-sizing: border-box;
+9 -3
View File
@@ -221,11 +221,17 @@ export class HcMain extends HassElement {
}
private async _generateLovelaceConfig() {
const { generateLovelaceConfigFromHass } = await import(
"../../../../src/panels/lovelace/common/generate-lovelace-config"
const { generateLovelaceDashboardStrategy } = await import(
"../../../../src/panels/lovelace/strategies/get-strategy"
);
this._handleNewLovelaceConfig(
await generateLovelaceConfigFromHass(this.hass!)
await generateLovelaceDashboardStrategy(
{
hass: this.hass!,
narrow: false,
},
"original-states"
)
);
}
+349
View File
@@ -0,0 +1,349 @@
import { DemoTrace } from "./types";
export const basicTrace: DemoTrace = {
trace: {
last_step: "action/2",
run_id: "0",
state: "stopped",
timestamp: {
start: "2021-03-25T04:36:51.223693+00:00",
finish: "2021-03-25T04:36:51.266132+00:00",
},
trigger: "state of input_boolean.toggle_1",
domain: "automation",
item_id: "1615419646544",
trace: {
"trigger/0": [
{
path: "trigger/0",
timestamp: "2021-03-25T04:36:51.223693+00:00",
},
],
"condition/0": [
{
path: "condition/0",
timestamp: "2021-03-25T04:36:51.228243+00:00",
changed_variables: {
trigger: {
platform: "state",
entity_id: "input_boolean.toggle_1",
from_state: {
entity_id: "input_boolean.toggle_1",
state: "on",
attributes: {
editable: true,
friendly_name: "Toggle 1",
},
last_changed: "2021-03-24T19:03:59.141440+00:00",
last_updated: "2021-03-24T19:03:59.141440+00:00",
context: {
id: "5d0918eb379214d07554bdab6a08bcff",
parent_id: null,
user_id: null,
},
},
to_state: {
entity_id: "input_boolean.toggle_1",
state: "off",
attributes: {
editable: true,
friendly_name: "Toggle 1",
},
last_changed: "2021-03-25T04:36:51.220696+00:00",
last_updated: "2021-03-25T04:36:51.220696+00:00",
context: {
id: "664d6d261450a9ecea6738e97269a149",
parent_id: null,
user_id: "d1b4e89da01445fa8bc98e39fac477ca",
},
},
for: null,
attribute: null,
description: "state of input_boolean.toggle_1",
},
},
result: {
result: true,
},
},
],
"action/0": [
{
path: "action/0",
timestamp: "2021-03-25T04:36:51.243018+00:00",
changed_variables: {
trigger: {
platform: "state",
entity_id: "input_boolean.toggle_1",
from_state: {
entity_id: "input_boolean.toggle_1",
state: "on",
attributes: {
editable: true,
friendly_name: "Toggle 1",
},
last_changed: "2021-03-24T19:03:59.141440+00:00",
last_updated: "2021-03-24T19:03:59.141440+00:00",
context: {
id: "5d0918eb379214d07554bdab6a08bcff",
parent_id: null,
user_id: null,
},
},
to_state: {
entity_id: "input_boolean.toggle_1",
state: "off",
attributes: {
editable: true,
friendly_name: "Toggle 1",
},
last_changed: "2021-03-25T04:36:51.220696+00:00",
last_updated: "2021-03-25T04:36:51.220696+00:00",
context: {
id: "664d6d261450a9ecea6738e97269a149",
parent_id: null,
user_id: "d1b4e89da01445fa8bc98e39fac477ca",
},
},
for: null,
attribute: null,
description: "state of input_boolean.toggle_1",
},
context: {
id: "6cfcae368e7b3686fad6c59e83ae76c9",
parent_id: "664d6d261450a9ecea6738e97269a149",
user_id: null,
},
},
result: {
params: {
domain: "input_boolean",
service: "toggle",
service_data: {},
target: {
entity_id: ["input_boolean.toggle_4"],
},
},
running_script: false,
limit: 10,
},
},
],
"action/1": [
{
path: "action/1",
timestamp: "2021-03-25T04:36:51.252406+00:00",
result: {
choice: 0,
},
},
],
"action/1/choose/0": [
{
path: "action/1/choose/0",
timestamp: "2021-03-25T04:36:51.254569+00:00",
result: {
result: true,
},
},
],
"action/1/choose/0/conditions/0": [
{
path: "action/1/choose/0/conditions/0",
timestamp: "2021-03-25T04:36:51.254697+00:00",
result: {
result: true,
},
},
],
"action/1/choose/0/sequence/0": [
{
path: "action/1/choose/0/sequence/0",
timestamp: "2021-03-25T04:36:51.257360+00:00",
result: {
params: {
domain: "input_boolean",
service: "toggle",
service_data: {},
target: {
entity_id: ["input_boolean.toggle_2"],
},
},
running_script: false,
limit: 10,
},
},
],
"action/1/choose/0/sequence/1": [
{
path: "action/1/choose/0/sequence/1",
timestamp: "2021-03-25T04:36:51.260658+00:00",
result: {
params: {
domain: "input_boolean",
service: "toggle",
service_data: {},
target: {
entity_id: ["input_boolean.toggle_3"],
},
},
running_script: false,
limit: 10,
},
},
],
"action/2": [
{
path: "action/2",
timestamp: "2021-03-25T04:36:51.264159+00:00",
result: {
params: {
domain: "input_boolean",
service: "toggle",
service_data: {},
target: {
entity_id: ["input_boolean.toggle_4"],
},
},
running_script: false,
limit: 10,
},
},
],
},
config: {
id: "1615419646544",
alias: "Ensure Party mode",
description: "",
trigger: [
{
platform: "state",
entity_id: "input_boolean.toggle_1",
},
],
condition: [
{
condition: "template",
alias: "Test if Paulus is home",
value_template: "{{ true }}",
},
],
action: [
{
service: "input_boolean.toggle",
target: {
entity_id: "input_boolean.toggle_4",
},
},
{
choose: [
{
alias: "If toggle 3 is on",
conditions: [
{
condition: "template",
value_template:
"{{ is_state('input_boolean.toggle_3', 'on') }}",
},
],
sequence: [
{
service: "input_boolean.toggle",
alias: "Toggle 2 while 3 is on",
target: {
entity_id: "input_boolean.toggle_2",
},
},
{
service: "input_boolean.toggle",
alias: "Toggle 3",
target: {
entity_id: "input_boolean.toggle_3",
},
},
],
},
],
default: [
{
service: "input_boolean.toggle",
alias: "Toggle 2",
target: {
entity_id: "input_boolean.toggle_2",
},
},
],
},
{
service: "input_boolean.toggle",
target: {
entity_id: "input_boolean.toggle_4",
},
},
],
mode: "single",
},
context: {
id: "6cfcae368e7b3686fad6c59e83ae76c9",
parent_id: "664d6d261450a9ecea6738e97269a149",
user_id: null,
},
script_execution: "finished",
},
logbookEntries: [
{
name: "Ensure Party mode",
message: "has been triggered by state of input_boolean.toggle_1",
source: "state of input_boolean.toggle_1",
entity_id: "automation.toggle_toggles",
context_id: "6cfcae368e7b3686fad6c59e83ae76c9",
when: "2021-03-25T04:36:51.240832+00:00",
domain: "automation",
},
{
when: "2021-03-25T04:36:51.249828+00:00",
name: "Toggle 4",
state: "on",
entity_id: "input_boolean.toggle_4",
context_entity_id: "automation.toggle_toggles",
context_entity_id_name: "Ensure Party mode",
context_event_type: "automation_triggered",
context_domain: "automation",
context_name: "Ensure Party mode",
},
{
when: "2021-03-25T04:36:51.258947+00:00",
name: "Toggle 2",
state: "on",
entity_id: "input_boolean.toggle_2",
context_entity_id: "automation.toggle_toggles",
context_entity_id_name: "Ensure Party mode",
context_event_type: "automation_triggered",
context_domain: "automation",
context_name: "Ensure Party mode",
},
{
when: "2021-03-25T04:36:51.261806+00:00",
name: "Toggle 3",
state: "off",
entity_id: "input_boolean.toggle_3",
context_entity_id: "automation.toggle_toggles",
context_entity_id_name: "Ensure Party mode",
context_event_type: "automation_triggered",
context_domain: "automation",
context_name: "Ensure Party mode",
},
{
when: "2021-03-25T04:36:51.265246+00:00",
name: "Toggle 4",
state: "off",
entity_id: "input_boolean.toggle_4",
context_entity_id: "automation.toggle_toggles",
context_entity_id_name: "Ensure Party mode",
context_event_type: "automation_triggered",
context_domain: "automation",
context_name: "Ensure Party mode",
},
],
};
@@ -0,0 +1,44 @@
import { LogbookEntry } from "../../../../src/data/logbook";
import { AutomationTraceExtended } from "../../../../src/data/trace";
import { DemoTrace } from "./types";
export const mockDemoTrace = (
tracePartial: Partial<AutomationTraceExtended>,
logbookEntries?: LogbookEntry[]
): DemoTrace => ({
trace: {
last_step: "",
run_id: "0",
state: "stopped",
timestamp: {
start: "2021-03-25T04:36:51.223693+00:00",
finish: "2021-03-25T04:36:51.266132+00:00",
},
trigger: "mocked trigger",
domain: "automation",
item_id: "1615419646544",
trace: {
"trigger/0": [
{
path: "trigger/0",
changed_variables: {
trigger: {
description: "mocked trigger",
},
},
timestamp: "2021-03-25T04:36:51.223693+00:00",
},
],
},
config: {
trigger: [],
action: [],
},
context: {
id: "abcd",
},
script_execution: "finished",
...tracePartial,
},
logbookEntries: logbookEntries || [],
});
@@ -0,0 +1,214 @@
import { DemoTrace } from "./types";
export const motionLightTrace: DemoTrace = {
trace: {
last_step: "action/3",
run_id: "1",
state: "stopped",
timestamp: {
start: "2021-03-14T06:07:01.768006+00:00",
finish: "2021-03-14T06:07:53.287525+00:00",
},
trigger: "state of binary_sensor.pauluss_macbook_pro_camera_in_use",
domain: "automation",
item_id: "1614732497392",
trace: {
"trigger/0": [
{
path: "trigger/0",
timestamp: "2021-03-25T04:36:51.223693+00:00",
},
],
"action/0": [
{
path: "action/0",
timestamp: "2021-03-14T06:07:01.771038+00:00",
changed_variables: {
trigger: {
platform: "state",
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
from_state: {
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
state: "off",
attributes: {
friendly_name: "Pauluss MacBook Pro Camera In Use",
icon: "mdi:camera-off",
},
last_changed: "2021-03-14T06:06:29.235325+00:00",
last_updated: "2021-03-14T06:06:29.235325+00:00",
context: {
id: "ad4864c5ce957c38a07b50378eeb245d",
parent_id: null,
user_id: null,
},
},
to_state: {
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
state: "on",
attributes: {
friendly_name: "Pauluss MacBook Pro Camera In Use",
icon: "mdi:camera",
},
last_changed: "2021-03-14T06:07:01.762009+00:00",
last_updated: "2021-03-14T06:07:01.762009+00:00",
context: {
id: "e22ddfd5f11dc4aad9a52fc10dab613b",
parent_id: null,
user_id: null,
},
},
for: null,
attribute: null,
description:
"state of binary_sensor.pauluss_macbook_pro_camera_in_use",
},
context: {
id: "43b6ee9293a551c5cc14e8eb60af54ba",
parent_id: "e22ddfd5f11dc4aad9a52fc10dab613b",
user_id: null,
},
},
},
],
"action/1": [
{ path: "action/1", timestamp: "2021-03-14T06:07:01.875316+00:00" },
],
"action/2": [
{
path: "action/2",
timestamp: "2021-03-14T06:07:53.195013+00:00",
changed_variables: {
wait: {
remaining: null,
trigger: {
platform: "state",
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
from_state: {
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
state: "on",
attributes: {
friendly_name: "Pauluss MacBook Pro Camera In Use",
icon: "mdi:camera",
},
last_changed: "2021-03-14T06:07:01.762009+00:00",
last_updated: "2021-03-14T06:07:01.762009+00:00",
context: {
id: "e22ddfd5f11dc4aad9a52fc10dab613b",
parent_id: null,
user_id: null,
},
},
to_state: {
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
state: "off",
attributes: {
friendly_name: "Pauluss MacBook Pro Camera In Use",
icon: "mdi:camera-off",
},
last_changed: "2021-03-14T06:07:53.186755+00:00",
last_updated: "2021-03-14T06:07:53.186755+00:00",
context: {
id: "b2308cc91d509ea8e0c623331ab178d6",
parent_id: null,
user_id: null,
},
},
for: null,
attribute: null,
description:
"state of binary_sensor.pauluss_macbook_pro_camera_in_use",
},
},
},
},
],
"action/3": [
{
path: "action/3",
timestamp: "2021-03-14T06:07:53.196014+00:00",
},
],
},
config: {
mode: "restart",
max_exceeded: "silent",
trigger: [
{
platform: "state",
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
from: "off",
to: "on",
},
],
action: [
{
service: "light.turn_on",
target: {
entity_id: "light.elgato_key_light_air",
},
},
{
wait_for_trigger: [
{
platform: "state",
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
from: "on",
to: "off",
},
],
},
{
delay: 0,
},
{
service: "light.turn_off",
target: {
entity_id: "light.elgato_key_light_air",
},
},
],
id: "1614732497392",
alias: "Auto Elgato",
description: "",
},
context: {
id: "43b6ee9293a551c5cc14e8eb60af54ba",
parent_id: "e22ddfd5f11dc4aad9a52fc10dab613b",
user_id: null,
},
script_execution: "finished",
},
logbookEntries: [
{
name: "Auto Elgato",
message:
"has been triggered by state of binary_sensor.pauluss_macbook_pro_camera_in_use",
source: "state of binary_sensor.pauluss_macbook_pro_camera_in_use",
entity_id: "automation.auto_elgato",
when: "2021-03-14T06:07:01.768492+00:00",
domain: "automation",
},
{
when: "2021-03-14T06:07:01.872187+00:00",
name: "Elgato Key Light Air",
state: "on",
entity_id: "light.elgato_key_light_air",
context_entity_id: "automation.auto_elgato",
context_entity_id_name: "Auto Elgato",
context_event_type: "automation_triggered",
context_domain: "automation",
context_name: "Auto Elgato",
},
{
when: "2021-03-14T06:07:53.284505+00:00",
name: "Elgato Key Light Air",
state: "off",
entity_id: "light.elgato_key_light_air",
context_entity_id: "automation.auto_elgato",
context_entity_id_name: "Auto Elgato",
context_event_type: "automation_triggered",
context_domain: "automation",
context_name: "Auto Elgato",
},
],
};
+7
View File
@@ -0,0 +1,7 @@
import { AutomationTraceExtended } from "../../../../src/data/trace";
import { LogbookEntry } from "../../../../src/data/logbook";
export interface DemoTrace {
trace: AutomationTraceExtended;
logbookEntries: LogbookEntry[];
}
@@ -0,0 +1,102 @@
import { safeDump } from "js-yaml";
import {
customElement,
html,
css,
LitElement,
TemplateResult,
property,
} from "lit-element";
import "../../../src/components/ha-card";
import { describeAction } from "../../../src/data/script_i18n";
import { provideHass } from "../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../src/types";
const actions = [
{ wait_template: "{{ true }}", alias: "Something with an alias" },
{ delay: "0:05" },
{ wait_template: "{{ true }}" },
{
condition: "template",
value_template: "{{ true }}",
},
{ event: "happy_event" },
{
device_id: "abcdefgh",
domain: "plex",
entity_id: "media_player.kitchen",
},
{ scene: "scene.kitchen_morning" },
{
wait_for_trigger: [
{
platform: "state",
entity_id: "input_boolean.toggle_1",
},
],
},
{
variables: {
hello: "world",
},
},
{
service: "input_boolean.toggle",
target: {
entity_id: "input_boolean.toggle_4",
},
},
];
@customElement("demo-automation-describe-action")
export class DemoAutomationDescribeAction extends LitElement {
@property({ attribute: false }) hass!: HomeAssistant;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
<ha-card header="Actions">
${actions.map(
(conf) => html`
<div class="action">
<span>${describeAction(this.hass, conf as any)}</span>
<pre>${safeDump(conf)}</pre>
</div>
`
)}
</ha-card>
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
.action {
padding: 16px;
display: flex;
align-items: center;
justify-content: space-between;
}
span {
margin-right: 16px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-automation-describe-action": DemoAutomationDescribeAction;
}
}
@@ -0,0 +1,65 @@
import { safeDump } from "js-yaml";
import {
customElement,
html,
css,
LitElement,
TemplateResult,
} from "lit-element";
import "../../../src/components/ha-card";
import { describeCondition } from "../../../src/data/automation_i18n";
const conditions = [
{ condition: "and" },
{ condition: "not" },
{ condition: "or" },
{ condition: "state" },
{ condition: "numeric_state" },
{ condition: "sun", after: "sunset" },
{ condition: "sun", after: "sunrise" },
{ condition: "zone" },
{ condition: "time" },
{ condition: "template" },
];
@customElement("demo-automation-describe-condition")
export class DemoAutomationDescribeCondition extends LitElement {
protected render(): TemplateResult {
return html`
<ha-card header="Conditions">
${conditions.map(
(conf) => html`
<div class="condition">
<span>${describeCondition(conf as any)}</span>
<pre>${safeDump(conf)}</pre>
</div>
`
)}
</ha-card>
`;
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
.condition {
padding: 16px;
display: flex;
align-items: center;
justify-content: space-between;
}
span {
margin-right: 16px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-automation-describe-condition": DemoAutomationDescribeCondition;
}
}
@@ -0,0 +1,68 @@
import { safeDump } from "js-yaml";
import {
customElement,
html,
css,
LitElement,
TemplateResult,
} from "lit-element";
import "../../../src/components/ha-card";
import { describeTrigger } from "../../../src/data/automation_i18n";
const triggers = [
{ platform: "state" },
{ platform: "mqtt" },
{ platform: "geo_location" },
{ platform: "homeassistant" },
{ platform: "numeric_state" },
{ platform: "sun" },
{ platform: "time_pattern" },
{ platform: "webhook" },
{ platform: "zone" },
{ platform: "tag" },
{ platform: "time" },
{ platform: "template" },
{ platform: "event" },
];
@customElement("demo-automation-describe-trigger")
export class DemoAutomationDescribeTrigger extends LitElement {
protected render(): TemplateResult {
return html`
<ha-card header="Triggers">
${triggers.map(
(conf) => html`
<div class="trigger">
<span>${describeTrigger(conf as any)}</span>
<pre>${safeDump(conf)}</pre>
</div>
`
)}
</ha-card>
`;
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
.trigger {
padding: 16px;
display: flex;
align-items: center;
justify-content: space-between;
}
span {
margin-right: 16px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-automation-describe-trigger": DemoAutomationDescribeTrigger;
}
}
@@ -0,0 +1,87 @@
import {
customElement,
html,
css,
LitElement,
TemplateResult,
property,
} from "lit-element";
import "../../../src/components/ha-card";
import "../../../src/components/trace/hat-script-graph";
import "../../../src/components/trace/hat-trace-timeline";
import { provideHass } from "../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../src/types";
import { mockDemoTrace } from "../data/traces/mock-demo-trace";
import { DemoTrace } from "../data/traces/types";
const traces: DemoTrace[] = [
mockDemoTrace({ state: "running" }),
mockDemoTrace({ state: "debugged" }),
mockDemoTrace({ state: "stopped", script_execution: "failed_conditions" }),
mockDemoTrace({ state: "stopped", script_execution: "failed_single" }),
mockDemoTrace({ state: "stopped", script_execution: "failed_max_runs" }),
mockDemoTrace({ state: "stopped", script_execution: "finished" }),
mockDemoTrace({ state: "stopped", script_execution: "aborted" }),
mockDemoTrace({
state: "stopped",
script_execution: "error",
error: 'Variable "beer" cannot be None',
}),
mockDemoTrace({ state: "stopped", script_execution: "cancelled" }),
];
@customElement("demo-automation-trace-timeline")
export class DemoAutomationTraceTimeline extends LitElement {
@property({ attribute: false }) hass?: HomeAssistant;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
${traces.map(
(trace) => html`
<ha-card .header=${trace.trace.config.alias}>
<div class="card-content">
<hat-trace-timeline
.hass=${this.hass}
.trace=${trace.trace}
.logbookEntries=${trace.logbookEntries}
></hat-trace-timeline>
<button @click=${() => console.log(trace)}>Log trace</button>
</div>
</ha-card>
`
)}
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px;
}
.card-content {
display: flex;
}
button {
position: absolute;
top: 0;
right: 0;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-automation-trace-timeline": DemoAutomationTraceTimeline;
}
}
@@ -0,0 +1,98 @@
import {
customElement,
html,
css,
LitElement,
TemplateResult,
internalProperty,
property,
} from "lit-element";
import "../../../src/components/ha-card";
import "../../../src/components/trace/hat-script-graph";
import "../../../src/components/trace/hat-trace-timeline";
import { provideHass } from "../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../src/types";
import { DemoTrace } from "../data/traces/types";
import { basicTrace } from "../data/traces/basic_trace";
import { motionLightTrace } from "../data/traces/motion-light-trace";
const traces: DemoTrace[] = [basicTrace, motionLightTrace];
@customElement("demo-automation-trace")
export class DemoAutomationTrace extends LitElement {
@property({ attribute: false }) hass?: HomeAssistant;
@internalProperty() private _selected = {};
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
${traces.map(
(trace, idx) => html`
<ha-card .header=${trace.trace.config.alias}>
<div class="card-content">
<hat-script-graph
.trace=${trace.trace}
.selected=${this._selected[idx]}
@graph-node-selected=${(ev) => {
this._selected = { ...this._selected, [idx]: ev.detail.path };
}}
></hat-script-graph>
<hat-trace-timeline
allowPick
.hass=${this.hass}
.trace=${trace.trace}
.logbookEntries=${trace.logbookEntries}
.selectedPath=${this._selected[idx]}
@value-changed=${(ev) => {
this._selected = {
...this._selected,
[idx]: ev.detail.value,
};
}}
></hat-trace-timeline>
<button @click=${() => console.log(trace)}>Log trace</button>
</div>
</ha-card>
`
)}
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px;
}
.card-content {
display: flex;
}
.card-content > * {
margin-right: 16px;
}
.card-content > *:last-child {
margin-right: 0;
}
button {
position: absolute;
top: 0;
right: 0;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-automation-trace": DemoAutomationTrace;
}
}
+350
View File
@@ -0,0 +1,350 @@
import {
customElement,
html,
css,
internalProperty,
LitElement,
TemplateResult,
property,
} from "lit-element";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-switch";
import { IntegrationManifest } from "../../../src/data/integration";
import { provideHass } from "../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../src/types";
import "../../../src/panels/config/integrations/ha-integration-card";
import "../../../src/panels/config/integrations/ha-ignored-config-entry-card";
import "../../../src/panels/config/integrations/ha-config-flow-card";
import type {
ConfigEntryExtended,
DataEntryFlowProgressExtended,
} from "../../../src/panels/config/integrations/ha-config-integrations";
import { DeviceRegistryEntry } from "../../../src/data/device_registry";
import { EntityRegistryEntry } from "../../../src/data/entity_registry";
import { classMap } from "lit-html/directives/class-map";
const createConfigEntry = (
title: string,
override: Partial<ConfigEntryExtended> = {}
): ConfigEntryExtended => ({
entry_id: title,
domain: "esphome",
localized_domain_name: "ESPHome",
title,
source: "zeroconf",
state: "loaded",
connection_class: "local_push",
supports_options: false,
supports_unload: true,
disabled_by: null,
reason: null,
...override,
});
const createManifest = (
isCustom: boolean,
isCloud: boolean,
name = "ESPHome"
): IntegrationManifest => ({
name,
domain: "esphome",
is_built_in: !isCustom,
config_flow: false,
documentation: "https://www.home-assistant.io/integrations/esphome/",
iot_class: isCloud ? "cloud_polling" : "local_polling",
});
const loadedEntry = createConfigEntry("Loaded");
const nameAsDomainEntry = createConfigEntry("ESPHome");
const longNameEntry = createConfigEntry(
"Entry with a super long name that is going to the next line"
);
const configPanelEntry = createConfigEntry("Config Panel", {
domain: "mqtt",
localized_domain_name: "MQTT",
});
const optionsFlowEntry = createConfigEntry("Options Flow", {
supports_options: true,
});
const setupErrorEntry = createConfigEntry("Setup Error", {
state: "setup_error",
});
const migrationErrorEntry = createConfigEntry("Migration Error", {
state: "migration_error",
});
const setupRetryEntry = createConfigEntry("Setup Retry", {
state: "setup_retry",
});
const setupRetryReasonEntry = createConfigEntry("Setup Retry", {
state: "setup_retry",
reason: "connection_error",
});
const setupRetryReasonMissingKeyEntry = createConfigEntry("Setup Retry", {
state: "setup_retry",
reason: "resolve_error",
});
const failedUnloadEntry = createConfigEntry("Failed Unload", {
state: "failed_unload",
});
const notLoadedEntry = createConfigEntry("Not Loaded", { state: "not_loaded" });
const disabledEntry = createConfigEntry("Disabled", {
state: "not_loaded",
disabled_by: "user",
});
const disabledFailedUnloadEntry = createConfigEntry(
"Disabled - Failed Unload",
{
state: "failed_unload",
disabled_by: "user",
}
);
const configFlows: DataEntryFlowProgressExtended[] = [
{
flow_id: "adbb401329d8439ebb78ef29837826a8",
handler: "roku",
context: {
source: "ssdp",
unique_id: "YF008D862864",
title_placeholders: {
name: "Living room Roku",
},
},
step_id: "discovery_confirm",
localized_title: "Living room Roku",
},
{
flow_id: "adbb401329d8439ebb78ef29837826a8",
handler: "hue",
context: {
source: "reauth",
unique_id: "YF008D862864",
title_placeholders: {
name: "Living room Roku",
},
},
step_id: "discovery_confirm",
localized_title: "Philips Hue",
},
];
const configEntries: Array<{
items: ConfigEntryExtended[];
is_custom?: boolean;
disabled?: boolean;
highlight?: string;
}> = [
{ items: [loadedEntry] },
{ items: [configPanelEntry] },
{ items: [optionsFlowEntry] },
{ items: [nameAsDomainEntry] },
{ items: [longNameEntry] },
{ items: [setupErrorEntry] },
{ items: [migrationErrorEntry] },
{ items: [setupRetryEntry] },
{ items: [setupRetryReasonEntry] },
{ items: [setupRetryReasonMissingKeyEntry] },
{ items: [failedUnloadEntry] },
{ items: [notLoadedEntry] },
{
items: [
loadedEntry,
setupErrorEntry,
migrationErrorEntry,
longNameEntry,
setupRetryEntry,
failedUnloadEntry,
notLoadedEntry,
disabledEntry,
nameAsDomainEntry,
configPanelEntry,
optionsFlowEntry,
],
},
{ disabled: true, items: [disabledEntry] },
{ disabled: true, items: [disabledFailedUnloadEntry] },
{
disabled: true,
items: [disabledEntry, disabledFailedUnloadEntry],
},
{
items: [loadedEntry, configPanelEntry],
highlight: "Loaded",
},
];
const createEntityRegistryEntries = (
item: ConfigEntryExtended
): EntityRegistryEntry[] => [
{
config_entry_id: item.entry_id,
device_id: "mock-device-id",
area_id: null,
disabled_by: null,
entity_id: "binary_sensor.updater",
name: null,
icon: null,
platform: "updater",
},
];
const createDeviceRegistryEntries = (
item: ConfigEntryExtended
): DeviceRegistryEntry[] => [
{
entry_type: null,
config_entries: [item.entry_id],
connections: [],
manufacturer: "ESPHome",
model: "Mock Device",
name: "Tag Reader",
sw_version: null,
id: "mock-device-id",
identifiers: [],
via_device_id: null,
area_id: null,
name_by_user: null,
disabled_by: null,
},
];
@customElement("demo-integration-card")
export class DemoIntegrationCard extends LitElement {
@property({ attribute: false }) hass?: HomeAssistant;
@internalProperty() isCustomIntegration = false;
@internalProperty() isCloud = false;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
<div class="container">
<div class="filters">
<ha-formfield label="Custom Integration">
<ha-switch @change=${this._toggleCustomIntegration}></ha-switch>
</ha-formfield>
<ha-formfield label="Relies on cloud">
<ha-switch @change=${this._toggleCloud}></ha-switch>
</ha-formfield>
</div>
<ha-ignored-config-entry-card
.hass=${this.hass}
.entry=${createConfigEntry("Ignored Entry")}
.manifest=${createManifest(this.isCustomIntegration, this.isCloud)}
></ha-ignored-config-entry-card>
${configFlows.map(
(flow) => html`
<ha-config-flow-card
.hass=${this.hass}
.flow=${flow}
.manifest=${createManifest(
this.isCustomIntegration,
this.isCloud,
flow.handler === "roku" ? "Roku" : "Philips Hue"
)}
></ha-config-flow-card>
`
)}
${configEntries.map(
(info) => html`
<ha-integration-card
class=${classMap({
highlight: info.highlight !== undefined,
})}
.hass=${this.hass}
domain="esphome"
.items=${info.items}
.manifest=${createManifest(
this.isCustomIntegration,
this.isCloud
)}
.entityRegistryEntries=${createEntityRegistryEntries(
info.items[0]
)}
.deviceRegistryEntries=${createDeviceRegistryEntries(
info.items[0]
)}
?disabled=${info.disabled}
.selectedConfigEntryId=${info.highlight}
></ha-integration-card>
`
)}
</div>
<div class="container">
<!-- One that is standalone to see how it increases height if height
not defined by other cards. -->
<ha-integration-card
.hass=${this.hass}
domain="esphome"
.items=${[
loadedEntry,
setupErrorEntry,
migrationErrorEntry,
setupRetryEntry,
failedUnloadEntry,
]}
.manifest=${createManifest(this.isCustomIntegration, this.isCloud)}
.entityRegistryEntries=${createEntityRegistryEntries(loadedEntry)}
.deviceRegistryEntries=${createDeviceRegistryEntries(loadedEntry)}
></ha-integration-card>
</div>
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en");
// Normally this string is loaded from backend
hass.addTranslations(
{
"component.esphome.config.error.connection_error":
"Can't connect to ESP. Please make sure your YAML file contains an 'api:' line.",
},
"en"
);
}
private _toggleCustomIntegration() {
this.isCustomIntegration = !this.isCustomIntegration;
}
private _toggleCloud() {
this.isCloud = !this.isCloud;
}
static get styles() {
return css`
.container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-gap: 16px 16px;
padding: 8px 16px 16px;
margin-bottom: 64px;
}
.container > * {
max-width: 500px;
}
ha-formfield {
margin: 8px 0;
display: block;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-integration-card": DemoIntegrationCard;
}
}
+5 -1
View File
@@ -81,4 +81,8 @@ class DemoMoreInfoLight extends LitElement {
}
}
customElements.define("demo-more-info-light", DemoMoreInfoLight);
declare global {
interface HTMLElementTagNameMap {
"demo-more-info-light": DemoMoreInfoLight;
}
}
+7 -35
View File
@@ -111,29 +111,9 @@ class HaGallery extends PolymerElement {
</template>
</ha-card>
<ha-card header="More Info Demos">
<div class="card-content intro">
<p>
More info screens show up when an entity is clicked.
</p>
</div>
<template is="dom-repeat" items="[[_moreInfoDemos]]">
<a href="#[[item]]">
<paper-item>
<paper-item-body>{{ item }}</paper-item-body>
<ha-icon icon="hass:chevron-right"></ha-icon>
</paper-item>
</a>
</template>
</ha-card>
<ha-card header="Util Demos">
<div class="card-content intro">
<p>
Test pages for our utility functions.
</p>
</div>
<template is="dom-repeat" items="[[_utilDemos]]">
<ha-card header="Other Demos">
<div class="card-content intro"></div>
<template is="dom-repeat" items="[[_restDemos]]">
<a href="#[[item]]">
<paper-item>
<paper-item-body>{{ item }}</paper-item-body>
@@ -178,13 +158,9 @@ class HaGallery extends PolymerElement {
type: Array,
computed: "_computeLovelace(_demos)",
},
_moreInfoDemos: {
_restDemos: {
type: Array,
computed: "_computeMoreInfos(_demos)",
},
_utilDemos: {
type: Array,
computed: "_computeUtil(_demos)",
computed: "_computeRest(_demos)",
},
};
}
@@ -237,12 +213,8 @@ class HaGallery extends PolymerElement {
return demos.filter((demo) => demo.includes("hui"));
}
_computeMoreInfos(demos) {
return demos.filter((demo) => demo.includes("more-info"));
}
_computeUtil(demos) {
return demos.filter((demo) => demo.includes("util"));
_computeRest(demos) {
return demos.filter((demo) => !demo.includes("hui"));
}
}
+18 -6
View File
@@ -14,7 +14,9 @@ import { html, TemplateResult } from "lit-html";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import { navigate } from "../../../src/common/navigate";
import "../../../src/common/search/search-input";
import { extractSearchParam } from "../../../src/common/url/search-params";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-svg-icon";
import {
@@ -137,6 +139,12 @@ class HassioAddonStore extends LitElement {
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
const repositoryUrl = extractSearchParam("repository_url");
navigate(this, "/hassio/store", true);
if (repositoryUrl) {
this._manageRepositories(repositoryUrl);
}
this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev));
this._loadData();
}
@@ -170,7 +178,7 @@ class HassioAddonStore extends LitElement {
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._manageRepositories();
this._manageRepositoriesClicked();
break;
case 1:
this.refreshData();
@@ -187,10 +195,14 @@ class HassioAddonStore extends LitElement {
}
}
private async _manageRepositories() {
private _manageRepositoriesClicked() {
this._manageRepositories();
}
private async _manageRepositories(url?: string) {
showRepositoriesDialog(this, {
supervisor: this.supervisor,
loadData: () => this._loadData(),
url,
});
}
@@ -199,9 +211,9 @@ class HassioAddonStore extends LitElement {
}
private async _loadData() {
fireEvent(this, "supervisor-colllection-refresh", { colllection: "addon" });
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "supervisor",
fireEvent(this, "supervisor-collection-refresh", { collection: "addon" });
fireEvent(this, "supervisor-collection-refresh", {
collection: "supervisor",
});
}
@@ -165,7 +165,7 @@ class HassioAddonConfig extends LitElement {
@click=${this._saveTapped}
.disabled=${!this._configHasChanged || !this._valid}
>
Save ${this.supervisor.localize("common.save")}
${this.supervisor.localize("common.save")}
</ha-progress-button>
</div>
</ha-card>
@@ -21,6 +21,7 @@ import { extractSearchParam } from "../../../src/common/url/search-params";
import "../../../src/components/ha-circular-progress";
import {
fetchHassioAddonInfo,
fetchHassioAddonsInfo,
HassioAddonDetails,
} from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
@@ -173,9 +174,17 @@ class HassioAddonDashboard extends LitElement {
protected async firstUpdated(): Promise<void> {
if (this.route.path === "") {
const addon = extractSearchParam("addon");
if (addon) {
navigate(this, `/hassio/addon/${addon}`, true);
const requestedAddon = extractSearchParam("addon");
if (requestedAddon) {
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
const validAddon = addonsInfo.addons.some(
(addon) => addon.slug === requestedAddon
);
if (!validAddon) {
this._error = this.supervisor.localize("my.error_addon_not_found");
} else {
navigate(this, `/hassio/addon/${requestedAddon}`, true);
}
}
}
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
@@ -191,8 +200,8 @@ class HassioAddonDashboard extends LitElement {
const path: string = pathSplit[pathSplit.length - 1];
if (["uninstall", "install", "update", "start", "stop"].includes(path)) {
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "supervisor",
fireEvent(this, "supervisor-collection-refresh", {
collection: "supervisor",
});
}
@@ -50,6 +50,7 @@ import {
startHassioAddon,
stopHassioAddon,
uninstallHassioAddon,
updateHassioAddon,
validateHassioAddonOption,
} from "../../../../src/data/hassio/addon";
import {
@@ -68,8 +69,8 @@ import { HomeAssistant } from "../../../../src/types";
import { bytesToString } from "../../../../src/util/bytes-to-string";
import "../../components/hassio-card-content";
import "../../components/supervisor-metric";
import { showDialogSupervisorAddonUpdate } from "../../dialogs/addon/show-dialog-addon-update";
import { showHassioMarkdownDialog } from "../../dialogs/markdown/show-dialog-hassio-markdown";
import { showDialogSupervisorUpdate } from "../../dialogs/update/show-dialog-update";
import { hassioStyle } from "../../resources/hassio-style";
import { addonArchIsSupported } from "../../util/addon";
@@ -241,14 +242,18 @@ class HassioAddonInfo extends LitElement {
? html`
Current version: ${this.addon.version}
<div class="changelog" @click=${this._openChangelog}>
(<span class="changelog-link">
${this.supervisor.localize("addon.dashboard.changelog")} </span
(<span class="changelog-link"
>${this.supervisor.localize(
"addon.dashboard.changelog"
)}</span
>)
</div>
`
: html`<span class="changelog-link" @click=${this._openChangelog}>
${this.supervisor.localize("addon.dashboard.changelog")}
</span>`}
: html`<span class="changelog-link" @click=${this._openChangelog}
>${this.supervisor.localize(
"addon.dashboard.changelog"
)}</span
>`}
</div>
<div class="description light-color">
@@ -476,7 +481,7 @@ class HassioAddonInfo extends LitElement {
</span>
<span slot="description">
${this.supervisor.localize(
"addon.dashboard.option.boot.description"
"addon.dashboard.option.watchdog.description"
)}
</span>
<ha-switch
@@ -498,7 +503,7 @@ class HassioAddonInfo extends LitElement {
</span>
<span slot="description">
${this.supervisor.localize(
"addon.dashboard.option.boot.description"
"addon.dashboard.option.auto_update.description"
)}
</span>
<ha-switch
@@ -983,7 +988,30 @@ class HassioAddonInfo extends LitElement {
}
private async _updateClicked(): Promise<void> {
showDialogSupervisorAddonUpdate(this, { addon: this.addon });
showDialogSupervisorUpdate(this, {
supervisor: this.supervisor,
name: this.addon.name,
version: this.addon.version_latest,
snapshotParams: {
name: `addon_${this.addon.slug}_${this.addon.version}`,
addons: [this.addon.slug],
homeassistant: false,
},
updateHandler: async () => await this._updateAddon(),
});
}
private async _updateAddon(): Promise<void> {
await updateHassioAddon(this.hass, this.addon.slug);
fireEvent(this, "supervisor-collection-refresh", {
collection: "addon",
});
const eventdata = {
success: true,
response: undefined,
path: "update",
};
fireEvent(this, "hass-api-called", eventdata);
}
private async _startClicked(ev: CustomEvent): Promise<void> {
+2 -2
View File
@@ -28,7 +28,7 @@ class SupervisorMetric extends LitElement {
</span>
<div slot="description" .title=${this.tooltip ?? ""}>
<span class="value">
${roundedValue}%
${roundedValue} %
</span>
<ha-bar
class="${classMap({
@@ -73,7 +73,7 @@ class SupervisorMetric extends LitElement {
);
}
.value {
width: 42px;
width: 48px;
padding-right: 4px;
}
`;
+24 -10
View File
@@ -19,13 +19,14 @@ import "../../../src/components/ha-svg-icon";
import {
extractApiErrorMessage,
HassioResponse,
ignoredStatusCodes,
ignoreSupervisorError,
} from "../../../src/data/hassio/common";
import { HassioHassOSInfo } from "../../../src/data/hassio/host";
import {
HassioHomeAssistantInfo,
HassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor";
import { updateCore } from "../../../src/data/supervisor/core";
import {
Supervisor,
supervisorApiWsRequest,
@@ -36,7 +37,7 @@ import {
} from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { showDialogSupervisorCoreUpdate } from "../dialogs/core/show-dialog-core-update";
import { showDialogSupervisorUpdate } from "../dialogs/update/show-dialog-update";
import { hassioStyle } from "../resources/hassio-style";
const computeVersion = (key: string, version: string): string => {
@@ -164,7 +165,17 @@ export class HassioUpdate extends LitElement {
private async _confirmUpdate(ev): Promise<void> {
const item = ev.currentTarget;
if (item.key === "core") {
showDialogSupervisorCoreUpdate(this, { core: this.supervisor.core });
showDialogSupervisorUpdate(this, {
supervisor: this.supervisor,
name: "Home Assistant Core",
version: this.supervisor.core.version_latest,
snapshotParams: {
name: `core_${this.supervisor.core.version}`,
folders: ["homeassistant"],
homeassistant: true,
},
updateHandler: async () => this._updateCore(),
});
return;
}
item.progress = true;
@@ -199,17 +210,13 @@ export class HassioUpdate extends LitElement {
} else {
await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath);
}
fireEvent(this, "supervisor-colllection-refresh", {
colllection: item.key,
fireEvent(this, "supervisor-collection-refresh", {
collection: item.key,
});
} catch (err) {
// Only show an error if the status code was not expected (user behind proxy)
// or no status at all(connection terminated)
if (
this.hass.connection.connected &&
err.status_code &&
!ignoredStatusCodes.has(err.status_code)
) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
showAlertDialog(this, {
title: this.supervisor.localize("common.error.update_failed"),
text: extractApiErrorMessage(err),
@@ -219,6 +226,13 @@ export class HassioUpdate extends LitElement {
item.progress = false;
}
private async _updateCore(): Promise<void> {
await updateCore(this.hass);
fireEvent(this, "supervisor-collection-refresh", {
collection: "core",
});
}
static get styles(): CSSResult[] {
return [
haStyle,
@@ -1,17 +0,0 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
export interface SupervisorDialogSupervisorAddonUpdateParams {
addon: HassioAddonDetails;
}
export const showDialogSupervisorAddonUpdate = (
element: HTMLElement,
dialogParams: SupervisorDialogSupervisorAddonUpdateParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-supervisor-addon-update",
dialogImport: () => import("./dialog-supervisor-addon-update"),
dialogParams,
});
};
@@ -1,175 +0,0 @@
import "@material/mwc-button/mwc-button";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-switch";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { createHassioPartialSnapshot } from "../../../../src/data/hassio/snapshot";
import { HassioHomeAssistantInfo } from "../../../../src/data/hassio/supervisor";
import { updateCore } from "../../../../src/data/supervisor/core";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { SupervisorDialogSupervisorCoreUpdateParams } from "./show-dialog-core-update";
@customElement("dialog-supervisor-core-update")
class DialogSupervisorCoreUpdate extends LitElement {
public hass!: HomeAssistant;
public core!: HassioHomeAssistantInfo;
@internalProperty() private _opened = false;
@internalProperty() private _createSnapshot = true;
@internalProperty() private _action: "snapshot" | "update" | null = null;
@internalProperty() private _error?: string;
public async showDialog(
params: SupervisorDialogSupervisorCoreUpdateParams
): Promise<void> {
this._opened = true;
this.core = params.core;
await this.updateComplete;
}
public closeDialog(): void {
this._action = null;
this._createSnapshot = true;
this._opened = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
public focus(): void {
this.updateComplete.then(() =>
(this.shadowRoot?.querySelector(
"[dialogInitialFocus]"
) as HTMLElement)?.focus()
);
}
protected render(): TemplateResult {
return html`
<ha-dialog .open=${this._opened} scrimClickAction escapeKeyAction>
${this._action === null
? html`<slot name="heading">
<h2 id="title" class="header_title">
Update Home Assistant Core
</h2>
</slot>
<div>
Are you sure you want to update Home Assistant Core to version
${this.core.version_latest}?
</div>
<ha-settings-row three-rows>
<span slot="heading">
Snapshot
</span>
<span slot="description">
Create a snapshot of Home Assistant Core before updating
</span>
<ha-switch
.checked=${this._createSnapshot}
haptic
title="Create snapshot"
@click=${this._toggleSnapshot}
>
</ha-switch>
</ha-settings-row>
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
Cancel
</mwc-button>
<mwc-button @click=${this._update} slot="primaryAction">
Update
</mwc-button>`
: html`<ha-circular-progress alt="Updating" size="large" active>
</ha-circular-progress>
<p class="progress-text">
${this._action === "update"
? `Updating Home Assistant Core to version ${this.core.version_latest}`
: "Creating snapshot of Home Assistant Core"}
</p>`}
${this._error ? html`<p class="error">${this._error}</p>` : ""}
</ha-dialog>
`;
}
private _toggleSnapshot() {
this._createSnapshot = !this._createSnapshot;
}
private async _update() {
if (this._createSnapshot) {
this._action = "snapshot";
try {
await createHassioPartialSnapshot(this.hass, {
name: `core_${this.core.version}`,
folders: ["homeassistant"],
homeassistant: true,
});
} catch (err) {
this._error = extractApiErrorMessage(err);
this._action = null;
return;
}
}
this._action = "update";
try {
await updateCore(this.hass);
} catch (err) {
if (this.hass.connection.connected) {
this._error = extractApiErrorMessage(err);
this._action = null;
return;
}
}
fireEvent(this, "supervisor-colllection-refresh", { colllection: "core" });
this.closeDialog();
}
static get styles(): CSSResult[] {
return [
haStyle,
haStyleDialog,
css`
.form {
color: var(--primary-text-color);
}
ha-settings-row {
margin-top: 32px;
padding: 0;
}
ha-circular-progress {
display: block;
margin: 32px;
text-align: center;
}
.progress-text {
text-align: center;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-supervisor-core-update": DialogSupervisorCoreUpdate;
}
}
@@ -1,17 +0,0 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { HassioHomeAssistantInfo } from "../../../../src/data/hassio/supervisor";
export interface SupervisorDialogSupervisorCoreUpdateParams {
core: HassioHomeAssistantInfo;
}
export const showDialogSupervisorCoreUpdate = (
element: HTMLElement,
dialogParams: SupervisorDialogSupervisorCoreUpdateParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-supervisor-core-update",
dialogImport: () => import("./dialog-supervisor-core-update"),
dialogParams,
});
};
@@ -18,7 +18,6 @@ import {
} from "lit-element";
import { cache } from "lit-html/directives/cache";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-chips";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel";
@@ -17,6 +17,7 @@ import {
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-circular-progress";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-svg-icon";
@@ -26,7 +27,6 @@ import {
} 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 { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
@@ -35,15 +35,12 @@ import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
class HassioRepositoriesDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) private _repos: HassioAddonRepository[] = [];
@property({ attribute: false })
private _dialogParams?: HassioRepositoryDialogParams;
@query("#repository_input", true) private _optionInput?: PaperInputElement;
@internalProperty() private _repositories?: HassioAddonRepository[];
@internalProperty() private _dialogParams?: HassioRepositoryDialogParams;
@internalProperty() private _opened = false;
@internalProperty() private _prosessing = false;
@@ -54,12 +51,13 @@ class HassioRepositoriesDialog extends LitElement {
dialogParams: HassioRepositoryDialogParams
): Promise<void> {
this._dialogParams = dialogParams;
this.supervisor = dialogParams.supervisor;
this._opened = true;
await this._loadData();
await this.updateComplete;
}
public closeDialog(): void {
this._dialogParams = undefined;
this._opened = false;
this._error = "";
}
@@ -71,9 +69,10 @@ class HassioRepositoriesDialog extends LitElement {
);
protected render(): TemplateResult {
const repositories = this._filteredRepositories(
this.supervisor.addon.repositories
);
if (!this._dialogParams?.supervisor || this._repositories === undefined) {
return html``;
}
const repositories = this._filteredRepositories(this._repositories);
return html`
<ha-dialog
.open=${this._opened}
@@ -82,7 +81,7 @@ class HassioRepositoriesDialog extends LitElement {
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this.supervisor.localize("dialog.repositories.title")
this._dialogParams!.supervisor.localize("dialog.repositories.title")
)}
>
${this._error ? html`<div class="error">${this._error}</div>` : ""}
@@ -98,7 +97,7 @@ class HassioRepositoriesDialog extends LitElement {
</paper-item-body>
<mwc-icon-button
.slug=${repo.slug}
.title=${this.supervisor.localize(
.title=${this._dialogParams!.supervisor.localize(
"dialog.repositories.remove"
)}
@click=${this._removeRepository}
@@ -117,18 +116,23 @@ class HassioRepositoriesDialog extends LitElement {
<paper-input
class="flex-auto"
id="repository_input"
.label=${this.supervisor.localize("dialog.repositories.add")}
.value=${this._dialogParams!.url || ""}
.label=${this._dialogParams!.supervisor.localize(
"dialog.repositories.add"
)}
@keydown=${this._handleKeyAdd}
></paper-input>
<mwc-button @click=${this._addRepository}>
${this._prosessing
? html`<ha-circular-progress active></ha-circular-progress>`
: this.supervisor.localize("dialog.repositories.add")}
: this._dialogParams!.supervisor.localize(
"dialog.repositories.add"
)}
</mwc-button>
</div>
</div>
<mwc-button slot="primaryAction" @click="${this.closeDialog}">
Close
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this._dialogParams?.supervisor.localize("common.close")}
</mwc-button>
</ha-dialog>
`;
@@ -159,6 +163,11 @@ class HassioRepositoriesDialog extends LitElement {
ha-paper-dropdown-menu {
display: block;
}
ha-circular-progress {
display: block;
margin: 32px;
text-align: center;
}
`,
];
}
@@ -179,13 +188,25 @@ class HassioRepositoriesDialog extends LitElement {
this._addRepository();
}
private async _loadData(): Promise<void> {
try {
const addonsinfo = await fetchHassioAddonsInfo(this.hass);
this._repositories = addonsinfo.repositories;
fireEvent(this, "supervisor-collection-refresh", { collection: "addon" });
} catch (err) {
this._error = extractApiErrorMessage(err);
}
}
private async _addRepository() {
const input = this._optionInput;
if (!input || !input.value) {
return;
}
this._prosessing = true;
const repositories = this._filteredRepositories(this._repos);
const repositories = this._filteredRepositories(this._repositories!);
const newRepositories = repositories.map((repo) => {
return repo.source;
});
@@ -195,11 +216,7 @@ class HassioRepositoriesDialog extends LitElement {
await setSupervisorOption(this.hass, {
addons_repositories: newRepositories,
});
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
this._repos = addonsInfo.repositories;
await this._dialogParams!.loadData();
await this._loadData();
input.value = "";
} catch (err) {
@@ -210,7 +227,7 @@ class HassioRepositoriesDialog extends LitElement {
private async _removeRepository(ev: Event) {
const slug = (ev.currentTarget as any).slug;
const repositories = this._filteredRepositories(this._repos);
const repositories = this._filteredRepositories(this._repositories!);
const repository = repositories.find((repo) => {
return repo.slug === slug;
});
@@ -229,11 +246,7 @@ class HassioRepositoriesDialog extends LitElement {
await setSupervisorOption(this.hass, {
addons_repositories: newRepositories,
});
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
this._repos = addonsInfo.repositories;
await this._dialogParams!.loadData();
await this._loadData();
} catch (err) {
this._error = extractApiErrorMessage(err);
}
@@ -4,7 +4,7 @@ import "./dialog-hassio-repositories";
export interface HassioRepositoryDialogParams {
supervisor: Supervisor;
loadData: () => Promise<void>;
url?: string;
}
export const showRepositoriesDialog = (
@@ -15,21 +15,18 @@ import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-switch";
import {
HassioAddonDetails,
updateHassioAddon,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
extractApiErrorMessage,
ignoreSupervisorError,
} from "../../../../src/data/hassio/common";
import { createHassioPartialSnapshot } from "../../../../src/data/hassio/snapshot";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { SupervisorDialogSupervisorAddonUpdateParams } from "./show-dialog-addon-update";
import { SupervisorDialogSupervisorUpdateParams } from "./show-dialog-update";
@customElement("dialog-supervisor-addon-update")
class DialogSupervisorAddonUpdate extends LitElement {
@customElement("dialog-supervisor-update")
class DialogSupervisorUpdate extends LitElement {
public hass!: HomeAssistant;
public addon!: HassioAddonDetails;
@internalProperty() private _opened = false;
@internalProperty() private _createSnapshot = true;
@@ -38,18 +35,22 @@ class DialogSupervisorAddonUpdate extends LitElement {
@internalProperty() private _error?: string;
@internalProperty()
private _dialogParams?: SupervisorDialogSupervisorUpdateParams;
public async showDialog(
params: SupervisorDialogSupervisorAddonUpdateParams
params: SupervisorDialogSupervisorUpdateParams
): Promise<void> {
this._opened = true;
this.addon = params.addon;
this._dialogParams = params;
await this.updateComplete;
}
public closeDialog(): void {
this._action = null;
this._createSnapshot = true;
this._opened = false;
this._error = undefined;
this._dialogParams = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -62,47 +63,77 @@ class DialogSupervisorAddonUpdate extends LitElement {
}
protected render(): TemplateResult {
if (!this._dialogParams) {
return html``;
}
return html`
<ha-dialog .open=${this._opened} scrimClickAction escapeKeyAction>
${this._action === null
? html`<slot name="heading">
<h2 id="title" class="header_title">
Update ${this.addon.name}
${this._dialogParams.supervisor.localize(
"confirm.update.title",
"name",
this._dialogParams.name
)}
</h2>
</slot>
<div>
Are you sure you want to update the ${this.addon.name} add-on to
version ${this.addon.version_latest}?
${this._dialogParams.supervisor.localize(
"confirm.update.text",
"name",
this._dialogParams.name,
"version",
this._dialogParams.version
)}
</div>
<ha-settings-row>
<span slot="heading">
Snapshot
${this._dialogParams.supervisor.localize(
"dialog.update.snapshot"
)}
</span>
<span slot="description">
Create a snapshot of the ${this.addon.name} add-on before
updating
${this._dialogParams.supervisor.localize(
"dialog.update.create_snapshot",
"name",
this._dialogParams.name
)}
</span>
<ha-switch
.checked=${this._createSnapshot}
haptic
title="Create snapshot"
@click=${this._toggleSnapshot}
>
</ha-switch>
</ha-settings-row>
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
Cancel
${this._dialogParams.supervisor.localize("common.cancel")}
</mwc-button>
<mwc-button @click=${this._update} slot="primaryAction">
Update
<mwc-button
.disabled=${this._error !== undefined}
@click=${this._update}
slot="primaryAction"
>
${this._dialogParams.supervisor.localize("common.update")}
</mwc-button>`
: html`<ha-circular-progress alt="Updating" size="large" active>
</ha-circular-progress>
<p class="progress-text">
${this._action === "update"
? `Updating ${this.addon.name} to version ${this.addon.version_latest}`
: "Creating snapshot of Home Assistant Core"}
? this._dialogParams.supervisor.localize(
"dialog.update.updating",
"name",
this._dialogParams.name,
"version",
this._dialogParams.version
)
: this._dialogParams.supervisor.localize(
"dialog.update.snapshotting",
"name",
this._dialogParams.name
)}
</p>`}
${this._error ? html`<p class="error">${this._error}</p>` : ""}
</ha-dialog>
@@ -117,11 +148,10 @@ class DialogSupervisorAddonUpdate extends LitElement {
if (this._createSnapshot) {
this._action = "snapshot";
try {
await createHassioPartialSnapshot(this.hass, {
name: `addon_${this.addon.slug}_${this.addon.version}`,
addons: [this.addon.slug],
homeassistant: false,
});
await createHassioPartialSnapshot(
this.hass,
this._dialogParams!.snapshotParams
);
} catch (err) {
this._error = extractApiErrorMessage(err);
this._action = null;
@@ -131,16 +161,15 @@ class DialogSupervisorAddonUpdate extends LitElement {
this._action = "update";
try {
await updateHassioAddon(this.hass, this.addon.slug);
await this._dialogParams!.updateHandler!();
} catch (err) {
this._error = extractApiErrorMessage(err);
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
this._error = extractApiErrorMessage(err);
}
this._action = null;
return;
}
fireEvent(this, "supervisor-colllection-refresh", { colllection: "addon" });
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "supervisor",
});
this.closeDialog();
}
@@ -174,6 +203,6 @@ class DialogSupervisorAddonUpdate extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"dialog-supervisor-addon-update": DialogSupervisorAddonUpdate;
"dialog-supervisor-update": DialogSupervisorUpdate;
}
}
@@ -0,0 +1,21 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface SupervisorDialogSupervisorUpdateParams {
supervisor: Supervisor;
name: string;
version: string;
snapshotParams: any;
updateHandler: () => Promise<void>;
}
export const showDialogSupervisorUpdate = (
element: HTMLElement,
dialogParams: SupervisorDialogSupervisorUpdateParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-supervisor-update",
dialogImport: () => import("./dialog-supervisor-update"),
dialogParams,
});
};
+4 -1
View File
@@ -44,7 +44,10 @@ export class HassioMain extends SupervisorBaseElement {
// We changed the navigate event to fire directly on the window, as that's
// where we are listening for it. However, the older panel_custom will
// listen on this element for navigation events, so we need to forward them.
window.addEventListener("location-changed", (ev) =>
// Joakim - April 26, 2021
// Due to changes in behavior in Google Chrome, we changed navigate to fire on the top element
top.addEventListener("location-changed", (ev) =>
// @ts-ignore
fireEvent(this, ev.type, ev.detail, {
bubbles: false,
+9 -3
View File
@@ -22,6 +22,9 @@ import { HomeAssistant, Route } from "../../src/types";
import { Supervisor } from "../../src/data/supervisor/supervisor";
const REDIRECTS: Redirects = {
supervisor: {
redirect: "/hassio/dashboard",
},
supervisor_logs: {
redirect: "/hassio/system",
},
@@ -34,15 +37,18 @@ const REDIRECTS: Redirects = {
supervisor_store: {
redirect: "/hassio/store",
},
supervisor: {
redirect: "/hassio/dashboard",
},
supervisor_addon: {
redirect: "/hassio/addon",
params: {
addon: "string",
},
},
supervisor_add_addon_repository: {
redirect: "/hassio/store",
params: {
repository_url: "url",
},
},
};
@customElement("hassio-my-redirect")
+1 -1
View File
@@ -31,7 +31,7 @@ class HassioPanel extends LitElement {
if (
Object.keys(supervisorCollection).some(
(colllection) => !this.supervisor[colllection]
(collection) => !this.supervisor[collection]
)
) {
return html`<hass-loading-screen></hass-loading-screen>`;
+49 -33
View File
@@ -23,19 +23,19 @@ import {
import { fetchSupervisorStore } from "../../src/data/supervisor/store";
import {
getSupervisorEventCollection,
subscribeSupervisorEvents,
Supervisor,
SupervisorObject,
supervisorCollection,
} from "../../src/data/supervisor/supervisor";
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
import { urlSyncMixin } from "../../src/state/url-sync-mixin";
import { HomeAssistant } from "../../src/types";
import { getTranslation } from "../../src/util/common-translation";
declare global {
interface HASSDomEvents {
"supervisor-update": Partial<Supervisor>;
"supervisor-colllection-refresh": { colllection: SupervisorObject };
"supervisor-collection-refresh": { collection: SupervisorObject };
}
}
@@ -53,8 +53,6 @@ export class SupervisorBaseElement extends urlSyncMixin(
Collection<unknown>
> = {};
@internalProperty() private _resources?: Record<string, any>;
@internalProperty() private _language = "en";
public connectedCallback(): void {
@@ -71,12 +69,39 @@ export class SupervisorBaseElement extends urlSyncMixin(
protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (changedProperties.has("hass")) {
const oldHass = changedProperties.get("hass") as
| HomeAssistant
| undefined;
if (
oldHass !== undefined &&
oldHass.language !== undefined &&
oldHass.language !== this.hass.language
) {
this._language = this.hass.language;
}
}
if (changedProperties.has("_language")) {
if (changedProperties.get("_language") !== this._language) {
this._initializeLocalize();
}
}
if (changedProperties.has("_collections")) {
if (this._collections) {
const unsubs = Object.keys(this._unsubs);
for (const collection of Object.keys(this._collections)) {
if (!unsubs.includes(collection)) {
this._unsubs[collection] = this._collections[
collection
].subscribe((data) =>
this._updateSupervisor({ [collection]: data })
);
}
}
}
}
}
protected _updateSupervisor(obj: Partial<Supervisor>): void {
@@ -85,7 +110,10 @@ export class SupervisorBaseElement extends urlSyncMixin(
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
if (this._language !== this.hass.language) {
if (
this._language !== this.hass.language &&
this.hass.language !== undefined
) {
this._language = this.hass.language;
}
this._initializeLocalize();
@@ -99,55 +127,43 @@ export class SupervisorBaseElement extends urlSyncMixin(
"/api/hassio/app/static/translations"
);
this._resources = {
[language]: data,
};
this.supervisor = {
...this.supervisor,
localize: await computeLocalize(
this.constructor.prototype,
this._language,
this._resources
),
localize: await computeLocalize(this.constructor.prototype, language, {
[language]: data,
}),
};
}
private async _handleSupervisorStoreRefreshEvent(ev) {
const colllection = ev.detail.colllection;
const collection = ev.detail.collection;
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
this._collections[colllection].refresh();
this._collections[collection].refresh();
return;
}
const response = await this.hass.callApi<HassioResponse<any>>(
"GET",
`hassio${supervisorCollection[colllection]}`
`hassio${supervisorCollection[collection]}`
);
this._updateSupervisor({ [colllection]: response.data });
this._updateSupervisor({ [collection]: response.data });
}
private async _initSupervisor(): Promise<void> {
this.addEventListener(
"supervisor-colllection-refresh",
"supervisor-collection-refresh",
this._handleSupervisorStoreRefreshEvent
);
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
Object.keys(supervisorCollection).forEach((colllection) => {
this._unsubs[colllection] = subscribeSupervisorEvents(
this.hass,
(data) => this._updateSupervisor({ [colllection]: data }),
colllection,
supervisorCollection[colllection]
);
if (this._collections[colllection]) {
this._collections[colllection].refresh();
Object.keys(supervisorCollection).forEach((collection) => {
if (collection in this._collections) {
this._collections[collection].refresh();
} else {
this._collections[colllection] = getSupervisorEventCollection(
this._collections[collection] = getSupervisorEventCollection(
this.hass.connection,
colllection,
supervisorCollection[colllection]
collection,
supervisorCollection[collection]
);
}
});
@@ -185,7 +201,7 @@ export class SupervisorBaseElement extends urlSyncMixin(
fetchSupervisorStore(this.hass),
]);
this.supervisor = {
this._updateSupervisor({
addon,
supervisor,
host,
@@ -195,7 +211,7 @@ export class SupervisorBaseElement extends urlSyncMixin(
network,
resolution,
store,
};
});
this.addEventListener("supervisor-update", (ev) =>
this._updateSupervisor(ev.detail)
+21 -3
View File
@@ -10,6 +10,7 @@ import {
property,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card";
@@ -19,7 +20,7 @@ import {
fetchHassioStats,
HassioStats,
} from "../../../src/data/hassio/common";
import { restartCore } from "../../../src/data/supervisor/core";
import { restartCore, updateCore } from "../../../src/data/supervisor/core";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
@@ -29,7 +30,7 @@ import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { bytesToString } from "../../../src/util/bytes-to-string";
import "../components/supervisor-metric";
import { showDialogSupervisorCoreUpdate } from "../dialogs/core/show-dialog-core-update";
import { showDialogSupervisorUpdate } from "../dialogs/update/show-dialog-update";
import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-core-info")
@@ -168,7 +169,24 @@ class HassioCoreInfo extends LitElement {
}
private async _coreUpdate(): Promise<void> {
showDialogSupervisorCoreUpdate(this, { core: this.supervisor.core });
showDialogSupervisorUpdate(this, {
supervisor: this.supervisor,
name: "Home Assistant Core",
version: this.supervisor.core.version_latest,
snapshotParams: {
name: `core_${this.supervisor.core.version}`,
folders: ["homeassistant"],
homeassistant: true,
},
updateHandler: async () => await this._updateCore(),
});
}
private async _updateCore(): Promise<void> {
await updateCore(this.hass);
fireEvent(this, "supervisor-collection-refresh", {
collection: "core",
});
}
static get styles(): CSSResult[] {
+12 -12
View File
@@ -21,7 +21,7 @@ import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row";
import {
extractApiErrorMessage,
ignoredStatusCodes,
ignoreSupervisorError,
} from "../../../src/data/hassio/common";
import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware";
import {
@@ -154,8 +154,8 @@ class HassioHostInfo extends LitElement {
)}
</span>
<span slot="description">
${this.supervisor.host.disk_life_time - 10}% -
${this.supervisor.host.disk_life_time}%
${this.supervisor.host.disk_life_time - 10} % -
${this.supervisor.host.disk_life_time} %
</span>
</ha-settings-row>`
: ""}
@@ -274,7 +274,7 @@ class HassioHostInfo extends LitElement {
await rebootHost(this.hass);
} catch (err) {
// Ignore connection errors, these are all expected
if (err.status_code && !ignoredStatusCodes.has(err.status_code)) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
showAlertDialog(this, {
title: this.supervisor.localize("system.host.failed_to_reboot"),
text: extractApiErrorMessage(err),
@@ -304,7 +304,7 @@ class HassioHostInfo extends LitElement {
await shutdownHost(this.hass);
} catch (err) {
// Ignore connection errors, these are all expected
if (err.status_code && !ignoredStatusCodes.has(err.status_code)) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
showAlertDialog(this, {
title: this.supervisor.localize("system.host.failed_to_shutdown"),
text: extractApiErrorMessage(err),
@@ -342,7 +342,7 @@ class HassioHostInfo extends LitElement {
try {
await updateOS(this.hass);
fireEvent(this, "supervisor-colllection-refresh", { colllection: "os" });
fireEvent(this, "supervisor-collection-refresh", { collection: "os" });
} catch (err) {
if (this.hass.connection.connected) {
showAlertDialog(this, {
@@ -378,8 +378,8 @@ class HassioHostInfo extends LitElement {
if (hostname && hostname !== curHostname) {
try {
await changeHostOptions(this.hass, { hostname });
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "host",
fireEvent(this, "supervisor-collection-refresh", {
collection: "host",
});
} catch (err) {
showAlertDialog(this, {
@@ -393,8 +393,8 @@ class HassioHostInfo extends LitElement {
private async _importFromUSB(): Promise<void> {
try {
await configSyncOS(this.hass);
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "host",
fireEvent(this, "supervisor-collection-refresh", {
collection: "host",
});
} catch (err) {
showAlertDialog(this, {
@@ -408,8 +408,8 @@ class HassioHostInfo extends LitElement {
private async _loadData(): Promise<void> {
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "network",
fireEvent(this, "supervisor-collection-refresh", {
collection: "network",
});
} else {
const network = await fetchNetworkInfo(this.hass);
+47 -35
View File
@@ -8,6 +8,7 @@ import {
property,
TemplateResult,
} from "lit-element";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card";
@@ -38,6 +39,7 @@ import "../components/supervisor-metric";
import { hassioStyle } from "../resources/hassio-style";
const UNSUPPORTED_REASON_URL = {
apparmor: "/more-info/unsupported/apparmor",
container: "/more-info/unsupported/container",
dbus: "/more-info/unsupported/dbus",
docker_configuration: "/more-info/unsupported/docker_configuration",
@@ -48,6 +50,7 @@ const UNSUPPORTED_REASON_URL = {
os: "/more-info/unsupported/os",
privileged: "/more-info/unsupported/privileged",
systemd: "/more-info/unsupported/systemd",
content_trust: "/more-info/unsupported/content_trust",
};
const UNHEALTHY_REASON_URL = {
@@ -55,6 +58,7 @@ const UNHEALTHY_REASON_URL = {
supervisor: "/more-info/unhealthy/supervisor",
setup: "/more-info/unhealthy/setup",
docker: "/more-info/unhealthy/docker",
untrusted: "/more-info/unhealthy/untrusted",
};
@customElement("hassio-supervisor-info")
@@ -148,30 +152,32 @@ class HassioSupervisorInfo extends LitElement {
</ha-settings-row>
${this.supervisor.supervisor.supported
? html` <ha-settings-row three-line>
<span slot="heading">
${this.supervisor.localize(
"system.supervisor.share_diagnostics"
)}
</span>
<div slot="description" class="diagnostics-description">
${this.supervisor.localize(
"system.supervisor.share_diagnostics_description"
)}
<button
class="link"
.title=${this.supervisor.localize("common.show_more")}
@click=${this._diagnosticsInformationDialog}
>
${this.supervisor.localize("common.learn_more")}
</button>
</div>
<ha-switch
haptic
.checked=${this.supervisor.supervisor.diagnostics}
@change=${this._toggleDiagnostics}
></ha-switch>
</ha-settings-row>`
? !atLeastVersion(this.hass.config.version, 2021, 4)
? html` <ha-settings-row three-line>
<span slot="heading">
${this.supervisor.localize(
"system.supervisor.share_diagnostics"
)}
</span>
<div slot="description" class="diagnostics-description">
${this.supervisor.localize(
"system.supervisor.share_diagnostics_description"
)}
<button
class="link"
.title=${this.supervisor.localize("common.show_more")}
@click=${this._diagnosticsInformationDialog}
>
${this.supervisor.localize("common.learn_more")}
</button>
</div>
<ha-switch
haptic
.checked=${this.supervisor.supervisor.diagnostics}
@change=${this._toggleDiagnostics}
></ha-switch>
</ha-settings-row>`
: ""
: html`<div class="error">
${this.supervisor.localize(
"system.supervisor.unsupported_title"
@@ -263,13 +269,15 @@ class HassioSupervisorInfo extends LitElement {
</b>
<br /><br />
${this.supervisor.localize("system.supervisor.beta_release_items")}
<li>Home Assistant Core</li>
<li>Home Assistant Supervisor</li>
<li>Home Assistant Operating System</li>
<ul>
<li>Home Assistant Core</li>
<li>Home Assistant Supervisor</li>
<li>Home Assistant Operating System</li>
</ul>
<br />
${this.supervisor.localize("system.supervisor.join_beta_action")}`,
${this.supervisor.localize("system.supervisor.beta_join_confirm")}`,
confirmText: this.supervisor.localize(
"system.supervisor.beta_join_confirm"
"system.supervisor.join_beta_action"
),
dismissText: this.supervisor.localize("common.cancel"),
});
@@ -317,8 +325,8 @@ class HassioSupervisorInfo extends LitElement {
private async _reloadSupervisor(): Promise<void> {
await reloadSupervisor(this.hass);
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "supervisor",
fireEvent(this, "supervisor-collection-refresh", {
collection: "supervisor",
});
}
@@ -367,9 +375,13 @@ class HassioSupervisorInfo extends LitElement {
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize("confirm.update", "name", "Supervisor"),
title: this.supervisor.localize(
"confirm.update.title",
"name",
"Supervisor"
),
text: this.supervisor.localize(
"confirm.text",
"confirm.update.text",
"name",
"Supervisor",
"version",
@@ -386,8 +398,8 @@ class HassioSupervisorInfo extends LitElement {
try {
await updateSupervisor(this.hass);
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "supervisor",
fireEvent(this, "supervisor-collection-refresh", {
collection: "supervisor",
});
} catch (err) {
showAlertDialog(this, {
+6 -7
View File
@@ -25,7 +25,7 @@
"@braintree/sanitize-url": "^5.0.0",
"@codemirror/commands": "^0.18.0",
"@codemirror/gutter": "^0.18.0",
"@codemirror/highlight": "^0.18.1",
"@codemirror/highlight": "^0.18.0",
"@codemirror/history": "^0.18.0",
"@codemirror/legacy-modes": "^0.18.0",
"@codemirror/rectangular-selection": "^0.18.0",
@@ -91,8 +91,6 @@
"@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "3.1.0",
"@thomasloven/round-slider": "0.5.2",
"@types/chromecast-caf-sender": "^1.0.3",
"@types/sortablejs": "^1.10.6",
"@vaadin/vaadin-combo-box": "^5.0.10",
"@vaadin/vaadin-date-picker": "^4.0.7",
"@vibrant/color": "^3.2.1-alpha.1",
@@ -102,7 +100,6 @@
"@webcomponents/webcomponentsjs": "^2.2.7",
"chart.js": "~2.8.0",
"chartjs-chart-timeline": "^0.3.0",
"codemirror": "^5.49.0",
"comlink": "^4.3.0",
"core-js": "^3.6.5",
"cropperjs": "^1.5.7",
@@ -111,7 +108,7 @@
"fecha": "^4.2.0",
"fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2",
"hls.js": "^0.13.2",
"hls.js": "^1.0.1",
"home-assistant-js-websocket": "^5.9.0",
"idb-keyval": "^3.2.0",
"intl-messageformat": "^8.3.9",
@@ -134,6 +131,7 @@
"sortablejs": "^1.10.2",
"superstruct": "^0.10.13",
"tinykeys": "^1.1.1",
"tsparticles": "^1.19.2",
"unfetch": "^4.1.0",
"vis-data": "^7.1.1",
"vis-network": "^8.5.4",
@@ -167,8 +165,8 @@
"@rollup/plugin-replace": "^2.3.2",
"@types/chai": "^4.1.7",
"@types/chromecast-caf-receiver": "^5.0.11",
"@types/chromecast-caf-sender": "^1.0.3",
"@types/codemirror": "^0.0.97",
"@types/hls.js": "^0.12.3",
"@types/js-yaml": "^3.12.1",
"@types/leaflet": "^1.4.3",
"@types/leaflet-draw": "^1.0.1",
@@ -176,6 +174,7 @@
"@types/memoize-one": "4.1.0",
"@types/mocha": "^7.0.2",
"@types/resize-observer-browser": "^0.1.3",
"@types/sortablejs": "^1.10.6",
"@types/webspeechapi": "^0.0.29",
"@typescript-eslint/eslint-plugin": "^4.4.0",
"@typescript-eslint/parser": "^4.4.0",
@@ -227,7 +226,7 @@
"terser-webpack-plugin": "^5.1.1",
"ts-lit-plugin": "^1.2.1",
"ts-mocha": "^7.0.0",
"typescript": "^4.0.3",
"typescript": "^4.2.4",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0",
"webpack": "^5.24.1",
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

+2 -2
View File
@@ -10,10 +10,10 @@ function patch(version) {
function today() {
const now = new Date();
return `${now.getFullYear()}${String(now.getMonth() + 1).padStart(
return `${now.getUTCFullYear()}${String(now.getUTCMonth() + 1).padStart(
2,
"0"
)}${String(now.getDate()).padStart(2, "0")}.0`;
)}${String(now.getUTCDate()).padStart(2, "0")}.0`;
}
function auto(version) {
+2 -2
View File
@@ -2,12 +2,12 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20210302.5",
version="20210423.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors",
author_email="hello@home-assistant.io",
license="Apache License 2.0",
license="Apache-2.0",
packages=find_packages(include=["hass_frontend", "hass_frontend.*"]),
include_package_data=True,
zip_safe=False,
+15
View File
@@ -8,6 +8,7 @@ import {
PropertyValues,
} from "lit-element";
import punycode from "punycode";
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
import { extractSearchParamsObject } from "../common/url/search-params";
import {
AuthProvider,
@@ -116,6 +117,20 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
this._fetchAuthProviders();
this._fetchDiscoveryInfo();
if (matchMedia("(prefers-color-scheme: dark)").matches) {
applyThemesOnElement(
document.documentElement,
{
default_theme: "default",
default_dark_theme: null,
themes: {},
darkMode: false,
},
"default",
{ dark: true }
);
}
if (!this.redirectUri) {
return;
}
+1 -1
View File
@@ -62,7 +62,7 @@ export const ensureConnectedCastSession = (cast: CastManager, auth: Auth) => {
return undefined;
}
return new Promise((resolve) => {
return new Promise<void>((resolve) => {
const unsub = cast.addEventListener("connection-changed", () => {
if (cast.castConnectedToOurHass) {
unsub();
+3
View File
@@ -56,6 +56,8 @@ export const FIXED_DOMAIN_ICONS = {
export const FIXED_DEVICE_CLASS_ICONS = {
current: "hass:current-ac",
carbon_dioxide: "mdi:molecule-co2",
carbon_monoxide: "mdi:molecule-co",
energy: "hass:flash",
humidity: "hass:water-percent",
illuminance: "hass:brightness-5",
@@ -103,6 +105,7 @@ export const DOMAINS_WITH_MORE_INFO = [
"lock",
"media_player",
"person",
"remote",
"script",
"sun",
"timer",
+5 -4
View File
@@ -1,9 +1,10 @@
import { format } from "fecha";
import { FrontendTranslationData } from "../../data/translation";
import { toLocaleDateStringSupportsOptions } from "./check_options_support";
export const formatDate = toLocaleDateStringSupportsOptions
? (dateObj: Date, locales: string) =>
dateObj.toLocaleDateString(locales, {
? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleDateString(locales.language, {
year: "numeric",
month: "long",
day: "numeric",
@@ -11,8 +12,8 @@ export const formatDate = toLocaleDateStringSupportsOptions
: (dateObj: Date) => format(dateObj, "longDate");
export const formatDateWeekday = toLocaleDateStringSupportsOptions
? (dateObj: Date, locales: string) =>
dateObj.toLocaleDateString(locales, {
? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleDateString(locales.language, {
weekday: "long",
month: "short",
day: "numeric",
+5 -4
View File
@@ -1,9 +1,10 @@
import { format } from "fecha";
import { FrontendTranslationData } from "../../data/translation";
import { toLocaleStringSupportsOptions } from "./check_options_support";
export const formatDateTime = toLocaleStringSupportsOptions
? (dateObj: Date, locales: string) =>
dateObj.toLocaleString(locales, {
? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleString(locales.language, {
year: "numeric",
month: "long",
day: "numeric",
@@ -13,8 +14,8 @@ export const formatDateTime = toLocaleStringSupportsOptions
: (dateObj: Date) => format(dateObj, "MMMM D, YYYY, HH:mm");
export const formatDateTimeWithSeconds = toLocaleStringSupportsOptions
? (dateObj: Date, locales: string) =>
dateObj.toLocaleString(locales, {
? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleString(locales.language, {
year: "numeric",
month: "long",
day: "numeric",
+7 -6
View File
@@ -1,17 +1,18 @@
import { format } from "fecha";
import { FrontendTranslationData } from "../../data/translation";
import { toLocaleTimeStringSupportsOptions } from "./check_options_support";
export const formatTime = toLocaleTimeStringSupportsOptions
? (dateObj: Date, locales: string) =>
dateObj.toLocaleTimeString(locales, {
? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleTimeString(locales.language, {
hour: "numeric",
minute: "2-digit",
})
: (dateObj: Date) => format(dateObj, "shortTime");
export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions
? (dateObj: Date, locales: string) =>
dateObj.toLocaleTimeString(locales, {
? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleTimeString(locales.language, {
hour: "numeric",
minute: "2-digit",
second: "2-digit",
@@ -19,8 +20,8 @@ export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions
: (dateObj: Date) => format(dateObj, "mediumTime");
export const formatTimeWeekday = toLocaleTimeStringSupportsOptions
? (dateObj: Date, locales: string) =>
dateObj.toLocaleTimeString(locales, {
? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleTimeString(locales.language, {
weekday: "long",
hour: "numeric",
minute: "2-digit",
+8 -3
View File
@@ -70,13 +70,18 @@ export const applyThemesOnElement = (
themeRules["text-accent-color"] =
rgbContrast(rgbAccentColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
}
// Nothing was changed
if (element._themes?.cacheKey === cacheKey) {
return;
}
}
if (selectedTheme && themes.themes[selectedTheme]) {
themeRules = themes.themes[selectedTheme];
}
if (!element._themes && !Object.keys(themeRules).length) {
if (!element._themes?.keys && !Object.keys(themeRules).length) {
// No styles to reset, and no styles to set
return;
}
@@ -87,8 +92,8 @@ export const applyThemesOnElement = (
: undefined;
// Add previous set keys to reset them, and new theme
const styles = { ...element._themes, ...newTheme?.styles };
element._themes = newTheme?.keys;
const styles = { ...element._themes?.keys, ...newTheme?.styles };
element._themes = { cacheKey, keys: newTheme?.keys };
// Set and/or reset styles
if (element.updateStyles) {
+7 -3
View File
@@ -1,6 +1,10 @@
export const ensureArray = (value?: any) => {
if (!value || Array.isArray(value)) {
type NonUndefined<T> = T extends undefined ? never : T;
export function ensureArray(value: undefined): undefined;
export function ensureArray<T>(value: T | T[]): NonUndefined<T>[];
export function ensureArray(value) {
if (value === undefined || Array.isArray(value)) {
return value;
}
return [value];
};
}
+13 -7
View File
@@ -1,5 +1,6 @@
import { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { FrontendTranslationData } from "../../data/translation";
import { formatDate } from "../datetime/format_date";
import { formatDateTime } from "../datetime/format_date_time";
import { formatTime } from "../datetime/format_time";
@@ -10,7 +11,7 @@ import { computeStateDomain } from "./compute_state_domain";
export const computeStateDisplay = (
localize: LocalizeFunc,
stateObj: HassEntity,
language: string,
locale: FrontendTranslationData,
state?: string
): string => {
const compareState = state !== undefined ? state : stateObj.state;
@@ -20,7 +21,7 @@ export const computeStateDisplay = (
}
if (stateObj.attributes.unit_of_measurement) {
return `${formatNumber(compareState, language)} ${
return `${formatNumber(compareState, locale)} ${
stateObj.attributes.unit_of_measurement
}`;
}
@@ -35,7 +36,7 @@ export const computeStateDisplay = (
stateObj.attributes.month - 1,
stateObj.attributes.day
);
return formatDate(date, language);
return formatDate(date, locale);
}
if (!stateObj.attributes.has_date) {
const now = new Date();
@@ -48,7 +49,7 @@ export const computeStateDisplay = (
stateObj.attributes.hour,
stateObj.attributes.minute
);
return formatTime(date, language);
return formatTime(date, locale);
}
date = new Date(
@@ -58,7 +59,7 @@ export const computeStateDisplay = (
stateObj.attributes.hour,
stateObj.attributes.minute
);
return formatDateTime(date, language);
return formatDateTime(date, locale);
}
if (domain === "humidifier") {
@@ -67,8 +68,13 @@ export const computeStateDisplay = (
}
}
if (domain === "counter") {
return formatNumber(compareState, language);
// `counter` `number` and `input_number` domains do not have a unit of measurement but should still use `formatNumber`
if (
domain === "counter" ||
domain === "number" ||
domain === "input_number"
) {
return formatNumber(compareState, locale);
}
return (
+13 -5
View File
@@ -12,16 +12,24 @@ declare global {
export const navigate = (_node: any, path: string, replace = false) => {
if (__DEMO__) {
if (replace) {
history.replaceState(null, "", `${location.pathname}#${path}`);
top.history.replaceState(
top.history.state?.root ? { root: true } : null,
"",
`${top.location.pathname}#${path}`
);
} else {
window.location.hash = path;
top.location.hash = path;
}
} else if (replace) {
history.replaceState(null, "", path);
top.history.replaceState(
top.history.state?.root ? { root: true } : null,
"",
path
);
} else {
history.pushState(null, "", path);
top.history.pushState(null, "", path);
}
fireEvent(window, "location-changed", {
fireEvent(top, "location-changed", {
replace,
});
};
+295 -207
View File
@@ -34,14 +34,12 @@ const _maxLen = 128;
function initTable() {
const table: number[][] = [];
const row: number[] = [0];
for (let i = 1; i <= _maxLen; i++) {
row.push(-i);
const row: number[] = [];
for (let i = 0; i <= _maxLen; i++) {
row[i] = 0;
}
for (let i = 0; i <= _maxLen; i++) {
const thisRow = row.slice(0);
thisRow[0] = -i;
table.push(thisRow);
table.push(row.slice(0));
}
return table;
}
@@ -50,7 +48,7 @@ function isSeparatorAtPos(value: string, index: number): boolean {
if (index < 0 || index >= value.length) {
return false;
}
const code = value.charCodeAt(index);
const code = value.codePointAt(index);
switch (code) {
case CharCode.Underline:
case CharCode.Dash:
@@ -62,8 +60,16 @@ function isSeparatorAtPos(value: string, index: number): boolean {
case CharCode.DoubleQuote:
case CharCode.Colon:
case CharCode.DollarSign:
case CharCode.LessThan:
case CharCode.OpenParen:
case CharCode.OpenSquareBracket:
return true;
case undefined:
return false;
default:
if (isEmojiImprecise(code)) {
return true;
}
return false;
}
}
@@ -92,10 +98,15 @@ function isPatternInWord(
patternLen: number,
wordLow: string,
wordPos: number,
wordLen: number
wordLen: number,
fillMinWordPosArr = false
): boolean {
while (patternPos < patternLen && wordPos < wordLen) {
if (patternLow[patternPos] === wordLow[wordPos]) {
if (fillMinWordPosArr) {
// Remember the min word position for each pattern position
_minWordMatchPos[patternPos] = wordPos;
}
patternPos += 1;
}
wordPos += 1;
@@ -104,42 +115,22 @@ function isPatternInWord(
}
enum Arrow {
Top = 0b1,
Diag = 0b10,
Left = 0b100,
Diag = 1,
Left = 2,
LeftLeft = 3,
}
/**
* A tuple of three values.
* An array representating a fuzzy match.
*
* 0. the score
* 1. the matches encoded as bitmask (2^53)
* 2. the offset at which matching started
* 1. the offset at which matching started
* 2. `<match_pos_N>`
* 3. `<match_pos_1>`
* 4. `<match_pos_0>` etc
*/
export type FuzzyScore = [number, number, number];
interface FilterGlobals {
_matchesCount: number;
_topMatch2: number;
_topScore: number;
_wordStart: number;
_firstMatchCanBeWeak: boolean;
_table: number[][];
_scores: number[][];
_arrows: Arrow[][];
}
function initGlobals(): FilterGlobals {
return {
_matchesCount: 0,
_topMatch2: 0,
_topScore: 0,
_wordStart: 0,
_firstMatchCanBeWeak: false,
_table: initTable(),
_scores: initTable(),
_arrows: <Arrow[][]>initTable(),
};
}
// export type FuzzyScore = [score: number, wordStart: number, ...matches: number[]];// [number, number, number];
export type FuzzyScore = Array<number>;
export function fuzzyScore(
pattern: string,
@@ -150,7 +141,6 @@ export function fuzzyScore(
wordStart: number,
firstMatchCanBeWeak: boolean
): FuzzyScore | undefined {
const globals = initGlobals();
const patternLen = pattern.length > _maxLen ? _maxLen : pattern.length;
const wordLen = word.length > _maxLen ? _maxLen : word.length;
@@ -172,18 +162,30 @@ export function fuzzyScore(
patternLen,
wordLow,
wordStart,
wordLen
wordLen,
true
)
) {
return undefined;
}
// Find the max matching word position for each pattern position
// NOTE: the min matching word position was filled in above, in the `isPatternInWord` call
_fillInMaxWordMatchPos(
patternLen,
wordLen,
patternStart,
wordStart,
patternLow,
wordLow
);
let row = 1;
let column = 1;
let patternPos = patternStart;
let wordPos = wordStart;
let hasStrongFirstMatch = false;
const hasStrongFirstMatch = [false];
// There will be a match, fill in tables
for (
@@ -191,83 +193,146 @@ export function fuzzyScore(
patternPos < patternLen;
row++, patternPos++
) {
// Reduce search space to possible matching word positions and to possible access from next row
const minWordMatchPos = _minWordMatchPos[patternPos];
const maxWordMatchPos = _maxWordMatchPos[patternPos];
const nextMaxWordMatchPos =
patternPos + 1 < patternLen ? _maxWordMatchPos[patternPos + 1] : wordLen;
for (
column = 1, wordPos = wordStart;
wordPos < wordLen;
column = minWordMatchPos - wordStart + 1, wordPos = minWordMatchPos;
wordPos < nextMaxWordMatchPos;
column++, wordPos++
) {
const score = _doScore(
pattern,
patternLow,
patternPos,
patternStart,
word,
wordLow,
wordPos
);
let score = Number.MIN_SAFE_INTEGER;
let canComeDiag = false;
if (patternPos === patternStart && score > 1) {
hasStrongFirstMatch = true;
if (wordPos <= maxWordMatchPos) {
score = _doScore(
pattern,
patternLow,
patternPos,
patternStart,
word,
wordLow,
wordPos,
wordLen,
wordStart,
_diag[row - 1][column - 1] === 0,
hasStrongFirstMatch
);
}
globals._scores[row][column] = score;
let diagScore = 0;
if (score !== Number.MAX_SAFE_INTEGER) {
canComeDiag = true;
diagScore = score + _table[row - 1][column - 1];
}
const diag =
globals._table[row - 1][column - 1] + (score > 1 ? 1 : score);
const top = globals._table[row - 1][column] + -1;
const left = globals._table[row][column - 1] + -1;
const canComeLeft = wordPos > minWordMatchPos;
const leftScore = canComeLeft
? _table[row][column - 1] + (_diag[row][column - 1] > 0 ? -5 : 0)
: 0; // penalty for a gap start
if (left >= top) {
// left or diag
if (left > diag) {
globals._table[row][column] = left;
globals._arrows[row][column] = Arrow.Left;
} else if (left === diag) {
globals._table[row][column] = left;
globals._arrows[row][column] = Arrow.Left || Arrow.Diag;
} else {
globals._table[row][column] = diag;
globals._arrows[row][column] = Arrow.Diag;
}
} else if (top > diag) {
globals._table[row][column] = top;
globals._arrows[row][column] = Arrow.Top;
} else if (top === diag) {
globals._table[row][column] = top;
globals._arrows[row][column] = Arrow.Top || Arrow.Diag;
const canComeLeftLeft =
wordPos > minWordMatchPos + 1 && _diag[row][column - 1] > 0;
const leftLeftScore = canComeLeftLeft
? _table[row][column - 2] + (_diag[row][column - 2] > 0 ? -5 : 0)
: 0; // penalty for a gap start
if (
canComeLeftLeft &&
(!canComeLeft || leftLeftScore >= leftScore) &&
(!canComeDiag || leftLeftScore >= diagScore)
) {
// always prefer choosing left left to jump over a diagonal because that means a match is earlier in the word
_table[row][column] = leftLeftScore;
_arrows[row][column] = Arrow.LeftLeft;
_diag[row][column] = 0;
} else if (canComeLeft && (!canComeDiag || leftScore >= diagScore)) {
// always prefer choosing left since that means a match is earlier in the word
_table[row][column] = leftScore;
_arrows[row][column] = Arrow.Left;
_diag[row][column] = 0;
} else if (canComeDiag) {
_table[row][column] = diagScore;
_arrows[row][column] = Arrow.Diag;
_diag[row][column] = _diag[row - 1][column - 1] + 1;
} else {
globals._table[row][column] = diag;
globals._arrows[row][column] = Arrow.Diag;
throw new Error(`not possible`);
}
}
}
if (_debug) {
printTables(pattern, patternStart, word, wordStart, globals);
printTables(pattern, patternStart, word, wordStart);
}
if (!hasStrongFirstMatch && !firstMatchCanBeWeak) {
if (!hasStrongFirstMatch[0] && !firstMatchCanBeWeak) {
return undefined;
}
globals._matchesCount = 0;
globals._topScore = -100;
globals._wordStart = wordStart;
globals._firstMatchCanBeWeak = firstMatchCanBeWeak;
row--;
column--;
_findAllMatches2(
row - 1,
column - 1,
patternLen === wordLen ? 1 : 0,
0,
false,
globals
);
if (globals._matchesCount === 0) {
return undefined;
const result: FuzzyScore = [_table[row][column], wordStart];
let backwardsDiagLength = 0;
let maxMatchColumn = 0;
while (row >= 1) {
// Find the column where we go diagonally up
let diagColumn = column;
do {
const arrow = _arrows[row][diagColumn];
if (arrow === Arrow.LeftLeft) {
diagColumn -= 2;
} else if (arrow === Arrow.Left) {
diagColumn -= 1;
} else {
// found the diagonal
break;
}
} while (diagColumn >= 1);
// Overturn the "forwards" decision if keeping the "backwards" diagonal would give a better match
if (
backwardsDiagLength > 1 && // only if we would have a contiguous match of 3 characters
patternLow[patternStart + row - 1] === wordLow[wordStart + column - 1] && // only if we can do a contiguous match diagonally
!isUpperCaseAtPos(diagColumn + wordStart - 1, word, wordLow) && // only if the forwards chose diagonal is not an uppercase
backwardsDiagLength + 1 > _diag[row][diagColumn] // only if our contiguous match would be longer than the "forwards" contiguous match
) {
diagColumn = column;
}
if (diagColumn === column) {
// this is a contiguous match
backwardsDiagLength++;
} else {
backwardsDiagLength = 1;
}
if (!maxMatchColumn) {
// remember the last matched column
maxMatchColumn = diagColumn;
}
row--;
column = diagColumn - 1;
result.push(column);
}
return [globals._topScore, globals._topMatch2, wordStart];
if (wordLen === patternLen) {
// the word matches the pattern with all characters!
// giving the score a total match boost (to come up ahead other words)
result[0] += 2;
}
// Add 1 penalty for each skipped character in the word
const skippedCharsCount = maxMatchColumn - patternLen;
result[0] -= skippedCharsCount;
return result;
}
function _doScore(
@@ -277,50 +342,81 @@ function _doScore(
patternStart: number,
word: string,
wordLow: string,
wordPos: number
) {
wordPos: number,
wordLen: number,
wordStart: number,
newMatchStart: boolean,
outFirstMatchStrong: boolean[]
): number {
if (patternLow[patternPos] !== wordLow[wordPos]) {
return -1;
return Number.MIN_SAFE_INTEGER;
}
let score = 1;
let isGapLocation = false;
if (wordPos === patternPos - patternStart) {
// common prefix: `foobar <-> foobaz`
// ^^^^^
if (pattern[patternPos] === word[wordPos]) {
return 7;
}
return 5;
}
if (
score = pattern[patternPos] === word[wordPos] ? 7 : 5;
} else if (
isUpperCaseAtPos(wordPos, word, wordLow) &&
(wordPos === 0 || !isUpperCaseAtPos(wordPos - 1, word, wordLow))
) {
// hitting upper-case: `foo <-> forOthers`
// ^^ ^
if (pattern[patternPos] === word[wordPos]) {
return 7;
}
return 5;
}
if (
score = pattern[patternPos] === word[wordPos] ? 7 : 5;
isGapLocation = true;
} else if (
isSeparatorAtPos(wordLow, wordPos) &&
(wordPos === 0 || !isSeparatorAtPos(wordLow, wordPos - 1))
) {
// hitting a separator: `. <-> foo.bar`
// ^
return 5;
}
if (
score = 5;
} else if (
isSeparatorAtPos(wordLow, wordPos - 1) ||
isWhitespaceAtPos(wordLow, wordPos - 1)
) {
// post separator: `foo <-> bar_foo`
// ^^^
return 5;
score = 5;
isGapLocation = true;
}
return 1;
if (score > 1 && patternPos === patternStart) {
outFirstMatchStrong[0] = true;
}
if (!isGapLocation) {
isGapLocation =
isUpperCaseAtPos(wordPos, word, wordLow) ||
isSeparatorAtPos(wordLow, wordPos - 1) ||
isWhitespaceAtPos(wordLow, wordPos - 1);
}
//
if (patternPos === patternStart) {
// first character in pattern
if (wordPos > wordStart) {
// the first pattern character would match a word character that is not at the word start
// so introduce a penalty to account for the gap preceding this match
score -= isGapLocation ? 3 : 5;
}
} else if (newMatchStart) {
// this would be the beginning of a new match (i.e. there would be a gap before this location)
score += isGapLocation ? 2 : 0;
} else {
// this is part of a contiguous match, so give it a slight bonus, but do so only if it would not be a prefered gap location
score += isGapLocation ? 0 : 1;
}
if (wordPos + 1 === wordLen) {
// we always penalize gaps, but this gives unfair advantages to a match that would match the last character in the word
// so pretend there is a gap after the last character in the word to normalize things
score -= isGapLocation ? 3 : 5;
}
return score;
}
function printTable(
@@ -360,104 +456,96 @@ function printTables(
pattern: string,
patternStart: number,
word: string,
wordStart: number,
globals: FilterGlobals
wordStart: number
): void {
pattern = pattern.substr(patternStart);
word = word.substr(wordStart);
console.log(
printTable(globals._table, pattern, pattern.length, word, word.length)
);
console.log(
printTable(globals._arrows, pattern, pattern.length, word, word.length)
);
console.log(
printTable(globals._scores, pattern, pattern.length, word, word.length)
);
console.log(printTable(_table, pattern, pattern.length, word, word.length));
console.log(printTable(_arrows, pattern, pattern.length, word, word.length));
console.log(printTable(_diag, pattern, pattern.length, word, word.length));
}
function _findAllMatches2(
row: number,
column: number,
total: number,
matches: number,
lastMatched: boolean,
globals: FilterGlobals
): void {
if (globals._matchesCount >= 10 || total < -25) {
// stop when having already 10 results, or
// when a potential alignment as already 5 gaps
return;
const _minWordMatchPos = initArr(2 * _maxLen); // min word position for a certain pattern position
const _maxWordMatchPos = initArr(2 * _maxLen); // max word position for a certain pattern position
const _diag = initTable(); // the length of a contiguous diagonal match
const _table = initTable();
const _arrows = <Arrow[][]>initTable();
function initArr(maxLen: number) {
const row: number[] = [];
for (let i = 0; i <= maxLen; i++) {
row[i] = 0;
}
return row;
}
let simpleMatchCount = 0;
function _fillInMaxWordMatchPos(
patternLen: number,
wordLen: number,
patternStart: number,
wordStart: number,
patternLow: string,
wordLow: string
) {
let patternPos = patternLen - 1;
let wordPos = wordLen - 1;
while (patternPos >= patternStart && wordPos >= wordStart) {
if (patternLow[patternPos] === wordLow[wordPos]) {
_maxWordMatchPos[patternPos] = wordPos;
patternPos--;
}
wordPos--;
}
}
while (row > 0 && column > 0) {
const score = globals._scores[row][column];
const arrow = globals._arrows[row][column];
export interface FuzzyScorer {
(
pattern: string,
lowPattern: string,
patternPos: number,
word: string,
lowWord: string,
wordPos: number,
firstMatchCanBeWeak: boolean
): FuzzyScore | undefined;
}
if (arrow === Arrow.Left) {
// left -> no match, skip a word character
column -= 1;
if (lastMatched) {
total -= 5; // new gap penalty
} else if (matches !== 0) {
total -= 1; // gap penalty after first match
}
lastMatched = false;
simpleMatchCount = 0;
} else if (arrow && Arrow.Diag) {
if (arrow && Arrow.Left) {
// left
_findAllMatches2(
row,
column - 1,
matches !== 0 ? total - 1 : total, // gap penalty after first match
matches,
lastMatched,
globals
);
}
// diag
total += score;
row -= 1;
column -= 1;
lastMatched = true;
// match -> set a 1 at the word pos
matches += 2 ** (column + globals._wordStart);
// count simple matches and boost a row of
// simple matches when they yield in a
// strong match.
if (score === 1) {
simpleMatchCount += 1;
if (row === 0 && !globals._firstMatchCanBeWeak) {
// when the first match is a weak
// match we discard it
return;
}
} else {
// boost
total += 1 + simpleMatchCount * (score - 1);
simpleMatchCount = 0;
}
export function createMatches(score: undefined | FuzzyScore): Match[] {
if (typeof score === "undefined") {
return [];
}
const res: Match[] = [];
const wordPos = score[1];
for (let i = score.length - 1; i > 1; i--) {
const pos = score[i] + wordPos;
const last = res[res.length - 1];
if (last && last.end === pos) {
last.end = pos + 1;
} else {
return;
res.push({ start: pos, end: pos + 1 });
}
}
total -= column >= 3 ? 9 : column * 3; // late start penalty
// dynamically keep track of the current top score
// and insert the current best score at head, the rest at tail
globals._matchesCount += 1;
if (total > globals._topScore) {
globals._topScore = total;
globals._topMatch2 = matches;
}
return res;
}
// #endregion
/**
* A fast function (therefore imprecise) to check if code points are emojis.
* Generated using https://github.com/alexdima/unicode-utils/blob/master/generate-emoji-test.js
*/
export function isEmojiImprecise(x: number): boolean {
return (
(x >= 0x1f1e6 && x <= 0x1f1ff) ||
x === 8986 ||
x === 8987 ||
x === 9200 ||
x === 9203 ||
(x >= 9728 && x <= 10175) ||
x === 11088 ||
x === 11093 ||
(x >= 127744 && x <= 128591) ||
(x >= 128640 && x <= 128764) ||
(x >= 128992 && x <= 129003) ||
(x >= 129280 && x <= 129535) ||
(x >= 129648 && x <= 129750)
);
}
+31 -13
View File
@@ -10,10 +10,13 @@ import { fuzzyScore } from "./filter";
* @return {number} Score representing how well the word matches the filter. Return of 0 means no match.
*/
export const fuzzySequentialMatch = (filter: string, ...words: string[]) => {
let topScore = 0;
export const fuzzySequentialMatch = (
filter: string,
item: ScorableTextItem
) => {
let topScore = Number.NEGATIVE_INFINITY;
for (const word of words) {
for (const word of item.strings) {
const scores = fuzzyScore(
filter,
filter.toLowerCase(),
@@ -28,22 +31,39 @@ export const fuzzySequentialMatch = (filter: string, ...words: string[]) => {
continue;
}
// The VS Code implementation of filter treats a score of "0" as just barely a match
// But we will typically use this matcher in a .filter(), which interprets 0 as a failure.
// By shifting all scores up by 1, we allow "0" matches, while retaining score precedence
const score = scores[0] + 1;
// The VS Code implementation of filter returns a 0 for a weak match.
// But if .filter() sees a "0", it considers that a failed match and will remove it.
// So, we set score to 1 in these cases so the match will be included, and mostly respect correct ordering.
const score = scores[0] === 0 ? 1 : scores[0];
if (score > topScore) {
topScore = score;
}
}
if (topScore === Number.NEGATIVE_INFINITY) {
return undefined;
}
return topScore;
};
/**
* An interface that objects must extend in order to use the fuzzy sequence matcher
*
* @param {number} score - A number representing the existence and strength of a match.
* - `< 0` means a good match that starts in the middle of the string
* - `> 0` means a good match that starts at the beginning of the string
* - `0` means just barely a match
* - `undefined` means not a match
*
* @param {string} strings - Array of strings (aliases) representing the item. The filter string will be compared against each of these for a match.
*
*/
export interface ScorableTextItem {
score?: number;
text: string;
altText?: string;
strings: string[];
}
type FuzzyFilterSort = <T extends ScorableTextItem>(
@@ -54,12 +74,10 @@ type FuzzyFilterSort = <T extends ScorableTextItem>(
export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => {
return items
.map((item) => {
item.score = item.altText
? fuzzySequentialMatch(filter, item.text, item.altText)
: fuzzySequentialMatch(filter, item.text);
item.score = fuzzySequentialMatch(filter, item);
return item;
})
.filter((item) => item.score !== undefined && item.score > 0)
.filter((item) => item.score !== undefined)
.sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) =>
scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0
);
+44 -8
View File
@@ -1,14 +1,36 @@
import { FrontendTranslationData, NumberFormat } from "../../data/translation";
/**
* Formats a number based on the specified language with thousands separator(s) and decimal character for better legibility.
* Formats a number based on the user's preference with thousands separator(s) and decimal character for better legibility.
*
* @param num The number to format
* @param language The language to use when formatting the number
* @param locale The user-selected language and number format, from `hass.locale`
* @param options Intl.NumberFormatOptions to use
*/
export const formatNumber = (
num: string | number,
language: string,
locale?: FrontendTranslationData,
options?: Intl.NumberFormatOptions
): string => {
let format: string | string[] | undefined;
switch (locale?.number_format) {
case NumberFormat.comma_decimal:
format = ["en-US", "en"]; // Use United States with fallback to English formatting 1,234,567.89
break;
case NumberFormat.decimal_comma:
format = ["de", "es", "it"]; // Use German with fallback to Spanish then Italian formatting 1.234.567,89
break;
case NumberFormat.space_comma:
format = ["fr", "sv", "cs"]; // Use French with fallback to Swedish and Czech formatting 1 234 567,89
break;
case NumberFormat.system:
format = undefined;
break;
default:
format = locale?.language;
}
// Polyfill for Number.isNaN, which is more reliable than the global isNaN()
Number.isNaN =
Number.isNaN ||
@@ -16,11 +38,25 @@ export const formatNumber = (
return typeof input === "number" && isNaN(input);
};
if (!Number.isNaN(Number(num)) && Intl) {
return new Intl.NumberFormat(
language,
getDefaultFormatOptions(num, options)
).format(Number(num));
if (
!Number.isNaN(Number(num)) &&
Intl &&
locale?.number_format !== NumberFormat.none
) {
try {
return new Intl.NumberFormat(
format,
getDefaultFormatOptions(num, options)
).format(Number(num));
} catch (error) {
// Don't fail when using "TEST" language
// eslint-disable-next-line no-console
console.error(error);
return new Intl.NumberFormat(
undefined,
getDefaultFormatOptions(num, options)
).format(Number(num));
}
}
return num.toString();
};
+18
View File
@@ -0,0 +1,18 @@
const isTemplateRegex = new RegExp("{%|{{");
export const isTemplate = (value: string): boolean =>
isTemplateRegex.test(value);
export const hasTemplate = (value: unknown): boolean => {
if (!value) {
return false;
}
if (typeof value === "string") {
return isTemplate(value);
}
if (typeof value === "object") {
const values = Array.isArray(value) ? value : Object.values(value!);
return values.some((val) => val && hasTemplate(val));
}
return false;
};
+2
View File
@@ -0,0 +1,2 @@
export const strStartsWith = (value: string, search: string) =>
value.substring(0, search.length) === search;
+5
View File
@@ -0,0 +1,5 @@
export const constructUrlCurrentPath = (searchParams: string): string => {
const base = window.location.pathname;
// Prevent trailing "?" if no parameters exist
return searchParams ? base + "?" + searchParams : base;
};
+14
View File
@@ -19,3 +19,17 @@ export const createSearchParam = (params: Record<string, string>): string => {
});
return urlParams.toString();
};
export const addSearchParam = (params: Record<string, string>): string => {
const urlParams = new URLSearchParams(window.location.search);
Object.entries(params).forEach(([key, value]) => {
urlParams.set(key, value);
});
return urlParams.toString();
};
export const removeSearchParam = (param: string): string => {
const urlParams = new URLSearchParams(window.location.search);
urlParams.delete(param);
return urlParams.toString();
};
+1 -1
View File
@@ -1,4 +1,4 @@
export const afterNextRender = (cb: () => void): void => {
export const afterNextRender = (cb: (value: unknown) => void): void => {
requestAnimationFrame(() => setTimeout(cb, 0));
};
@@ -1,65 +0,0 @@
import { html, LitElement } from "lit-element";
import { fireEvent } from "../../common/dom/fire_event";
import "./ha-progress-button";
class HaCallApiButton extends LitElement {
render() {
return html`
<ha-progress-button
.progress="${this.progress}"
@click="${this._buttonTapped}"
?disabled="${this.disabled}"
><slot></slot
></ha-progress-button>
`;
}
constructor() {
super();
this.method = "POST";
this.data = {};
this.disabled = false;
this.progress = false;
}
static get properties() {
return {
hass: {},
progress: Boolean,
path: String,
method: String,
data: {},
disabled: Boolean,
};
}
get progressButton() {
return this.renderRoot.querySelector("ha-progress-button");
}
async _buttonTapped() {
this.progress = true;
const eventData = {
method: this.method,
path: this.path,
data: this.data,
};
try {
const resp = await this.hass.callApi(this.method, this.path, this.data);
this.progress = false;
this.progressButton.actionSuccess();
eventData.success = true;
eventData.response = resp;
} catch (err) {
this.progress = false;
this.progressButton.actionError();
eventData.success = false;
eventData.response = err;
}
fireEvent(this, "hass-api-called", eventData);
}
}
customElements.define("ha-call-api-button", HaCallApiButton);
@@ -0,0 +1,77 @@
import { css, CSSResult, html, LitElement, property, query } from "lit-element";
import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types";
import "./ha-progress-button";
class HaCallApiButton extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public method: "POST" | "GET" | "PUT" | "DELETE" = "POST";
@property() public data = {};
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public progress = false;
@property() public path?: string;
@query("ha-progress-button", true) private _progressButton;
render() {
return html`
<ha-progress-button
.progress=${this.progress}
@click=${this._buttonTapped}
?disabled=${this.disabled}
><slot></slot
></ha-progress-button>
`;
}
async _buttonTapped() {
this.progress = true;
const eventData: {
method: string;
path: string;
data: any;
success?: boolean;
response?: any;
} = {
method: this.method,
path: this.path!,
data: this.data,
};
try {
const resp = await this.hass.callApi(this.method, this.path!, this.data);
this.progress = false;
this._progressButton.actionSuccess();
eventData.success = true;
eventData.response = resp;
} catch (err) {
this.progress = false;
this._progressButton.actionError();
eventData.success = false;
eventData.response = err;
}
fireEvent(this, "hass-api-called", eventData as any);
}
static get styles(): CSSResult {
return css`
:host([disabled]) {
pointer-events: none;
}
`;
}
}
customElements.define("ha-call-api-button", HaCallApiButton);
declare global {
interface HTMLElementTagNameMap {
"ha-call-api-button": HaCallApiButton;
}
}
+18 -11
View File
@@ -63,7 +63,7 @@ export interface DataTableSortColumnData {
}
export interface DataTableColumnData extends DataTableSortColumnData {
title: string;
title: TemplateResult | string;
type?: "numeric" | "icon" | "icon-button";
template?: <T>(data: any, row: T) => TemplateResult | string;
width?: string;
@@ -74,7 +74,7 @@ export interface DataTableColumnData extends DataTableSortColumnData {
}
type ClonedDataTableColumnData = Omit<DataTableColumnData, "title"> & {
title?: string;
title?: TemplateResult | string;
};
export interface DataTableRowData {
@@ -132,7 +132,7 @@ export class HaDataTable extends LitElement {
@query("slot[name='header']") private _header!: HTMLSlotElement;
private _items: DataTableRowData[] = [];
@internalProperty() private _items: DataTableRowData[] = [];
private _checkableRowsCount?: number;
@@ -160,9 +160,9 @@ export class HaDataTable extends LitElement {
public connectedCallback() {
super.connectedCallback();
if (this._filteredData.length) {
if (this._items.length) {
// Force update of location of rows
this._filteredData = [...this._filteredData];
this._items = [...this._items];
}
}
@@ -236,20 +236,19 @@ export class HaDataTable extends LitElement {
"auto-height": this.autoHeight,
})}"
role="table"
aria-rowcount=${this._filteredData.length}
aria-rowcount=${this._filteredData.length + 1}
style=${styleMap({
height: this.autoHeight
? `${(this._filteredData.length || 1) * 53 + 57}px`
: `calc(100% - ${this._headerHeight}px)`,
})}
>
<div class="mdc-data-table__header-row" role="row">
<div class="mdc-data-table__header-row" role="row" aria-rowindex="1">
${this.selectable
? html`
<div
class="mdc-data-table__header-cell mdc-data-table__header-cell--checkbox"
role="columnheader"
scope="col"
>
<ha-checkbox
class="mdc-data-table__row-checkbox"
@@ -292,7 +291,13 @@ export class HaDataTable extends LitElement {
})
: ""}
role="columnheader"
scope="col"
aria-sort=${ifDefined(
sorted
? this._sortDirection === "desc"
? "descending"
: "ascending"
: undefined
)}
@click=${this._handleHeaderClick}
.columnId=${key}
>
@@ -338,7 +343,7 @@ export class HaDataTable extends LitElement {
}
return html`
<div
aria-rowindex=${index}
aria-rowindex=${index! + 2}
role="row"
.rowId=${row[this.id]}
@click=${this._handleRowClick}
@@ -545,7 +550,9 @@ export class HaDataTable extends LitElement {
private _checkedRowsChanged() {
// force scroller to update, change it's items
this._filteredData = [...this._filteredData];
if (this._items.length) {
this._items = [...this._items];
}
fireEvent(this, "selection-changed", {
value: this._checkedRows,
});
+5 -5
View File
@@ -100,7 +100,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
public excludeDomains?: string[];
/**
* Show only deviced with entities of these device classes.
* Show only devices with entities of these device classes.
* @type {Array}
* @attr include-device-classes
*/
@@ -113,7 +113,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
@internalProperty() private _opened?: boolean;
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
@query("ha-combo-box", true) public comboBox!: HaComboBox;
private _init = false;
@@ -242,11 +242,11 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
);
public open() {
this._comboBox?.open();
this.comboBox?.open();
}
public focus() {
this._comboBox?.focus();
this.comboBox?.focus();
}
public hassSubscribe(): UnsubscribeFunc[] {
@@ -269,7 +269,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
(changedProps.has("_opened") && this._opened)
) {
this._init = true;
(this._comboBox as any).items = this._getDevices(
(this.comboBox as any).items = this._getDevices(
this.devices!,
this.areas!,
this.entities!,
+1 -1
View File
@@ -371,7 +371,7 @@ class HaChartBase extends mixinBehaviors(
return value;
}
const date = new Date(values[index].value);
return formatTime(date, this.hass.language);
return formatTime(date, this.hass.locale);
}
drawChart() {
+3 -3
View File
@@ -99,7 +99,7 @@ export class HaEntityPicker extends LitElement {
@property({ type: Boolean }) private _opened = false;
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
@query("vaadin-combo-box-light", true) private comboBox!: HTMLElement;
public open() {
this.updateComplete.then(() => {
@@ -208,7 +208,7 @@ export class HaEntityPicker extends LitElement {
this.entityFilter,
this.includeDeviceClasses
);
(this._comboBox as any).filteredItems = this._states;
(this.comboBox as any).filteredItems = this._states;
this._initedStates = true;
}
}
@@ -296,7 +296,7 @@ export class HaEntityPicker extends LitElement {
private _filterChanged(ev: CustomEvent): void {
const filterString = ev.detail.value.toLowerCase();
(this._comboBox as any).filteredItems = this._states.filter(
(this.comboBox as any).filteredItems = this._states.filter(
(state) =>
state.entity_id.toLowerCase().includes(filterString) ||
computeStateName(state).toLowerCase().includes(filterString)
+17 -8
View File
@@ -17,6 +17,7 @@ import { forwardHaptic } from "../../data/haptics";
import { HomeAssistant } from "../../types";
import "../ha-icon-button";
import "../ha-switch";
import "../ha-formfield";
const isOn = (stateObj?: HassEntity) =>
stateObj !== undefined &&
@@ -29,6 +30,8 @@ export class HaEntityToggle extends LitElement {
@property() public stateObj?: HassEntity;
@property() public label?: string;
@internalProperty() private _isOn = false;
protected render(): TemplateResult {
@@ -55,15 +58,21 @@ export class HaEntityToggle extends LitElement {
`;
}
const switchTemplate = html`<ha-switch
aria-label=${`Toggle ${computeStateName(this.stateObj)} ${
this._isOn ? "off" : "on"
}`}
.checked=${this._isOn}
.disabled=${UNAVAILABLE_STATES.includes(this.stateObj.state)}
@change=${this._toggleChanged}
></ha-switch>`;
if (!this.label) {
return switchTemplate;
}
return html`
<ha-switch
aria-label=${`Toggle ${computeStateName(this.stateObj)} ${
this._isOn ? "off" : "on"
}`}
.checked=${this._isOn}
.disabled=${UNAVAILABLE_STATES.includes(this.stateObj.state)}
@change=${this._toggleChanged}
></ha-switch>
<ha-formfield .label=${this.label}>${switchTemplate}</ha-formfield>
`;
}
@@ -116,12 +116,8 @@ export class HaStateLabelBadge extends LitElement {
: state.state === UNKNOWN
? "-"
: state.attributes.unit_of_measurement
? formatNumber(state.state, this.hass!.language)
: computeStateDisplay(
this.hass!.localize,
state,
this.hass!.language
);
? formatNumber(state.state, this.hass!.locale)
: computeStateDisplay(this.hass!.localize, state, this.hass!.locale);
}
}
+1 -1
View File
@@ -84,7 +84,7 @@ class StateInfo extends LitElement {
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.language !== this.hass.language) {
if (!oldHass || oldHass.locale !== this.hass.locale) {
this.rtl = computeRTL(this.hass);
}
}
+10
View File
@@ -0,0 +1,10 @@
import { html } from "lit-element";
import { HomeAssistant } from "../types";
import { documentationUrl } from "../util/documentation-url";
export const analyticsLearnMore = (hass: HomeAssistant) => html`<a
.href=${documentationUrl(hass, "/integrations/analytics/")}
target="_blank"
rel="noreferrer"
>${hass.localize("ui.panel.config.core.section.core.analytics.learn_more")}</a
>`;
+199
View File
@@ -0,0 +1,199 @@
import "@polymer/paper-tooltip/paper-tooltip";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event";
import { Analytics, AnalyticsPreferences } from "../data/analytics";
import { haStyle } from "../resources/styles";
import { HomeAssistant } from "../types";
import "./ha-checkbox";
import type { HaCheckbox } from "./ha-checkbox";
import "./ha-settings-row";
const ADDITIONAL_PREFERENCES = ["usage", "statistics"];
declare global {
interface HASSDomEvents {
"analytics-preferences-changed": { preferences: AnalyticsPreferences };
}
}
@customElement("ha-analytics")
export class HaAnalytics extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public analytics?: Analytics;
protected render(): TemplateResult {
const loading = this.analytics === undefined;
const baseEnabled = !loading && this.analytics!.preferences.base;
return html`
<ha-settings-row>
<span slot="prefix">
<ha-checkbox
@change=${this._handleRowCheckboxClick}
.checked=${baseEnabled}
.preference=${"base"}
.disabled=${loading}
name="base"
>
</ha-checkbox>
</span>
<span slot="heading" data-for="base">
${this.hass.localize(
`ui.panel.config.core.section.core.analytics.preference.base.title`
)}
</span>
<span slot="description" data-for="base">
${this.hass.localize(
`ui.panel.config.core.section.core.analytics.preference.base.description`
)}
</span>
</ha-settings-row>
${ADDITIONAL_PREFERENCES.map(
(preference) =>
html`<ha-settings-row>
<span slot="prefix">
<ha-checkbox
@change=${this._handleRowCheckboxClick}
.checked=${this.analytics?.preferences[preference]}
.preference=${preference}
name=${preference}
>
</ha-checkbox>
${!baseEnabled
? html`<paper-tooltip animation-delay="0" position="right"
>${this.hass.localize(
"ui.panel.config.core.section.core.analytics.needs_base"
)}
</paper-tooltip>`
: ""}
</span>
<span slot="heading" data-for=${preference}>
${preference === "usage"
? isComponentLoaded(this.hass, "hassio")
? this.hass.localize(
`ui.panel.config.core.section.core.analytics.preference.usage_supervisor.title`
)
: this.hass.localize(
`ui.panel.config.core.section.core.analytics.preference.usage.title`
)
: this.hass.localize(
`ui.panel.config.core.section.core.analytics.preference.${preference}.title`
)}
</span>
<span slot="description" data-for=${preference}>
${preference !== "usage"
? this.hass.localize(
`ui.panel.config.core.section.core.analytics.preference.${preference}.description`
)
: isComponentLoaded(this.hass, "hassio")
? this.hass.localize(
`ui.panel.config.core.section.core.analytics.preference.usage_supervisor.description`
)
: this.hass.localize(
`ui.panel.config.core.section.core.analytics.preference.usage.description`
)}
</span>
</ha-settings-row>`
)}
<ha-settings-row>
<span slot="prefix">
<ha-checkbox
@change=${this._handleRowCheckboxClick}
.checked=${this.analytics?.preferences.diagnostics}
.preference=${"diagnostics"}
.disabled=${loading}
name="diagnostics"
>
</ha-checkbox>
</span>
<span slot="heading" data-for="diagnostics">
${this.hass.localize(
`ui.panel.config.core.section.core.analytics.preference.diagnostics.title`
)}
</span>
<span slot="description" data-for="diagnostics">
${this.hass.localize(
`ui.panel.config.core.section.core.analytics.preference.diagnostics.description`
)}
</span>
</ha-settings-row>
`;
}
protected updated(changedProps) {
super.updated(changedProps);
this.shadowRoot!.querySelectorAll("*[data-for]").forEach((el) => {
const forEl = (el as HTMLElement).dataset.for;
delete (el as HTMLElement).dataset.for;
el.addEventListener("click", () => {
const toFocus = this.shadowRoot!.querySelector(
`*[name=${forEl}]`
) as HTMLElement | null;
if (toFocus) {
toFocus.focus();
toFocus.click();
}
});
});
}
private _handleRowCheckboxClick(ev: Event) {
const checkbox = ev.currentTarget as HaCheckbox;
const preference = (checkbox as any).preference;
const preferences = this.analytics ? { ...this.analytics.preferences } : {};
if (preferences[preference] === checkbox.checked) {
return;
}
preferences[preference] = checkbox.checked;
if (ADDITIONAL_PREFERENCES.includes(preference) && checkbox.checked) {
preferences.base = true;
} else if (preference === "base" && !checkbox.checked) {
preferences.usage = false;
preferences.statistics = false;
}
fireEvent(this, "analytics-preferences-changed", { preferences });
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
.error {
color: var(--error-color);
}
ha-settings-row {
padding: 0;
}
span[slot="heading"],
span[slot="description"] {
cursor: pointer;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-analytics": HaAnalytics;
}
}
+5 -5
View File
@@ -127,7 +127,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
@internalProperty() private _opened?: boolean;
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
@query("vaadin-combo-box-light", true) public comboBox!: HTMLElement;
private _init = false;
@@ -140,7 +140,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
this._devices = devices;
}),
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entities = entities.filter((entity) => entity.area_id);
this._entities = entities;
}),
];
}
@@ -193,13 +193,13 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
deviceEntityLookup[entity.device_id].push(entity);
}
inputDevices = devices;
inputEntities = entities;
inputEntities = entities.filter((entity) => entity.area_id);
} else {
if (deviceFilter) {
inputDevices = devices;
}
if (entityFilter) {
inputEntities = entities;
inputEntities = entities.filter((entity) => entity.area_id);
}
}
@@ -319,7 +319,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
(changedProps.has("_opened") && this._opened)
) {
this._init = true;
(this._comboBox as any).items = this._getAreas(
(this.comboBox as any).items = this._getAreas(
this._areas!,
this._devices!,
this._entities!,
-3
View File
@@ -1,4 +1,3 @@
import "@material/mwc-button";
import "@material/mwc-menu";
import type { Corner, Menu } from "@material/mwc-menu";
import {
@@ -11,8 +10,6 @@ import {
query,
TemplateResult,
} from "lit-element";
import "./ha-icon-button";
@customElement("ha-button-menu")
export class HaButtonMenu extends LitElement {
@property() public corner: Corner = "TOP_START";
@@ -0,0 +1,207 @@
import "@material/mwc-icon-button";
import type { Corner } from "@material/mwc-menu";
import { mdiFilterVariant } from "@mdi/js";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
} from "lit-element";
import "@material/mwc-menu/mwc-menu-surface";
import { fireEvent } from "../common/dom/fire_event";
import { findRelated, RelatedResult } from "../data/search";
import type { HomeAssistant } from "../types";
import "./ha-svg-icon";
import "./ha-area-picker";
import "./device/ha-device-picker";
import "./entity/ha-entity-picker";
import { computeStateName } from "../common/entity/compute_state_name";
import { computeDeviceName } from "../data/device_registry";
declare global {
// for fire event
interface HASSDomEvents {
"related-changed": {
value?: FilterValue;
items?: RelatedResult;
filter?: string;
};
}
}
interface FilterValue {
area?: string;
device?: string;
entity?: string;
}
@customElement("ha-button-related-filter-menu")
export class HaRelatedFilterButtonMenu extends LitElement {
@property() public hass!: HomeAssistant;
@property() public corner: Corner = "TOP_START";
@property({ type: Boolean, reflect: true }) public narrow = false;
@property({ type: Boolean }) public disabled = false;
@property({ attribute: false }) public value?: FilterValue;
/**
* Show no entities of these domains.
* @type {Array}
* @attr exclude-domains
*/
@property({ type: Array, attribute: "exclude-domains" })
public excludeDomains?: string[];
@internalProperty() private _open = false;
protected render(): TemplateResult {
return html`
<mwc-icon-button @click=${this._handleClick}>
<ha-svg-icon .path=${mdiFilterVariant}></ha-svg-icon>
</mwc-icon-button>
<mwc-menu-surface
.open=${this._open}
.anchor=${this}
.fullwidth=${this.narrow}
.corner=${this.corner}
@closed=${this._onClosed}
>
<ha-area-picker
.label=${this.hass.localize(
"ui.components.related-filter-menu.filter_by_area"
)}
.hass=${this.hass}
.value=${this.value?.area}
no-add
@value-changed=${this._areaPicked}
></ha-area-picker>
<ha-device-picker
.label=${this.hass.localize(
"ui.components.related-filter-menu.filter_by_device"
)}
.hass=${this.hass}
.value=${this.value?.device}
@value-changed=${this._devicePicked}
></ha-device-picker>
<ha-entity-picker
.label=${this.hass.localize(
"ui.components.related-filter-menu.filter_by_entity"
)}
.hass=${this.hass}
.value=${this.value?.entity}
.excludeDomains=${this.excludeDomains}
@value-changed=${this._entityPicked}
></ha-entity-picker>
</mwc-menu-surface>
`;
}
private _handleClick(): void {
if (this.disabled) {
return;
}
this._open = true;
}
private _onClosed(): void {
this._open = false;
}
private async _entityPicked(ev: CustomEvent) {
const entityId = ev.detail.value;
if (!entityId) {
fireEvent(this, "related-changed", { value: undefined });
return;
}
const filter = this.hass.localize(
"ui.components.related-filter-menu.filtered_by_entity",
"entity_name",
computeStateName((ev.currentTarget as any).comboBox.selectedItem)
);
const items = await findRelated(this.hass, "entity", entityId);
fireEvent(this, "related-changed", {
value: { entity: entityId },
filter,
items,
});
}
private async _devicePicked(ev: CustomEvent) {
const deviceId = ev.detail.value;
if (!deviceId) {
fireEvent(this, "related-changed", { value: undefined });
return;
}
const filter = this.hass.localize(
"ui.components.related-filter-menu.filtered_by_device",
"device_name",
computeDeviceName(
(ev.currentTarget as any).comboBox.selectedItem,
this.hass
)
);
const items = await findRelated(this.hass, "device", deviceId);
fireEvent(this, "related-changed", {
value: { device: deviceId },
filter,
items,
});
}
private async _areaPicked(ev: CustomEvent) {
const areaId = ev.detail.value;
if (!areaId) {
fireEvent(this, "related-changed", { value: undefined });
return;
}
const filter = this.hass.localize(
"ui.components.related-filter-menu.filtered_by_area",
"area_name",
(ev.currentTarget as any).comboBox.selectedItem.name
);
const items = await findRelated(this.hass, "area", areaId);
fireEvent(this, "related-changed", {
value: { area: areaId },
filter,
items,
});
}
static get styles(): CSSResult {
return css`
:host {
display: inline-block;
position: relative;
}
:host([narrow]) {
position: static;
}
ha-area-picker,
ha-device-picker,
ha-entity-picker {
display: block;
width: 300px;
padding: 4px 16px;
box-sizing: border-box;
}
:host([narrow]) ha-area-picker,
:host([narrow]) ha-device-picker {
width: 100%;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-button-related-filter-menu": HaRelatedFilterButtonMenu;
}
}
@@ -1,6 +1,5 @@
// @ts-ignore
import chipStyles from "@material/chips/dist/mdc.chips.min.css";
import { ripple } from "@material/mwc-ripple/ripple-directive";
import {
css,
CSSResult,
@@ -12,6 +11,7 @@ import {
unsafeCSS,
} from "lit-element";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-chip";
declare global {
// for fire event
@@ -20,8 +20,8 @@ declare global {
}
}
@customElement("ha-chips")
export class HaChips extends LitElement {
@customElement("ha-chip-set")
export class HaChipSet extends LitElement {
@property() public items = [];
protected render(): TemplateResult {
@@ -33,18 +33,9 @@ export class HaChips extends LitElement {
${this.items.map(
(item, idx) =>
html`
<div class="mdc-chip" .index=${idx} @click=${this._handleClick}>
<div class="mdc-chip__ripple" .ripple="${ripple()}"></div>
<span role="gridcell">
<span
role="button"
tabindex="0"
class="mdc-chip__primary-action"
>
<span class="mdc-chip__text">${item}</span>
</span>
</span>
</div>
<ha-chip .index=${idx} @click=${this._handleClick}>
${item}
</ha-chip>
`
)}
</div>
@@ -60,9 +51,9 @@ export class HaChips extends LitElement {
static get styles(): CSSResult {
return css`
${unsafeCSS(chipStyles)}
.mdc-chip {
background-color: rgba(var(--rgb-primary-text-color), 0.15);
color: var(--primary-text-color);
ha-chip {
margin: 4px;
}
`;
}
@@ -70,6 +61,6 @@ export class HaChips extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"ha-chips": HaChips;
"ha-chip-set": HaChipSet;
}
}
+74
View File
@@ -0,0 +1,74 @@
// @ts-ignore
import chipStyles from "@material/chips/dist/mdc.chips.min.css";
import { ripple } from "@material/mwc-ripple/ripple-directive";
import "./ha-icon";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
unsafeCSS,
} from "lit-element";
declare global {
// for fire event
interface HASSDomEvents {
"chip-clicked": { index: string };
}
}
@customElement("ha-chip")
export class HaChip extends LitElement {
@property() public index = 0;
@property({ type: Boolean }) public hasIcon = false;
protected render(): TemplateResult {
return html`
<div class="mdc-chip" .index=${this.index}>
${this.hasIcon
? html`<div class="mdc-chip__icon mdc-chip__icon--leading">
<slot name="icon"></slot>
</div>`
: null}
<div class="mdc-chip__ripple" .ripple="${ripple()}"></div>
<span role="gridcell">
<span role="button" tabindex="0" class="mdc-chip__primary-action">
<span class="mdc-chip__text"><slot></slot></span>
</span>
</span>
</div>
`;
}
static get styles(): CSSResult {
return css`
${unsafeCSS(chipStyles)}
.mdc-chip {
background-color: var(
--ha-chip-background-color,
rgba(var(--rgb-primary-text-color), 0.15)
);
color: var(--ha-chip-text-color, var(--primary-text-color));
}
.mdc-chip:hover {
color: var(--ha-chip-text-color, var(--primary-text-color));
}
.mdc-chip__icon--leading {
--mdc-icon-size: 20px;
color: var(--ha-chip-icon-color, var(--ha-chip-text-color));
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-chip": HaChip;
}
}
+9 -9
View File
@@ -53,14 +53,14 @@ class HaClimateState extends LitElement {
if (this.stateObj.attributes.current_temperature != null) {
return `${formatNumber(
this.stateObj.attributes.current_temperature,
this.hass!.language
this.hass.locale
)} ${this.hass.config.unit_system.temperature}`;
}
if (this.stateObj.attributes.current_humidity != null) {
return `${formatNumber(
this.stateObj.attributes.current_humidity,
this.hass!.language
this.hass.locale
)} %`;
}
@@ -78,17 +78,17 @@ class HaClimateState extends LitElement {
) {
return `${formatNumber(
this.stateObj.attributes.target_temp_low,
this.hass!.language
this.hass.locale
)}-${formatNumber(
this.stateObj.attributes.target_temp_high,
this.hass!.language
this.hass.locale
)} ${this.hass.config.unit_system.temperature}`;
}
if (this.stateObj.attributes.temperature != null) {
return `${formatNumber(
this.stateObj.attributes.temperature,
this.hass!.language
this.hass.locale
)} ${this.hass.config.unit_system.temperature}`;
}
if (
@@ -97,17 +97,17 @@ class HaClimateState extends LitElement {
) {
return `${formatNumber(
this.stateObj.attributes.target_humidity_low,
this.hass!.language
this.hass.locale
)}-${formatNumber(
this.stateObj.attributes.target_humidity_high,
this.hass!.language
)}%`;
this.hass.locale
)} %`;
}
if (this.stateObj.attributes.humidity != null) {
return `${formatNumber(
this.stateObj.attributes.humidity,
this.hass!.language
this.hass.locale
)} %`;
}
+11
View File
@@ -47,6 +47,17 @@ export class HaCodeEditor extends UpdatingElement {
return this.codemirror ? this.codemirror.state.doc.toString() : this._value;
}
public get hasComments(): boolean {
if (!this.codemirror || !this._loadedCodeMirror) {
return false;
}
const className = this._loadedCodeMirror.HighlightStyle.get(
this.codemirror.state,
this._loadedCodeMirror.tags.comment
);
return !!this.shadowRoot!.querySelector(`span.${className}`);
}
public connectedCallback() {
super.connectedCallback();
if (!this.codemirror) {
+6 -2
View File
@@ -86,6 +86,10 @@ export class HaComboBox extends LitElement {
});
}
public get selectedItem() {
return (this._comboBox as any).selectedItem;
}
protected render(): TemplateResult {
return html`
<vaadin-combo-box-light
@@ -149,9 +153,9 @@ export class HaComboBox extends LitElement {
fireEvent(this, ev.type, ev.detail);
}
private _filterChanged(ev: PolymerChangedEvent<boolean>) {
private _filterChanged(ev: PolymerChangedEvent<string>) {
// @ts-ignore
fireEvent(this, ev.type, ev.detail);
fireEvent(this, ev.type, ev.detail, { composed: false });
}
private _valueChanged(ev: PolymerChangedEvent<string>) {
+119 -56
View File
@@ -1,62 +1,61 @@
import "@vaadin/vaadin-date-picker/theme/material/vaadin-date-picker";
import "@vaadin/vaadin-date-picker/theme/material/vaadin-date-picker-light";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
PropertyValues,
query,
} from "lit-element";
import "@polymer/paper-input/paper-input";
import { fireEvent } from "../common/dom/fire_event";
import { mdiCalendar } from "@mdi/js";
import "./ha-svg-icon";
const VaadinDatePicker = customElements.get("vaadin-date-picker");
const documentContainer = document.createElement("template");
documentContainer.setAttribute("style", "display: none;");
documentContainer.innerHTML = `
<dom-module id="ha-date-input-styles" theme-for="vaadin-text-field">
<template>
<style>
[part="input-field"] {
top: 2px;
height: 30px;
color: var(--primary-text-color);
}
[part="value"] {
text-align: center;
}
</style>
</template>
</dom-module>
`;
document.head.appendChild(documentContainer.content);
export class HaDateInput extends VaadinDatePicker {
constructor() {
super();
this.i18n.formatDate = this._formatISODate;
this.i18n.parseDate = this._parseISODate;
}
ready() {
super.ready();
const styleEl = document.createElement("style");
styleEl.innerHTML = `
:host {
width: 12ex;
margin-top: -6px;
--material-body-font-size: 16px;
--_material-text-field-input-line-background-color: var(--primary-text-color);
--_material-text-field-input-line-opacity: 1;
--material-primary-color: var(--primary-text-color);
}
`;
this.shadowRoot.appendChild(styleEl);
this._inputElement.querySelector("[part='toggle-button']").style.display =
"none";
}
private _formatISODate(d) {
const i18n = {
monthNames: [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
],
weekdays: [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
],
weekdaysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
firstDayOfWeek: 0,
week: "Week",
calendar: "Calendar",
clear: "Clear",
today: "Today",
cancel: "Cancel",
formatTitle: (monthName, fullYear) => {
return monthName + " " + fullYear;
},
formatDate: (d: { day: number; month: number; year: number }) => {
return [
("0000" + String(d.year)).slice(-4),
("0" + String(d.month + 1)).slice(-2),
("0" + String(d.day)).slice(-2),
].join("-");
}
private _parseISODate(text) {
},
parseDate: (text: string) => {
const parts = text.split("-");
const today = new Date();
let date;
@@ -80,11 +79,75 @@ export class HaDateInput extends VaadinDatePicker {
return { day: date, month, year };
}
return undefined;
},
};
@customElement("ha-date-input")
export class HaDateInput extends LitElement {
@property() public value?: string;
@property({ type: Boolean }) public disabled = false;
@property() public label?: string;
@query("vaadin-date-picker-light", true) private _datePicker;
private _inited = false;
updated(changedProps: PropertyValues) {
if (changedProps.has("value")) {
this._datePicker.value = this.value;
this._inited = true;
}
}
render() {
return html`<vaadin-date-picker-light
.disabled=${this.disabled}
@value-changed=${this._valueChanged}
attr-for-value="value"
.i18n=${i18n}
>
<paper-input .label=${this.label} no-label-float>
<ha-svg-icon slot="suffix" .path=${mdiCalendar}></ha-svg-icon>
</paper-input>
</vaadin-date-picker-light>`;
}
private _valueChanged(ev: CustomEvent) {
if (
!this.value ||
(this._inited && !this._compareStringDates(ev.detail.value, this.value))
) {
this.value = ev.detail.value;
fireEvent(this, "change");
fireEvent(this, "value-changed", { value: ev.detail.value });
}
}
private _compareStringDates(a: string, b: string): boolean {
const aParts = a.split("-");
const bParts = b.split("-");
let i = 0;
for (const aPart of aParts) {
if (Number(aPart) !== Number(bParts[i])) {
return false;
}
i++;
}
return true;
}
static get styles(): CSSResult {
return css`
paper-input {
width: 110px;
}
ha-svg-icon {
color: var(--secondary-text-color);
}
`;
}
}
customElements.define("ha-date-input", HaDateInput as any);
declare global {
interface HTMLElementTagNameMap {
"ha-date-input": HaDateInput;
+3 -3
View File
@@ -43,7 +43,7 @@ export class HaDateRangePicker extends LitElement {
protected updated(changedProps: PropertyValues) {
if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.language !== this.hass.language) {
if (!oldHass || oldHass.locale !== this.hass.locale) {
this._hour24format = this._compute24hourFormat();
this._rtlDirection = computeRTLDirection(this.hass);
}
@@ -62,7 +62,7 @@ export class HaDateRangePicker extends LitElement {
<div slot="input" class="date-range-inputs">
<ha-svg-icon .path=${mdiCalendar}></ha-svg-icon>
<paper-input
.value=${formatDateTime(this.startDate, this.hass.language)}
.value=${formatDateTime(this.startDate, this.hass.locale)}
.label=${this.hass.localize(
"ui.components.date-range-picker.start_date"
)}
@@ -71,7 +71,7 @@ export class HaDateRangePicker extends LitElement {
readonly
></paper-input>
<paper-input
.value=${formatDateTime(this.endDate, this.hass.language)}
.value=${formatDateTime(this.endDate, this.hass.locale)}
label=${this.hass.localize(
"ui.components.date-range-picker.end_date"
)}
+3 -2
View File
@@ -11,6 +11,7 @@ import { ifDefined } from "lit-html/directives/if-defined";
import { styleMap } from "lit-html/directives/style-map";
import { formatNumber } from "../common/string/format_number";
import { afterNextRender } from "../common/util/render-status";
import { FrontendTranslationData } from "../data/translation";
import { getValueInPercentage, normalize } from "../util/calculate";
const getAngle = (value: number, min: number, max: number) => {
@@ -29,7 +30,7 @@ export class Gauge extends LitElement {
@property({ type: Number }) public value = 0;
@property({ type: String }) public language = "";
@property() public locale!: FrontendTranslationData;
@property() public label = "";
@@ -90,7 +91,7 @@ export class Gauge extends LitElement {
</svg>
<svg class="text">
<text class="value-text">
${formatNumber(this.value, this.language)} ${this.label}
${formatNumber(this.value, this.locale)} ${this.label}
</text>
</svg>`;
}
+7 -8
View File
@@ -1,3 +1,4 @@
import type HlsType from "hls.js";
import {
css,
CSSResult,
@@ -15,8 +16,6 @@ import { nextRender } from "../common/util/render-status";
import { getExternalConfig } from "../external_app/external_config";
import type { HomeAssistant } from "../types";
type HLSModule = typeof import("hls.js");
@customElement("ha-hls-player")
class HaHLSPlayer extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -43,7 +42,7 @@ class HaHLSPlayer extends LitElement {
@internalProperty() private _attached = false;
private _hlsPolyfillInstance?: Hls;
private _hlsPolyfillInstance?: HlsType;
private _useExoPlayer = false;
@@ -107,8 +106,8 @@ class HaHLSPlayer extends LitElement {
const useExoPlayerPromise = this._getUseExoPlayer();
const masterPlaylistPromise = fetch(this.url);
const hls = ((await import("hls.js")) as any).default as HLSModule;
let hlsSupported = hls.isSupported();
const Hls = (await import("hls.js")).default;
let hlsSupported = Hls.isSupported();
if (!hlsSupported) {
hlsSupported =
@@ -144,8 +143,8 @@ class HaHLSPlayer extends LitElement {
// If codec is HEVC and ExoPlayer is supported, use ExoPlayer.
if (this._useExoPlayer && match !== null && match[1] !== undefined) {
this._renderHLSExoPlayer(playlist_url);
} else if (hls.isSupported()) {
this._renderHLSPolyfill(videoEl, hls, playlist_url);
} else if (Hls.isSupported()) {
this._renderHLSPolyfill(videoEl, Hls, playlist_url);
} else {
this._renderHLSNative(videoEl, playlist_url);
}
@@ -182,7 +181,7 @@ class HaHLSPlayer extends LitElement {
private async _renderHLSPolyfill(
videoEl: HTMLVideoElement,
Hls: HLSModule,
Hls: typeof HlsType,
url: string
) {
const hls = new Hls({
+20 -11
View File
@@ -1,12 +1,9 @@
import { customElement, html, LitElement, property } from "lit-element";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { TimeSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../paper-time-input";
const test = new Date().toLocaleString();
const useAMPM = test.includes("AM") || test.includes("PM");
@customElement("ha-selector-time")
export class HaTimeSelector extends LitElement {
@property() public hass!: HomeAssistant;
@@ -19,16 +16,24 @@ export class HaTimeSelector extends LitElement {
@property({ type: Boolean }) public disabled = false;
private _useAmPm = memoizeOne((language: string) => {
const test = new Date().toLocaleString(language);
return test.includes("AM") || test.includes("PM");
});
protected render() {
const useAMPM = this._useAmPm(this.hass.locale.language);
const parts = this.value?.split(":") || [];
const hours = useAMPM ? parts[0] ?? "12" : parts[0] ?? "0";
const hours = parts[0];
return html`
<paper-time-input
.label=${this.label}
.hour=${useAMPM && Number(hours) > 12 ? Number(hours) - 12 : hours}
.min=${parts[1] ?? "00"}
.sec=${parts[2] ?? "00"}
.hour=${hours &&
(useAMPM && Number(hours) > 12 ? Number(hours) - 12 : hours)}
.min=${parts[1]}
.sec=${parts[2]}
.format=${useAMPM ? 12 : 24}
.amPm=${useAMPM && (Number(hours) > 12 ? "PM" : "AM")}
.disabled=${this.disabled}
@@ -42,12 +47,16 @@ export class HaTimeSelector extends LitElement {
private _timeChanged(ev) {
let value = ev.target.value;
if (useAMPM) {
let hours = Number(ev.target.hour);
const useAMPM = this._useAmPm(this.hass.locale.language);
let hours = Number(ev.target.hour || 0);
if (value && useAMPM) {
if (ev.target.amPm === "PM") {
hours += 12;
}
value = `${hours}:${ev.target.min}:${ev.target.sec}`;
value = `${hours}:${ev.target.min || "00"}:${ev.target.sec || "00"}`;
}
if (value === this.value) {
return;
}
fireEvent(this, "value-changed", {
value,
+108 -61
View File
@@ -1,3 +1,4 @@
import { mdiHelpCircle } from "@mdi/js";
import { HassService, HassServiceTarget } from "home-assistant-js-websocket";
import {
css,
@@ -18,11 +19,12 @@ import { ENTITY_COMPONENT_DOMAINS } from "../data/entity";
import { Selector } from "../data/selector";
import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types";
import { documentationUrl } from "../util/documentation-url";
import "./ha-checkbox";
import "./ha-selector/ha-selector";
import "./ha-service-picker";
import "./ha-settings-row";
import "./ha-yaml-editor";
import "./ha-checkbox";
import type { HaYamlEditor } from "./ha-yaml-editor";
interface ExtHassService extends Omit<HassService, "fields"> {
@@ -36,6 +38,7 @@ interface ExtHassService extends Omit<HassService, "fields"> {
example?: any;
selector?: Selector;
}[];
hasSelector: string[];
}
@customElement("ha-service-control")
@@ -48,17 +51,17 @@ export class HaServiceControl extends LitElement {
data?: Record<string, any>;
};
@internalProperty() private _value!: this["value"];
@property({ reflect: true, type: Boolean }) public narrow!: boolean;
@property({ type: Boolean }) public showAdvanced?: boolean;
@internalProperty() private _serviceData?: ExtHassService;
@internalProperty() private _checkedKeys = new Set();
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
protected updated(changedProperties: PropertyValues) {
protected updated(changedProperties: PropertyValues<this>) {
if (!changedProperties.has("value")) {
return;
}
@@ -70,13 +73,11 @@ export class HaServiceControl extends LitElement {
this._checkedKeys = new Set();
}
this._serviceData = this.value?.service
? this._getServiceInfo(this.value.service)
: undefined;
const serviceData = this._getServiceInfo(this.value?.service);
if (
this._serviceData &&
"target" in this._serviceData &&
serviceData &&
"target" in serviceData &&
(this.value?.data?.entity_id ||
this.value?.data?.area_id ||
this.value?.data?.device_id)
@@ -95,21 +96,23 @@ export class HaServiceControl extends LitElement {
target.device_id = this.value.data.device_id;
}
this.value = {
this._value = {
...this.value,
target,
data: { ...this.value.data },
};
delete this.value.data!.entity_id;
delete this.value.data!.device_id;
delete this.value.data!.area_id;
delete this._value.data!.entity_id;
delete this._value.data!.device_id;
delete this._value.data!.area_id;
} else {
this._value = this.value;
}
if (this.value?.data) {
if (this._value?.data) {
const yamlEditor = this._yamlEditor;
if (yamlEditor && yamlEditor.value !== this.value.data) {
yamlEditor.setValue(this.value.data);
if (yamlEditor && yamlEditor.value !== this._value.data) {
yamlEditor.setValue(this._value.data);
}
}
}
@@ -119,7 +122,7 @@ export class HaServiceControl extends LitElement {
return ENTITY_COMPONENT_DOMAINS.includes(domain) ? [domain] : null;
});
private _getServiceInfo = memoizeOne((service: string):
private _getServiceInfo = memoizeOne((service?: string):
| ExtHassService
| undefined => {
if (!service) {
@@ -147,32 +150,60 @@ export class HaServiceControl extends LitElement {
return {
...serviceDomains[domain][serviceName],
fields,
hasSelector: fields.length
? fields.filter((field) => field.selector).map((field) => field.key)
: [],
};
});
protected render() {
const legacy =
this._serviceData?.fields.length &&
!this._serviceData.fields.some((field) => field.selector);
const serviceData = this._getServiceInfo(this._value?.service);
const shouldRenderServiceDataYaml =
(serviceData?.fields.length && !serviceData.hasSelector.length) ||
(serviceData &&
Object.keys(this._value?.data || {}).some(
(key) => !serviceData!.hasSelector.includes(key)
));
const entityId =
legacy &&
this._serviceData?.fields.find((field) => field.key === "entity_id");
shouldRenderServiceDataYaml &&
serviceData?.fields.find((field) => field.key === "entity_id");
const hasOptional = Boolean(
!legacy &&
this._serviceData?.fields.some(
(field) => field.selector && !field.required
)
!shouldRenderServiceDataYaml &&
serviceData?.fields.some((field) => field.selector && !field.required)
);
return html`<ha-service-picker
.hass=${this.hass}
.value=${this.value?.service}
.value=${this._value?.service}
@value-changed=${this._serviceChanged}
></ha-service-picker>
<p>${this._serviceData?.description}</p>
${this._serviceData && "target" in this._serviceData
<div class="description">
<p>${serviceData?.description}</p>
${this.value?.service
? html` <a
href="${documentationUrl(
this.hass,
"/integrations/" + computeDomain(this.value?.service)
)}"
title="${this.hass.localize(
"ui.components.service-control.integration_doc"
)}"
target="_blank"
rel="noreferrer"
>
<mwc-icon-button>
<ha-svg-icon
path=${mdiHelpCircle}
class="help-icon"
></ha-svg-icon>
</mwc-icon-button>
</a>`
: ""}
</div>
${serviceData && "target" in serviceData
? html`<ha-settings-row .narrow=${this.narrow}>
${hasOptional
? html`<div slot="prefix" class="checkbox-spacer"></div>`
@@ -188,38 +219,42 @@ export class HaServiceControl extends LitElement {
)}</span
><ha-selector
.hass=${this.hass}
.selector=${this._serviceData.target
? { target: this._serviceData.target }
.selector=${serviceData.target
? { target: serviceData.target }
: {
target: {
entity: { domain: computeDomain(this.value!.service) },
entity: { domain: computeDomain(this._value!.service) },
},
}}
@value-changed=${this._targetChanged}
.value=${this.value?.target}
.value=${this._value?.target}
></ha-selector
></ha-settings-row>`
: entityId
? html`<ha-entity-picker
.hass=${this.hass}
.value=${this.value?.data?.entity_id}
.value=${this._value?.data?.entity_id}
.label=${entityId.description}
.includeDomains=${this._domainFilter(this.value!.service)}
.includeDomains=${this._domainFilter(this._value!.service)}
@value-changed=${this._entityPicked}
allow-custom-entity
></ha-entity-picker>`
: ""}
${legacy
${shouldRenderServiceDataYaml
? html`<ha-yaml-editor
.label=${this.hass.localize(
"ui.components.service-control.service_data"
)}
.name=${"data"}
.defaultValue=${this.value?.data}
.defaultValue=${this._value?.data}
@value-changed=${this._dataChanged}
></ha-yaml-editor>`
: this._serviceData?.fields.map((dataField) =>
dataField.selector && (!dataField.advanced || this.showAdvanced)
: serviceData?.fields.map((dataField) =>
dataField.selector &&
(!dataField.advanced ||
this.showAdvanced ||
(this._value?.data &&
this._value.data[dataField.key] !== undefined))
? html`<ha-settings-row .narrow=${this.narrow}>
${dataField.required
? hasOptional
@@ -228,8 +263,8 @@ export class HaServiceControl extends LitElement {
: html`<ha-checkbox
.key=${dataField.key}
.checked=${this._checkedKeys.has(dataField.key) ||
(this.value?.data &&
this.value.data[dataField.key] !== undefined)}
(this._value?.data &&
this._value.data[dataField.key] !== undefined)}
@change=${this._checkboxChanged}
slot="prefix"
></ha-checkbox>`}
@@ -238,15 +273,15 @@ export class HaServiceControl extends LitElement {
><ha-selector
.disabled=${!dataField.required &&
!this._checkedKeys.has(dataField.key) &&
(!this.value?.data ||
this.value.data[dataField.key] === undefined)}
(!this._value?.data ||
this._value.data[dataField.key] === undefined)}
.hass=${this.hass}
.selector=${dataField.selector}
.key=${dataField.key}
@value-changed=${this._serviceDataChanged}
.value=${this.value?.data &&
this.value.data[dataField.key] !== undefined
? this.value.data[dataField.key]
.value=${this._value?.data &&
this._value.data[dataField.key] !== undefined
? this._value.data[dataField.key]
: dataField.default}
></ha-selector
></ha-settings-row>`
@@ -261,13 +296,13 @@ export class HaServiceControl extends LitElement {
this._checkedKeys.add(key);
} else {
this._checkedKeys.delete(key);
const data = { ...this.value?.data };
const data = { ...this._value?.data };
delete data[key];
fireEvent(this, "value-changed", {
value: {
...this.value,
...this._value,
data,
},
});
@@ -277,7 +312,7 @@ export class HaServiceControl extends LitElement {
private _serviceChanged(ev: PolymerChangedEvent<string>) {
ev.stopPropagation();
if (ev.detail.value === this.value?.service) {
if (ev.detail.value === this._value?.service) {
return;
}
fireEvent(this, "value-changed", {
@@ -288,17 +323,17 @@ export class HaServiceControl extends LitElement {
private _entityPicked(ev: CustomEvent) {
ev.stopPropagation();
const newValue = ev.detail.value;
if (this.value?.data?.entity_id === newValue) {
if (this._value?.data?.entity_id === newValue) {
return;
}
let value;
if (!newValue && this.value?.data) {
value = { ...this.value };
if (!newValue && this._value?.data) {
value = { ...this._value };
delete value.data.entity_id;
} else {
value = {
...this.value,
data: { ...this.value?.data, entity_id: ev.detail.value },
...this._value,
data: { ...this._value?.data, entity_id: ev.detail.value },
};
}
fireEvent(this, "value-changed", {
@@ -309,15 +344,15 @@ export class HaServiceControl extends LitElement {
private _targetChanged(ev: CustomEvent) {
ev.stopPropagation();
const newValue = ev.detail.value;
if (this.value?.target === newValue) {
if (this._value?.target === newValue) {
return;
}
let value;
if (!newValue) {
value = { ...this.value };
value = { ...this._value };
delete value.target;
} else {
value = { ...this.value, target: ev.detail.value };
value = { ...this._value, target: ev.detail.value };
}
fireEvent(this, "value-changed", {
value,
@@ -328,11 +363,14 @@ export class HaServiceControl extends LitElement {
ev.stopPropagation();
const key = (ev.currentTarget as any).key;
const value = ev.detail.value;
if (this.value?.data && this.value.data[key] === value) {
if (
this._value?.data?.[key] === value ||
(!this._value?.data?.[key] && (value === "" || value === undefined))
) {
return;
}
const data = { ...this.value?.data, [key]: value };
const data = { ...this._value?.data, [key]: value };
if (value === "" || value === undefined) {
delete data[key];
@@ -340,7 +378,7 @@ export class HaServiceControl extends LitElement {
fireEvent(this, "value-changed", {
value: {
...this.value,
...this._value,
data,
},
});
@@ -353,7 +391,7 @@ export class HaServiceControl extends LitElement {
}
fireEvent(this, "value-changed", {
value: {
...this.value,
...this._value,
data: ev.detail.value,
},
});
@@ -396,6 +434,15 @@ export class HaServiceControl extends LitElement {
ha-checkbox {
margin-left: -16px;
}
.help-icon {
color: var(--secondary-text-color);
}
.description {
justify-content: space-between;
display: flex;
align-items: center;
padding-right: 2px;
}
`;
}
}
+2 -2
View File
@@ -245,7 +245,7 @@ class HaSidebar extends LitElement {
hass.panelUrl !== oldHass.panelUrl ||
hass.user !== oldHass.user ||
hass.localize !== oldHass.localize ||
hass.language !== oldHass.language ||
hass.locale !== oldHass.locale ||
hass.states !== oldHass.states ||
hass.defaultPanel !== oldHass.defaultPanel
);
@@ -281,7 +281,7 @@ class HaSidebar extends LitElement {
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.language !== this.hass.language) {
if (!oldHass || oldHass.locale !== this.hass.locale) {
this.rtl = computeRTL(this.hass);
}
+36 -29
View File
@@ -125,35 +125,41 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
return html``;
}
return html`<div class="mdc-chip-set items">
${ensureArray(this.value?.area_id)?.map((area_id) => {
const area = this._areas![area_id];
return this._renderChip(
"area_id",
area_id,
area?.name || area_id,
undefined,
mdiSofa
);
})}
${ensureArray(this.value?.device_id)?.map((device_id) => {
const device = this._devices![device_id];
return this._renderChip(
"device_id",
device_id,
device ? computeDeviceName(device, this.hass) : device_id,
undefined,
mdiDevices
);
})}
${ensureArray(this.value?.entity_id)?.map((entity_id) => {
const entity = this.hass.states[entity_id];
return this._renderChip(
"entity_id",
entity_id,
entity ? computeStateName(entity) : entity_id,
entity ? stateIcon(entity) : undefined
);
})}
${this.value?.area_id
? ensureArray(this.value.area_id).map((area_id) => {
const area = this._areas![area_id];
return this._renderChip(
"area_id",
area_id,
area?.name || area_id,
undefined,
mdiSofa
);
})
: ""}
${this.value?.device_id
? ensureArray(this.value.device_id).map((device_id) => {
const device = this._devices![device_id];
return this._renderChip(
"device_id",
device_id,
device ? computeDeviceName(device, this.hass) : device_id,
undefined,
mdiDevices
);
})
: ""}
${this.value?.entity_id
? ensureArray(this.value.entity_id).map((entity_id) => {
const entity = this.hass.states[entity_id];
return this._renderChip(
"entity_id",
entity_id,
entity ? computeStateName(entity) : entity_id,
entity ? stateIcon(entity) : undefined
);
})
: ""}
</div>
${this._renderPicker()}
<div class="mdc-chip-set">
@@ -344,6 +350,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
.includeDeviceClasses=${this.includeDeviceClasses}
.includeDomains=${this.includeDomains}
@value-changed=${this._targetPicked}
allow-custom-entity
></ha-entity-picker>`;
}
return html``;
+14 -3
View File
@@ -2,6 +2,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { computeStateDisplay } from "../common/entity/compute_state_display";
import { formatNumber } from "../common/string/format_number";
import LocalizeMixin from "../mixins/localize-mixin";
/*
@@ -55,21 +56,31 @@ class HaWaterHeaterState extends LocalizeMixin(PolymerElement) {
computeTarget(hass, stateObj) {
if (!hass || !stateObj) return null;
// We're using "!= null" on purpose so that we match both null and undefined.
if (
stateObj.attributes.target_temp_low != null &&
stateObj.attributes.target_temp_high != null
) {
return `${stateObj.attributes.target_temp_low} - ${stateObj.attributes.target_temp_high} ${hass.config.unit_system.temperature}`;
return `${formatNumber(
stateObj.attributes.target_temp_low,
this.hass.locale
)} - ${formatNumber(
stateObj.attributes.target_temp_high,
this.hass.locale
)} ${hass.config.unit_system.temperature}`;
}
if (stateObj.attributes.temperature != null) {
return `${stateObj.attributes.temperature} ${hass.config.unit_system.temperature}`;
return `${formatNumber(
stateObj.attributes.temperature,
this.hass.locale
)} ${hass.config.unit_system.temperature}`;
}
return "";
}
_localizeState(stateObj) {
return computeStateDisplay(this.hass.localize, stateObj);
return computeStateDisplay(this.hass.localize, stateObj, this.hass.locale);
}
}
customElements.define("ha-water_heater-state", HaWaterHeaterState);
+16 -6
View File
@@ -107,6 +107,10 @@ export class PaperTimeInput extends PolymerElement {
#millisec {
width: 38px;
}
.no-suffix {
margin-left: -2px;
}
</style>
<label hidden$="[[hideLabel]]">[[label]]</label>
@@ -129,11 +133,12 @@ export class PaperTimeInput extends PolymerElement {
always-float-label$="[[alwaysFloatInputLabels]]"
disabled="[[disabled]]"
>
<span suffix="" slot="suffix">:</span>
<span suffix slot="suffix">:</span>
</paper-input>
<!-- Min Input -->
<paper-input
class$="[[_computeClassNames(enableSecond)]]"
id="min"
type="number"
value="{{min}}"
@@ -155,6 +160,7 @@ export class PaperTimeInput extends PolymerElement {
<!-- Sec Input -->
<paper-input
class$="[[_computeClassNames(enableMillisecond)]]"
id="sec"
type="number"
value="{{sec}}"
@@ -297,28 +303,28 @@ export class PaperTimeInput extends PolymerElement {
notify: true,
},
/**
* Suffix for the hour input
* Label for the hour input
*/
hourLabel: {
type: String,
value: "",
},
/**
* Suffix for the min input
* Label for the min input
*/
minLabel: {
type: String,
value: ":",
value: "",
},
/**
* Suffix for the sec input
* Label for the sec input
*/
secLabel: {
type: String,
value: "",
},
/**
* Suffix for the milli sec input
* Label for the milli sec input
*/
millisecLabel: {
type: String,
@@ -479,6 +485,10 @@ export class PaperTimeInput extends PolymerElement {
_equal(n1, n2) {
return n1 === n2;
}
_computeClassNames(hasSuffix) {
return hasSuffix ? " " : "no-suffix";
}
}
customElements.define("paper-time-input", PaperTimeInput);
+1 -1
View File
@@ -361,7 +361,7 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
const item = items[0];
const date = data.datasets[item.datasetIndex].data[item.index].x;
return formatDateTimeWithSeconds(date, this.hass.language);
return formatDateTimeWithSeconds(date, this.hass.locale);
};
const chartOptions = {
@@ -201,8 +201,8 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
const formatTooltipLabel = (item, data) => {
const values = data.datasets[item.datasetIndex].data[item.index];
const start = formatDateTimeWithSeconds(values[0], this.hass.language);
const end = formatDateTimeWithSeconds(values[1], this.hass.language);
const start = formatDateTimeWithSeconds(values[0], this.hass.locale);
const end = formatDateTimeWithSeconds(values[1], this.hass.locale);
const state = values[2];
return [state, start, end];
+125
View File
@@ -0,0 +1,125 @@
import { mdiCircleOutline } from "@mdi/js";
import {
LitElement,
customElement,
html,
css,
property,
TemplateResult,
internalProperty,
} from "lit-element";
import { buttonLinkStyle } from "../../resources/styles";
import "../ha-svg-icon";
@customElement("ha-timeline")
export class HaTimeline extends LitElement {
@property({ type: Boolean, reflect: true }) public label = false;
@property({ type: Boolean, reflect: true }) public raised = false;
@property({ type: Boolean }) public lastItem = false;
@property({ type: String }) public icon?: string;
@property({ attribute: false }) public moreItems?: TemplateResult[];
@internalProperty() private _showMore = false;
protected render() {
return html`
<div class="timeline-start">
${this.label
? ""
: html`
<ha-svg-icon .path=${this.icon || mdiCircleOutline}></ha-svg-icon>
`}
${this.lastItem ? "" : html`<div class="line"></div>`}
</div>
<div class="content">
<slot></slot>
${!this.moreItems
? ""
: html`
<div>
${this._showMore ||
// If there is only 1 item hidden behind "show more", just show it
// instead of showing the more info link. We're not animals.
this.moreItems.length === 1
? this.moreItems
: html`
<button class="link" @click=${this._handleShowMore}>
Show ${this.moreItems.length} more items
</button>
`}
</div>
`}
</div>
`;
}
private _handleShowMore() {
this._showMore = true;
}
static get styles() {
return [
css`
:host {
display: flex;
flex-direction: row;
}
:host(:not([lastItem])) {
min-height: 50px;
}
:host([label]) {
margin-top: -8px;
font-style: italic;
color: var(--timeline-label-color, var(--secondary-text-color));
}
.timeline-start {
display: flex;
flex-direction: column;
align-items: center;
margin-right: 8px;
width: 24px;
}
ha-svg-icon {
color: var(
--timeline-ball-color,
var(--timeline-color, var(--secondary-text-color))
);
border-radius: 50%;
}
:host([raised]) ha-svg-icon {
transform: scale(1.3);
}
.line {
flex: 1;
width: 2px;
background-color: var(
--timeline-line-color,
var(--timeline-color, var(--secondary-text-color))
);
margin: 4px 0;
}
.content {
margin-top: 2px;
}
:host(:not([lastItem])) .content {
padding-bottom: 16px;
}
:host([label]) .content {
margin-top: 0;
padding-top: 6px;
}
`,
buttonLinkStyle,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-timeline": HaTimeline;
}
}
+166
View File
@@ -0,0 +1,166 @@
import { css, customElement, LitElement, property, svg } from "lit-element";
import { NODE_SIZE, SPACING } from "./hat-graph";
@customElement("hat-graph-node")
export class HatGraphNode extends LitElement {
@property() iconPath?: string;
@property({ reflect: true, type: Boolean }) disabled?: boolean;
@property({ reflect: true, type: Boolean }) graphstart?: boolean;
@property({ reflect: true, type: Boolean }) nofocus?: boolean;
@property({ reflect: true, type: Number }) badge?: number;
connectedCallback() {
super.connectedCallback();
if (!this.hasAttribute("tabindex") && !this.nofocus)
this.setAttribute("tabindex", "0");
}
render() {
const height = NODE_SIZE + (this.graphstart ? 2 : SPACING + 1);
const width = SPACING + NODE_SIZE;
return svg`
<svg
width="${width}px"
height="${height}px"
viewBox="-${Math.ceil(width / 2)} -${
this.graphstart
? Math.ceil(height / 2)
: Math.ceil((NODE_SIZE + SPACING * 2) / 2)
} ${width} ${height}"
>
${
this.graphstart
? ``
: svg`
<path
class="connector"
d="
M 0 ${-SPACING - NODE_SIZE / 2}
L 0 0
"
line-caps="round"
/>
`
}
<g class="node">
<circle
cx="0"
cy="0"
r="${NODE_SIZE / 2}"
/>
}
${
this.badge
? svg`
<g class="number">
<circle
cx="8"
cy="${-NODE_SIZE / 2}"
r="8"
></circle>
<text
x="8"
y="${-NODE_SIZE / 2}"
text-anchor="middle"
alignment-baseline="middle"
>${this.badge > 9 ? "9+" : this.badge}</text>
</g>
`
: ""
}
<g
style="pointer-events: none"
transform="translate(${-12} ${-12})"
>
${this.iconPath ? svg`<path class="icon" d="${this.iconPath}"/>` : ""}
</g>
</g>
</svg>
`;
}
static get styles() {
return css`
:host {
display: flex;
flex-direction: column;
}
:host(.track) {
--stroke-clr: var(--track-clr);
--icon-clr: var(--default-icon-clr);
}
:host(.active) circle {
--stroke-clr: var(--active-clr);
--icon-clr: var(--default-icon-clr);
}
:host(:focus) {
outline: none;
}
:host(:hover) circle {
--stroke-clr: var(--hover-clr);
--icon-clr: var(--default-icon-clr);
}
:host([disabled]) circle {
stroke: var(--disabled-clr);
}
:host-context([disabled]) {
--stroke-clr: var(--disabled-clr);
}
:host([nofocus]):host-context(.active),
:host([nofocus]):host-context(:focus) {
--circle-clr: var(--active-clr);
--icon-clr: var(--default-icon-clr);
}
circle,
path.connector {
stroke: var(--stroke-clr);
stroke-width: 2;
fill: none;
}
circle {
fill: var(--background-clr);
stroke: var(--circle-clr, var(--stroke-clr));
}
.number circle {
fill: var(--track-clr);
stroke: none;
stroke-width: 0;
}
.number text {
font-size: smaller;
}
path.icon {
fill: var(--icon-clr);
}
:host(.triggered) svg {
overflow: visible;
}
:host(.triggered) circle {
animation: glow 10s;
}
@keyframes glow {
0% {
filter: drop-shadow(0px 0px 5px rgba(var(--rgb-trigger-color), 0));
}
10% {
filter: drop-shadow(0px 0px 10px rgba(var(--rgb-trigger-color), 1));
}
100% {
filter: drop-shadow(0px 0px 5px rgba(var(--rgb-trigger-color), 0));
}
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hat-graph-node": HatGraphNode;
}
}
+55
View File
@@ -0,0 +1,55 @@
import { css, customElement, LitElement, property, svg } from "lit-element";
import { NODE_SIZE, SPACING } from "./hat-graph";
@customElement("hat-graph-spacer")
export class HatGraphSpacer extends LitElement {
@property({ reflect: true, type: Boolean }) disabled?: boolean;
render() {
return svg`
<svg
width="${SPACING}px"
height="${SPACING + NODE_SIZE + 1}px"
viewBox="-${SPACING / 2} 0 10 ${SPACING + NODE_SIZE + 1}"
>
<path
class="connector"
d="
M 0 ${SPACING + NODE_SIZE + 1}
L 0 0
"
line-caps="round"
/>
}
</svg>
`;
}
static get styles() {
return css`
:host {
display: flex;
flex-direction: column;
}
:host(.track) {
--stroke-clr: var(--track-clr);
--icon-clr: var(--default-icon-clr);
}
:host-context([disabled]) {
--stroke-clr: var(--disabled-clr);
}
path.connector {
stroke: var(--stroke-clr);
stroke-width: 2;
fill: none;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hat-graph-spacer": HatGraphSpacer;
}
}
+225
View File
@@ -0,0 +1,225 @@
import {
css,
customElement,
html,
LitElement,
property,
svg,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
export const BRANCH_HEIGHT = 20;
export const SPACING = 10;
export const NODE_SIZE = 30;
const track_converter = {
fromAttribute: (value) => value.split(",").map((v) => parseInt(v)),
toAttribute: (value) =>
value instanceof Array ? value.join(",") : `${value}`,
};
export interface NodeInfo {
path: string;
config: any;
}
interface BranchConfig {
x: number;
height: number;
start: boolean;
end: boolean;
}
@customElement("hat-graph")
export class HatGraph extends LitElement {
@property({ type: Number }) _num_items = 0;
@property({ reflect: true, type: Boolean }) branching?: boolean;
@property({ reflect: true, converter: track_converter })
track_start?: number[];
@property({ reflect: true, converter: track_converter }) track_end?: number[];
@property({ reflect: true, type: Boolean }) disabled?: boolean;
@property({ reflect: true, type: Boolean }) selected?: boolean;
@property({ reflect: true, type: Boolean }) short = false;
async updateChildren() {
this._num_items = this.children.length;
}
render() {
const branches: BranchConfig[] = [];
let total_width = 0;
let max_height = 0;
let min_height = Number.POSITIVE_INFINITY;
if (this.branching) {
for (const c of Array.from(this.children)) {
if (c.slot === "head") continue;
const rect = c.getBoundingClientRect();
branches.push({
x: rect.width / 2 + total_width,
height: rect.height,
start: c.getAttribute("graphStart") != null,
end: c.getAttribute("graphEnd") != null,
});
total_width += rect.width;
max_height = Math.max(max_height, rect.height);
min_height = Math.min(min_height, rect.height);
}
}
return html`
<slot name="head" @slotchange=${this.updateChildren}> </slot>
${this.branching
? svg`
<svg
id="top"
width="${total_width}"
height="${BRANCH_HEIGHT}"
>
${branches.map((branch, i) => {
if (branch.start) return "";
return svg`
<path
class="${classMap({
line: true,
track: this.track_start?.includes(i) ?? false,
})}"
id="${this.track_start?.includes(i) ? "track-start" : ""}"
index=${i}
d="
M ${total_width / 2} 0
L ${branch.x} ${BRANCH_HEIGHT}
"/>
`;
})}
<use xlink:href="#track-start" />
</svg>
`
: ""}
<div id="branches">
${this.branching
? svg`
<svg
id="lines"
width="${total_width}"
height="${max_height}"
>
${branches.map((branch, i) => {
if (branch.end) return "";
return svg`
<path
class="${classMap({
line: true,
track: this.track_end?.includes(i) ?? false,
})}"
index=${i}
d="
M ${branch.x} ${branch.height}
l 0 ${max_height - branch.height}
"/>
`;
})}
</svg>
`
: ""}
<slot @slotchange=${this.updateChildren}></slot>
</div>
${this.branching && !this.short
? svg`
<svg
id="bottom"
width="${total_width}"
height="${BRANCH_HEIGHT + SPACING}"
>
${branches.map((branch, i) => {
if (branch.end) return "";
return svg`
<path
class="${classMap({
line: true,
track: this.track_end?.includes(i) ?? false,
})}"
id="${this.track_end?.includes(i) ? "track-end" : ""}"
index=${i}
d="
M ${branch.x} 0
L ${branch.x} ${SPACING}
L ${total_width / 2} ${BRANCH_HEIGHT + SPACING}
"/>
`;
})}
<use xlink:href="#track-end" />
</svg>
`
: ""}
`;
}
static get styles() {
return css`
:host {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
--stroke-clr: var(--stroke-color, var(--secondary-text-color));
--active-clr: var(--active-color, var(--primary-color));
--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));
--default-trigger-color: 3, 169, 244;
--rgb-trigger-color: var(--trigger-color, var(--default-trigger-color));
--background-clr: var(--background-color, white);
--default-icon-clr: var(--icon-color, black);
--icon-clr: var(--stroke-clr);
}
:host(:focus) {
outline: none;
}
#branches {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}
:host([branching]) #branches {
flex-direction: row;
align-items: start;
}
:host([branching]) ::slotted(*) {
z-index: 1;
}
:host([branching]) ::slotted([slot="head"]) {
margin-bottom: ${-BRANCH_HEIGHT / 2}px;
}
#lines {
position: absolute;
}
path.line {
stroke: var(--stroke-clr);
stroke-width: 2;
fill: none;
}
path.line.track {
stroke: var(--track-clr);
}
:host([disabled]) path.line {
stroke: var(--disabled-clr);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hat-graph": HatGraph;
}
}
+26
View File
@@ -0,0 +1,26 @@
import { LitElement, css, html, customElement } from "lit-element";
@customElement("hat-logbook-note")
class HatLogbookNote extends LitElement {
render() {
return html`
Not all shown logbook entries might be related to this automation.
`;
}
static styles = css`
:host {
display: block;
text-align: center;
font-style: italic;
padding: 16px;
margin-top: 8px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"hat-logbook-note": HatLogbookNote;
}
}
+573
View File
@@ -0,0 +1,573 @@
import {
html,
LitElement,
property,
customElement,
PropertyValues,
css,
} from "lit-element";
import "@material/mwc-icon-button/mwc-icon-button";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-svg-icon";
import {
AutomationTraceExtended,
ChooseActionTraceStep,
ConditionTraceStep,
} from "../../data/trace";
import {
mdiAbTesting,
mdiArrowUp,
mdiAsterisk,
mdiCallSplit,
mdiCheckboxBlankOutline,
mdiCheckBoxOutline,
mdiChevronDown,
mdiChevronRight,
mdiChevronUp,
mdiClose,
mdiCodeBrackets,
mdiDevices,
mdiExclamation,
mdiRefresh,
mdiTimerOutline,
mdiTrafficLight,
} from "@mdi/js";
import "./hat-graph-node";
import { classMap } from "lit-html/directives/class-map";
import { NODE_SIZE, SPACING, NodeInfo } from "./hat-graph";
import { Condition, Trigger } from "../../data/automation";
import {
Action,
ChooseAction,
DelayAction,
DeviceAction,
EventAction,
RepeatAction,
SceneAction,
ServiceAction,
WaitAction,
WaitForTriggerAction,
} from "../../data/script";
import { ensureArray } from "../../common/ensure-array";
import "./hat-graph-spacer";
declare global {
interface HASSDomEvents {
"graph-node-selected": NodeInfo;
}
}
@customElement("hat-script-graph")
class HatScriptGraph extends LitElement {
@property({ attribute: false }) public trace!: AutomationTraceExtended;
@property({ attribute: false }) public selected;
@property() trackedNodes: Record<string, any> = {};
private selectNode(config, path) {
return () => {
fireEvent(this, "graph-node-selected", { config, path });
};
}
private render_trigger(config: Trigger, i: number) {
const path = `trigger/${i}`;
const tracked = this.trace && path in this.trace.trace;
if (tracked) {
this.trackedNodes[path] = { config, path };
}
return html`
<hat-graph-node
graphStart
@focus=${this.selectNode(config, path)}
class=${classMap({
track: tracked,
active: this.selected === path,
})}
.iconPath=${mdiAsterisk}
tabindex=${tracked ? "0" : "-1"}
></hat-graph-node>
`;
}
private render_condition(config: Condition, i: number) {
const path = `condition/${i}`;
const trace = this.trace.trace[path] as ConditionTraceStep[] | undefined;
const track_path =
trace?.[0].result === undefined ? 0 : trace[0].result.result ? 1 : 2;
if (trace) {
this.trackedNodes[path] = { config, path };
}
return html`
<hat-graph
branching
@focus=${this.selectNode(config, path)}
class=${classMap({
track: track_path,
active: this.selected === path,
})}
.track_start=${[track_path]}
.track_end=${[track_path]}
tabindex=${trace ? "-1" : "0"}
short
>
<hat-graph-node
slot="head"
class=${classMap({
track: trace !== undefined,
})}
.iconPath=${mdiAbTesting}
nofocus
graphEnd
></hat-graph-node>
<div
style=${`width: ${NODE_SIZE + SPACING}px;`}
graphStart
graphEnd
></div>
<div></div>
<hat-graph-node
.iconPath=${mdiClose}
graphEnd
nofocus
class=${classMap({
track: track_path === 2,
})}
></hat-graph-node>
</hat-graph>
`;
}
private render_choose_node(config: ChooseAction, path: string) {
const trace = this.trace.trace[path] as ChooseActionTraceStep[] | undefined;
const trace_path = trace?.[0].result
? trace[0].result.choice === "default"
? [config.choose?.length || 0]
: [trace[0].result.choice]
: [];
return html`
<hat-graph
tabindex=${trace === undefined ? "-1" : "0"}
branching
.track_start=${trace_path}
.track_end=${trace_path}
@focus=${this.selectNode(config, path)}
class=${classMap({
track: trace !== undefined,
active: this.selected === path,
})}
>
<hat-graph-node
.iconPath=${mdiCallSplit}
class=${classMap({
track: trace !== undefined,
})}
slot="head"
nofocus
></hat-graph-node>
${config.choose?.map((branch, i) => {
const branch_path = `${path}/choose/${i}`;
const track_this =
trace !== undefined && trace[0].result?.choice === i;
if (track_this) {
this.trackedNodes[branch_path] = { config, path: branch_path };
}
return html`
<hat-graph>
<hat-graph-node
.iconPath=${!trace || track_this
? mdiCheckBoxOutline
: mdiCheckboxBlankOutline}
@focus=${this.selectNode(config, branch_path)}
class=${classMap({
active: this.selected === branch_path,
track: track_this,
})}
></hat-graph-node>
${ensureArray(branch.sequence).map((action, j) =>
this.render_node(action, `${branch_path}/sequence/${j}`)
)}
</hat-graph>
`;
})}
<hat-graph>
<hat-graph-spacer
class=${classMap({
track:
trace !== undefined && trace[0].result?.choice === "default",
})}
></hat-graph-spacer>
${ensureArray(config.default)?.map((action, i) =>
this.render_node(action, `${path}/default/${i}`)
)}
</hat-graph>
</hat-graph>
`;
}
private render_condition_node(node: Condition, path: string) {
const trace = (this.trace.trace[path] as ConditionTraceStep[]) || undefined;
const track_path =
trace?.[0].result === undefined ? 0 : trace[0].result.result ? 1 : 2;
return html`
<hat-graph
branching
@focus=${this.selectNode(node, path)}
class=${classMap({
track: track_path,
active: this.selected === path,
})}
.track_start=${[track_path]}
.track_end=${[track_path]}
tabindex=${trace === undefined ? "-1" : "0"}
short
>
<hat-graph-node
slot="head"
class=${classMap({
track: Boolean(trace),
})}
.iconPath=${mdiAbTesting}
nofocus
graphEnd
></hat-graph-node>
<div
style=${`width: ${NODE_SIZE + SPACING}px;`}
graphStart
graphEnd
></div>
<div></div>
<hat-graph-node
.iconPath=${mdiClose}
graphEnd
nofocus
class=${classMap({
track: track_path === 2,
})}
></hat-graph-node>
</hat-graph>
`;
}
private render_delay_node(node: DelayAction, path: string) {
return html`
<hat-graph-node
.iconPath=${mdiTimerOutline}
@focus=${this.selectNode(node, path)}
class=${classMap({
track: path in this.trace.trace,
active: this.selected === path,
})}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node>
`;
}
private render_device_node(node: DeviceAction, path: string) {
return html`
<hat-graph-node
.iconPath=${mdiDevices}
@focus=${this.selectNode(node, path)}
class=${classMap({
track: path in this.trace.trace,
active: this.selected === path,
})}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node>
`;
}
private render_event_node(node: EventAction, path: string) {
return html`
<hat-graph-node
.iconPath=${mdiExclamation}
@focus=${this.selectNode(node, path)}
class=${classMap({
track: path in this.trace.trace,
active: this.selected === path,
})}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node>
`;
}
private render_repeat_node(node: RepeatAction, path: string) {
const trace: any = this.trace.trace[path];
const track_path = trace ? [0, 1] : [];
const repeats = this.trace?.trace[`${path}/repeat/sequence/0`]?.length;
return html`
<hat-graph
.track_start=${track_path}
.track_end=${track_path}
tabindex=${trace === undefined ? "-1" : "0"}
branching
@focus=${this.selectNode(node, path)}
class=${classMap({
track: path in this.trace.trace,
active: this.selected === path,
})}
>
<hat-graph-node
.iconPath=${mdiRefresh}
class=${classMap({
track: trace,
})}
slot="head"
nofocus
></hat-graph-node>
<hat-graph-node
.iconPath=${mdiArrowUp}
nofocus
class=${classMap({
track: track_path.includes(1),
})}
.badge=${repeats}
></hat-graph-node>
<hat-graph>
${ensureArray(node.repeat.sequence).map((action, i) =>
this.render_node(action, `${path}/repeat/sequence/${i}`)
)}
</hat-graph>
</hat-graph>
`;
}
private render_scene_node(node: SceneAction, path: string) {
return html`
<hat-graph-node
.iconPath=${mdiExclamation}
@focus=${this.selectNode(node, path)}
class=${classMap({
track: path in this.trace.trace,
active: this.selected === path,
})}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node>
`;
}
private render_service_node(node: ServiceAction, path: string) {
return html`
<hat-graph-node
.iconPath=${mdiChevronRight}
@focus=${this.selectNode(node, path)}
class=${classMap({
track: path in this.trace.trace,
active: this.selected === path,
})}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node>
`;
}
private render_wait_node(
node: WaitAction | WaitForTriggerAction,
path: string
) {
return html`
<hat-graph-node
.iconPath=${mdiTrafficLight}
@focus=${this.selectNode(node, path)}
class=${classMap({
track: path in this.trace.trace,
active: this.selected === path,
})}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node>
`;
}
private render_other_node(node: Action, path: string) {
return html`
<hat-graph-node
.iconPath=${mdiCodeBrackets}
@focus=${this.selectNode(node, path)}
class=${classMap({
track: path in this.trace.trace,
active: this.selected === path,
})}
></hat-graph-node>
`;
}
private render_node(node: Action, path: string) {
const NODE_TYPES = {
choose: this.render_choose_node,
condition: this.render_condition_node,
delay: this.render_delay_node,
device_id: this.render_device_node,
event: this.render_event_node,
repeat: this.render_repeat_node,
scene: this.render_scene_node,
service: this.render_service_node,
wait_template: this.render_wait_node,
wait_for_trigger: this.render_wait_node,
other: this.render_other_node,
};
const type = Object.keys(NODE_TYPES).find((key) => key in node) || "other";
const nodeEl = NODE_TYPES[type].bind(this)(node, path);
if (this.trace && path in this.trace.trace) {
this.trackedNodes[path] = { config: node, path };
}
return nodeEl;
}
protected render() {
const paths = Object.keys(this.trackedNodes);
const manual_triggered = this.trace && "trigger" in this.trace.trace;
let track_path = manual_triggered ? undefined : [0];
const trigger_nodes = ensureArray(this.trace.config.trigger).map(
(trigger, i) => {
if (this.trace && `trigger/${i}` in this.trace.trace) {
track_path = [i];
}
return this.render_trigger(trigger, i);
}
);
try {
return html`
<hat-graph class="parent">
<div></div>
<hat-graph
branching
id="trigger"
.short=${trigger_nodes.length < 2}
.track_start=${track_path}
.track_end=${track_path}
>
${trigger_nodes}
</hat-graph>
<hat-graph id="condition">
${ensureArray(this.trace.config.condition)?.map((condition, i) =>
this.render_condition(condition!, i)
)}
</hat-graph>
${ensureArray(this.trace.config.action).map((action, i) =>
this.render_node(action, `action/${i}`)
)}
</hat-graph>
<div class="actions">
<mwc-icon-button
.disabled=${paths.length === 0 || paths[0] === this.selected}
@click=${this.previousTrackedNode}
>
<ha-svg-icon .path=${mdiChevronUp}></ha-svg-icon>
</mwc-icon-button>
<mwc-icon-button
.disabled=${paths.length === 0 ||
paths[paths.length - 1] === this.selected}
@click=${this.nextTrackedNode}
>
<ha-svg-icon .path=${mdiChevronDown}></ha-svg-icon>
</mwc-icon-button>
</div>
`;
} catch (err) {
if (__DEV__) {
// eslint-disable-next-line no-console
console.log("Error creating script graph:", err);
}
return html`
<div class="error">
Error rendering graph. Please download trace and share with the
developers.
</div>
`;
}
}
protected update(changedProps: PropertyValues<this>) {
if (changedProps.has("trace")) {
this.trackedNodes = {};
}
super.update(changedProps);
}
protected updated(changedProps: PropertyValues<this>) {
super.updated(changedProps);
// Select first node if new trace loaded but no selection given.
if (changedProps.has("trace")) {
const tracked = this.getTrackedNodes();
const paths = Object.keys(tracked);
// If trace changed and we have no or an invalid selection, select first option.
if (this.selected === "" || !(this.selected in paths)) {
// Find first tracked node with node info
for (const path of paths) {
if (tracked[path]) {
fireEvent(this, "graph-node-selected", tracked[path]);
break;
}
}
}
if (this.trace) {
const sortKeys = Object.keys(this.trace.trace);
const keys = Object.keys(this.trackedNodes).sort(
(a, b) => sortKeys.indexOf(a) - sortKeys.indexOf(b)
);
const sortedTrackedNodes = keys.reduce((obj, key) => {
obj[key] = this.trackedNodes[key];
return obj;
}, {});
this.trackedNodes = sortedTrackedNodes;
}
}
}
public getTrackedNodes() {
return this.trackedNodes;
}
public previousTrackedNode() {
const tracked = this.getTrackedNodes();
const nodes = Object.keys(tracked);
for (let i = nodes.indexOf(this.selected) - 1; i >= 0; i--) {
if (tracked[nodes[i]]) {
fireEvent(this, "graph-node-selected", tracked[nodes[i]]);
break;
}
}
}
public nextTrackedNode() {
const tracked = this.getTrackedNodes();
const nodes = Object.keys(tracked);
for (let i = nodes.indexOf(this.selected) + 1; i < nodes.length; i++) {
if (tracked[nodes[i]]) {
fireEvent(this, "graph-node-selected", tracked[nodes[i]]);
break;
}
}
}
static get styles() {
return css`
:host {
display: flex;
}
.actions {
display: flex;
flex-direction: column;
}
.parent {
margin-left: 8px;
}
.error {
padding: 16px;
max-width: 300px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hat-script-graph": HatScriptGraph;
}
}
+600
View File
@@ -0,0 +1,600 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import {
AutomationTraceExtended,
ChooseActionTraceStep,
getDataFromPath,
TriggerTraceStep,
isTriggerPath,
} from "../../data/trace";
import { HomeAssistant } from "../../types";
import "./ha-timeline";
import type { HaTimeline } from "./ha-timeline";
import {
mdiAlertCircle,
mdiCircle,
mdiCircleOutline,
mdiProgressClock,
mdiProgressWrench,
mdiRecordCircleOutline,
} from "@mdi/js";
import { LogbookEntry } from "../../data/logbook";
import {
ChooseAction,
ChooseActionChoice,
getActionType,
} from "../../data/script";
import relativeTime from "../../common/datetime/relative_time";
import { fireEvent } from "../../common/dom/fire_event";
import { describeAction } from "../../data/script_i18n";
import { ifDefined } from "lit-html/directives/if-defined";
const LOGBOOK_ENTRIES_BEFORE_FOLD = 2;
/* eslint max-classes-per-file: "off" */
// Report time entry when more than this time has passed
const SIGNIFICANT_TIME_CHANGE = 1000; // 1 seconds
const isSignificantTimeChange = (a: Date, b: Date) =>
Math.abs(b.getTime() - a.getTime()) > SIGNIFICANT_TIME_CHANGE;
class RenderedTimeTracker {
private lastReportedTime: Date;
constructor(
private hass: HomeAssistant,
private entries: TemplateResult[],
trace: AutomationTraceExtended
) {
this.lastReportedTime = new Date(trace.timestamp.start);
}
setLastReportedTime(date: Date) {
this.lastReportedTime = date;
}
renderTime(from: Date, to: Date): void {
this.entries.push(html`
<ha-timeline label>
${relativeTime(from, this.hass.localize, {
compareTime: to,
includeTense: false,
})}
later
</ha-timeline>
`);
this.lastReportedTime = to;
}
maybeRenderTime(timestamp: Date): boolean {
if (!isSignificantTimeChange(timestamp, this.lastReportedTime)) {
this.lastReportedTime = timestamp;
return false;
}
this.renderTime(this.lastReportedTime, timestamp);
return true;
}
}
class LogbookRenderer {
private curIndex: number;
private pendingItems: Array<[Date, LogbookEntry]> = [];
constructor(
private entries: TemplateResult[],
private timeTracker: RenderedTimeTracker,
private logbookEntries: LogbookEntry[]
) {
// Skip the "automation got triggered item"
this.curIndex =
logbookEntries.length > 0 && logbookEntries[0].domain === "automation"
? 1
: 0;
}
get curItem() {
return this.logbookEntries[this.curIndex];
}
get hasNext() {
return this.curIndex !== this.logbookEntries.length;
}
maybeRenderItem() {
const logbookEntry = this.curItem;
this.curIndex++;
const entryDate = new Date(logbookEntry.when);
if (this.pendingItems.length === 0) {
this.pendingItems.push([entryDate, logbookEntry]);
return;
}
const previousEntryDate = this.pendingItems[
this.pendingItems.length - 1
][0];
// If logbook entry is too long after the last one,
// add a time passed label
if (isSignificantTimeChange(previousEntryDate, entryDate)) {
this._renderLogbookEntries();
this.timeTracker.renderTime(previousEntryDate, entryDate);
}
this.pendingItems.push([entryDate, logbookEntry]);
}
flush() {
if (this.pendingItems.length > 0) {
this._renderLogbookEntries();
}
}
private _renderLogbookEntries() {
this.timeTracker.maybeRenderTime(this.pendingItems[0][0]);
const parts: TemplateResult[] = [];
let i;
for (
i = 0;
i < Math.min(this.pendingItems.length, LOGBOOK_ENTRIES_BEFORE_FOLD);
i++
) {
parts.push(this._renderLogbookEntryHelper(this.pendingItems[i][1]));
}
let moreItems: TemplateResult[] | undefined;
// If we didn't render all items, push rest into `moreItems`
if (i < this.pendingItems.length) {
moreItems = [];
for (; i < this.pendingItems.length; i++) {
moreItems.push(this._renderLogbookEntryHelper(this.pendingItems[i][1]));
}
}
this.entries.push(html`
<ha-timeline .icon=${mdiCircleOutline} .moreItems=${moreItems}>
${parts}
</ha-timeline>
`);
// Clear rendered items.
this.timeTracker.setLastReportedTime(
this.pendingItems[this.pendingItems.length - 1][0]
);
this.pendingItems = [];
}
private _renderLogbookEntryHelper(entry: LogbookEntry) {
return html`${entry.name} (${entry.entity_id})
${entry.message || `turned ${entry.state}`}<br />`;
}
}
class ActionRenderer {
private curIndex = 0;
private keys: string[];
constructor(
private hass: HomeAssistant,
private entries: TemplateResult[],
private trace: AutomationTraceExtended,
private logbookRenderer: LogbookRenderer,
private timeTracker: RenderedTimeTracker
) {
this.keys = Object.keys(trace.trace);
}
get curItem() {
return this._getItem(this.curIndex);
}
get hasNext() {
return this.curIndex !== this.keys.length;
}
renderItem() {
this.curIndex = this._renderItem(this.curIndex);
}
private _getItem(index: number) {
return this.trace.trace[this.keys[index]];
}
private _renderItem(
index: number,
actionType?: ReturnType<typeof getActionType>
): number {
const value = this._getItem(index);
if (isTriggerPath(value[0].path)) {
return this._handleTrigger(index, value[0] as TriggerTraceStep);
}
const timestamp = new Date(value[0].timestamp);
// Render all logbook items that are in front of this item.
while (
this.logbookRenderer.hasNext &&
new Date(this.logbookRenderer.curItem.when) < timestamp
) {
this.logbookRenderer.maybeRenderItem();
}
this.logbookRenderer.flush();
this.timeTracker.maybeRenderTime(timestamp);
const path = value[0].path;
let data;
try {
data = getDataFromPath(this.trace.config, path);
} catch (err) {
this.entries.push(
html`Unable to extract path ${path}. Download trace and report as bug`
);
return index + 1;
}
const isTopLevel = path.split("/").length === 2;
if (!isTopLevel && !actionType) {
this._renderEntry(path, path.replace(/\//g, " "));
return index + 1;
}
if (!actionType) {
actionType = getActionType(data);
}
if (actionType === "choose") {
return this._handleChoose(index);
}
this._renderEntry(path, describeAction(this.hass, data, actionType));
return index + 1;
}
private _handleTrigger(index: number, triggerStep: TriggerTraceStep): number {
this._renderEntry(
triggerStep.path,
`Triggered ${
triggerStep.path === "trigger"
? "manually"
: `by the ${this.trace.trigger}`
} at
${formatDateTimeWithSeconds(
new Date(triggerStep.timestamp),
this.hass.locale
)}`,
mdiCircle
);
return index + 1;
}
private _handleChoose(index: number): number {
// startLevel: choose root config
// +1: 'default
// +2: executed sequence
// +1: 'choose'
// +2: current choice
// +3: 'conditions'
// +4: evaluated condition
// +3: 'sequence'
// +4: executed sequence
const choosePath = this.keys[index];
const startLevel = choosePath.split("/").length - 1;
const chooseTrace = this._getItem(index)[0] as ChooseActionTraceStep;
const defaultExecuted = chooseTrace.result?.choice === "default";
const chooseConfig = this._getDataFromPath(
this.keys[index]
) as ChooseAction;
const name = chooseConfig.alias || "Choose";
if (defaultExecuted) {
this._renderEntry(choosePath, `${name}: Default action executed`);
} else if (chooseTrace.result) {
const choiceConfig = this._getDataFromPath(
`${this.keys[index]}/choose/${chooseTrace.result.choice}`
) as ChooseActionChoice | undefined;
const choiceName = choiceConfig
? `${
choiceConfig.alias || `Choice ${chooseTrace.result.choice}`
} executed`
: `Error: ${chooseTrace.error}`;
this._renderEntry(choosePath, `${name}: ${choiceName}`);
} else {
this._renderEntry(choosePath, `${name}: No action taken`);
}
let i;
// Skip over conditions
for (i = index + 1; i < this.keys.length; 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 (
(defaultExecuted && parts[startLevel + 1] === "default") ||
(!defaultExecuted && parts[startLevel + 3] === "sequence")
) {
break;
}
}
// Render choice
while (i < this.keys.length) {
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;
}
// We know it's an action sequence, so force the type like that
// for rendering.
i = this._renderItem(i, getActionType(this._getDataFromPath(path)));
}
return i;
}
private _renderEntry(
path: string,
description: string,
icon = mdiRecordCircleOutline
) {
this.entries.push(html`
<ha-timeline .icon=${icon} data-path=${path}>
${description}
</ha-timeline>
`);
}
private _getDataFromPath(path: string) {
return getDataFromPath(this.trace.config, path);
}
}
@customElement("hat-trace-timeline")
export class HaAutomationTracer extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public trace?: AutomationTraceExtended;
@property({ attribute: false }) public logbookEntries?: LogbookEntry[];
@property({ attribute: false }) public selectedPath?: string;
@property({ type: Boolean }) public allowPick = false;
protected render(): TemplateResult {
if (!this.trace) {
return html``;
}
const entries: TemplateResult[] = [];
const timeTracker = new RenderedTimeTracker(this.hass, entries, this.trace);
const logbookRenderer = new LogbookRenderer(
entries,
timeTracker,
this.logbookEntries || []
);
const actionRenderer = new ActionRenderer(
this.hass,
entries,
this.trace,
logbookRenderer,
timeTracker
);
while (actionRenderer.hasNext) {
actionRenderer.renderItem();
}
while (logbookRenderer.hasNext) {
logbookRenderer.maybeRenderItem();
}
logbookRenderer.flush();
// Render footer
const renderFinishedAt = () =>
formatDateTimeWithSeconds(
new Date(this.trace!.timestamp.finish!),
this.hass.locale
);
const renderRuntime = () => `(runtime:
${(
(new Date(this.trace!.timestamp.finish!).getTime() -
new Date(this.trace!.timestamp.start).getTime()) /
1000
).toFixed(2)}
seconds)`;
let entry: {
description: TemplateResult | string;
icon: string;
className?: string;
};
if (this.trace.state === "running") {
entry = {
description: "Still running",
icon: mdiProgressClock,
};
} else if (this.trace.state === "debugged") {
entry = {
description: "Debugged",
icon: mdiProgressWrench,
};
} else if (this.trace.script_execution === "finished") {
entry = {
description: `Finished at ${renderFinishedAt()} ${renderRuntime()}`,
icon: mdiCircle,
};
} else if (this.trace.script_execution === "aborted") {
entry = {
description: `Aborted at ${renderFinishedAt()} ${renderRuntime()}`,
icon: mdiAlertCircle,
};
} else if (this.trace.script_execution === "cancelled") {
entry = {
description: `Cancelled at ${renderFinishedAt()} ${renderRuntime()}`,
icon: mdiAlertCircle,
};
} else {
let reason: string;
let isError = false;
let extra: TemplateResult | undefined;
switch (this.trace.script_execution) {
case "failed_conditions":
reason = "a condition failed";
break;
case "failed_single":
reason = "only a single execution is allowed";
break;
case "failed_max_runs":
reason = "maximum number of parallel runs reached";
break;
case "error":
reason = "an error was encountered";
isError = true;
extra = html`<br /><br />${this.trace.error!}`;
break;
default:
reason = `of unknown reason "${this.trace.script_execution}"`;
isError = true;
}
entry = {
description: html`Stopped because ${reason} at ${renderFinishedAt()}
${renderRuntime()}${extra || ""}`,
icon: mdiAlertCircle,
className: isError ? "error" : undefined,
};
}
// null means it was stopped by a condition
if (entry) {
entries.push(html`
<ha-timeline
lastItem
.icon=${entry.icon}
class=${ifDefined(entry.className)}
>
${entry.description}
</ha-timeline>
`);
}
return html`${entries}`;
}
protected updated(props: PropertyValues) {
super.updated(props);
// Pick first path when we load a new trace.
if (
this.allowPick &&
props.has("trace") &&
this.trace &&
this.selectedPath &&
!(this.selectedPath in this.trace.trace)
) {
const element = this.shadowRoot!.querySelector<HaTimeline>(
"ha-timeline[data-path]"
);
if (element) {
fireEvent(this, "value-changed", { value: element.dataset.path });
this.selectedPath = element.dataset.path;
}
}
if (props.has("trace") || props.has("selectedPath")) {
this.shadowRoot!.querySelectorAll<HaTimeline>(
"ha-timeline[data-path]"
).forEach((el) => {
el.toggleAttribute("selected", this.selectedPath === el.dataset.path);
if (!this.allowPick || el.tabIndex === 0) {
return;
}
el.tabIndex = 0;
const selectEl = () => {
this.selectedPath = el.dataset.path;
fireEvent(this, "value-changed", { value: el.dataset.path });
};
el.addEventListener("click", selectEl);
el.addEventListener("keydown", (ev: KeyboardEvent) => {
if (ev.key === "Enter" || ev.key === " ") {
selectEl();
}
});
el.addEventListener("mouseover", () => {
el.raised = true;
});
el.addEventListener("mouseout", () => {
el.raised = false;
});
});
}
}
static get styles(): CSSResult[] {
return [
css`
ha-timeline[lastItem].condition {
--timeline-ball-color: var(--error-color);
}
ha-timeline[data-path] {
cursor: pointer;
}
ha-timeline[selected] {
--timeline-ball-color: var(--primary-color);
}
ha-timeline:focus {
outline: none;
--timeline-ball-color: var(--accent-color);
}
.error {
--timeline-ball-color: var(--error-color);
color: var(--error-color);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hat-trace-timeline": HaAutomationTracer;
}
}
+27
View File
@@ -0,0 +1,27 @@
import { HomeAssistant } from "../types";
export interface AnalyticsPreferences {
base?: boolean;
diagnostics?: boolean;
usage?: boolean;
statistics?: boolean;
}
export interface Analytics {
preferences: AnalyticsPreferences;
onboarded: boolean;
}
export const getAnalyticsDetails = (hass: HomeAssistant) =>
hass.callWS<Analytics>({
type: "analytics",
});
export const setAnalyticsPreferences = (
hass: HomeAssistant,
preferences: AnalyticsPreferences
) =>
hass.callWS<AnalyticsPreferences>({
type: "analytics/preferences",
preferences,
});

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