Compare commits

..

310 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
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
384 changed files with 33656 additions and 6576 deletions

View File

@@ -84,7 +84,8 @@
"@typescript-eslint/no-unused-vars": 0, "@typescript-eslint/no-unused-vars": 0,
"@typescript-eslint/explicit-function-return-type": 0, "@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/explicit-module-boundary-types": 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"], "plugins": ["disable", "import", "lit", "prettier", "@typescript-eslint"],
"processor": "disable/disable" "processor": "disable/disable"

View File

@@ -1,8 +1,6 @@
name: Report a bug with the UI, Frontend or Lovelace 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 labels: bug
title: ""
issue_body: true
body: body:
- type: markdown - type: markdown
attributes: attributes:
@@ -97,11 +95,7 @@ body:
If your issue is about how an entity is shown in the UI, please add the 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 state and attributes for all situations. You can find this information
at Developer Tools -> States. at Developer Tools -> States.
value: | render: txt
```yaml
# Paste your state here.
```
- type: textarea - type: textarea
attributes: attributes:
label: Problem-relevant frontend configuration label: Problem-relevant frontend configuration
@@ -110,29 +104,18 @@ body:
configuration of the used cards. Fill this out even if it seems configuration of the used cards. Fill this out even if it seems
unimportant to you. Please be sure to remove personal information like unimportant to you. Please be sure to remove personal information like
passwords, private URLs and other credentials. passwords, private URLs and other credentials.
value: | render: yaml
```yaml
# Paste your YAML here.
```
- type: textarea - type: textarea
attributes: attributes:
label: Javascript errors shown in your browser console/inspector label: Javascript errors shown in your browser console/inspector
description: > description: >
If you come across any Javascript or other error logs, e.g., in your If you come across any Javascript or other error logs, e.g., in your
browser console/inspector please provide them. browser console/inspector please provide them.
value: | render: txt
```txt - type: textarea
# Paste your logs here.
```
- type: markdown
attributes: attributes:
value: | label: Additional information
## Additional information description: >
- type: markdown
attributes:
value: |
If you have any additional information for us, use the field below. If you have any additional information for us, use the field below.
Please note, you can attach screenshots or screen recordings here, Please note, you can attach screenshots or screen recordings here, by
by dragging and dropping files in the field below. dragging and dropping files in the field below.

View File

@@ -7,7 +7,7 @@ on:
branches: branches:
- dev - dev
paths: paths:
- translations/en.json - src/translations/en.json
env: env:
NODE_VERSION: 12 NODE_VERSION: 12

View File

@@ -100,7 +100,7 @@ class HcLayout extends LitElement {
display: block; display: block;
margin: 0; margin: 0;
} }
.hero { .hero {
border-radius: 4px 4px 0 0; border-radius: 4px 4px 0 0;
} }

View File

@@ -35,11 +35,12 @@ class HcLovelace extends LitElement {
} }
const lovelace: Lovelace = { const lovelace: Lovelace = {
config: this.lovelaceConfig, config: this.lovelaceConfig,
rawConfig: this.lovelaceConfig,
editMode: false, editMode: false,
urlPath: this.urlPath!, urlPath: this.urlPath!,
enableFullEditMode: () => undefined, enableFullEditMode: () => undefined,
mode: "storage", mode: "storage",
language: "en", locale: this.hass.locale,
saveConfig: async () => undefined, saveConfig: async () => undefined,
deleteConfig: async () => undefined, deleteConfig: async () => undefined,
setEditMode: () => undefined, setEditMode: () => undefined,
@@ -94,6 +95,7 @@ class HcLovelace extends LitElement {
return css` return css`
:host { :host {
min-height: 100vh; min-height: 100vh;
height: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
box-sizing: border-box; box-sizing: border-box;

View File

@@ -221,11 +221,17 @@ export class HcMain extends HassElement {
} }
private async _generateLovelaceConfig() { private async _generateLovelaceConfig() {
const { generateLovelaceConfigFromHass } = await import( const { generateLovelaceDashboardStrategy } = await import(
"../../../../src/panels/lovelace/common/generate-lovelace-config" "../../../../src/panels/lovelace/strategies/get-strategy"
); );
this._handleNewLovelaceConfig( this._handleNewLovelaceConfig(
await generateLovelaceConfigFromHass(this.hass!) await generateLovelaceDashboardStrategy(
{
hass: this.hass!,
narrow: false,
},
"original-states"
)
); );
} }

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",
},
],
};

View File

@@ -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 || [],
});

View File

@@ -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",
},
],
};

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@@ -111,29 +111,9 @@ class HaGallery extends PolymerElement {
</template> </template>
</ha-card> </ha-card>
<ha-card header="More Info Demos"> <ha-card header="Other Demos">
<div class="card-content intro"> <div class="card-content intro"></div>
<p> <template is="dom-repeat" items="[[_restDemos]]">
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]]">
<a href="#[[item]]"> <a href="#[[item]]">
<paper-item> <paper-item>
<paper-item-body>{{ item }}</paper-item-body> <paper-item-body>{{ item }}</paper-item-body>
@@ -178,13 +158,9 @@ class HaGallery extends PolymerElement {
type: Array, type: Array,
computed: "_computeLovelace(_demos)", computed: "_computeLovelace(_demos)",
}, },
_moreInfoDemos: { _restDemos: {
type: Array, type: Array,
computed: "_computeMoreInfos(_demos)", computed: "_computeRest(_demos)",
},
_utilDemos: {
type: Array,
computed: "_computeUtil(_demos)",
}, },
}; };
} }
@@ -237,12 +213,8 @@ class HaGallery extends PolymerElement {
return demos.filter((demo) => demo.includes("hui")); return demos.filter((demo) => demo.includes("hui"));
} }
_computeMoreInfos(demos) { _computeRest(demos) {
return demos.filter((demo) => demo.includes("more-info")); return demos.filter((demo) => !demo.includes("hui"));
}
_computeUtil(demos) {
return demos.filter((demo) => demo.includes("util"));
} }
} }

View File

@@ -14,7 +14,9 @@ import { html, TemplateResult } from "lit-html";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version"; import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event"; import { fireEvent } from "../../../src/common/dom/fire_event";
import { navigate } from "../../../src/common/navigate";
import "../../../src/common/search/search-input"; import "../../../src/common/search/search-input";
import { extractSearchParam } from "../../../src/common/url/search-params";
import "../../../src/components/ha-button-menu"; import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-svg-icon"; import "../../../src/components/ha-svg-icon";
import { import {
@@ -137,6 +139,12 @@ class HassioAddonStore extends LitElement {
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); 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.addEventListener("hass-api-called", (ev) => this.apiCalled(ev));
this._loadData(); this._loadData();
} }
@@ -170,7 +178,7 @@ class HassioAddonStore extends LitElement {
private _handleAction(ev: CustomEvent<ActionDetail>) { private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) { switch (ev.detail.index) {
case 0: case 0:
this._manageRepositories(); this._manageRepositoriesClicked();
break; break;
case 1: case 1:
this.refreshData(); this.refreshData();
@@ -187,10 +195,14 @@ class HassioAddonStore extends LitElement {
} }
} }
private async _manageRepositories() { private _manageRepositoriesClicked() {
this._manageRepositories();
}
private async _manageRepositories(url?: string) {
showRepositoriesDialog(this, { showRepositoriesDialog(this, {
supervisor: this.supervisor, supervisor: this.supervisor,
loadData: () => this._loadData(), url,
}); });
} }
@@ -199,9 +211,9 @@ class HassioAddonStore extends LitElement {
} }
private async _loadData() { private async _loadData() {
fireEvent(this, "supervisor-colllection-refresh", { colllection: "addon" }); fireEvent(this, "supervisor-collection-refresh", { collection: "addon" });
fireEvent(this, "supervisor-colllection-refresh", { fireEvent(this, "supervisor-collection-refresh", {
colllection: "supervisor", collection: "supervisor",
}); });
} }

View File

@@ -165,7 +165,7 @@ class HassioAddonConfig extends LitElement {
@click=${this._saveTapped} @click=${this._saveTapped}
.disabled=${!this._configHasChanged || !this._valid} .disabled=${!this._configHasChanged || !this._valid}
> >
Save ${this.supervisor.localize("common.save")} ${this.supervisor.localize("common.save")}
</ha-progress-button> </ha-progress-button>
</div> </div>
</ha-card> </ha-card>

View File

@@ -21,6 +21,7 @@ import { extractSearchParam } from "../../../src/common/url/search-params";
import "../../../src/components/ha-circular-progress"; import "../../../src/components/ha-circular-progress";
import { import {
fetchHassioAddonInfo, fetchHassioAddonInfo,
fetchHassioAddonsInfo,
HassioAddonDetails, HassioAddonDetails,
} from "../../../src/data/hassio/addon"; } from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common"; import { extractApiErrorMessage } from "../../../src/data/hassio/common";
@@ -173,9 +174,17 @@ class HassioAddonDashboard extends LitElement {
protected async firstUpdated(): Promise<void> { protected async firstUpdated(): Promise<void> {
if (this.route.path === "") { if (this.route.path === "") {
const addon = extractSearchParam("addon"); const requestedAddon = extractSearchParam("addon");
if (addon) { if (requestedAddon) {
navigate(this, `/hassio/addon/${addon}`, true); 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)); this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
@@ -191,8 +200,8 @@ class HassioAddonDashboard extends LitElement {
const path: string = pathSplit[pathSplit.length - 1]; const path: string = pathSplit[pathSplit.length - 1];
if (["uninstall", "install", "update", "start", "stop"].includes(path)) { if (["uninstall", "install", "update", "start", "stop"].includes(path)) {
fireEvent(this, "supervisor-colllection-refresh", { fireEvent(this, "supervisor-collection-refresh", {
colllection: "supervisor", collection: "supervisor",
}); });
} }

View File

@@ -50,6 +50,7 @@ import {
startHassioAddon, startHassioAddon,
stopHassioAddon, stopHassioAddon,
uninstallHassioAddon, uninstallHassioAddon,
updateHassioAddon,
validateHassioAddonOption, validateHassioAddonOption,
} from "../../../../src/data/hassio/addon"; } from "../../../../src/data/hassio/addon";
import { import {
@@ -68,8 +69,8 @@ import { HomeAssistant } from "../../../../src/types";
import { bytesToString } from "../../../../src/util/bytes-to-string"; import { bytesToString } from "../../../../src/util/bytes-to-string";
import "../../components/hassio-card-content"; import "../../components/hassio-card-content";
import "../../components/supervisor-metric"; import "../../components/supervisor-metric";
import { showDialogSupervisorAddonUpdate } from "../../dialogs/addon/show-dialog-addon-update";
import { showHassioMarkdownDialog } from "../../dialogs/markdown/show-dialog-hassio-markdown"; import { showHassioMarkdownDialog } from "../../dialogs/markdown/show-dialog-hassio-markdown";
import { showDialogSupervisorUpdate } from "../../dialogs/update/show-dialog-update";
import { hassioStyle } from "../../resources/hassio-style"; import { hassioStyle } from "../../resources/hassio-style";
import { addonArchIsSupported } from "../../util/addon"; import { addonArchIsSupported } from "../../util/addon";
@@ -241,14 +242,18 @@ class HassioAddonInfo extends LitElement {
? html` ? html`
Current version: ${this.addon.version} Current version: ${this.addon.version}
<div class="changelog" @click=${this._openChangelog}> <div class="changelog" @click=${this._openChangelog}>
(<span class="changelog-link"> (<span class="changelog-link"
${this.supervisor.localize("addon.dashboard.changelog")} </span >${this.supervisor.localize(
"addon.dashboard.changelog"
)}</span
>) >)
</div> </div>
` `
: html`<span class="changelog-link" @click=${this._openChangelog}> : html`<span class="changelog-link" @click=${this._openChangelog}
${this.supervisor.localize("addon.dashboard.changelog")} >${this.supervisor.localize(
</span>`} "addon.dashboard.changelog"
)}</span
>`}
</div> </div>
<div class="description light-color"> <div class="description light-color">
@@ -476,7 +481,7 @@ class HassioAddonInfo extends LitElement {
</span> </span>
<span slot="description"> <span slot="description">
${this.supervisor.localize( ${this.supervisor.localize(
"addon.dashboard.option.boot.description" "addon.dashboard.option.watchdog.description"
)} )}
</span> </span>
<ha-switch <ha-switch
@@ -498,7 +503,7 @@ class HassioAddonInfo extends LitElement {
</span> </span>
<span slot="description"> <span slot="description">
${this.supervisor.localize( ${this.supervisor.localize(
"addon.dashboard.option.boot.description" "addon.dashboard.option.auto_update.description"
)} )}
</span> </span>
<ha-switch <ha-switch
@@ -983,7 +988,30 @@ class HassioAddonInfo extends LitElement {
} }
private async _updateClicked(): Promise<void> { 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> { private async _startClicked(ev: CustomEvent): Promise<void> {

View File

@@ -28,7 +28,7 @@ class SupervisorMetric extends LitElement {
</span> </span>
<div slot="description" .title=${this.tooltip ?? ""}> <div slot="description" .title=${this.tooltip ?? ""}>
<span class="value"> <span class="value">
${roundedValue}% ${roundedValue} %
</span> </span>
<ha-bar <ha-bar
class="${classMap({ class="${classMap({
@@ -73,7 +73,7 @@ class SupervisorMetric extends LitElement {
); );
} }
.value { .value {
width: 42px; width: 48px;
padding-right: 4px; padding-right: 4px;
} }
`; `;

View File

@@ -19,13 +19,14 @@ import "../../../src/components/ha-svg-icon";
import { import {
extractApiErrorMessage, extractApiErrorMessage,
HassioResponse, HassioResponse,
ignoredStatusCodes, ignoreSupervisorError,
} from "../../../src/data/hassio/common"; } from "../../../src/data/hassio/common";
import { HassioHassOSInfo } from "../../../src/data/hassio/host"; import { HassioHassOSInfo } from "../../../src/data/hassio/host";
import { import {
HassioHomeAssistantInfo, HassioHomeAssistantInfo,
HassioSupervisorInfo, HassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor"; } from "../../../src/data/hassio/supervisor";
import { updateCore } from "../../../src/data/supervisor/core";
import { import {
Supervisor, Supervisor,
supervisorApiWsRequest, supervisorApiWsRequest,
@@ -36,7 +37,7 @@ import {
} from "../../../src/dialogs/generic/show-dialog-box"; } from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles"; import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types"; 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"; import { hassioStyle } from "../resources/hassio-style";
const computeVersion = (key: string, version: string): string => { const computeVersion = (key: string, version: string): string => {
@@ -164,7 +165,17 @@ export class HassioUpdate extends LitElement {
private async _confirmUpdate(ev): Promise<void> { private async _confirmUpdate(ev): Promise<void> {
const item = ev.currentTarget; const item = ev.currentTarget;
if (item.key === "core") { 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; return;
} }
item.progress = true; item.progress = true;
@@ -199,17 +210,13 @@ export class HassioUpdate extends LitElement {
} else { } else {
await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath); await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath);
} }
fireEvent(this, "supervisor-colllection-refresh", { fireEvent(this, "supervisor-collection-refresh", {
colllection: item.key, collection: item.key,
}); });
} catch (err) { } catch (err) {
// Only show an error if the status code was not expected (user behind proxy) // Only show an error if the status code was not expected (user behind proxy)
// or no status at all(connection terminated) // or no status at all(connection terminated)
if ( if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
this.hass.connection.connected &&
err.status_code &&
!ignoredStatusCodes.has(err.status_code)
) {
showAlertDialog(this, { showAlertDialog(this, {
title: this.supervisor.localize("common.error.update_failed"), title: this.supervisor.localize("common.error.update_failed"),
text: extractApiErrorMessage(err), text: extractApiErrorMessage(err),
@@ -219,6 +226,13 @@ export class HassioUpdate extends LitElement {
item.progress = false; item.progress = false;
} }
private async _updateCore(): Promise<void> {
await updateCore(this.hass);
fireEvent(this, "supervisor-collection-refresh", {
collection: "core",
});
}
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
haStyle, haStyle,

View File

@@ -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,
});
};

View File

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

View File

@@ -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,
});
};

View File

@@ -18,7 +18,6 @@ import {
} from "lit-element"; } from "lit-element";
import { cache } from "lit-html/directives/cache"; import { cache } from "lit-html/directives/cache";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-chips";
import "../../../../src/components/ha-circular-progress"; import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog"; import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel"; import "../../../../src/components/ha-expansion-panel";

View File

@@ -17,6 +17,7 @@ import {
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-circular-progress"; import "../../../../src/components/ha-circular-progress";
import { createCloseHeading } from "../../../../src/components/ha-dialog"; import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-svg-icon";
@@ -26,7 +27,6 @@ import {
} from "../../../../src/data/hassio/addon"; } from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { setSupervisorOption } from "../../../../src/data/hassio/supervisor"; import { setSupervisorOption } from "../../../../src/data/hassio/supervisor";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types"; import type { HomeAssistant } from "../../../../src/types";
import { HassioRepositoryDialogParams } from "./show-dialog-repositories"; import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
@@ -35,15 +35,12 @@ import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
class HassioRepositoriesDialog extends LitElement { class HassioRepositoriesDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @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; @query("#repository_input", true) private _optionInput?: PaperInputElement;
@internalProperty() private _repositories?: HassioAddonRepository[];
@internalProperty() private _dialogParams?: HassioRepositoryDialogParams;
@internalProperty() private _opened = false; @internalProperty() private _opened = false;
@internalProperty() private _prosessing = false; @internalProperty() private _prosessing = false;
@@ -54,12 +51,13 @@ class HassioRepositoriesDialog extends LitElement {
dialogParams: HassioRepositoryDialogParams dialogParams: HassioRepositoryDialogParams
): Promise<void> { ): Promise<void> {
this._dialogParams = dialogParams; this._dialogParams = dialogParams;
this.supervisor = dialogParams.supervisor;
this._opened = true; this._opened = true;
await this._loadData();
await this.updateComplete; await this.updateComplete;
} }
public closeDialog(): void { public closeDialog(): void {
this._dialogParams = undefined;
this._opened = false; this._opened = false;
this._error = ""; this._error = "";
} }
@@ -71,9 +69,10 @@ class HassioRepositoriesDialog extends LitElement {
); );
protected render(): TemplateResult { protected render(): TemplateResult {
const repositories = this._filteredRepositories( if (!this._dialogParams?.supervisor || this._repositories === undefined) {
this.supervisor.addon.repositories return html``;
); }
const repositories = this._filteredRepositories(this._repositories);
return html` return html`
<ha-dialog <ha-dialog
.open=${this._opened} .open=${this._opened}
@@ -82,7 +81,7 @@ class HassioRepositoriesDialog extends LitElement {
escapeKeyAction escapeKeyAction
.heading=${createCloseHeading( .heading=${createCloseHeading(
this.hass, this.hass,
this.supervisor.localize("dialog.repositories.title") this._dialogParams!.supervisor.localize("dialog.repositories.title")
)} )}
> >
${this._error ? html`<div class="error">${this._error}</div>` : ""} ${this._error ? html`<div class="error">${this._error}</div>` : ""}
@@ -98,7 +97,7 @@ class HassioRepositoriesDialog extends LitElement {
</paper-item-body> </paper-item-body>
<mwc-icon-button <mwc-icon-button
.slug=${repo.slug} .slug=${repo.slug}
.title=${this.supervisor.localize( .title=${this._dialogParams!.supervisor.localize(
"dialog.repositories.remove" "dialog.repositories.remove"
)} )}
@click=${this._removeRepository} @click=${this._removeRepository}
@@ -117,18 +116,23 @@ class HassioRepositoriesDialog extends LitElement {
<paper-input <paper-input
class="flex-auto" class="flex-auto"
id="repository_input" 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} @keydown=${this._handleKeyAdd}
></paper-input> ></paper-input>
<mwc-button @click=${this._addRepository}> <mwc-button @click=${this._addRepository}>
${this._prosessing ${this._prosessing
? html`<ha-circular-progress active></ha-circular-progress>` ? html`<ha-circular-progress active></ha-circular-progress>`
: this.supervisor.localize("dialog.repositories.add")} : this._dialogParams!.supervisor.localize(
"dialog.repositories.add"
)}
</mwc-button> </mwc-button>
</div> </div>
</div> </div>
<mwc-button slot="primaryAction" @click="${this.closeDialog}"> <mwc-button slot="primaryAction" @click=${this.closeDialog}>
Close ${this._dialogParams?.supervisor.localize("common.close")}
</mwc-button> </mwc-button>
</ha-dialog> </ha-dialog>
`; `;
@@ -159,6 +163,11 @@ class HassioRepositoriesDialog extends LitElement {
ha-paper-dropdown-menu { ha-paper-dropdown-menu {
display: block; display: block;
} }
ha-circular-progress {
display: block;
margin: 32px;
text-align: center;
}
`, `,
]; ];
} }
@@ -179,13 +188,25 @@ class HassioRepositoriesDialog extends LitElement {
this._addRepository(); 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() { private async _addRepository() {
const input = this._optionInput; const input = this._optionInput;
if (!input || !input.value) { if (!input || !input.value) {
return; return;
} }
this._prosessing = true; this._prosessing = true;
const repositories = this._filteredRepositories(this._repos); const repositories = this._filteredRepositories(this._repositories!);
const newRepositories = repositories.map((repo) => { const newRepositories = repositories.map((repo) => {
return repo.source; return repo.source;
}); });
@@ -195,11 +216,7 @@ class HassioRepositoriesDialog extends LitElement {
await setSupervisorOption(this.hass, { await setSupervisorOption(this.hass, {
addons_repositories: newRepositories, addons_repositories: newRepositories,
}); });
await this._loadData();
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
this._repos = addonsInfo.repositories;
await this._dialogParams!.loadData();
input.value = ""; input.value = "";
} catch (err) { } catch (err) {
@@ -210,7 +227,7 @@ class HassioRepositoriesDialog extends LitElement {
private async _removeRepository(ev: Event) { private async _removeRepository(ev: Event) {
const slug = (ev.currentTarget as any).slug; const slug = (ev.currentTarget as any).slug;
const repositories = this._filteredRepositories(this._repos); const repositories = this._filteredRepositories(this._repositories!);
const repository = repositories.find((repo) => { const repository = repositories.find((repo) => {
return repo.slug === slug; return repo.slug === slug;
}); });
@@ -229,11 +246,7 @@ class HassioRepositoriesDialog extends LitElement {
await setSupervisorOption(this.hass, { await setSupervisorOption(this.hass, {
addons_repositories: newRepositories, addons_repositories: newRepositories,
}); });
await this._loadData();
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
this._repos = addonsInfo.repositories;
await this._dialogParams!.loadData();
} catch (err) { } catch (err) {
this._error = extractApiErrorMessage(err); this._error = extractApiErrorMessage(err);
} }

View File

@@ -4,7 +4,7 @@ import "./dialog-hassio-repositories";
export interface HassioRepositoryDialogParams { export interface HassioRepositoryDialogParams {
supervisor: Supervisor; supervisor: Supervisor;
loadData: () => Promise<void>; url?: string;
} }
export const showRepositoriesDialog = ( export const showRepositoriesDialog = (

View File

@@ -15,21 +15,18 @@ import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-switch"; import "../../../../src/components/ha-switch";
import { import {
HassioAddonDetails, extractApiErrorMessage,
updateHassioAddon, ignoreSupervisorError,
} from "../../../../src/data/hassio/addon"; } from "../../../../src/data/hassio/common";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { createHassioPartialSnapshot } from "../../../../src/data/hassio/snapshot"; import { createHassioPartialSnapshot } from "../../../../src/data/hassio/snapshot";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types"; import type { HomeAssistant } from "../../../../src/types";
import { SupervisorDialogSupervisorAddonUpdateParams } from "./show-dialog-addon-update"; import { SupervisorDialogSupervisorUpdateParams } from "./show-dialog-update";
@customElement("dialog-supervisor-addon-update") @customElement("dialog-supervisor-update")
class DialogSupervisorAddonUpdate extends LitElement { class DialogSupervisorUpdate extends LitElement {
public hass!: HomeAssistant; public hass!: HomeAssistant;
public addon!: HassioAddonDetails;
@internalProperty() private _opened = false; @internalProperty() private _opened = false;
@internalProperty() private _createSnapshot = true; @internalProperty() private _createSnapshot = true;
@@ -38,18 +35,22 @@ class DialogSupervisorAddonUpdate extends LitElement {
@internalProperty() private _error?: string; @internalProperty() private _error?: string;
@internalProperty()
private _dialogParams?: SupervisorDialogSupervisorUpdateParams;
public async showDialog( public async showDialog(
params: SupervisorDialogSupervisorAddonUpdateParams params: SupervisorDialogSupervisorUpdateParams
): Promise<void> { ): Promise<void> {
this._opened = true; this._opened = true;
this.addon = params.addon; this._dialogParams = params;
await this.updateComplete; await this.updateComplete;
} }
public closeDialog(): void { public closeDialog(): void {
this._action = null; this._action = null;
this._createSnapshot = true; this._createSnapshot = true;
this._opened = false; this._error = undefined;
this._dialogParams = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
@@ -62,47 +63,77 @@ class DialogSupervisorAddonUpdate extends LitElement {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._dialogParams) {
return html``;
}
return html` return html`
<ha-dialog .open=${this._opened} scrimClickAction escapeKeyAction> <ha-dialog .open=${this._opened} scrimClickAction escapeKeyAction>
${this._action === null ${this._action === null
? html`<slot name="heading"> ? html`<slot name="heading">
<h2 id="title" class="header_title"> <h2 id="title" class="header_title">
Update ${this.addon.name} ${this._dialogParams.supervisor.localize(
"confirm.update.title",
"name",
this._dialogParams.name
)}
</h2> </h2>
</slot> </slot>
<div> <div>
Are you sure you want to update the ${this.addon.name} add-on to ${this._dialogParams.supervisor.localize(
version ${this.addon.version_latest}? "confirm.update.text",
"name",
this._dialogParams.name,
"version",
this._dialogParams.version
)}
</div> </div>
<ha-settings-row> <ha-settings-row>
<span slot="heading"> <span slot="heading">
Snapshot ${this._dialogParams.supervisor.localize(
"dialog.update.snapshot"
)}
</span> </span>
<span slot="description"> <span slot="description">
Create a snapshot of the ${this.addon.name} add-on before ${this._dialogParams.supervisor.localize(
updating "dialog.update.create_snapshot",
"name",
this._dialogParams.name
)}
</span> </span>
<ha-switch <ha-switch
.checked=${this._createSnapshot} .checked=${this._createSnapshot}
haptic haptic
title="Create snapshot"
@click=${this._toggleSnapshot} @click=${this._toggleSnapshot}
> >
</ha-switch> </ha-switch>
</ha-settings-row> </ha-settings-row>
<mwc-button @click=${this.closeDialog} slot="secondaryAction"> <mwc-button @click=${this.closeDialog} slot="secondaryAction">
Cancel ${this._dialogParams.supervisor.localize("common.cancel")}
</mwc-button> </mwc-button>
<mwc-button @click=${this._update} slot="primaryAction"> <mwc-button
Update .disabled=${this._error !== undefined}
@click=${this._update}
slot="primaryAction"
>
${this._dialogParams.supervisor.localize("common.update")}
</mwc-button>` </mwc-button>`
: html`<ha-circular-progress alt="Updating" size="large" active> : html`<ha-circular-progress alt="Updating" size="large" active>
</ha-circular-progress> </ha-circular-progress>
<p class="progress-text"> <p class="progress-text">
${this._action === "update" ${this._action === "update"
? `Updating ${this.addon.name} to version ${this.addon.version_latest}` ? this._dialogParams.supervisor.localize(
: "Creating snapshot of Home Assistant Core"} "dialog.update.updating",
"name",
this._dialogParams.name,
"version",
this._dialogParams.version
)
: this._dialogParams.supervisor.localize(
"dialog.update.snapshotting",
"name",
this._dialogParams.name
)}
</p>`} </p>`}
${this._error ? html`<p class="error">${this._error}</p>` : ""} ${this._error ? html`<p class="error">${this._error}</p>` : ""}
</ha-dialog> </ha-dialog>
@@ -117,11 +148,10 @@ class DialogSupervisorAddonUpdate extends LitElement {
if (this._createSnapshot) { if (this._createSnapshot) {
this._action = "snapshot"; this._action = "snapshot";
try { try {
await createHassioPartialSnapshot(this.hass, { await createHassioPartialSnapshot(
name: `addon_${this.addon.slug}_${this.addon.version}`, this.hass,
addons: [this.addon.slug], this._dialogParams!.snapshotParams
homeassistant: false, );
});
} catch (err) { } catch (err) {
this._error = extractApiErrorMessage(err); this._error = extractApiErrorMessage(err);
this._action = null; this._action = null;
@@ -131,16 +161,15 @@ class DialogSupervisorAddonUpdate extends LitElement {
this._action = "update"; this._action = "update";
try { try {
await updateHassioAddon(this.hass, this.addon.slug); await this._dialogParams!.updateHandler!();
} catch (err) { } catch (err) {
this._error = extractApiErrorMessage(err); if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
this._error = extractApiErrorMessage(err);
}
this._action = null; this._action = null;
return; return;
} }
fireEvent(this, "supervisor-colllection-refresh", { colllection: "addon" });
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "supervisor",
});
this.closeDialog(); this.closeDialog();
} }
@@ -174,6 +203,6 @@ class DialogSupervisorAddonUpdate extends LitElement {
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"dialog-supervisor-addon-update": DialogSupervisorAddonUpdate; "dialog-supervisor-update": DialogSupervisorUpdate;
} }
} }

View File

@@ -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,
});
};

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 // 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 // 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. // 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 // @ts-ignore
fireEvent(this, ev.type, ev.detail, { fireEvent(this, ev.type, ev.detail, {
bubbles: false, bubbles: false,

View File

@@ -22,6 +22,9 @@ import { HomeAssistant, Route } from "../../src/types";
import { Supervisor } from "../../src/data/supervisor/supervisor"; import { Supervisor } from "../../src/data/supervisor/supervisor";
const REDIRECTS: Redirects = { const REDIRECTS: Redirects = {
supervisor: {
redirect: "/hassio/dashboard",
},
supervisor_logs: { supervisor_logs: {
redirect: "/hassio/system", redirect: "/hassio/system",
}, },
@@ -34,15 +37,18 @@ const REDIRECTS: Redirects = {
supervisor_store: { supervisor_store: {
redirect: "/hassio/store", redirect: "/hassio/store",
}, },
supervisor: {
redirect: "/hassio/dashboard",
},
supervisor_addon: { supervisor_addon: {
redirect: "/hassio/addon", redirect: "/hassio/addon",
params: { params: {
addon: "string", addon: "string",
}, },
}, },
supervisor_add_addon_repository: {
redirect: "/hassio/store",
params: {
repository_url: "url",
},
},
}; };
@customElement("hassio-my-redirect") @customElement("hassio-my-redirect")

View File

@@ -31,7 +31,7 @@ class HassioPanel extends LitElement {
if ( if (
Object.keys(supervisorCollection).some( Object.keys(supervisorCollection).some(
(colllection) => !this.supervisor[colllection] (collection) => !this.supervisor[collection]
) )
) { ) {
return html`<hass-loading-screen></hass-loading-screen>`; return html`<hass-loading-screen></hass-loading-screen>`;

View File

@@ -23,19 +23,19 @@ import {
import { fetchSupervisorStore } from "../../src/data/supervisor/store"; import { fetchSupervisorStore } from "../../src/data/supervisor/store";
import { import {
getSupervisorEventCollection, getSupervisorEventCollection,
subscribeSupervisorEvents,
Supervisor, Supervisor,
SupervisorObject, SupervisorObject,
supervisorCollection, supervisorCollection,
} from "../../src/data/supervisor/supervisor"; } from "../../src/data/supervisor/supervisor";
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin"; import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
import { urlSyncMixin } from "../../src/state/url-sync-mixin"; import { urlSyncMixin } from "../../src/state/url-sync-mixin";
import { HomeAssistant } from "../../src/types";
import { getTranslation } from "../../src/util/common-translation"; import { getTranslation } from "../../src/util/common-translation";
declare global { declare global {
interface HASSDomEvents { interface HASSDomEvents {
"supervisor-update": Partial<Supervisor>; "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> Collection<unknown>
> = {}; > = {};
@internalProperty() private _resources?: Record<string, any>;
@internalProperty() private _language = "en"; @internalProperty() private _language = "en";
public connectedCallback(): void { public connectedCallback(): void {
@@ -71,12 +69,39 @@ export class SupervisorBaseElement extends urlSyncMixin(
protected updated(changedProperties: PropertyValues) { protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties); 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.has("_language")) {
if (changedProperties.get("_language") !== this._language) { if (changedProperties.get("_language") !== this._language) {
this._initializeLocalize(); 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 { protected _updateSupervisor(obj: Partial<Supervisor>): void {
@@ -85,7 +110,10 @@ export class SupervisorBaseElement extends urlSyncMixin(
protected firstUpdated(changedProps: PropertyValues): void { protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps); 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._language = this.hass.language;
} }
this._initializeLocalize(); this._initializeLocalize();
@@ -99,55 +127,43 @@ export class SupervisorBaseElement extends urlSyncMixin(
"/api/hassio/app/static/translations" "/api/hassio/app/static/translations"
); );
this._resources = {
[language]: data,
};
this.supervisor = { this.supervisor = {
...this.supervisor, ...this.supervisor,
localize: await computeLocalize( localize: await computeLocalize(this.constructor.prototype, language, {
this.constructor.prototype, [language]: data,
this._language, }),
this._resources
),
}; };
} }
private async _handleSupervisorStoreRefreshEvent(ev) { private async _handleSupervisorStoreRefreshEvent(ev) {
const colllection = ev.detail.colllection; const collection = ev.detail.collection;
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) { if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
this._collections[colllection].refresh(); this._collections[collection].refresh();
return; return;
} }
const response = await this.hass.callApi<HassioResponse<any>>( const response = await this.hass.callApi<HassioResponse<any>>(
"GET", "GET",
`hassio${supervisorCollection[colllection]}` `hassio${supervisorCollection[collection]}`
); );
this._updateSupervisor({ [colllection]: response.data }); this._updateSupervisor({ [collection]: response.data });
} }
private async _initSupervisor(): Promise<void> { private async _initSupervisor(): Promise<void> {
this.addEventListener( this.addEventListener(
"supervisor-colllection-refresh", "supervisor-collection-refresh",
this._handleSupervisorStoreRefreshEvent this._handleSupervisorStoreRefreshEvent
); );
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) { if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
Object.keys(supervisorCollection).forEach((colllection) => { Object.keys(supervisorCollection).forEach((collection) => {
this._unsubs[colllection] = subscribeSupervisorEvents( if (collection in this._collections) {
this.hass, this._collections[collection].refresh();
(data) => this._updateSupervisor({ [colllection]: data }),
colllection,
supervisorCollection[colllection]
);
if (this._collections[colllection]) {
this._collections[colllection].refresh();
} else { } else {
this._collections[colllection] = getSupervisorEventCollection( this._collections[collection] = getSupervisorEventCollection(
this.hass.connection, this.hass.connection,
colllection, collection,
supervisorCollection[colllection] supervisorCollection[collection]
); );
} }
}); });
@@ -185,7 +201,7 @@ export class SupervisorBaseElement extends urlSyncMixin(
fetchSupervisorStore(this.hass), fetchSupervisorStore(this.hass),
]); ]);
this.supervisor = { this._updateSupervisor({
addon, addon,
supervisor, supervisor,
host, host,
@@ -195,7 +211,7 @@ export class SupervisorBaseElement extends urlSyncMixin(
network, network,
resolution, resolution,
store, store,
}; });
this.addEventListener("supervisor-update", (ev) => this.addEventListener("supervisor-update", (ev) =>
this._updateSupervisor(ev.detail) this._updateSupervisor(ev.detail)

View File

@@ -10,6 +10,7 @@ import {
property, property,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-button-menu"; import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
@@ -19,7 +20,7 @@ import {
fetchHassioStats, fetchHassioStats,
HassioStats, HassioStats,
} from "../../../src/data/hassio/common"; } 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 { Supervisor } from "../../../src/data/supervisor/supervisor";
import { import {
showAlertDialog, showAlertDialog,
@@ -29,7 +30,7 @@ import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types"; import { HomeAssistant } from "../../../src/types";
import { bytesToString } from "../../../src/util/bytes-to-string"; import { bytesToString } from "../../../src/util/bytes-to-string";
import "../components/supervisor-metric"; 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"; import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-core-info") @customElement("hassio-core-info")
@@ -168,7 +169,24 @@ class HassioCoreInfo extends LitElement {
} }
private async _coreUpdate(): Promise<void> { 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[] { static get styles(): CSSResult[] {

View File

@@ -21,7 +21,7 @@ import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row"; import "../../../src/components/ha-settings-row";
import { import {
extractApiErrorMessage, extractApiErrorMessage,
ignoredStatusCodes, ignoreSupervisorError,
} from "../../../src/data/hassio/common"; } from "../../../src/data/hassio/common";
import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware"; import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware";
import { import {
@@ -154,8 +154,8 @@ class HassioHostInfo extends LitElement {
)} )}
</span> </span>
<span slot="description"> <span slot="description">
${this.supervisor.host.disk_life_time - 10}% - ${this.supervisor.host.disk_life_time - 10} % -
${this.supervisor.host.disk_life_time}% ${this.supervisor.host.disk_life_time} %
</span> </span>
</ha-settings-row>` </ha-settings-row>`
: ""} : ""}
@@ -274,7 +274,7 @@ class HassioHostInfo extends LitElement {
await rebootHost(this.hass); await rebootHost(this.hass);
} catch (err) { } catch (err) {
// Ignore connection errors, these are all expected // 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, { showAlertDialog(this, {
title: this.supervisor.localize("system.host.failed_to_reboot"), title: this.supervisor.localize("system.host.failed_to_reboot"),
text: extractApiErrorMessage(err), text: extractApiErrorMessage(err),
@@ -304,7 +304,7 @@ class HassioHostInfo extends LitElement {
await shutdownHost(this.hass); await shutdownHost(this.hass);
} catch (err) { } catch (err) {
// Ignore connection errors, these are all expected // 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, { showAlertDialog(this, {
title: this.supervisor.localize("system.host.failed_to_shutdown"), title: this.supervisor.localize("system.host.failed_to_shutdown"),
text: extractApiErrorMessage(err), text: extractApiErrorMessage(err),
@@ -342,7 +342,7 @@ class HassioHostInfo extends LitElement {
try { try {
await updateOS(this.hass); await updateOS(this.hass);
fireEvent(this, "supervisor-colllection-refresh", { colllection: "os" }); fireEvent(this, "supervisor-collection-refresh", { collection: "os" });
} catch (err) { } catch (err) {
if (this.hass.connection.connected) { if (this.hass.connection.connected) {
showAlertDialog(this, { showAlertDialog(this, {
@@ -378,8 +378,8 @@ class HassioHostInfo extends LitElement {
if (hostname && hostname !== curHostname) { if (hostname && hostname !== curHostname) {
try { try {
await changeHostOptions(this.hass, { hostname }); await changeHostOptions(this.hass, { hostname });
fireEvent(this, "supervisor-colllection-refresh", { fireEvent(this, "supervisor-collection-refresh", {
colllection: "host", collection: "host",
}); });
} catch (err) { } catch (err) {
showAlertDialog(this, { showAlertDialog(this, {
@@ -393,8 +393,8 @@ class HassioHostInfo extends LitElement {
private async _importFromUSB(): Promise<void> { private async _importFromUSB(): Promise<void> {
try { try {
await configSyncOS(this.hass); await configSyncOS(this.hass);
fireEvent(this, "supervisor-colllection-refresh", { fireEvent(this, "supervisor-collection-refresh", {
colllection: "host", collection: "host",
}); });
} catch (err) { } catch (err) {
showAlertDialog(this, { showAlertDialog(this, {
@@ -408,8 +408,8 @@ class HassioHostInfo extends LitElement {
private async _loadData(): Promise<void> { private async _loadData(): Promise<void> {
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) { if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
fireEvent(this, "supervisor-colllection-refresh", { fireEvent(this, "supervisor-collection-refresh", {
colllection: "network", collection: "network",
}); });
} else { } else {
const network = await fetchNetworkInfo(this.hass); const network = await fetchNetworkInfo(this.hass);

View File

@@ -8,6 +8,7 @@ import {
property, property,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event"; import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
@@ -38,6 +39,7 @@ import "../components/supervisor-metric";
import { hassioStyle } from "../resources/hassio-style"; import { hassioStyle } from "../resources/hassio-style";
const UNSUPPORTED_REASON_URL = { const UNSUPPORTED_REASON_URL = {
apparmor: "/more-info/unsupported/apparmor",
container: "/more-info/unsupported/container", container: "/more-info/unsupported/container",
dbus: "/more-info/unsupported/dbus", dbus: "/more-info/unsupported/dbus",
docker_configuration: "/more-info/unsupported/docker_configuration", docker_configuration: "/more-info/unsupported/docker_configuration",
@@ -48,6 +50,7 @@ const UNSUPPORTED_REASON_URL = {
os: "/more-info/unsupported/os", os: "/more-info/unsupported/os",
privileged: "/more-info/unsupported/privileged", privileged: "/more-info/unsupported/privileged",
systemd: "/more-info/unsupported/systemd", systemd: "/more-info/unsupported/systemd",
content_trust: "/more-info/unsupported/content_trust",
}; };
const UNHEALTHY_REASON_URL = { const UNHEALTHY_REASON_URL = {
@@ -55,6 +58,7 @@ const UNHEALTHY_REASON_URL = {
supervisor: "/more-info/unhealthy/supervisor", supervisor: "/more-info/unhealthy/supervisor",
setup: "/more-info/unhealthy/setup", setup: "/more-info/unhealthy/setup",
docker: "/more-info/unhealthy/docker", docker: "/more-info/unhealthy/docker",
untrusted: "/more-info/unhealthy/untrusted",
}; };
@customElement("hassio-supervisor-info") @customElement("hassio-supervisor-info")
@@ -148,30 +152,32 @@ class HassioSupervisorInfo extends LitElement {
</ha-settings-row> </ha-settings-row>
${this.supervisor.supervisor.supported ${this.supervisor.supervisor.supported
? html` <ha-settings-row three-line> ? !atLeastVersion(this.hass.config.version, 2021, 4)
<span slot="heading"> ? html` <ha-settings-row three-line>
${this.supervisor.localize( <span slot="heading">
"system.supervisor.share_diagnostics" ${this.supervisor.localize(
)} "system.supervisor.share_diagnostics"
</span> )}
<div slot="description" class="diagnostics-description"> </span>
${this.supervisor.localize( <div slot="description" class="diagnostics-description">
"system.supervisor.share_diagnostics_description" ${this.supervisor.localize(
)} "system.supervisor.share_diagnostics_description"
<button )}
class="link" <button
.title=${this.supervisor.localize("common.show_more")} class="link"
@click=${this._diagnosticsInformationDialog} .title=${this.supervisor.localize("common.show_more")}
> @click=${this._diagnosticsInformationDialog}
${this.supervisor.localize("common.learn_more")} >
</button> ${this.supervisor.localize("common.learn_more")}
</div> </button>
<ha-switch </div>
haptic <ha-switch
.checked=${this.supervisor.supervisor.diagnostics} haptic
@change=${this._toggleDiagnostics} .checked=${this.supervisor.supervisor.diagnostics}
></ha-switch> @change=${this._toggleDiagnostics}
</ha-settings-row>` ></ha-switch>
</ha-settings-row>`
: ""
: html`<div class="error"> : html`<div class="error">
${this.supervisor.localize( ${this.supervisor.localize(
"system.supervisor.unsupported_title" "system.supervisor.unsupported_title"
@@ -263,13 +269,15 @@ class HassioSupervisorInfo extends LitElement {
</b> </b>
<br /><br /> <br /><br />
${this.supervisor.localize("system.supervisor.beta_release_items")} ${this.supervisor.localize("system.supervisor.beta_release_items")}
<li>Home Assistant Core</li> <ul>
<li>Home Assistant Supervisor</li> <li>Home Assistant Core</li>
<li>Home Assistant Operating System</li> <li>Home Assistant Supervisor</li>
<li>Home Assistant Operating System</li>
</ul>
<br /> <br />
${this.supervisor.localize("system.supervisor.join_beta_action")}`, ${this.supervisor.localize("system.supervisor.beta_join_confirm")}`,
confirmText: this.supervisor.localize( confirmText: this.supervisor.localize(
"system.supervisor.beta_join_confirm" "system.supervisor.join_beta_action"
), ),
dismissText: this.supervisor.localize("common.cancel"), dismissText: this.supervisor.localize("common.cancel"),
}); });
@@ -317,8 +325,8 @@ class HassioSupervisorInfo extends LitElement {
private async _reloadSupervisor(): Promise<void> { private async _reloadSupervisor(): Promise<void> {
await reloadSupervisor(this.hass); await reloadSupervisor(this.hass);
fireEvent(this, "supervisor-colllection-refresh", { fireEvent(this, "supervisor-collection-refresh", {
colllection: "supervisor", collection: "supervisor",
}); });
} }
@@ -367,9 +375,13 @@ class HassioSupervisorInfo extends LitElement {
button.progress = true; button.progress = true;
const confirmed = await showConfirmationDialog(this, { 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( text: this.supervisor.localize(
"confirm.text", "confirm.update.text",
"name", "name",
"Supervisor", "Supervisor",
"version", "version",
@@ -386,8 +398,8 @@ class HassioSupervisorInfo extends LitElement {
try { try {
await updateSupervisor(this.hass); await updateSupervisor(this.hass);
fireEvent(this, "supervisor-colllection-refresh", { fireEvent(this, "supervisor-collection-refresh", {
colllection: "supervisor", collection: "supervisor",
}); });
} catch (err) { } catch (err) {
showAlertDialog(this, { showAlertDialog(this, {

View File

@@ -25,7 +25,7 @@
"@braintree/sanitize-url": "^5.0.0", "@braintree/sanitize-url": "^5.0.0",
"@codemirror/commands": "^0.18.0", "@codemirror/commands": "^0.18.0",
"@codemirror/gutter": "^0.18.0", "@codemirror/gutter": "^0.18.0",
"@codemirror/highlight": "^0.18.1", "@codemirror/highlight": "^0.18.0",
"@codemirror/history": "^0.18.0", "@codemirror/history": "^0.18.0",
"@codemirror/legacy-modes": "^0.18.0", "@codemirror/legacy-modes": "^0.18.0",
"@codemirror/rectangular-selection": "^0.18.0", "@codemirror/rectangular-selection": "^0.18.0",
@@ -91,8 +91,6 @@
"@polymer/paper-tooltip": "^3.0.1", "@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "3.1.0", "@polymer/polymer": "3.1.0",
"@thomasloven/round-slider": "0.5.2", "@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-combo-box": "^5.0.10",
"@vaadin/vaadin-date-picker": "^4.0.7", "@vaadin/vaadin-date-picker": "^4.0.7",
"@vibrant/color": "^3.2.1-alpha.1", "@vibrant/color": "^3.2.1-alpha.1",
@@ -102,7 +100,6 @@
"@webcomponents/webcomponentsjs": "^2.2.7", "@webcomponents/webcomponentsjs": "^2.2.7",
"chart.js": "~2.8.0", "chart.js": "~2.8.0",
"chartjs-chart-timeline": "^0.3.0", "chartjs-chart-timeline": "^0.3.0",
"codemirror": "^5.49.0",
"comlink": "^4.3.0", "comlink": "^4.3.0",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"cropperjs": "^1.5.7", "cropperjs": "^1.5.7",
@@ -111,7 +108,7 @@
"fecha": "^4.2.0", "fecha": "^4.2.0",
"fuse.js": "^6.0.0", "fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2", "google-timezones-json": "^1.0.2",
"hls.js": "^0.13.2", "hls.js": "^1.0.1",
"home-assistant-js-websocket": "^5.9.0", "home-assistant-js-websocket": "^5.9.0",
"idb-keyval": "^3.2.0", "idb-keyval": "^3.2.0",
"intl-messageformat": "^8.3.9", "intl-messageformat": "^8.3.9",
@@ -134,6 +131,7 @@
"sortablejs": "^1.10.2", "sortablejs": "^1.10.2",
"superstruct": "^0.10.13", "superstruct": "^0.10.13",
"tinykeys": "^1.1.1", "tinykeys": "^1.1.1",
"tsparticles": "^1.19.2",
"unfetch": "^4.1.0", "unfetch": "^4.1.0",
"vis-data": "^7.1.1", "vis-data": "^7.1.1",
"vis-network": "^8.5.4", "vis-network": "^8.5.4",
@@ -167,8 +165,8 @@
"@rollup/plugin-replace": "^2.3.2", "@rollup/plugin-replace": "^2.3.2",
"@types/chai": "^4.1.7", "@types/chai": "^4.1.7",
"@types/chromecast-caf-receiver": "^5.0.11", "@types/chromecast-caf-receiver": "^5.0.11",
"@types/chromecast-caf-sender": "^1.0.3",
"@types/codemirror": "^0.0.97", "@types/codemirror": "^0.0.97",
"@types/hls.js": "^0.12.3",
"@types/js-yaml": "^3.12.1", "@types/js-yaml": "^3.12.1",
"@types/leaflet": "^1.4.3", "@types/leaflet": "^1.4.3",
"@types/leaflet-draw": "^1.0.1", "@types/leaflet-draw": "^1.0.1",
@@ -176,6 +174,7 @@
"@types/memoize-one": "4.1.0", "@types/memoize-one": "4.1.0",
"@types/mocha": "^7.0.2", "@types/mocha": "^7.0.2",
"@types/resize-observer-browser": "^0.1.3", "@types/resize-observer-browser": "^0.1.3",
"@types/sortablejs": "^1.10.6",
"@types/webspeechapi": "^0.0.29", "@types/webspeechapi": "^0.0.29",
"@typescript-eslint/eslint-plugin": "^4.4.0", "@typescript-eslint/eslint-plugin": "^4.4.0",
"@typescript-eslint/parser": "^4.4.0", "@typescript-eslint/parser": "^4.4.0",
@@ -227,7 +226,7 @@
"terser-webpack-plugin": "^5.1.1", "terser-webpack-plugin": "^5.1.1",
"ts-lit-plugin": "^1.2.1", "ts-lit-plugin": "^1.2.1",
"ts-mocha": "^7.0.0", "ts-mocha": "^7.0.0",
"typescript": "^4.0.3", "typescript": "^4.2.4",
"vinyl-buffer": "^1.0.1", "vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0", "vinyl-source-stream": "^2.0.0",
"webpack": "^5.24.1", "webpack": "^5.24.1",

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -10,10 +10,10 @@ function patch(version) {
function today() { function today() {
const now = new Date(); const now = new Date();
return `${now.getFullYear()}${String(now.getMonth() + 1).padStart( return `${now.getUTCFullYear()}${String(now.getUTCMonth() + 1).padStart(
2, 2,
"0" "0"
)}${String(now.getDate()).padStart(2, "0")}.0`; )}${String(now.getUTCDate()).padStart(2, "0")}.0`;
} }
function auto(version) { function auto(version) {

View File

@@ -2,12 +2,12 @@ from setuptools import setup, find_packages
setup( setup(
name="home-assistant-frontend", name="home-assistant-frontend",
version="20210302.6", version="20210423.0",
description="The Home Assistant frontend", description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer", url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors", author="The Home Assistant Authors",
author_email="hello@home-assistant.io", author_email="hello@home-assistant.io",
license="Apache License 2.0", license="Apache-2.0",
packages=find_packages(include=["hass_frontend", "hass_frontend.*"]), packages=find_packages(include=["hass_frontend", "hass_frontend.*"]),
include_package_data=True, include_package_data=True,
zip_safe=False, zip_safe=False,

View File

@@ -8,6 +8,7 @@ import {
PropertyValues, PropertyValues,
} from "lit-element"; } from "lit-element";
import punycode from "punycode"; import punycode from "punycode";
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
import { extractSearchParamsObject } from "../common/url/search-params"; import { extractSearchParamsObject } from "../common/url/search-params";
import { import {
AuthProvider, AuthProvider,
@@ -116,6 +117,20 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
this._fetchAuthProviders(); this._fetchAuthProviders();
this._fetchDiscoveryInfo(); 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) { if (!this.redirectUri) {
return; return;
} }

View File

@@ -62,7 +62,7 @@ export const ensureConnectedCastSession = (cast: CastManager, auth: Auth) => {
return undefined; return undefined;
} }
return new Promise((resolve) => { return new Promise<void>((resolve) => {
const unsub = cast.addEventListener("connection-changed", () => { const unsub = cast.addEventListener("connection-changed", () => {
if (cast.castConnectedToOurHass) { if (cast.castConnectedToOurHass) {
unsub(); unsub();

View File

@@ -56,6 +56,8 @@ export const FIXED_DOMAIN_ICONS = {
export const FIXED_DEVICE_CLASS_ICONS = { export const FIXED_DEVICE_CLASS_ICONS = {
current: "hass:current-ac", current: "hass:current-ac",
carbon_dioxide: "mdi:molecule-co2",
carbon_monoxide: "mdi:molecule-co",
energy: "hass:flash", energy: "hass:flash",
humidity: "hass:water-percent", humidity: "hass:water-percent",
illuminance: "hass:brightness-5", illuminance: "hass:brightness-5",
@@ -103,6 +105,7 @@ export const DOMAINS_WITH_MORE_INFO = [
"lock", "lock",
"media_player", "media_player",
"person", "person",
"remote",
"script", "script",
"sun", "sun",
"timer", "timer",

View File

@@ -1,9 +1,10 @@
import { format } from "fecha"; import { format } from "fecha";
import { FrontendTranslationData } from "../../data/translation";
import { toLocaleDateStringSupportsOptions } from "./check_options_support"; import { toLocaleDateStringSupportsOptions } from "./check_options_support";
export const formatDate = toLocaleDateStringSupportsOptions export const formatDate = toLocaleDateStringSupportsOptions
? (dateObj: Date, locales: string) => ? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleDateString(locales, { dateObj.toLocaleDateString(locales.language, {
year: "numeric", year: "numeric",
month: "long", month: "long",
day: "numeric", day: "numeric",
@@ -11,8 +12,8 @@ export const formatDate = toLocaleDateStringSupportsOptions
: (dateObj: Date) => format(dateObj, "longDate"); : (dateObj: Date) => format(dateObj, "longDate");
export const formatDateWeekday = toLocaleDateStringSupportsOptions export const formatDateWeekday = toLocaleDateStringSupportsOptions
? (dateObj: Date, locales: string) => ? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleDateString(locales, { dateObj.toLocaleDateString(locales.language, {
weekday: "long", weekday: "long",
month: "short", month: "short",
day: "numeric", day: "numeric",

View File

@@ -1,9 +1,10 @@
import { format } from "fecha"; import { format } from "fecha";
import { FrontendTranslationData } from "../../data/translation";
import { toLocaleStringSupportsOptions } from "./check_options_support"; import { toLocaleStringSupportsOptions } from "./check_options_support";
export const formatDateTime = toLocaleStringSupportsOptions export const formatDateTime = toLocaleStringSupportsOptions
? (dateObj: Date, locales: string) => ? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleString(locales, { dateObj.toLocaleString(locales.language, {
year: "numeric", year: "numeric",
month: "long", month: "long",
day: "numeric", day: "numeric",
@@ -13,8 +14,8 @@ export const formatDateTime = toLocaleStringSupportsOptions
: (dateObj: Date) => format(dateObj, "MMMM D, YYYY, HH:mm"); : (dateObj: Date) => format(dateObj, "MMMM D, YYYY, HH:mm");
export const formatDateTimeWithSeconds = toLocaleStringSupportsOptions export const formatDateTimeWithSeconds = toLocaleStringSupportsOptions
? (dateObj: Date, locales: string) => ? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleString(locales, { dateObj.toLocaleString(locales.language, {
year: "numeric", year: "numeric",
month: "long", month: "long",
day: "numeric", day: "numeric",

View File

@@ -1,17 +1,18 @@
import { format } from "fecha"; import { format } from "fecha";
import { FrontendTranslationData } from "../../data/translation";
import { toLocaleTimeStringSupportsOptions } from "./check_options_support"; import { toLocaleTimeStringSupportsOptions } from "./check_options_support";
export const formatTime = toLocaleTimeStringSupportsOptions export const formatTime = toLocaleTimeStringSupportsOptions
? (dateObj: Date, locales: string) => ? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleTimeString(locales, { dateObj.toLocaleTimeString(locales.language, {
hour: "numeric", hour: "numeric",
minute: "2-digit", minute: "2-digit",
}) })
: (dateObj: Date) => format(dateObj, "shortTime"); : (dateObj: Date) => format(dateObj, "shortTime");
export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions
? (dateObj: Date, locales: string) => ? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleTimeString(locales, { dateObj.toLocaleTimeString(locales.language, {
hour: "numeric", hour: "numeric",
minute: "2-digit", minute: "2-digit",
second: "2-digit", second: "2-digit",
@@ -19,8 +20,8 @@ export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions
: (dateObj: Date) => format(dateObj, "mediumTime"); : (dateObj: Date) => format(dateObj, "mediumTime");
export const formatTimeWeekday = toLocaleTimeStringSupportsOptions export const formatTimeWeekday = toLocaleTimeStringSupportsOptions
? (dateObj: Date, locales: string) => ? (dateObj: Date, locales: FrontendTranslationData) =>
dateObj.toLocaleTimeString(locales, { dateObj.toLocaleTimeString(locales.language, {
weekday: "long", weekday: "long",
hour: "numeric", hour: "numeric",
minute: "2-digit", minute: "2-digit",

View File

@@ -70,13 +70,18 @@ export const applyThemesOnElement = (
themeRules["text-accent-color"] = themeRules["text-accent-color"] =
rgbContrast(rgbAccentColor, [33, 33, 33]) < 6 ? "#fff" : "#212121"; rgbContrast(rgbAccentColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
} }
// Nothing was changed
if (element._themes?.cacheKey === cacheKey) {
return;
}
} }
if (selectedTheme && themes.themes[selectedTheme]) { if (selectedTheme && themes.themes[selectedTheme]) {
themeRules = 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 // No styles to reset, and no styles to set
return; return;
} }
@@ -87,8 +92,8 @@ export const applyThemesOnElement = (
: undefined; : undefined;
// Add previous set keys to reset them, and new theme // Add previous set keys to reset them, and new theme
const styles = { ...element._themes, ...newTheme?.styles }; const styles = { ...element._themes?.keys, ...newTheme?.styles };
element._themes = newTheme?.keys; element._themes = { cacheKey, keys: newTheme?.keys };
// Set and/or reset styles // Set and/or reset styles
if (element.updateStyles) { if (element.updateStyles) {

View File

@@ -1,6 +1,10 @@
export const ensureArray = (value?: any) => { type NonUndefined<T> = T extends undefined ? never : T;
if (!value || Array.isArray(value)) {
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;
} }
return [value]; return [value];
}; }

View File

@@ -1,5 +1,6 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity"; import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { FrontendTranslationData } from "../../data/translation";
import { formatDate } from "../datetime/format_date"; import { formatDate } from "../datetime/format_date";
import { formatDateTime } from "../datetime/format_date_time"; import { formatDateTime } from "../datetime/format_date_time";
import { formatTime } from "../datetime/format_time"; import { formatTime } from "../datetime/format_time";
@@ -10,7 +11,7 @@ import { computeStateDomain } from "./compute_state_domain";
export const computeStateDisplay = ( export const computeStateDisplay = (
localize: LocalizeFunc, localize: LocalizeFunc,
stateObj: HassEntity, stateObj: HassEntity,
language: string, locale: FrontendTranslationData,
state?: string state?: string
): string => { ): string => {
const compareState = state !== undefined ? state : stateObj.state; const compareState = state !== undefined ? state : stateObj.state;
@@ -20,7 +21,7 @@ export const computeStateDisplay = (
} }
if (stateObj.attributes.unit_of_measurement) { if (stateObj.attributes.unit_of_measurement) {
return `${formatNumber(compareState, language)} ${ return `${formatNumber(compareState, locale)} ${
stateObj.attributes.unit_of_measurement stateObj.attributes.unit_of_measurement
}`; }`;
} }
@@ -35,7 +36,7 @@ export const computeStateDisplay = (
stateObj.attributes.month - 1, stateObj.attributes.month - 1,
stateObj.attributes.day stateObj.attributes.day
); );
return formatDate(date, language); return formatDate(date, locale);
} }
if (!stateObj.attributes.has_date) { if (!stateObj.attributes.has_date) {
const now = new Date(); const now = new Date();
@@ -48,7 +49,7 @@ export const computeStateDisplay = (
stateObj.attributes.hour, stateObj.attributes.hour,
stateObj.attributes.minute stateObj.attributes.minute
); );
return formatTime(date, language); return formatTime(date, locale);
} }
date = new Date( date = new Date(
@@ -58,7 +59,7 @@ export const computeStateDisplay = (
stateObj.attributes.hour, stateObj.attributes.hour,
stateObj.attributes.minute stateObj.attributes.minute
); );
return formatDateTime(date, language); return formatDateTime(date, locale);
} }
if (domain === "humidifier") { if (domain === "humidifier") {
@@ -67,8 +68,13 @@ export const computeStateDisplay = (
} }
} }
if (domain === "counter") { // `counter` `number` and `input_number` domains do not have a unit of measurement but should still use `formatNumber`
return formatNumber(compareState, language); if (
domain === "counter" ||
domain === "number" ||
domain === "input_number"
) {
return formatNumber(compareState, locale);
} }
return ( return (

View File

@@ -12,16 +12,24 @@ declare global {
export const navigate = (_node: any, path: string, replace = false) => { export const navigate = (_node: any, path: string, replace = false) => {
if (__DEMO__) { if (__DEMO__) {
if (replace) { if (replace) {
history.replaceState(null, "", `${location.pathname}#${path}`); top.history.replaceState(
top.history.state?.root ? { root: true } : null,
"",
`${top.location.pathname}#${path}`
);
} else { } else {
window.location.hash = path; top.location.hash = path;
} }
} else if (replace) { } else if (replace) {
history.replaceState(null, "", path); top.history.replaceState(
top.history.state?.root ? { root: true } : null,
"",
path
);
} else { } else {
history.pushState(null, "", path); top.history.pushState(null, "", path);
} }
fireEvent(window, "location-changed", { fireEvent(top, "location-changed", {
replace, replace,
}); });
}; };

View File

@@ -34,14 +34,12 @@ const _maxLen = 128;
function initTable() { function initTable() {
const table: number[][] = []; const table: number[][] = [];
const row: number[] = [0]; const row: number[] = [];
for (let i = 1; i <= _maxLen; i++) { for (let i = 0; i <= _maxLen; i++) {
row.push(-i); row[i] = 0;
} }
for (let i = 0; i <= _maxLen; i++) { for (let i = 0; i <= _maxLen; i++) {
const thisRow = row.slice(0); table.push(row.slice(0));
thisRow[0] = -i;
table.push(thisRow);
} }
return table; return table;
} }
@@ -50,7 +48,7 @@ function isSeparatorAtPos(value: string, index: number): boolean {
if (index < 0 || index >= value.length) { if (index < 0 || index >= value.length) {
return false; return false;
} }
const code = value.charCodeAt(index); const code = value.codePointAt(index);
switch (code) { switch (code) {
case CharCode.Underline: case CharCode.Underline:
case CharCode.Dash: case CharCode.Dash:
@@ -62,8 +60,16 @@ function isSeparatorAtPos(value: string, index: number): boolean {
case CharCode.DoubleQuote: case CharCode.DoubleQuote:
case CharCode.Colon: case CharCode.Colon:
case CharCode.DollarSign: case CharCode.DollarSign:
case CharCode.LessThan:
case CharCode.OpenParen:
case CharCode.OpenSquareBracket:
return true; return true;
case undefined:
return false;
default: default:
if (isEmojiImprecise(code)) {
return true;
}
return false; return false;
} }
} }
@@ -92,10 +98,15 @@ function isPatternInWord(
patternLen: number, patternLen: number,
wordLow: string, wordLow: string,
wordPos: number, wordPos: number,
wordLen: number wordLen: number,
fillMinWordPosArr = false
): boolean { ): boolean {
while (patternPos < patternLen && wordPos < wordLen) { while (patternPos < patternLen && wordPos < wordLen) {
if (patternLow[patternPos] === wordLow[wordPos]) { if (patternLow[patternPos] === wordLow[wordPos]) {
if (fillMinWordPosArr) {
// Remember the min word position for each pattern position
_minWordMatchPos[patternPos] = wordPos;
}
patternPos += 1; patternPos += 1;
} }
wordPos += 1; wordPos += 1;
@@ -104,42 +115,22 @@ function isPatternInWord(
} }
enum Arrow { enum Arrow {
Top = 0b1, Diag = 1,
Diag = 0b10, Left = 2,
Left = 0b100, LeftLeft = 3,
} }
/** /**
* A tuple of three values. * An array representating a fuzzy match.
*
* 0. the score * 0. the score
* 1. the matches encoded as bitmask (2^53) * 1. the offset at which matching started
* 2. 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]; // export type FuzzyScore = [score: number, wordStart: number, ...matches: number[]];// [number, number, number];
export type FuzzyScore = Array<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 function fuzzyScore( export function fuzzyScore(
pattern: string, pattern: string,
@@ -150,7 +141,6 @@ export function fuzzyScore(
wordStart: number, wordStart: number,
firstMatchCanBeWeak: boolean firstMatchCanBeWeak: boolean
): FuzzyScore | undefined { ): FuzzyScore | undefined {
const globals = initGlobals();
const patternLen = pattern.length > _maxLen ? _maxLen : pattern.length; const patternLen = pattern.length > _maxLen ? _maxLen : pattern.length;
const wordLen = word.length > _maxLen ? _maxLen : word.length; const wordLen = word.length > _maxLen ? _maxLen : word.length;
@@ -172,18 +162,30 @@ export function fuzzyScore(
patternLen, patternLen,
wordLow, wordLow,
wordStart, wordStart,
wordLen wordLen,
true
) )
) { ) {
return undefined; 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 row = 1;
let column = 1; let column = 1;
let patternPos = patternStart; let patternPos = patternStart;
let wordPos = wordStart; let wordPos = wordStart;
let hasStrongFirstMatch = false; const hasStrongFirstMatch = [false];
// There will be a match, fill in tables // There will be a match, fill in tables
for ( for (
@@ -191,83 +193,146 @@ export function fuzzyScore(
patternPos < patternLen; patternPos < patternLen;
row++, patternPos++ 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 ( for (
column = 1, wordPos = wordStart; column = minWordMatchPos - wordStart + 1, wordPos = minWordMatchPos;
wordPos < wordLen; wordPos < nextMaxWordMatchPos;
column++, wordPos++ column++, wordPos++
) { ) {
const score = _doScore( let score = Number.MIN_SAFE_INTEGER;
pattern, let canComeDiag = false;
patternLow,
patternPos,
patternStart,
word,
wordLow,
wordPos
);
if (patternPos === patternStart && score > 1) { if (wordPos <= maxWordMatchPos) {
hasStrongFirstMatch = true; 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 = const canComeLeft = wordPos > minWordMatchPos;
globals._table[row - 1][column - 1] + (score > 1 ? 1 : score); const leftScore = canComeLeft
const top = globals._table[row - 1][column] + -1; ? _table[row][column - 1] + (_diag[row][column - 1] > 0 ? -5 : 0)
const left = globals._table[row][column - 1] + -1; : 0; // penalty for a gap start
if (left >= top) { const canComeLeftLeft =
// left or diag wordPos > minWordMatchPos + 1 && _diag[row][column - 1] > 0;
if (left > diag) { const leftLeftScore = canComeLeftLeft
globals._table[row][column] = left; ? _table[row][column - 2] + (_diag[row][column - 2] > 0 ? -5 : 0)
globals._arrows[row][column] = Arrow.Left; : 0; // penalty for a gap start
} else if (left === diag) {
globals._table[row][column] = left; if (
globals._arrows[row][column] = Arrow.Left || Arrow.Diag; canComeLeftLeft &&
} else { (!canComeLeft || leftLeftScore >= leftScore) &&
globals._table[row][column] = diag; (!canComeDiag || leftLeftScore >= diagScore)
globals._arrows[row][column] = Arrow.Diag; ) {
} // always prefer choosing left left to jump over a diagonal because that means a match is earlier in the word
} else if (top > diag) { _table[row][column] = leftLeftScore;
globals._table[row][column] = top; _arrows[row][column] = Arrow.LeftLeft;
globals._arrows[row][column] = Arrow.Top; _diag[row][column] = 0;
} else if (top === diag) { } else if (canComeLeft && (!canComeDiag || leftScore >= diagScore)) {
globals._table[row][column] = top; // always prefer choosing left since that means a match is earlier in the word
globals._arrows[row][column] = Arrow.Top || Arrow.Diag; _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 { } else {
globals._table[row][column] = diag; throw new Error(`not possible`);
globals._arrows[row][column] = Arrow.Diag;
} }
} }
} }
if (_debug) { if (_debug) {
printTables(pattern, patternStart, word, wordStart, globals); printTables(pattern, patternStart, word, wordStart);
} }
if (!hasStrongFirstMatch && !firstMatchCanBeWeak) { if (!hasStrongFirstMatch[0] && !firstMatchCanBeWeak) {
return undefined; return undefined;
} }
globals._matchesCount = 0; row--;
globals._topScore = -100; column--;
globals._wordStart = wordStart;
globals._firstMatchCanBeWeak = firstMatchCanBeWeak;
_findAllMatches2( const result: FuzzyScore = [_table[row][column], wordStart];
row - 1,
column - 1, let backwardsDiagLength = 0;
patternLen === wordLen ? 1 : 0, let maxMatchColumn = 0;
0,
false, while (row >= 1) {
globals // Find the column where we go diagonally up
); let diagColumn = column;
if (globals._matchesCount === 0) { do {
return undefined; 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( function _doScore(
@@ -277,50 +342,81 @@ function _doScore(
patternStart: number, patternStart: number,
word: string, word: string,
wordLow: string, wordLow: string,
wordPos: number wordPos: number,
) { wordLen: number,
wordStart: number,
newMatchStart: boolean,
outFirstMatchStrong: boolean[]
): number {
if (patternLow[patternPos] !== wordLow[wordPos]) { if (patternLow[patternPos] !== wordLow[wordPos]) {
return -1; return Number.MIN_SAFE_INTEGER;
} }
let score = 1;
let isGapLocation = false;
if (wordPos === patternPos - patternStart) { if (wordPos === patternPos - patternStart) {
// common prefix: `foobar <-> foobaz` // common prefix: `foobar <-> foobaz`
// ^^^^^ // ^^^^^
if (pattern[patternPos] === word[wordPos]) { score = pattern[patternPos] === word[wordPos] ? 7 : 5;
return 7; } else if (
}
return 5;
}
if (
isUpperCaseAtPos(wordPos, word, wordLow) && isUpperCaseAtPos(wordPos, word, wordLow) &&
(wordPos === 0 || !isUpperCaseAtPos(wordPos - 1, word, wordLow)) (wordPos === 0 || !isUpperCaseAtPos(wordPos - 1, word, wordLow))
) { ) {
// hitting upper-case: `foo <-> forOthers` // hitting upper-case: `foo <-> forOthers`
// ^^ ^ // ^^ ^
if (pattern[patternPos] === word[wordPos]) { score = pattern[patternPos] === word[wordPos] ? 7 : 5;
return 7; isGapLocation = true;
} } else if (
return 5;
}
if (
isSeparatorAtPos(wordLow, wordPos) && isSeparatorAtPos(wordLow, wordPos) &&
(wordPos === 0 || !isSeparatorAtPos(wordLow, wordPos - 1)) (wordPos === 0 || !isSeparatorAtPos(wordLow, wordPos - 1))
) { ) {
// hitting a separator: `. <-> foo.bar` // hitting a separator: `. <-> foo.bar`
// ^ // ^
return 5; score = 5;
} } else if (
if (
isSeparatorAtPos(wordLow, wordPos - 1) || isSeparatorAtPos(wordLow, wordPos - 1) ||
isWhitespaceAtPos(wordLow, wordPos - 1) isWhitespaceAtPos(wordLow, wordPos - 1)
) { ) {
// post separator: `foo <-> bar_foo` // 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( function printTable(
@@ -360,104 +456,96 @@ function printTables(
pattern: string, pattern: string,
patternStart: number, patternStart: number,
word: string, word: string,
wordStart: number, wordStart: number
globals: FilterGlobals
): void { ): void {
pattern = pattern.substr(patternStart); pattern = pattern.substr(patternStart);
word = word.substr(wordStart); word = word.substr(wordStart);
console.log( console.log(printTable(_table, pattern, pattern.length, word, word.length));
printTable(globals._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));
console.log(
printTable(globals._arrows, pattern, pattern.length, word, word.length)
);
console.log(
printTable(globals._scores, pattern, pattern.length, word, word.length)
);
} }
function _findAllMatches2( const _minWordMatchPos = initArr(2 * _maxLen); // min word position for a certain pattern position
row: number, const _maxWordMatchPos = initArr(2 * _maxLen); // max word position for a certain pattern position
column: number, const _diag = initTable(); // the length of a contiguous diagonal match
total: number, const _table = initTable();
matches: number, const _arrows = <Arrow[][]>initTable();
lastMatched: boolean,
globals: FilterGlobals function initArr(maxLen: number) {
): void { const row: number[] = [];
if (globals._matchesCount >= 10 || total < -25) { for (let i = 0; i <= maxLen; i++) {
// stop when having already 10 results, or row[i] = 0;
// when a potential alignment as already 5 gaps
return;
} }
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) { export interface FuzzyScorer {
const score = globals._scores[row][column]; (
const arrow = globals._arrows[row][column]; pattern: string,
lowPattern: string,
patternPos: number,
word: string,
lowWord: string,
wordPos: number,
firstMatchCanBeWeak: boolean
): FuzzyScore | undefined;
}
if (arrow === Arrow.Left) { export function createMatches(score: undefined | FuzzyScore): Match[] {
// left -> no match, skip a word character if (typeof score === "undefined") {
column -= 1; return [];
if (lastMatched) { }
total -= 5; // new gap penalty const res: Match[] = [];
} else if (matches !== 0) { const wordPos = score[1];
total -= 1; // gap penalty after first match for (let i = score.length - 1; i > 1; i--) {
} const pos = score[i] + wordPos;
lastMatched = false; const last = res[res.length - 1];
simpleMatchCount = 0; if (last && last.end === pos) {
} else if (arrow && Arrow.Diag) { last.end = pos + 1;
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;
}
} else { } else {
return; res.push({ start: pos, end: pos + 1 });
} }
} }
return res;
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;
}
} }
// #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)
);
}

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. * @return {number} Score representing how well the word matches the filter. Return of 0 means no match.
*/ */
export const fuzzySequentialMatch = (filter: string, ...words: string[]) => { export const fuzzySequentialMatch = (
let topScore = 0; filter: string,
item: ScorableTextItem
) => {
let topScore = Number.NEGATIVE_INFINITY;
for (const word of words) { for (const word of item.strings) {
const scores = fuzzyScore( const scores = fuzzyScore(
filter, filter,
filter.toLowerCase(), filter.toLowerCase(),
@@ -28,22 +31,39 @@ export const fuzzySequentialMatch = (filter: string, ...words: string[]) => {
continue; continue;
} }
// The VS Code implementation of filter treats a score of "0" as just barely a match // The VS Code implementation of filter returns a 0 for a weak match.
// But we will typically use this matcher in a .filter(), which interprets 0 as a failure. // But if .filter() sees a "0", it considers that a failed match and will remove it.
// By shifting all scores up by 1, we allow "0" matches, while retaining score precedence // So, we set score to 1 in these cases so the match will be included, and mostly respect correct ordering.
const score = scores[0] + 1; const score = scores[0] === 0 ? 1 : scores[0];
if (score > topScore) { if (score > topScore) {
topScore = score; topScore = score;
} }
} }
if (topScore === Number.NEGATIVE_INFINITY) {
return undefined;
}
return topScore; 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 { export interface ScorableTextItem {
score?: number; score?: number;
text: string; strings: string[];
altText?: string;
} }
type FuzzyFilterSort = <T extends ScorableTextItem>( type FuzzyFilterSort = <T extends ScorableTextItem>(
@@ -54,12 +74,10 @@ type FuzzyFilterSort = <T extends ScorableTextItem>(
export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => { export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => {
return items return items
.map((item) => { .map((item) => {
item.score = item.altText item.score = fuzzySequentialMatch(filter, item);
? fuzzySequentialMatch(filter, item.text, item.altText)
: fuzzySequentialMatch(filter, item.text);
return item; return item;
}) })
.filter((item) => item.score !== undefined && item.score > 0) .filter((item) => item.score !== undefined)
.sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) => .sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) =>
scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0 scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0
); );

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 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 = ( export const formatNumber = (
num: string | number, num: string | number,
language: string, locale?: FrontendTranslationData,
options?: Intl.NumberFormatOptions options?: Intl.NumberFormatOptions
): string => { ): 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() // Polyfill for Number.isNaN, which is more reliable than the global isNaN()
Number.isNaN = Number.isNaN =
Number.isNaN || Number.isNaN ||
@@ -16,11 +38,25 @@ export const formatNumber = (
return typeof input === "number" && isNaN(input); return typeof input === "number" && isNaN(input);
}; };
if (!Number.isNaN(Number(num)) && Intl) { if (
return new Intl.NumberFormat( !Number.isNaN(Number(num)) &&
language, Intl &&
getDefaultFormatOptions(num, options) locale?.number_format !== NumberFormat.none
).format(Number(num)); ) {
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(); return num.toString();
}; };

View File

@@ -1,4 +1,5 @@
const isTemplateRegex = new RegExp("{%|{{|{#"); const isTemplateRegex = new RegExp("{%|{{");
export const isTemplate = (value: string): boolean => export const isTemplate = (value: string): boolean =>
isTemplateRegex.test(value); isTemplateRegex.test(value);
@@ -11,7 +12,7 @@ export const hasTemplate = (value: unknown): boolean => {
} }
if (typeof value === "object") { if (typeof value === "object") {
const values = Array.isArray(value) ? value : Object.values(value!); const values = Array.isArray(value) ? value : Object.values(value!);
return values.some((val) => hasTemplate(val)); return values.some((val) => val && hasTemplate(val));
} }
return false; return false;
}; };

View File

@@ -0,0 +1,2 @@
export const strStartsWith = (value: string, search: string) =>
value.substring(0, search.length) === search;

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

View File

@@ -19,3 +19,17 @@ export const createSearchParam = (params: Record<string, string>): string => {
}); });
return urlParams.toString(); 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();
};

View File

@@ -1,4 +1,4 @@
export const afterNextRender = (cb: () => void): void => { export const afterNextRender = (cb: (value: unknown) => void): void => {
requestAnimationFrame(() => setTimeout(cb, 0)); requestAnimationFrame(() => setTimeout(cb, 0));
}; };

View File

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

View File

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

View File

@@ -63,7 +63,7 @@ export interface DataTableSortColumnData {
} }
export interface DataTableColumnData extends DataTableSortColumnData { export interface DataTableColumnData extends DataTableSortColumnData {
title: string; title: TemplateResult | string;
type?: "numeric" | "icon" | "icon-button"; type?: "numeric" | "icon" | "icon-button";
template?: <T>(data: any, row: T) => TemplateResult | string; template?: <T>(data: any, row: T) => TemplateResult | string;
width?: string; width?: string;
@@ -74,7 +74,7 @@ export interface DataTableColumnData extends DataTableSortColumnData {
} }
type ClonedDataTableColumnData = Omit<DataTableColumnData, "title"> & { type ClonedDataTableColumnData = Omit<DataTableColumnData, "title"> & {
title?: string; title?: TemplateResult | string;
}; };
export interface DataTableRowData { export interface DataTableRowData {
@@ -132,7 +132,7 @@ export class HaDataTable extends LitElement {
@query("slot[name='header']") private _header!: HTMLSlotElement; @query("slot[name='header']") private _header!: HTMLSlotElement;
private _items: DataTableRowData[] = []; @internalProperty() private _items: DataTableRowData[] = [];
private _checkableRowsCount?: number; private _checkableRowsCount?: number;
@@ -160,9 +160,9 @@ export class HaDataTable extends LitElement {
public connectedCallback() { public connectedCallback() {
super.connectedCallback(); super.connectedCallback();
if (this._filteredData.length) { if (this._items.length) {
// Force update of location of rows // 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, "auto-height": this.autoHeight,
})}" })}"
role="table" role="table"
aria-rowcount=${this._filteredData.length} aria-rowcount=${this._filteredData.length + 1}
style=${styleMap({ style=${styleMap({
height: this.autoHeight height: this.autoHeight
? `${(this._filteredData.length || 1) * 53 + 57}px` ? `${(this._filteredData.length || 1) * 53 + 57}px`
: `calc(100% - ${this._headerHeight}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 ${this.selectable
? html` ? html`
<div <div
class="mdc-data-table__header-cell mdc-data-table__header-cell--checkbox" class="mdc-data-table__header-cell mdc-data-table__header-cell--checkbox"
role="columnheader" role="columnheader"
scope="col"
> >
<ha-checkbox <ha-checkbox
class="mdc-data-table__row-checkbox" class="mdc-data-table__row-checkbox"
@@ -292,7 +291,13 @@ export class HaDataTable extends LitElement {
}) })
: ""} : ""}
role="columnheader" role="columnheader"
scope="col" aria-sort=${ifDefined(
sorted
? this._sortDirection === "desc"
? "descending"
: "ascending"
: undefined
)}
@click=${this._handleHeaderClick} @click=${this._handleHeaderClick}
.columnId=${key} .columnId=${key}
> >
@@ -338,7 +343,7 @@ export class HaDataTable extends LitElement {
} }
return html` return html`
<div <div
aria-rowindex=${index} aria-rowindex=${index! + 2}
role="row" role="row"
.rowId=${row[this.id]} .rowId=${row[this.id]}
@click=${this._handleRowClick} @click=${this._handleRowClick}
@@ -545,7 +550,9 @@ export class HaDataTable extends LitElement {
private _checkedRowsChanged() { private _checkedRowsChanged() {
// force scroller to update, change it's items // force scroller to update, change it's items
this._filteredData = [...this._filteredData]; if (this._items.length) {
this._items = [...this._items];
}
fireEvent(this, "selection-changed", { fireEvent(this, "selection-changed", {
value: this._checkedRows, value: this._checkedRows,
}); });

View File

@@ -100,7 +100,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
public excludeDomains?: string[]; public excludeDomains?: string[];
/** /**
* Show only deviced with entities of these device classes. * Show only devices with entities of these device classes.
* @type {Array} * @type {Array}
* @attr include-device-classes * @attr include-device-classes
*/ */
@@ -113,7 +113,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
@internalProperty() private _opened?: boolean; @internalProperty() private _opened?: boolean;
@query("ha-combo-box", true) private _comboBox!: HaComboBox; @query("ha-combo-box", true) public comboBox!: HaComboBox;
private _init = false; private _init = false;
@@ -242,11 +242,11 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
); );
public open() { public open() {
this._comboBox?.open(); this.comboBox?.open();
} }
public focus() { public focus() {
this._comboBox?.focus(); this.comboBox?.focus();
} }
public hassSubscribe(): UnsubscribeFunc[] { public hassSubscribe(): UnsubscribeFunc[] {
@@ -269,7 +269,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
(changedProps.has("_opened") && this._opened) (changedProps.has("_opened") && this._opened)
) { ) {
this._init = true; this._init = true;
(this._comboBox as any).items = this._getDevices( (this.comboBox as any).items = this._getDevices(
this.devices!, this.devices!,
this.areas!, this.areas!,
this.entities!, this.entities!,

View File

@@ -371,7 +371,7 @@ class HaChartBase extends mixinBehaviors(
return value; return value;
} }
const date = new Date(values[index].value); const date = new Date(values[index].value);
return formatTime(date, this.hass.language); return formatTime(date, this.hass.locale);
} }
drawChart() { drawChart() {

View File

@@ -99,7 +99,7 @@ export class HaEntityPicker extends LitElement {
@property({ type: Boolean }) private _opened = false; @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() { public open() {
this.updateComplete.then(() => { this.updateComplete.then(() => {
@@ -208,7 +208,7 @@ export class HaEntityPicker extends LitElement {
this.entityFilter, this.entityFilter,
this.includeDeviceClasses this.includeDeviceClasses
); );
(this._comboBox as any).filteredItems = this._states; (this.comboBox as any).filteredItems = this._states;
this._initedStates = true; this._initedStates = true;
} }
} }
@@ -296,7 +296,7 @@ export class HaEntityPicker extends LitElement {
private _filterChanged(ev: CustomEvent): void { private _filterChanged(ev: CustomEvent): void {
const filterString = ev.detail.value.toLowerCase(); const filterString = ev.detail.value.toLowerCase();
(this._comboBox as any).filteredItems = this._states.filter( (this.comboBox as any).filteredItems = this._states.filter(
(state) => (state) =>
state.entity_id.toLowerCase().includes(filterString) || state.entity_id.toLowerCase().includes(filterString) ||
computeStateName(state).toLowerCase().includes(filterString) computeStateName(state).toLowerCase().includes(filterString)

View File

@@ -17,6 +17,7 @@ import { forwardHaptic } from "../../data/haptics";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-icon-button"; import "../ha-icon-button";
import "../ha-switch"; import "../ha-switch";
import "../ha-formfield";
const isOn = (stateObj?: HassEntity) => const isOn = (stateObj?: HassEntity) =>
stateObj !== undefined && stateObj !== undefined &&
@@ -29,6 +30,8 @@ export class HaEntityToggle extends LitElement {
@property() public stateObj?: HassEntity; @property() public stateObj?: HassEntity;
@property() public label?: string;
@internalProperty() private _isOn = false; @internalProperty() private _isOn = false;
protected render(): TemplateResult { 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` return html`
<ha-switch <ha-formfield .label=${this.label}>${switchTemplate}</ha-formfield>
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>
`; `;
} }

View File

@@ -116,12 +116,8 @@ export class HaStateLabelBadge extends LitElement {
: state.state === UNKNOWN : state.state === UNKNOWN
? "-" ? "-"
: state.attributes.unit_of_measurement : state.attributes.unit_of_measurement
? formatNumber(state.state, this.hass!.language) ? formatNumber(state.state, this.hass!.locale)
: computeStateDisplay( : computeStateDisplay(this.hass!.localize, state, this.hass!.locale);
this.hass!.localize,
state,
this.hass!.language
);
} }
} }

View File

@@ -84,7 +84,7 @@ class StateInfo extends LitElement {
} }
const oldHass = changedProps.get("hass") as HomeAssistant | undefined; 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); this.rtl = computeRTL(this.hass);
} }
} }

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

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

View File

@@ -127,7 +127,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
@internalProperty() private _opened?: boolean; @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; private _init = false;
@@ -140,7 +140,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
this._devices = devices; this._devices = devices;
}), }),
subscribeEntityRegistry(this.hass.connection!, (entities) => { 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); deviceEntityLookup[entity.device_id].push(entity);
} }
inputDevices = devices; inputDevices = devices;
inputEntities = entities; inputEntities = entities.filter((entity) => entity.area_id);
} else { } else {
if (deviceFilter) { if (deviceFilter) {
inputDevices = devices; inputDevices = devices;
} }
if (entityFilter) { 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) (changedProps.has("_opened") && this._opened)
) { ) {
this._init = true; this._init = true;
(this._comboBox as any).items = this._getAreas( (this.comboBox as any).items = this._getAreas(
this._areas!, this._areas!,
this._devices!, this._devices!,
this._entities!, this._entities!,

View File

@@ -1,4 +1,3 @@
import "@material/mwc-button";
import "@material/mwc-menu"; import "@material/mwc-menu";
import type { Corner, Menu } from "@material/mwc-menu"; import type { Corner, Menu } from "@material/mwc-menu";
import { import {
@@ -11,8 +10,6 @@ import {
query, query,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import "./ha-icon-button";
@customElement("ha-button-menu") @customElement("ha-button-menu")
export class HaButtonMenu extends LitElement { export class HaButtonMenu extends LitElement {
@property() public corner: Corner = "TOP_START"; @property() public corner: Corner = "TOP_START";

View File

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

View File

@@ -1,6 +1,5 @@
// @ts-ignore // @ts-ignore
import chipStyles from "@material/chips/dist/mdc.chips.min.css"; import chipStyles from "@material/chips/dist/mdc.chips.min.css";
import { ripple } from "@material/mwc-ripple/ripple-directive";
import { import {
css, css,
CSSResult, CSSResult,
@@ -12,6 +11,7 @@ import {
unsafeCSS, unsafeCSS,
} from "lit-element"; } from "lit-element";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import "./ha-chip";
declare global { declare global {
// for fire event // for fire event
@@ -20,8 +20,8 @@ declare global {
} }
} }
@customElement("ha-chips") @customElement("ha-chip-set")
export class HaChips extends LitElement { export class HaChipSet extends LitElement {
@property() public items = []; @property() public items = [];
protected render(): TemplateResult { protected render(): TemplateResult {
@@ -33,18 +33,9 @@ export class HaChips extends LitElement {
${this.items.map( ${this.items.map(
(item, idx) => (item, idx) =>
html` html`
<div class="mdc-chip" .index=${idx} @click=${this._handleClick}> <ha-chip .index=${idx} @click=${this._handleClick}>
<div class="mdc-chip__ripple" .ripple="${ripple()}"></div> ${item}
<span role="gridcell"> </ha-chip>
<span
role="button"
tabindex="0"
class="mdc-chip__primary-action"
>
<span class="mdc-chip__text">${item}</span>
</span>
</span>
</div>
` `
)} )}
</div> </div>
@@ -60,9 +51,9 @@ export class HaChips extends LitElement {
static get styles(): CSSResult { static get styles(): CSSResult {
return css` return css`
${unsafeCSS(chipStyles)} ${unsafeCSS(chipStyles)}
.mdc-chip {
background-color: rgba(var(--rgb-primary-text-color), 0.15); ha-chip {
color: var(--primary-text-color); margin: 4px;
} }
`; `;
} }
@@ -70,6 +61,6 @@ export class HaChips extends LitElement {
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-chips": HaChips; "ha-chip-set": HaChipSet;
} }
} }

74
src/components/ha-chip.ts Normal file
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;
}
}

View File

@@ -53,14 +53,14 @@ class HaClimateState extends LitElement {
if (this.stateObj.attributes.current_temperature != null) { if (this.stateObj.attributes.current_temperature != null) {
return `${formatNumber( return `${formatNumber(
this.stateObj.attributes.current_temperature, this.stateObj.attributes.current_temperature,
this.hass!.language this.hass.locale
)} ${this.hass.config.unit_system.temperature}`; )} ${this.hass.config.unit_system.temperature}`;
} }
if (this.stateObj.attributes.current_humidity != null) { if (this.stateObj.attributes.current_humidity != null) {
return `${formatNumber( return `${formatNumber(
this.stateObj.attributes.current_humidity, this.stateObj.attributes.current_humidity,
this.hass!.language this.hass.locale
)} %`; )} %`;
} }
@@ -78,17 +78,17 @@ class HaClimateState extends LitElement {
) { ) {
return `${formatNumber( return `${formatNumber(
this.stateObj.attributes.target_temp_low, this.stateObj.attributes.target_temp_low,
this.hass!.language this.hass.locale
)}-${formatNumber( )}-${formatNumber(
this.stateObj.attributes.target_temp_high, this.stateObj.attributes.target_temp_high,
this.hass!.language this.hass.locale
)} ${this.hass.config.unit_system.temperature}`; )} ${this.hass.config.unit_system.temperature}`;
} }
if (this.stateObj.attributes.temperature != null) { if (this.stateObj.attributes.temperature != null) {
return `${formatNumber( return `${formatNumber(
this.stateObj.attributes.temperature, this.stateObj.attributes.temperature,
this.hass!.language this.hass.locale
)} ${this.hass.config.unit_system.temperature}`; )} ${this.hass.config.unit_system.temperature}`;
} }
if ( if (
@@ -97,17 +97,17 @@ class HaClimateState extends LitElement {
) { ) {
return `${formatNumber( return `${formatNumber(
this.stateObj.attributes.target_humidity_low, this.stateObj.attributes.target_humidity_low,
this.hass!.language this.hass.locale
)}-${formatNumber( )}-${formatNumber(
this.stateObj.attributes.target_humidity_high, this.stateObj.attributes.target_humidity_high,
this.hass!.language this.hass.locale
)}%`; )} %`;
} }
if (this.stateObj.attributes.humidity != null) { if (this.stateObj.attributes.humidity != null) {
return `${formatNumber( return `${formatNumber(
this.stateObj.attributes.humidity, this.stateObj.attributes.humidity,
this.hass!.language this.hass.locale
)} %`; )} %`;
} }

View File

@@ -47,6 +47,17 @@ export class HaCodeEditor extends UpdatingElement {
return this.codemirror ? this.codemirror.state.doc.toString() : this._value; 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() { public connectedCallback() {
super.connectedCallback(); super.connectedCallback();
if (!this.codemirror) { if (!this.codemirror) {

View File

@@ -86,6 +86,10 @@ export class HaComboBox extends LitElement {
}); });
} }
public get selectedItem() {
return (this._comboBox as any).selectedItem;
}
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<vaadin-combo-box-light <vaadin-combo-box-light
@@ -149,9 +153,9 @@ export class HaComboBox extends LitElement {
fireEvent(this, ev.type, ev.detail); fireEvent(this, ev.type, ev.detail);
} }
private _filterChanged(ev: PolymerChangedEvent<boolean>) { private _filterChanged(ev: PolymerChangedEvent<string>) {
// @ts-ignore // @ts-ignore
fireEvent(this, ev.type, ev.detail); fireEvent(this, ev.type, ev.detail, { composed: false });
} }
private _valueChanged(ev: PolymerChangedEvent<string>) { private _valueChanged(ev: PolymerChangedEvent<string>) {

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 i18n = {
monthNames: [
const documentContainer = document.createElement("template"); "January",
documentContainer.setAttribute("style", "display: none;"); "February",
documentContainer.innerHTML = ` "March",
<dom-module id="ha-date-input-styles" theme-for="vaadin-text-field"> "April",
<template> "May",
<style> "June",
[part="input-field"] { "July",
top: 2px; "August",
height: 30px; "September",
color: var(--primary-text-color); "October",
} "November",
[part="value"] { "December",
text-align: center; ],
} weekdays: [
</style> "Sunday",
</template> "Monday",
</dom-module> "Tuesday",
`; "Wednesday",
document.head.appendChild(documentContainer.content); "Thursday",
"Friday",
export class HaDateInput extends VaadinDatePicker { "Saturday",
constructor() { ],
super(); weekdaysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
firstDayOfWeek: 0,
this.i18n.formatDate = this._formatISODate; week: "Week",
this.i18n.parseDate = this._parseISODate; calendar: "Calendar",
} clear: "Clear",
today: "Today",
ready() { cancel: "Cancel",
super.ready(); formatTitle: (monthName, fullYear) => {
const styleEl = document.createElement("style"); return monthName + " " + fullYear;
styleEl.innerHTML = ` },
:host { formatDate: (d: { day: number; month: number; year: number }) => {
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) {
return [ return [
("0000" + String(d.year)).slice(-4), ("0000" + String(d.year)).slice(-4),
("0" + String(d.month + 1)).slice(-2), ("0" + String(d.month + 1)).slice(-2),
("0" + String(d.day)).slice(-2), ("0" + String(d.day)).slice(-2),
].join("-"); ].join("-");
} },
parseDate: (text: string) => {
private _parseISODate(text) {
const parts = text.split("-"); const parts = text.split("-");
const today = new Date(); const today = new Date();
let date; let date;
@@ -80,11 +79,75 @@ export class HaDateInput extends VaadinDatePicker {
return { day: date, month, year }; return { day: date, month, year };
} }
return undefined; 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 { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-date-input": HaDateInput; "ha-date-input": HaDateInput;

View File

@@ -43,7 +43,7 @@ export class HaDateRangePicker extends LitElement {
protected updated(changedProps: PropertyValues) { protected updated(changedProps: PropertyValues) {
if (changedProps.has("hass")) { if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined; 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._hour24format = this._compute24hourFormat();
this._rtlDirection = computeRTLDirection(this.hass); this._rtlDirection = computeRTLDirection(this.hass);
} }
@@ -62,7 +62,7 @@ export class HaDateRangePicker extends LitElement {
<div slot="input" class="date-range-inputs"> <div slot="input" class="date-range-inputs">
<ha-svg-icon .path=${mdiCalendar}></ha-svg-icon> <ha-svg-icon .path=${mdiCalendar}></ha-svg-icon>
<paper-input <paper-input
.value=${formatDateTime(this.startDate, this.hass.language)} .value=${formatDateTime(this.startDate, this.hass.locale)}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.components.date-range-picker.start_date" "ui.components.date-range-picker.start_date"
)} )}
@@ -71,7 +71,7 @@ export class HaDateRangePicker extends LitElement {
readonly readonly
></paper-input> ></paper-input>
<paper-input <paper-input
.value=${formatDateTime(this.endDate, this.hass.language)} .value=${formatDateTime(this.endDate, this.hass.locale)}
label=${this.hass.localize( label=${this.hass.localize(
"ui.components.date-range-picker.end_date" "ui.components.date-range-picker.end_date"
)} )}

View File

@@ -11,6 +11,7 @@ import { ifDefined } from "lit-html/directives/if-defined";
import { styleMap } from "lit-html/directives/style-map"; import { styleMap } from "lit-html/directives/style-map";
import { formatNumber } from "../common/string/format_number"; import { formatNumber } from "../common/string/format_number";
import { afterNextRender } from "../common/util/render-status"; import { afterNextRender } from "../common/util/render-status";
import { FrontendTranslationData } from "../data/translation";
import { getValueInPercentage, normalize } from "../util/calculate"; import { getValueInPercentage, normalize } from "../util/calculate";
const getAngle = (value: number, min: number, max: number) => { 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: Number }) public value = 0;
@property({ type: String }) public language = ""; @property() public locale!: FrontendTranslationData;
@property() public label = ""; @property() public label = "";
@@ -90,7 +91,7 @@ export class Gauge extends LitElement {
</svg> </svg>
<svg class="text"> <svg class="text">
<text class="value-text"> <text class="value-text">
${formatNumber(this.value, this.language)} ${this.label} ${formatNumber(this.value, this.locale)} ${this.label}
</text> </text>
</svg>`; </svg>`;
} }

View File

@@ -1,3 +1,4 @@
import type HlsType from "hls.js";
import { import {
css, css,
CSSResult, CSSResult,
@@ -15,8 +16,6 @@ import { nextRender } from "../common/util/render-status";
import { getExternalConfig } from "../external_app/external_config"; import { getExternalConfig } from "../external_app/external_config";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
type HLSModule = typeof import("hls.js");
@customElement("ha-hls-player") @customElement("ha-hls-player")
class HaHLSPlayer extends LitElement { class HaHLSPlayer extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -43,7 +42,7 @@ class HaHLSPlayer extends LitElement {
@internalProperty() private _attached = false; @internalProperty() private _attached = false;
private _hlsPolyfillInstance?: Hls; private _hlsPolyfillInstance?: HlsType;
private _useExoPlayer = false; private _useExoPlayer = false;
@@ -107,8 +106,8 @@ class HaHLSPlayer extends LitElement {
const useExoPlayerPromise = this._getUseExoPlayer(); const useExoPlayerPromise = this._getUseExoPlayer();
const masterPlaylistPromise = fetch(this.url); const masterPlaylistPromise = fetch(this.url);
const hls = ((await import("hls.js")) as any).default as HLSModule; const Hls = (await import("hls.js")).default;
let hlsSupported = hls.isSupported(); let hlsSupported = Hls.isSupported();
if (!hlsSupported) { if (!hlsSupported) {
hlsSupported = hlsSupported =
@@ -144,8 +143,8 @@ class HaHLSPlayer extends LitElement {
// If codec is HEVC and ExoPlayer is supported, use ExoPlayer. // If codec is HEVC and ExoPlayer is supported, use ExoPlayer.
if (this._useExoPlayer && match !== null && match[1] !== undefined) { if (this._useExoPlayer && match !== null && match[1] !== undefined) {
this._renderHLSExoPlayer(playlist_url); this._renderHLSExoPlayer(playlist_url);
} else if (hls.isSupported()) { } else if (Hls.isSupported()) {
this._renderHLSPolyfill(videoEl, hls, playlist_url); this._renderHLSPolyfill(videoEl, Hls, playlist_url);
} else { } else {
this._renderHLSNative(videoEl, playlist_url); this._renderHLSNative(videoEl, playlist_url);
} }
@@ -182,7 +181,7 @@ class HaHLSPlayer extends LitElement {
private async _renderHLSPolyfill( private async _renderHLSPolyfill(
videoEl: HTMLVideoElement, videoEl: HTMLVideoElement,
Hls: HLSModule, Hls: typeof HlsType,
url: string url: string
) { ) {
const hls = new Hls({ const hls = new Hls({

View File

@@ -1,12 +1,9 @@
import { customElement, html, LitElement, property } from "lit-element"; import { customElement, html, LitElement, property } from "lit-element";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { TimeSelector } from "../../data/selector"; import { TimeSelector } from "../../data/selector";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../paper-time-input"; import "../paper-time-input";
const test = new Date().toLocaleString();
const useAMPM = test.includes("AM") || test.includes("PM");
@customElement("ha-selector-time") @customElement("ha-selector-time")
export class HaTimeSelector extends LitElement { export class HaTimeSelector extends LitElement {
@property() public hass!: HomeAssistant; @property() public hass!: HomeAssistant;
@@ -19,16 +16,24 @@ export class HaTimeSelector extends LitElement {
@property({ type: Boolean }) public disabled = false; @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() { protected render() {
const useAMPM = this._useAmPm(this.hass.locale.language);
const parts = this.value?.split(":") || []; const parts = this.value?.split(":") || [];
const hours = useAMPM ? parts[0] ?? "12" : parts[0] ?? "0"; const hours = parts[0];
return html` return html`
<paper-time-input <paper-time-input
.label=${this.label} .label=${this.label}
.hour=${useAMPM && Number(hours) > 12 ? Number(hours) - 12 : hours} .hour=${hours &&
.min=${parts[1] ?? "00"} (useAMPM && Number(hours) > 12 ? Number(hours) - 12 : hours)}
.sec=${parts[2] ?? "00"} .min=${parts[1]}
.sec=${parts[2]}
.format=${useAMPM ? 12 : 24} .format=${useAMPM ? 12 : 24}
.amPm=${useAMPM && (Number(hours) > 12 ? "PM" : "AM")} .amPm=${useAMPM && (Number(hours) > 12 ? "PM" : "AM")}
.disabled=${this.disabled} .disabled=${this.disabled}
@@ -42,12 +47,16 @@ export class HaTimeSelector extends LitElement {
private _timeChanged(ev) { private _timeChanged(ev) {
let value = ev.target.value; let value = ev.target.value;
if (useAMPM) { const useAMPM = this._useAmPm(this.hass.locale.language);
let hours = Number(ev.target.hour); let hours = Number(ev.target.hour || 0);
if (value && useAMPM) {
if (ev.target.amPm === "PM") { if (ev.target.amPm === "PM") {
hours += 12; 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", { fireEvent(this, "value-changed", {
value, value,

View File

@@ -1,3 +1,4 @@
import { mdiHelpCircle } from "@mdi/js";
import { HassService, HassServiceTarget } from "home-assistant-js-websocket"; import { HassService, HassServiceTarget } from "home-assistant-js-websocket";
import { import {
css, css,
@@ -18,11 +19,12 @@ import { ENTITY_COMPONENT_DOMAINS } from "../data/entity";
import { Selector } from "../data/selector"; import { Selector } from "../data/selector";
import { PolymerChangedEvent } from "../polymer-types"; import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { documentationUrl } from "../util/documentation-url";
import "./ha-checkbox";
import "./ha-selector/ha-selector"; import "./ha-selector/ha-selector";
import "./ha-service-picker"; import "./ha-service-picker";
import "./ha-settings-row"; import "./ha-settings-row";
import "./ha-yaml-editor"; import "./ha-yaml-editor";
import "./ha-checkbox";
import type { HaYamlEditor } from "./ha-yaml-editor"; import type { HaYamlEditor } from "./ha-yaml-editor";
interface ExtHassService extends Omit<HassService, "fields"> { interface ExtHassService extends Omit<HassService, "fields"> {
@@ -36,6 +38,7 @@ interface ExtHassService extends Omit<HassService, "fields"> {
example?: any; example?: any;
selector?: Selector; selector?: Selector;
}[]; }[];
hasSelector: string[];
} }
@customElement("ha-service-control") @customElement("ha-service-control")
@@ -48,17 +51,17 @@ export class HaServiceControl extends LitElement {
data?: Record<string, any>; data?: Record<string, any>;
}; };
@internalProperty() private _value!: this["value"];
@property({ reflect: true, type: Boolean }) public narrow!: boolean; @property({ reflect: true, type: Boolean }) public narrow!: boolean;
@property({ type: Boolean }) public showAdvanced?: boolean; @property({ type: Boolean }) public showAdvanced?: boolean;
@internalProperty() private _serviceData?: ExtHassService;
@internalProperty() private _checkedKeys = new Set(); @internalProperty() private _checkedKeys = new Set();
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor; @query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
protected updated(changedProperties: PropertyValues) { protected updated(changedProperties: PropertyValues<this>) {
if (!changedProperties.has("value")) { if (!changedProperties.has("value")) {
return; return;
} }
@@ -70,13 +73,11 @@ export class HaServiceControl extends LitElement {
this._checkedKeys = new Set(); this._checkedKeys = new Set();
} }
this._serviceData = this.value?.service const serviceData = this._getServiceInfo(this.value?.service);
? this._getServiceInfo(this.value.service)
: undefined;
if ( if (
this._serviceData && serviceData &&
"target" in this._serviceData && "target" in serviceData &&
(this.value?.data?.entity_id || (this.value?.data?.entity_id ||
this.value?.data?.area_id || this.value?.data?.area_id ||
this.value?.data?.device_id) this.value?.data?.device_id)
@@ -95,21 +96,23 @@ export class HaServiceControl extends LitElement {
target.device_id = this.value.data.device_id; target.device_id = this.value.data.device_id;
} }
this.value = { this._value = {
...this.value, ...this.value,
target, target,
data: { ...this.value.data }, data: { ...this.value.data },
}; };
delete this.value.data!.entity_id; delete this._value.data!.entity_id;
delete this.value.data!.device_id; delete this._value.data!.device_id;
delete this.value.data!.area_id; delete this._value.data!.area_id;
} else {
this._value = this.value;
} }
if (this.value?.data) { if (this._value?.data) {
const yamlEditor = this._yamlEditor; const yamlEditor = this._yamlEditor;
if (yamlEditor && yamlEditor.value !== this.value.data) { if (yamlEditor && yamlEditor.value !== this._value.data) {
yamlEditor.setValue(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; return ENTITY_COMPONENT_DOMAINS.includes(domain) ? [domain] : null;
}); });
private _getServiceInfo = memoizeOne((service: string): private _getServiceInfo = memoizeOne((service?: string):
| ExtHassService | ExtHassService
| undefined => { | undefined => {
if (!service) { if (!service) {
@@ -147,32 +150,60 @@ export class HaServiceControl extends LitElement {
return { return {
...serviceDomains[domain][serviceName], ...serviceDomains[domain][serviceName],
fields, fields,
hasSelector: fields.length
? fields.filter((field) => field.selector).map((field) => field.key)
: [],
}; };
}); });
protected render() { protected render() {
const legacy = const serviceData = this._getServiceInfo(this._value?.service);
this._serviceData?.fields.length &&
!this._serviceData.fields.some((field) => field.selector); const shouldRenderServiceDataYaml =
(serviceData?.fields.length && !serviceData.hasSelector.length) ||
(serviceData &&
Object.keys(this._value?.data || {}).some(
(key) => !serviceData!.hasSelector.includes(key)
));
const entityId = const entityId =
legacy && shouldRenderServiceDataYaml &&
this._serviceData?.fields.find((field) => field.key === "entity_id"); serviceData?.fields.find((field) => field.key === "entity_id");
const hasOptional = Boolean( const hasOptional = Boolean(
!legacy && !shouldRenderServiceDataYaml &&
this._serviceData?.fields.some( serviceData?.fields.some((field) => field.selector && !field.required)
(field) => field.selector && !field.required
)
); );
return html`<ha-service-picker return html`<ha-service-picker
.hass=${this.hass} .hass=${this.hass}
.value=${this.value?.service} .value=${this._value?.service}
@value-changed=${this._serviceChanged} @value-changed=${this._serviceChanged}
></ha-service-picker> ></ha-service-picker>
<p>${this._serviceData?.description}</p> <div class="description">
${this._serviceData && "target" in this._serviceData <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}> ? html`<ha-settings-row .narrow=${this.narrow}>
${hasOptional ${hasOptional
? html`<div slot="prefix" class="checkbox-spacer"></div>` ? html`<div slot="prefix" class="checkbox-spacer"></div>`
@@ -188,38 +219,42 @@ export class HaServiceControl extends LitElement {
)}</span )}</span
><ha-selector ><ha-selector
.hass=${this.hass} .hass=${this.hass}
.selector=${this._serviceData.target .selector=${serviceData.target
? { target: this._serviceData.target } ? { target: serviceData.target }
: { : {
target: { target: {
entity: { domain: computeDomain(this.value!.service) }, entity: { domain: computeDomain(this._value!.service) },
}, },
}} }}
@value-changed=${this._targetChanged} @value-changed=${this._targetChanged}
.value=${this.value?.target} .value=${this._value?.target}
></ha-selector ></ha-selector
></ha-settings-row>` ></ha-settings-row>`
: entityId : entityId
? html`<ha-entity-picker ? html`<ha-entity-picker
.hass=${this.hass} .hass=${this.hass}
.value=${this.value?.data?.entity_id} .value=${this._value?.data?.entity_id}
.label=${entityId.description} .label=${entityId.description}
.includeDomains=${this._domainFilter(this.value!.service)} .includeDomains=${this._domainFilter(this._value!.service)}
@value-changed=${this._entityPicked} @value-changed=${this._entityPicked}
allow-custom-entity allow-custom-entity
></ha-entity-picker>` ></ha-entity-picker>`
: ""} : ""}
${legacy ${shouldRenderServiceDataYaml
? html`<ha-yaml-editor ? html`<ha-yaml-editor
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.components.service-control.service_data" "ui.components.service-control.service_data"
)} )}
.name=${"data"} .name=${"data"}
.defaultValue=${this.value?.data} .defaultValue=${this._value?.data}
@value-changed=${this._dataChanged} @value-changed=${this._dataChanged}
></ha-yaml-editor>` ></ha-yaml-editor>`
: this._serviceData?.fields.map((dataField) => : serviceData?.fields.map((dataField) =>
dataField.selector && (!dataField.advanced || this.showAdvanced) dataField.selector &&
(!dataField.advanced ||
this.showAdvanced ||
(this._value?.data &&
this._value.data[dataField.key] !== undefined))
? html`<ha-settings-row .narrow=${this.narrow}> ? html`<ha-settings-row .narrow=${this.narrow}>
${dataField.required ${dataField.required
? hasOptional ? hasOptional
@@ -228,8 +263,8 @@ export class HaServiceControl extends LitElement {
: html`<ha-checkbox : html`<ha-checkbox
.key=${dataField.key} .key=${dataField.key}
.checked=${this._checkedKeys.has(dataField.key) || .checked=${this._checkedKeys.has(dataField.key) ||
(this.value?.data && (this._value?.data &&
this.value.data[dataField.key] !== undefined)} this._value.data[dataField.key] !== undefined)}
@change=${this._checkboxChanged} @change=${this._checkboxChanged}
slot="prefix" slot="prefix"
></ha-checkbox>`} ></ha-checkbox>`}
@@ -238,15 +273,15 @@ export class HaServiceControl extends LitElement {
><ha-selector ><ha-selector
.disabled=${!dataField.required && .disabled=${!dataField.required &&
!this._checkedKeys.has(dataField.key) && !this._checkedKeys.has(dataField.key) &&
(!this.value?.data || (!this._value?.data ||
this.value.data[dataField.key] === undefined)} this._value.data[dataField.key] === undefined)}
.hass=${this.hass} .hass=${this.hass}
.selector=${dataField.selector} .selector=${dataField.selector}
.key=${dataField.key} .key=${dataField.key}
@value-changed=${this._serviceDataChanged} @value-changed=${this._serviceDataChanged}
.value=${this.value?.data && .value=${this._value?.data &&
this.value.data[dataField.key] !== undefined this._value.data[dataField.key] !== undefined
? this.value.data[dataField.key] ? this._value.data[dataField.key]
: dataField.default} : dataField.default}
></ha-selector ></ha-selector
></ha-settings-row>` ></ha-settings-row>`
@@ -261,13 +296,13 @@ export class HaServiceControl extends LitElement {
this._checkedKeys.add(key); this._checkedKeys.add(key);
} else { } else {
this._checkedKeys.delete(key); this._checkedKeys.delete(key);
const data = { ...this.value?.data }; const data = { ...this._value?.data };
delete data[key]; delete data[key];
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: { value: {
...this.value, ...this._value,
data, data,
}, },
}); });
@@ -277,7 +312,7 @@ export class HaServiceControl extends LitElement {
private _serviceChanged(ev: PolymerChangedEvent<string>) { private _serviceChanged(ev: PolymerChangedEvent<string>) {
ev.stopPropagation(); ev.stopPropagation();
if (ev.detail.value === this.value?.service) { if (ev.detail.value === this._value?.service) {
return; return;
} }
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
@@ -288,17 +323,17 @@ export class HaServiceControl extends LitElement {
private _entityPicked(ev: CustomEvent) { private _entityPicked(ev: CustomEvent) {
ev.stopPropagation(); ev.stopPropagation();
const newValue = ev.detail.value; const newValue = ev.detail.value;
if (this.value?.data?.entity_id === newValue) { if (this._value?.data?.entity_id === newValue) {
return; return;
} }
let value; let value;
if (!newValue && this.value?.data) { if (!newValue && this._value?.data) {
value = { ...this.value }; value = { ...this._value };
delete value.data.entity_id; delete value.data.entity_id;
} else { } else {
value = { value = {
...this.value, ...this._value,
data: { ...this.value?.data, entity_id: ev.detail.value }, data: { ...this._value?.data, entity_id: ev.detail.value },
}; };
} }
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
@@ -309,15 +344,15 @@ export class HaServiceControl extends LitElement {
private _targetChanged(ev: CustomEvent) { private _targetChanged(ev: CustomEvent) {
ev.stopPropagation(); ev.stopPropagation();
const newValue = ev.detail.value; const newValue = ev.detail.value;
if (this.value?.target === newValue) { if (this._value?.target === newValue) {
return; return;
} }
let value; let value;
if (!newValue) { if (!newValue) {
value = { ...this.value }; value = { ...this._value };
delete value.target; delete value.target;
} else { } else {
value = { ...this.value, target: ev.detail.value }; value = { ...this._value, target: ev.detail.value };
} }
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value, value,
@@ -328,11 +363,14 @@ export class HaServiceControl extends LitElement {
ev.stopPropagation(); ev.stopPropagation();
const key = (ev.currentTarget as any).key; const key = (ev.currentTarget as any).key;
const value = ev.detail.value; 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; return;
} }
const data = { ...this.value?.data, [key]: value }; const data = { ...this._value?.data, [key]: value };
if (value === "" || value === undefined) { if (value === "" || value === undefined) {
delete data[key]; delete data[key];
@@ -340,7 +378,7 @@ export class HaServiceControl extends LitElement {
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: { value: {
...this.value, ...this._value,
data, data,
}, },
}); });
@@ -353,7 +391,7 @@ export class HaServiceControl extends LitElement {
} }
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: { value: {
...this.value, ...this._value,
data: ev.detail.value, data: ev.detail.value,
}, },
}); });
@@ -396,6 +434,15 @@ export class HaServiceControl extends LitElement {
ha-checkbox { ha-checkbox {
margin-left: -16px; margin-left: -16px;
} }
.help-icon {
color: var(--secondary-text-color);
}
.description {
justify-content: space-between;
display: flex;
align-items: center;
padding-right: 2px;
}
`; `;
} }
} }

View File

@@ -245,7 +245,7 @@ class HaSidebar extends LitElement {
hass.panelUrl !== oldHass.panelUrl || hass.panelUrl !== oldHass.panelUrl ||
hass.user !== oldHass.user || hass.user !== oldHass.user ||
hass.localize !== oldHass.localize || hass.localize !== oldHass.localize ||
hass.language !== oldHass.language || hass.locale !== oldHass.locale ||
hass.states !== oldHass.states || hass.states !== oldHass.states ||
hass.defaultPanel !== oldHass.defaultPanel hass.defaultPanel !== oldHass.defaultPanel
); );
@@ -281,7 +281,7 @@ class HaSidebar extends LitElement {
} }
const oldHass = changedProps.get("hass") as HomeAssistant | undefined; 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); this.rtl = computeRTL(this.hass);
} }

View File

@@ -125,35 +125,41 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
return html``; return html``;
} }
return html`<div class="mdc-chip-set items"> return html`<div class="mdc-chip-set items">
${ensureArray(this.value?.area_id)?.map((area_id) => { ${this.value?.area_id
const area = this._areas![area_id]; ? ensureArray(this.value.area_id).map((area_id) => {
return this._renderChip( const area = this._areas![area_id];
"area_id", return this._renderChip(
area_id, "area_id",
area?.name || area_id, area_id,
undefined, area?.name || area_id,
mdiSofa undefined,
); mdiSofa
})} );
${ensureArray(this.value?.device_id)?.map((device_id) => { })
const device = this._devices![device_id]; : ""}
return this._renderChip( ${this.value?.device_id
"device_id", ? ensureArray(this.value.device_id).map((device_id) => {
device_id, const device = this._devices![device_id];
device ? computeDeviceName(device, this.hass) : device_id, return this._renderChip(
undefined, "device_id",
mdiDevices device_id,
); device ? computeDeviceName(device, this.hass) : device_id,
})} undefined,
${ensureArray(this.value?.entity_id)?.map((entity_id) => { mdiDevices
const entity = this.hass.states[entity_id]; );
return this._renderChip( })
"entity_id", : ""}
entity_id, ${this.value?.entity_id
entity ? computeStateName(entity) : entity_id, ? ensureArray(this.value.entity_id).map((entity_id) => {
entity ? stateIcon(entity) : undefined const entity = this.hass.states[entity_id];
); return this._renderChip(
})} "entity_id",
entity_id,
entity ? computeStateName(entity) : entity_id,
entity ? stateIcon(entity) : undefined
);
})
: ""}
</div> </div>
${this._renderPicker()} ${this._renderPicker()}
<div class="mdc-chip-set"> <div class="mdc-chip-set">
@@ -344,6 +350,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
.includeDeviceClasses=${this.includeDeviceClasses} .includeDeviceClasses=${this.includeDeviceClasses}
.includeDomains=${this.includeDomains} .includeDomains=${this.includeDomains}
@value-changed=${this._targetPicked} @value-changed=${this._targetPicked}
allow-custom-entity
></ha-entity-picker>`; ></ha-entity-picker>`;
} }
return html``; return html``;

View File

@@ -2,6 +2,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */ /* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import { computeStateDisplay } from "../common/entity/compute_state_display"; import { computeStateDisplay } from "../common/entity/compute_state_display";
import { formatNumber } from "../common/string/format_number";
import LocalizeMixin from "../mixins/localize-mixin"; import LocalizeMixin from "../mixins/localize-mixin";
/* /*
@@ -55,21 +56,31 @@ class HaWaterHeaterState extends LocalizeMixin(PolymerElement) {
computeTarget(hass, stateObj) { computeTarget(hass, stateObj) {
if (!hass || !stateObj) return null; if (!hass || !stateObj) return null;
// We're using "!= null" on purpose so that we match both null and undefined. // We're using "!= null" on purpose so that we match both null and undefined.
if ( if (
stateObj.attributes.target_temp_low != null && stateObj.attributes.target_temp_low != null &&
stateObj.attributes.target_temp_high != 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) { 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 ""; return "";
} }
_localizeState(stateObj) { _localizeState(stateObj) {
return computeStateDisplay(this.hass.localize, stateObj); return computeStateDisplay(this.hass.localize, stateObj, this.hass.locale);
} }
} }
customElements.define("ha-water_heater-state", HaWaterHeaterState); customElements.define("ha-water_heater-state", HaWaterHeaterState);

View File

@@ -107,6 +107,10 @@ export class PaperTimeInput extends PolymerElement {
#millisec { #millisec {
width: 38px; width: 38px;
} }
.no-suffix {
margin-left: -2px;
}
</style> </style>
<label hidden$="[[hideLabel]]">[[label]]</label> <label hidden$="[[hideLabel]]">[[label]]</label>
@@ -129,11 +133,12 @@ export class PaperTimeInput extends PolymerElement {
always-float-label$="[[alwaysFloatInputLabels]]" always-float-label$="[[alwaysFloatInputLabels]]"
disabled="[[disabled]]" disabled="[[disabled]]"
> >
<span suffix="" slot="suffix">:</span> <span suffix slot="suffix">:</span>
</paper-input> </paper-input>
<!-- Min Input --> <!-- Min Input -->
<paper-input <paper-input
class$="[[_computeClassNames(enableSecond)]]"
id="min" id="min"
type="number" type="number"
value="{{min}}" value="{{min}}"
@@ -155,6 +160,7 @@ export class PaperTimeInput extends PolymerElement {
<!-- Sec Input --> <!-- Sec Input -->
<paper-input <paper-input
class$="[[_computeClassNames(enableMillisecond)]]"
id="sec" id="sec"
type="number" type="number"
value="{{sec}}" value="{{sec}}"
@@ -297,28 +303,28 @@ export class PaperTimeInput extends PolymerElement {
notify: true, notify: true,
}, },
/** /**
* Suffix for the hour input * Label for the hour input
*/ */
hourLabel: { hourLabel: {
type: String, type: String,
value: "", value: "",
}, },
/** /**
* Suffix for the min input * Label for the min input
*/ */
minLabel: { minLabel: {
type: String, type: String,
value: ":", value: "",
}, },
/** /**
* Suffix for the sec input * Label for the sec input
*/ */
secLabel: { secLabel: {
type: String, type: String,
value: "", value: "",
}, },
/** /**
* Suffix for the milli sec input * Label for the milli sec input
*/ */
millisecLabel: { millisecLabel: {
type: String, type: String,
@@ -479,6 +485,10 @@ export class PaperTimeInput extends PolymerElement {
_equal(n1, n2) { _equal(n1, n2) {
return n1 === n2; return n1 === n2;
} }
_computeClassNames(hasSuffix) {
return hasSuffix ? " " : "no-suffix";
}
} }
customElements.define("paper-time-input", PaperTimeInput); customElements.define("paper-time-input", PaperTimeInput);

View File

@@ -361,7 +361,7 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
const item = items[0]; const item = items[0];
const date = data.datasets[item.datasetIndex].data[item.index].x; const date = data.datasets[item.datasetIndex].data[item.index].x;
return formatDateTimeWithSeconds(date, this.hass.language); return formatDateTimeWithSeconds(date, this.hass.locale);
}; };
const chartOptions = { const chartOptions = {

View File

@@ -201,8 +201,8 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
const formatTooltipLabel = (item, data) => { const formatTooltipLabel = (item, data) => {
const values = data.datasets[item.datasetIndex].data[item.index]; const values = data.datasets[item.datasetIndex].data[item.index];
const start = formatDateTimeWithSeconds(values[0], this.hass.language); const start = formatDateTimeWithSeconds(values[0], this.hass.locale);
const end = formatDateTimeWithSeconds(values[1], this.hass.language); const end = formatDateTimeWithSeconds(values[1], this.hass.locale);
const state = values[2]; const state = values[2];
return [state, start, end]; return [state, start, end];

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

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

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

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

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

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

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
src/data/analytics.ts Normal file
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