Compare commits

...

264 Commits

Author SHA1 Message Date
Bram Kragten
66432608ed Merge pull request #8774 from home-assistant/dev 2021-03-31 17:27:43 +02:00
Bram Kragten
d8153ac8fc Merge branch 'master' into dev 2021-03-31 17:02:45 +02:00
Bram Kragten
27d9f82f7d Bumped version to 20210331.0 2021-03-31 16:59:29 +02:00
Bram Kragten
5b55bcd879 Use logbook for trace logbook items (#8773) 2021-03-31 16:57:48 +02:00
Bram Kragten
5cfd28881b Update cloud-google-pref.ts 2021-03-31 16:53:18 +02:00
Philip Allgaier
bc54a42e01 Fix typo in analytics info text (#8772) 2021-03-31 15:55:29 +02:00
Bram Kragten
03f9964c59 Fix max being undefined in automation (#8771) 2021-03-31 15:36:09 +02:00
Philip Allgaier
f159219d2c Add Lovelace edit mode URL param (#8574)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-03-31 15:10:55 +02:00
Thomas Lovén
e714f32737 Refactoring automation trace graphs (#8763)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-03-31 15:09:00 +02:00
Philip Allgaier
20858db96d Use service name in action confirmation popup (#8493) 2021-03-31 14:12:06 +02:00
dependabot[bot]
89b82bb778 Bump y18n from 3.2.1 to 3.2.2 (#8765)
Bumps [y18n](https://github.com/yargs/y18n) from 3.2.1 to 3.2.2.
- [Release notes](https://github.com/yargs/y18n/releases)
- [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yargs/y18n/commits)

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

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

* review comments

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

* Update ha-automation-picker.ts

* Update ha-combo-box.ts

* imports

* Rename component

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

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

* Hydrate with latest dev and resolve conflicts

* Replace custom pill element with material chips

* Add category icons. Fix dark mode text colors

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

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

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

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

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

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

* Update src/components/ha-chip.ts

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

* Update src/components/ha-chip.ts

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

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

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-09 19:31:27 +01:00
Paulus Schoutsen
e50fd80b2e Add particles to onboarding (#8567) 2021-03-09 15:30:29 +01:00
Bram Kragten
68ea1abc05 Fallback to yaml for service data if unknown key is in data (#8595) 2021-03-09 15:26:26 +01:00
Bram Kragten
2e76b306c4 Add guard for when default url is not available (#8593) 2021-03-09 11:56:26 +01:00
Bram Kragten
ca3cac4ed3 Fix missing areas in area picker (#8594) 2021-03-09 11:23:31 +01:00
Bram Kragten
41852460e1 Improve code mirror comments check (#8585) 2021-03-09 11:23:02 +01:00
Bram Kragten
9ec4e083d9 Change layout of automation yaml editor (#8560) 2021-03-09 11:21:59 +01:00
GitHub Action
9560a1c4a7 Translation update 2021-03-09 01:24:35 +00:00
Milan Meulemans
4f5a47ace7 Fix typo (#8587) 2021-03-08 13:23:30 +01:00
David F. Mulcahey
01c4d662f2 ZHA UI enhancements (#8573)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-03-08 13:23:14 +01:00
Bram Kragten
9bdda77e89 Fix demo states translation (#8586) 2021-03-08 13:04:58 +01:00
Bram Kragten
194024edb9 Fix demo states translation (#8586) 2021-03-08 13:02:28 +01:00
Ģirts
bef0d3a6a1 Remove margin from button card icon if icon is all that is set (#8584) 2021-03-08 12:47:19 +01:00
Bram Kragten
47a024b795 Update translations 2021-03-08 09:46:13 +01:00
GitHub Action
39847f9c9d Translation update 2021-03-08 01:25:07 +00:00
Bram Kragten
fa7bd28c92 Bumped version to 20210302.6 2021-03-07 23:20:37 +01:00
Philip Allgaier
279f78e4a8 Ensure dev-tools state attribute checkbox state gets stored (#8579) 2021-03-07 23:19:13 +01:00
Bram Kragten
8ec3cbdb33 Fix codemirror cursor color (#8571) 2021-03-07 23:18:55 +01:00
Philip Allgaier
7449f7e73f Handle delay templates properly + error handling tweaks (#8578)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-03-07 23:17:05 +01:00
Bram Kragten
d680fde759 Don't allow UI editor for service calls with templates (#8581) 2021-03-07 23:16:49 +01:00
Philip Allgaier
f24f21ca91 Handle delay templates properly + error handling tweaks (#8578)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-03-07 23:15:53 +01:00
Philip Allgaier
c8ea37eec0 Consistent blank before "%" (#8366) 2021-03-07 23:05:20 +01:00
tkdrob
b71f452795 Fix spelling (#8582) 2021-03-07 22:33:02 +01:00
Bram Kragten
7ea1ece169 Don't allow UI editor for service calls with templates (#8581) 2021-03-07 21:02:44 +01:00
Philip Allgaier
aece3a37c0 Ensure dev-tools state attribute checkbox state gets stored (#8579) 2021-03-07 16:40:54 +01:00
GitHub Action
705871f8dc Translation update 2021-03-07 01:26:43 +00:00
Bram Kragten
4a11975349 Fix codemirror cursor color (#8571) 2021-03-06 23:11:57 +01:00
Philip Allgaier
4d3d27f2c4 Move log item level position + color in detail popup header (#8270) 2021-03-06 18:58:29 +01:00
Paulus Schoutsen
d784a30d42 Allow sharing blueprints (#8565) 2021-03-06 14:19:56 +01:00
Paulus Schoutsen
35f776284b Better place where device edit button is on desktop (#8566) 2021-03-06 14:18:42 +01:00
Ville Skyttä
f659a6fe37 Grammar and spelling fixes (#8568)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-03-06 14:17:30 +01:00
GitHub Action
ad53c99fc4 Translation update 2021-03-06 01:22:48 +00:00
Philip Allgaier
fa0172d00c Fix a few translation typos (#8563) 2021-03-05 22:24:44 +01:00
Bram Kragten
ba77a88714 Bumped version to 20210302.5 2021-03-05 17:27:46 +01:00
Bram Kragten
9b39087102 Fix codemirror active line (#8558)
fixes #8556
2021-03-05 17:27:14 +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
a00961b9ef Bumped version to 20210302.4 2021-03-04 17:03:14 +01:00
Bram Kragten
701c188bab Bump codemirror to 0.18 (#8546) 2021-03-04 17:02:51 +01:00
Bram Kragten
e81002807f Add max height to yaml editor (#8527) 2021-03-04 17:02:33 +01: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
e14d652651 Bumped version to 20210302.3 2021-03-03 13:19:12 +01:00
Bram Kragten
98ae5270ef Bumped version to 20210302.2 2021-03-03 12:50:25 +01:00
Bram Kragten
19ccf0ab40 Bump codemirror (#8524) 2021-03-03 12:50:16 +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
Bram Kragten
6021bec5ee Bumped version to 20210302.1 2021-03-03 10:42:45 +01:00
Bram Kragten
7fcadc85fa Dont show config changes when user saved it (#8520) 2021-03-03 10:42:13 +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
Bram Kragten
5d7f971a82 Merge pull request #8517 from home-assistant/dev 2021-03-02 16:16:22 +01:00
Bram Kragten
d8cdbac15e Bumped version to 20210302.0 2021-03-02 15:49:51 +01:00
Joakim Sørensen
03b8c1348c Use localize in the Supervisor panel (#8515) 2021-03-02 14:46:30 +01:00
Bram Kragten
25a0be7672 Bump codemirror view (#8512)
Fixes search bug in Chrome
2021-03-02 10:05:16 +01:00
GitHub Action
08f1ce2d54 Translation update 2021-03-02 01:09:30 +00:00
Joakim Sørensen
bea20d0495 🌐 Add MVP for translation in the Supervisor panel (#8425)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-03-02 00:37:39 +01:00
Bram Kragten
c42430ccf9 Merge pull request #8509 from home-assistant/dev 2021-03-01 23:33:33 +01:00
Bram Kragten
5ae10e8516 Bumped version to 20210301.0 2021-03-01 23:17:24 +01:00
Philip Allgaier
e3f4a9ce5b Bump MDI icons to 5.9.55 (#8508) 2021-03-01 23:08:04 +01:00
Bram Kragten
cf1fb606fb Bump codemirror + add more styling (#8503) 2021-03-01 22:55:02 +01:00
Bram Kragten
54ec81b67d Fix en-gb translations (#8502)
Renamed lang key in lokalise
2021-03-01 22:48:36 +01:00
Philip Allgaier
f2a9725572 Make spelling more consistent (#8507) 2021-03-01 22:41:52 +01:00
Bram Kragten
4765114e80 Allow decimal slider steps (#8501) 2021-03-01 18:41:36 +01:00
Bram Kragten
5ff757ad65 Handle reconnect while in raw edit (#8500) 2021-03-01 16:55:39 +01:00
David F. Mulcahey
1642c68493 Add view in visualization button to the device page for ZHA devices (#8090) 2021-03-01 15:54:49 +01:00
Philip Allgaier
f31f10cea9 Take cover "opening" and "closing" into account (#8490) 2021-03-01 12:58:12 +01:00
Philip Allgaier
76e0bbb55d Make section row text color themeable (#8488) 2021-03-01 12:56:56 +01:00
Joakim Sørensen
f43af9c0a5 Show config if options or schema (#8487) 2021-03-01 12:55:14 +01:00
Paulus Schoutsen
f7a3d2705c Preserve url params in redirect uri (#8495) 2021-03-01 12:54:39 +01:00
Joakim Sørensen
22c8af0cc5 Fix add-on store search (#8479) 2021-03-01 12:41:55 +01:00
Joakim Sørensen
f263a5221d Adjust header and wording in update dialogs (#8476) 2021-03-01 12:40:52 +01:00
Bram Kragten
3834ab8ede Service dev tools: Add service picker to YAML mode (#8482) 2021-03-01 11:09:15 +01:00
GitHub Action
e2e167630d Translation update 2021-03-01 01:25:12 +00:00
GitHub Action
01dd44300b Translation update 2021-02-28 01:24:14 +00:00
Bram Kragten
b30160d671 Fix device picker (#8481) 2021-02-27 21:20:28 +01:00
GitHub Action
f44d505b41 Translation update 2021-02-27 01:20:54 +00:00
Bram Kragten
16fa6904d9 Merge pull request #8474 from home-assistant/dev 2021-02-26 22:05:19 +01:00
Bram Kragten
b58c17e75e make setup.py quite 2021-02-26 22:03:32 +01:00
Bram Kragten
ae590d42dc Bumped version to 20210226.0 2021-02-26 21:39:43 +01:00
Bram Kragten
d7917160c0 Update translations 2021-02-26 21:39:28 +01:00
Joakim Sørensen
01e4414d17 Ignore error if we are not connected (#8472) 2021-02-26 21:37:06 +01:00
Joakim Sørensen
0bc2eb530d Remove closing event from dialog (#8470) 2021-02-26 18:14:55 +01:00
Bram Kragten
12b124e5a3 Add search, history to codemirror (#8469)
And prevent jump on focus
2021-02-26 18:01:48 +01:00
Joakim Sørensen
478a4b2593 Add snapshot to core update dialogs (#8468) 2021-02-26 15:07:29 +01:00
Joakim Sørensen
9752e30eb4 Add snapshot to add-on update dialog. (#8463) 2021-02-26 14:44:27 +01:00
Joakim Sørensen
af6e87ba31 Fix messaging when addon is not available (#8454) 2021-02-26 14:15:36 +01:00
Bram Kragten
64d390ad0f Fix wrong tag component (#8451)
Will rename the keys in Lokalise after merge
2021-02-26 14:01:16 +01:00
Joakim Sørensen
c94bcb6896 Subscribe to message instead of event (#8443) 2021-02-26 13:47:48 +01:00
Joakim Sørensen
97f9df2f2d Add toggle to show_hide optional fields in add-on config (#8430) 2021-02-26 13:47:08 +01:00
GitHub Action
4e7f68a86c Translation update 2021-02-26 01:21:07 +00:00
Philip Allgaier
2f7f677549 Restore previous codemirror tab behavior (#8461)
* Restore previous tab behavior

* Handle via ondemand logic

* Combine imports
2021-02-25 22:24:07 +01:00
Bram Kragten
12a8a1531d Merge pull request #8459 from home-assistant/dev 2021-02-25 18:54:46 +01:00
Bram Kragten
f44d867d3a Bumped version to 20210225.0 2021-02-25 18:40:34 +01:00
Bram Kragten
6f636187f7 Clean translations (#8458) 2021-02-25 17:18:38 +01:00
Marc Randolph
9414f89e50 Add theme variables for text of picture cards (#8022) 2021-02-25 16:47:33 +01:00
Bram Kragten
60bf1a5451 Fix integrations page (#8457) 2021-02-25 15:59:04 +01:00
Philip Allgaier
32ba8f4731 Make clear that automation run button skips conditions + remove word "execute" from UI (#8259)
* Do not skip conditions when triggering an automation

* Remove usage of word "execute"

* More concise function names
2021-02-25 14:17:31 +01:00
Philip Allgaier
81f96de2bd Fix codemirror caret color (#8452) 2021-02-25 13:49:42 +01:00
Jesse Hills
0c417755ed Fix my redirect for tags (#8450) 2021-02-25 12:03:03 +01:00
GitHub Action
93e5bde797 Translation update 2021-02-25 01:20:40 +00:00
Bram Kragten
c85f69c9ee Merge pull request #8448 from home-assistant/dev
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Philip Allgaier <mail@spacegaier.de>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: GitHub Action <github-action@users.noreply.github.com>
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
Co-authored-by: Álvaro Fernández Rojas <noltari@gmail.com>
Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
Co-authored-by: Kendell R <KTibow@users.noreply.github.com>
Co-authored-by: larena1 <60823161+larena1@users.noreply.github.com>
2021-02-24 20:36:51 +01:00
Bram Kragten
b6eaf0a7c5 Fix setting service data on load when in yaml mode 2021-02-24 20:17:31 +01:00
Bram Kragten
5f1851bade Bumped version to 20210224.0 2021-02-24 20:06:20 +01:00
Bram Kragten
5c66a02711 My redirects tweaks (#8447) 2021-02-24 20:05:40 +01:00
Bram Kragten
bde925a0e3 Migrate to codemirror 6 (#8382) 2021-02-24 19:16:54 +01:00
larena1
0f574a765b Fix excessive rerendering of history charts (#8340)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-02-24 17:48:50 +01:00
Kendell R
782b941531 Save attribute checkbox state (#8010) 2021-02-24 17:44:37 +01:00
Kendell R
f42c0a0717 Add clipboard button (#8411) 2021-02-24 17:36:18 +01:00
Marc Mueller
13ac14d449 Add additional weblink attributes (#8295) 2021-02-24 17:34:44 +01:00
Philip Allgaier
db9cea81db Correctly color script state icon + handle "single" mode for cancel buttons (#8383) 2021-02-24 17:18:38 +01:00
Bram Kragten
7c1fd542da Allow to disable config entry (#8442) 2021-02-24 17:10:59 +01:00
Joakim Sørensen
54a2b2534a Add add-on selector/picker (#8422) 2021-02-24 17:05:42 +01:00
Álvaro Fernández Rojas
f5fb6c1e03 Support binary sensor batteries (#8367) 2021-02-24 17:00:07 +01:00
Bram Kragten
781c0701fc Show correct fields in UI mode (#8445) 2021-02-24 14:27:00 +01:00
GitHub Action
742f1f85dc Translation update 2021-02-24 01:20:56 +00:00
Joakim Sørensen
a648e9be49 Fix atLeastVersion (#8437)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-02-23 22:17:22 +01:00
Joakim Sørensen
fd9441dde2 Fix blank page in ingress when resizing window (#8439) 2021-02-23 16:16:24 +01:00
Philip Allgaier
b5ec59c396 Dev-tools service: Tweak to target description (#8434) 2021-02-23 16:10:15 +01:00
Bram Kragten
60e4594abd Fix area picker with both entity and device filter (#8438) 2021-02-23 15:07:45 +01:00
GitHub Action
79692ef58a Translation update 2021-02-23 01:19:57 +00:00
J. Nick Koston
ace7ee5622 Add support for percentage step size to fans (#8393) 2021-02-22 15:59:59 -06:00
Philip Allgaier
741ac679a0 Ensure we have all mandatory action keys present in action editor (#8424) 2021-02-22 21:10:06 +01:00
Bram Kragten
216526e391 Merge pull request #8433 from home-assistant/dev 2021-02-22 20:18:48 +01:00
Bram Kragten
d76af2cb61 Bumped version to 20210222.0 2021-02-22 20:06:30 +01:00
Bram Kragten
b7d4c40736 Show flows in progress when picking a handler (#8368) 2021-02-22 20:06:18 +01:00
Bram Kragten
6092af8de6 Re-do developer tools service (#8410) 2021-02-22 19:53:52 +01:00
Bram Kragten
627424b8b9 Migrate mfa to Lit (#8276)
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
2021-02-22 19:53:37 +01:00
Joakim Sørensen
e33aff7cf3 Fix mouseevent in blueprint import popup (#8432) 2021-02-22 17:56:43 +01:00
Joakim Sørensen
ef0bfb237a bump webpack-manifest-plugin to 3.0.0 (#8426) 2021-02-22 10:30:02 +01:00
Joakim Sørensen
c042c5568b Fix WS command for validating ingress session (#8427) 2021-02-22 10:28:18 +01:00
GitHub Action
d84a7ee358 Translation update 2021-02-22 01:20:19 +00:00
Bram Kragten
311e1cfb00 Merge pull request #8354 from home-assistant/dev 2021-02-08 15:28:40 +01:00
393 changed files with 36205 additions and 24323 deletions

View File

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

View File

@@ -85,6 +85,11 @@ gulp.task("copy-translations-app", async () => {
copyTranslations(staticDir);
});
gulp.task("copy-translations-supervisor", async () => {
const staticDir = paths.hassio_output_static;
copyTranslations(staticDir);
});
gulp.task("copy-static-app", async () => {
const staticDir = paths.app_output_static;
// Basic static files

View File

@@ -10,6 +10,8 @@ require("./gen-icons-json.js");
require("./webpack.js");
require("./compress.js");
require("./rollup.js");
require("./gather-static.js");
require("./translations.js");
gulp.task(
"develop-hassio",
@@ -20,6 +22,8 @@ gulp.task(
"clean-hassio",
"gen-icons-json",
"gen-index-hassio-dev",
"build-supervisor-translations",
"copy-translations-supervisor",
env.useRollup() ? "rollup-watch-hassio" : "webpack-watch-hassio"
)
);
@@ -32,6 +36,8 @@ gulp.task(
},
"clean-hassio",
"gen-icons-json",
"build-supervisor-translations",
"copy-translations-supervisor",
env.useRollup() ? "rollup-prod-hassio" : "webpack-prod-hassio",
"gen-index-hassio-prod",
...// Don't compress running tests

View File

@@ -266,6 +266,7 @@ gulp.task(taskName, function () {
TRANSLATION_FRAGMENTS.forEach((fragment) => {
delete data.ui.panel[fragment];
});
delete data.supervisor;
return data;
})
)
@@ -342,6 +343,62 @@ gulp.task(
}
);
gulp.task("build-translation-fragment-supervisor", function () {
return gulp
.src(fullDir + "/*.json")
.pipe(transform((data) => data.supervisor))
.pipe(gulp.dest(workDir + "/supervisor"));
});
gulp.task("build-translation-flatten-supervisor", function () {
return gulp
.src(workDir + "/supervisor/*.json")
.pipe(
transform(function (data) {
// Polymer.AppLocalizeBehavior requires flattened json
return flatten(data);
})
)
.pipe(gulp.dest(outDir));
});
gulp.task("build-translation-write-metadata", function writeMetadata() {
return gulp
.src(
[
path.join(paths.translations_src, "translationMetadata.json"),
workDir + "/testMetadata.json",
workDir + "/translationFingerprints.json",
],
{ allowEmpty: true }
)
.pipe(merge({}))
.pipe(
transform(function (data) {
const newData = {};
Object.entries(data).forEach(([key, value]) => {
// Filter out translations without native name.
if (value.nativeName) {
newData[key] = value;
} else {
console.warn(
`Skipping language ${key}. Native name was not translated.`
);
}
});
return newData;
})
)
.pipe(
transform((data) => ({
fragments: TRANSLATION_FRAGMENTS,
translations: data,
}))
)
.pipe(rename("translationMetadata.json"))
.pipe(gulp.dest(workDir));
});
gulp.task(
"build-translations",
gulp.series(
@@ -353,42 +410,20 @@ gulp.task(
gulp.parallel(...splitTasks),
"build-flattened-translations",
"build-translation-fingerprints",
function writeMetadata() {
return gulp
.src(
[
path.join(paths.translations_src, "translationMetadata.json"),
workDir + "/testMetadata.json",
workDir + "/translationFingerprints.json",
],
{ allowEmpty: true }
)
.pipe(merge({}))
.pipe(
transform(function (data) {
const newData = {};
Object.entries(data).forEach(([key, value]) => {
// Filter out translations without native name.
if (data[key].nativeName) {
newData[key] = data[key];
} else {
console.warn(
`Skipping language ${key}. Native name was not translated.`
);
}
if (data[key]) newData[key] = value;
});
return newData;
})
)
.pipe(
transform((data) => ({
fragments: TRANSLATION_FRAGMENTS,
translations: data,
}))
)
.pipe(rename("translationMetadata.json"))
.pipe(gulp.dest(workDir));
}
"build-translation-write-metadata"
)
);
gulp.task(
"build-supervisor-translations",
gulp.series(
"clean-translations",
"ensure-translations-build-dir",
"build-master-translation",
"build-merged-translations",
"build-translation-fragment-supervisor",
"build-translation-flatten-supervisor",
"build-translation-fingerprints",
"build-translation-write-metadata"
)
);

View File

@@ -137,7 +137,12 @@ gulp.task("webpack-watch-hassio", () => {
isProdBuild: false,
latestBuild: true,
})
).watch({}, doneHandler());
).watch({ ignored: /build-translations/ }, doneHandler());
gulp.watch(
path.join(paths.translations_src, "en.json"),
gulp.series("build-supervisor-translations", "copy-translations-supervisor")
);
});
gulp.task("webpack-prod-hassio", () =>

View File

@@ -34,6 +34,7 @@ module.exports = {
hassio_dir: path.resolve(__dirname, "../hassio"),
hassio_output_root: path.resolve(__dirname, "../hassio/build"),
hassio_output_static: path.resolve(__dirname, "../hassio/build/static"),
hassio_output_latest: path.resolve(
__dirname,
"../hassio/build/frontend_latest"

View File

@@ -1,7 +1,7 @@
const webpack = require("webpack");
const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");
const ManifestPlugin = require("webpack-manifest-plugin");
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
const paths = require("./paths.js");
const bundle = require("./bundle");
const log = require("fancy-log");
@@ -68,7 +68,7 @@ const createWebpackConfig = ({
],
},
plugins: [
new ManifestPlugin({
new WebpackManifestPlugin({
// Only include the JS of entrypoints
filter: (file) => file.isInitial && !file.name.endsWith(".map"),
}),

View File

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

View File

@@ -39,7 +39,7 @@ class HcLovelace extends LitElement {
urlPath: this.urlPath!,
enableFullEditMode: () => undefined,
mode: "storage",
language: "en",
locale: this.hass.locale,
saveConfig: async () => undefined,
deleteConfig: async () => undefined,
setEditMode: () => undefined,
@@ -94,6 +94,7 @@ class HcLovelace extends LitElement {
return css`
:host {
min-height: 100vh;
height: 0;
display: flex;
flex-direction: column;
box-sizing: border-box;

View File

@@ -0,0 +1,382 @@
import { DemoTrace } from "./types";
export const basicTrace: DemoTrace = {
trace: {
last_action: "action/2",
last_condition: "condition/0",
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: {
"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,
},
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",
},
},
},
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,247 @@
import { DemoTrace } from "./types";
export const motionLightTrace: DemoTrace = {
trace: {
last_action: "action/3",
last_condition: null,
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: {
"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,
},
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",
},
},
},
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,64 @@
import {
customElement,
html,
css,
LitElement,
TemplateResult,
property,
} from "lit-element";
import "../../../src/components/ha-card";
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;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
${traces.map(
(trace) => html`
<ha-card .heading=${trace.trace.config.alias}>
<div class="card-content">
<hat-trace-timeline
.hass=${this.hass}
.trace=${trace.trace}
.logbookEntries=${trace.logbookEntries}
></hat-trace-timeline>
</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;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-automation-trace": DemoAutomationTrace;
}
}

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>
</ha-card>
<ha-card header="More Info Demos">
<div class="card-content intro">
<p>
More info screens show up when an entity is clicked.
</p>
</div>
<template is="dom-repeat" items="[[_moreInfoDemos]]">
<a href="#[[item]]">
<paper-item>
<paper-item-body>{{ item }}</paper-item-body>
<ha-icon icon="hass:chevron-right"></ha-icon>
</paper-item>
</a>
</template>
</ha-card>
<ha-card header="Util Demos">
<div class="card-content intro">
<p>
Test pages for our utility functions.
</p>
</div>
<template is="dom-repeat" items="[[_utilDemos]]">
<ha-card header="Other Demos">
<div class="card-content intro"></div>
<template is="dom-repeat" items="[[_restDemos]]">
<a href="#[[item]]">
<paper-item>
<paper-item-body>{{ item }}</paper-item-body>
@@ -178,13 +158,9 @@ class HaGallery extends PolymerElement {
type: Array,
computed: "_computeLovelace(_demos)",
},
_moreInfoDemos: {
_restDemos: {
type: Array,
computed: "_computeMoreInfos(_demos)",
},
_utilDemos: {
type: Array,
computed: "_computeUtil(_demos)",
computed: "_computeRest(_demos)",
},
};
}
@@ -237,12 +213,8 @@ class HaGallery extends PolymerElement {
return demos.filter((demo) => demo.includes("hui"));
}
_computeMoreInfos(demos) {
return demos.filter((demo) => demo.includes("more-info"));
}
_computeUtil(demos) {
return demos.filter((demo) => demo.includes("util"));
_computeRest(demos) {
return demos.filter((demo) => !demo.includes("hui"));
}
}

View File

@@ -15,6 +15,7 @@ import {
HassioAddonInfo,
HassioAddonRepository,
} from "../../../src/data/hassio/addon";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { HomeAssistant } from "../../../src/types";
import "../components/hassio-card-content";
import { filterAndSort } from "../components/hassio-filter-addons";
@@ -23,6 +24,8 @@ import { hassioStyle } from "../resources/hassio-style";
class HassioAddonRepositoryEl extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public repo!: HassioAddonRepository;
@property({ attribute: false }) public addons!: HassioAddonInfo[];
@@ -54,7 +57,11 @@ class HassioAddonRepositoryEl extends LitElement {
return html`
<div class="content">
<p class="description">
No results found in "${repo.name}."
${this.supervisor.localize(
"store.no_results_found",
"repository",
repo.name
)}
</p>
</div>
`;
@@ -83,11 +90,13 @@ class HassioAddonRepositoryEl extends LitElement {
: mdiPuzzle}
.iconTitle=${addon.installed
? addon.update_available
? "New version available"
: "Add-on is installed"
? this.supervisor.localize(
"common.new_version_available"
)
: this.supervisor.localize("addon.installed")
: addon.available
? "Add-on is not installed"
: "Add-on is not available on your system"}
? this.supervisor.localize("addon.not_installed")
: this.supervisor.localize("addon.not_available")}
.iconClass=${addon.installed
? addon.update_available
? "update"

View File

@@ -14,7 +14,9 @@ import { html, TemplateResult } from "lit-html";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import { navigate } from "../../../src/common/navigate";
import "../../../src/common/search/search-input";
import { extractSearchParam } from "../../../src/common/url/search-params";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-svg-icon";
import {
@@ -69,20 +71,24 @@ class HassioAddonStore extends LitElement {
if (this.supervisor.addon.repositories) {
repos = this.addonRepositories(
this.supervisor.addon.repositories,
this.supervisor.addon.addons
this.supervisor.addon.addons,
this._filter
);
}
return html`
<hass-tabs-subpage
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow}
.route=${this.route}
hassio
main-page
.tabs=${supervisorTabs}
main-page
supervisor
>
<span slot="header">Add-on Store</span>
<span slot="header">
${this.supervisor.localize("panel.store")}
</span>
<ha-button-menu
corner="BOTTOM_START"
slot="toolbar-icon"
@@ -92,15 +98,15 @@ class HassioAddonStore extends LitElement {
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item>
Repositories
${this.supervisor.localize("store.repositories")}
</mwc-list-item>
<mwc-list-item>
Reload
${this.supervisor.localize("common.reload")}
</mwc-list-item>
${this.hass.userData?.showAdvanced &&
atLeastVersion(this.hass.config.version, 0, 117)
? html`<mwc-list-item>
Registries
${this.supervisor.localize("store.registries")}
</mwc-list-item>`
: ""}
</ha-button-menu>
@@ -121,11 +127,9 @@ class HassioAddonStore extends LitElement {
${!this.hass.userData?.showAdvanced
? html`
<div class="advanced">
Missing add-ons? Enable advanced mode on
<a href="/profile" target="_top">
your profile page
${this.supervisor.localize("store.missing_addons")}
</a>
.
</div>
`
: ""}
@@ -135,12 +139,22 @@ class HassioAddonStore extends LitElement {
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
const repositoryUrl = extractSearchParam("repository_url");
navigate(this, "/hassio/store", true);
if (repositoryUrl) {
this._manageRepositories(repositoryUrl);
}
this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev));
this._loadData();
}
private addonRepositories = memoizeOne(
(repositories: HassioAddonRepository[], addons: HassioAddonInfo[]) => {
(
repositories: HassioAddonRepository[],
addons: HassioAddonInfo[],
filter?: string
) => {
return repositories.sort(sortRepos).map((repo) => {
const filteredAddons = addons.filter(
(addon) => addon.repository === repo.slug
@@ -152,7 +166,8 @@ class HassioAddonStore extends LitElement {
.hass=${this.hass}
.repo=${repo}
.addons=${filteredAddons}
.filter=${this._filter!}
.filter=${filter!}
.supervisor=${this.supervisor}
></hassio-addon-repository>
`
: html``;
@@ -163,7 +178,7 @@ class HassioAddonStore extends LitElement {
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._manageRepositories();
this._manageRepositoriesClicked();
break;
case 1:
this.refreshData();
@@ -180,20 +195,26 @@ class HassioAddonStore extends LitElement {
}
}
private async _manageRepositories() {
private _manageRepositoriesClicked() {
this._manageRepositories();
}
private async _manageRepositories(url?: string) {
showRepositoriesDialog(this, {
repos: this.supervisor.addon.repositories,
loadData: () => this._loadData(),
supervisor: this.supervisor,
url,
});
}
private async _manageRegistries() {
showRegistriesDialog(this);
showRegistriesDialog(this, { supervisor: this.supervisor });
}
private async _loadData() {
fireEvent(this, "supervisor-store-refresh", { store: "addon" });
fireEvent(this, "supervisor-store-refresh", { store: "supervisor" });
fireEvent(this, "supervisor-collection-refresh", { collection: "addon" });
fireEvent(this, "supervisor-collection-refresh", {
collection: "supervisor",
});
}
private async _filterChanged(e) {

View File

@@ -25,6 +25,7 @@ import {
fetchHassioHardwareAudio,
HassioHardwareAudioDevice,
} from "../../../../src/data/hassio/hardware";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
@@ -34,6 +35,8 @@ import { hassioStyle } from "../../resources/hassio-style";
class HassioAddonAudio extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon!: HassioAddonDetails;
@internalProperty() private _error?: string;
@@ -48,12 +51,16 @@ class HassioAddonAudio extends LitElement {
protected render(): TemplateResult {
return html`
<ha-card header="Audio">
<ha-card
.header=${this.supervisor.localize("addon.configuration.audio.header")}
>
<div class="card-content">
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
<paper-dropdown-menu
label="Input"
.label=${this.supervisor.localize(
"addon.configuration.audio.input"
)}
@iron-select=${this._setInputDevice}
>
<paper-listbox
@@ -64,15 +71,17 @@ class HassioAddonAudio extends LitElement {
${this._inputDevices &&
this._inputDevices.map((item) => {
return html`
<paper-item device=${item.device || ""}
>${item.name}</paper-item
>
<paper-item device=${item.device || ""}>
${item.name}
</paper-item>
`;
})}
</paper-listbox>
</paper-dropdown-menu>
<paper-dropdown-menu
label="Output"
.label=${this.supervisor.localize(
"addon.configuration.audio.output"
)}
@iron-select=${this._setOutputDevice}
>
<paper-listbox
@@ -93,7 +102,7 @@ class HassioAddonAudio extends LitElement {
</div>
<div class="card-actions">
<ha-progress-button @click=${this._saveSettings}>
Save
${this.supervisor.localize("common.save")}
</ha-progress-button>
</div>
</ha-card>
@@ -152,7 +161,7 @@ class HassioAddonAudio extends LitElement {
const noDevice: HassioHardwareAudioDevice = {
device: "default",
name: "Default",
name: this.supervisor.localize("addon.configuration.audio.default"),
};
try {
@@ -189,7 +198,7 @@ class HassioAddonAudio extends LitElement {
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
}
} catch {
this._error = "Failed to set addon audio device";

View File

@@ -9,6 +9,7 @@ import {
} from "lit-element";
import "../../../../src/components/ha-circular-progress";
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
@@ -20,26 +21,28 @@ import "./hassio-addon-network";
class HassioAddonConfigDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon?: HassioAddonDetails;
protected render(): TemplateResult {
if (!this.addon) {
return html`<ha-circular-progress active></ha-circular-progress>`;
}
const hasOptions =
this.addon.options && Object.keys(this.addon.options).length;
const hasSchema =
hasOptions && this.addon.schema && Object.keys(this.addon.schema).length;
const hasConfiguration =
(this.addon.options && Object.keys(this.addon.options).length) ||
(this.addon.schema && Object.keys(this.addon.schema).length);
return html`
<div class="content">
${hasOptions || hasSchema || this.addon.network || this.addon.audio
${hasConfiguration || this.addon.network || this.addon.audio
? html`
${hasOptions || hasSchema
${hasConfiguration
? html`
<hassio-addon-config
.hass=${this.hass}
.addon=${this.addon}
.supervisor=${this.supervisor}
></hassio-addon-config>
`
: ""}
@@ -48,6 +51,7 @@ class HassioAddonConfigDashboard extends LitElement {
<hassio-addon-network
.hass=${this.hass}
.addon=${this.addon}
.supervisor=${this.supervisor}
></hassio-addon-network>
`
: ""}
@@ -56,11 +60,12 @@ class HassioAddonConfigDashboard extends LitElement {
<hassio-addon-audio
.hass=${this.hass}
.addon=${this.addon}
.supervisor=${this.supervisor}
></hassio-addon-audio>
`
: ""}
`
: "This add-on does not expose configuration for you to mess with.... 👋"}
: this.supervisor.localize("addon.configuration.no_configuration")}
</div>
`;
}

View File

@@ -15,11 +15,15 @@ import {
query,
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-button-menu";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../src/components/ha-form/ha-form";
import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-switch";
import "../../../../src/components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../src/components/ha-yaml-editor";
import {
@@ -28,6 +32,7 @@ import {
setHassioAddonOption,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
@@ -42,12 +47,16 @@ class HassioAddonConfig extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean }) private _configHasChanged = false;
@property({ type: Boolean }) private _valid = true;
@internalProperty() private _canShowSchema = false;
@internalProperty() private _showOptional = false;
@internalProperty() private _error?: string;
@internalProperty() private _options?: Record<string, unknown>;
@@ -56,33 +65,70 @@ class HassioAddonConfig extends LitElement {
@query("ha-yaml-editor") private _editor?: HaYamlEditor;
public computeLabel = (entry: HaFormSchema): string => {
return (
this.addon.translations[this.hass.language]?.configuration?.[entry.name]
?.name ||
this.addon.translations.en?.configuration?.[entry.name].name ||
entry.name
);
};
private _filteredShchema = memoizeOne(
(options: Record<string, unknown>, schema: HaFormSchema[]) => {
return schema.filter((entry) => entry.name in options || entry.required);
}
);
protected render(): TemplateResult {
const showForm =
!this._yamlMode && this._canShowSchema && this.addon.schema;
const hasHiddenOptions =
showForm &&
JSON.stringify(this.addon.schema) !==
JSON.stringify(
this._filteredShchema(this.addon.options, this.addon.schema!)
);
return html`
<h1>${this.addon.name}</h1>
<ha-card>
<div class="header">
<h2>Configuration</h2>
<h2>
${this.supervisor.localize("addon.configuration.options.header")}
</h2>
<div class="card-menu">
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
<mwc-icon-button slot="trigger">
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item .disabled=${!this._canShowSchema}>
${this._yamlMode ? "Edit in UI" : "Edit in YAML"}
${this._yamlMode
? this.supervisor.localize(
"addon.configuration.options.edit_in_ui"
)
: this.supervisor.localize(
"addon.configuration.options.edit_in_yaml"
)}
</mwc-list-item>
<mwc-list-item class="warning">
Reset to defaults
${this.supervisor.localize("common.reset_defaults")}
</mwc-list-item>
</ha-button-menu>
</div>
</div>
<div class="card-content">
${!this._yamlMode && this._canShowSchema && this.addon.schema
${showForm
? html`<ha-form
.data=${this._options!}
@value-changed=${this._configChanged}
.schema=${this.addon.schema}
.computeLabel=${this.computeLabel}
.schema=${this._showOptional
? this.addon.schema!
: this._filteredShchema(
this.addon.options,
this.addon.schema!
)}
></ha-form>`
: html` <ha-yaml-editor
@value-changed=${this._configChanged}
@@ -92,14 +138,34 @@ class HassioAddonConfig extends LitElement {
(this._canShowSchema && this.addon.schema) ||
this._valid
? ""
: html` <div class="errors">Invalid YAML</div> `}
: html`
<div class="errors">
${this.supervisor.localize(
"addon.configuration.options.invalid_yaml"
)}
</div>
`}
</div>
${hasHiddenOptions
? html`<ha-formfield
class="show-additional"
.label=${this.supervisor.localize(
"addon.configuration.options.show_unused_optional"
)}
>
<ha-switch
@change=${this._toggleOptional}
.checked=${this._showOptional}
>
</ha-switch>
</ha-formfield>`
: ""}
<div class="card-actions right">
<ha-progress-button
@click=${this._saveTapped}
.disabled=${!this._configHasChanged || !this._valid}
>
Save
${this.supervisor.localize("common.save")}
</ha-progress-button>
</div>
</ha-card>
@@ -108,12 +174,10 @@ class HassioAddonConfig extends LitElement {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._canShowSchema =
Object.keys(this.addon.options).length !== 0 &&
!this.addon.schema!.find(
// @ts-ignore
(entry) => !SUPPORTED_UI_TYPES.includes(entry.type) || entry.multiple
);
this._canShowSchema = !this.addon.schema!.find(
// @ts-ignore
(entry) => !SUPPORTED_UI_TYPES.includes(entry.type) || entry.multiple
);
this._yamlMode = !this._canShowSchema;
}
@@ -146,6 +210,10 @@ class HassioAddonConfig extends LitElement {
}
}
private _toggleOptional() {
this._showOptional = !this._showOptional;
}
private _configChanged(ev): void {
if (this.addon.schema && this._canShowSchema && !this._yamlMode) {
this._valid = true;
@@ -162,10 +230,10 @@ class HassioAddonConfig extends LitElement {
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.addon.name,
text: "Are you sure you want to reset all your options?",
confirmText: "reset options",
dismissText: "no",
title: this.supervisor.localize("confirm.reset_options.title"),
text: this.supervisor.localize("confirm.reset_options.text"),
confirmText: this.supervisor.localize("common.reset_options"),
dismissText: this.supervisor.localize("common.cancel"),
});
if (!confirmed) {
@@ -187,9 +255,11 @@ class HassioAddonConfig extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to reset addon configuration, ${extractApiErrorMessage(
err
)}`;
this._error = this.supervisor.localize(
"addon.common.update_available",
"error",
extractApiErrorMessage(err)
);
}
button.progress = false;
}
@@ -213,12 +283,14 @@ class HassioAddonConfig extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
}
} catch (err) {
this._error = `Failed to save addon configuration, ${extractApiErrorMessage(
err
)}`;
this._error = this.supervisor.localize(
"addon.configuration.options.failed_to_save",
"error",
extractApiErrorMessage(err)
);
}
button.progress = false;
}
@@ -275,6 +347,10 @@ class HassioAddonConfig extends LitElement {
.card-actions.right {
justify-content: flex-end;
}
.show-additional {
padding: 16px;
}
`,
];
}

View File

@@ -19,6 +19,7 @@ import {
setHassioAddonOption,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
@@ -38,6 +39,8 @@ interface NetworkItemInput extends PaperInputElement {
class HassioAddonNetwork extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon!: HassioAddonDetails;
@internalProperty() private _error?: string;
@@ -55,16 +58,30 @@ class HassioAddonNetwork extends LitElement {
}
return html`
<ha-card header="Network">
<ha-card
.header=${this.supervisor.localize(
"addon.configuration.network.header"
)}
>
<div class="card-content">
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
<table>
<tbody>
<tr>
<th>Container</th>
<th>Host</th>
<th>Description</th>
<th>
${this.supervisor.localize(
"addon.configuration.network.container"
)}
</th>
<th>
${this.supervisor.localize(
"addon.configuration.network.host"
)}
</th>
<th>
${this.supervisor.localize("common.description")}
</th>
</tr>
${this._config!.map((item) => {
return html`
@@ -73,13 +90,15 @@ class HassioAddonNetwork extends LitElement {
<td>
<paper-input
@value-changed=${this._configChanged}
placeholder="disabled"
placeholder="${this.supervisor.localize(
"addon.configuration.network.disabled"
)}"
.value=${item.host ? String(item.host) : ""}
.container=${item.container}
no-label-float
></paper-input>
</td>
<td>${item.description}</td>
<td>${this._computeDescription(item)}</td>
</tr>
`;
})}
@@ -88,10 +107,10 @@ class HassioAddonNetwork extends LitElement {
</div>
<div class="card-actions">
<ha-progress-button class="warning" @click=${this._resetTapped}>
Reset to defaults
${this.supervisor.localize("common.reset_defaults")}
</ha-progress-button>
<ha-progress-button @click=${this._saveTapped}>
Save
${this.supervisor.localize("common.save")}
</ha-progress-button>
</div>
</ha-card>
@@ -105,6 +124,15 @@ class HassioAddonNetwork extends LitElement {
}
}
private _computeDescription = (item: NetworkItem): string => {
return (
this.addon.translations[this.hass.language]?.network?.[item.container]
?.description ||
this.addon.translations.en?.network?.[item.container]?.description ||
item.description
);
};
private _setNetworkConfig(): void {
const network = this.addon.network || {};
const description = this.addon.network_description || {};
@@ -147,12 +175,14 @@ class HassioAddonNetwork extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
}
} catch (err) {
this._error = `Failed to set addon network configuration, ${extractApiErrorMessage(
err
)}`;
this._error = this.supervisor.localize(
"addon.failed_to_reset",
"error",
extractApiErrorMessage(err)
);
}
button.progress = false;
@@ -181,12 +211,14 @@ class HassioAddonNetwork extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
}
} catch (err) {
this._error = `Failed to set addon network configuration, ${extractApiErrorMessage(
err
)}`;
this._error = this.supervisor.localize(
"addon.failed_to_save",
"error",
extractApiErrorMessage(err)
);
}
button.progress = false;
}

View File

@@ -1,3 +1,4 @@
import "../../../../src/components/ha-card";
import {
css,
CSSResult,
@@ -19,11 +20,14 @@ import "../../../../src/layouts/hass-loading-screen";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
@customElement("hassio-addon-documentation-tab")
class HassioAddonDocumentationDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon?: HassioAddonDetails;
@internalProperty() private _error?: string;
@@ -81,9 +85,11 @@ class HassioAddonDocumentationDashboard extends LitElement {
this.addon!.slug
);
} catch (err) {
this._error = `Failed to get addon documentation, ${extractApiErrorMessage(
err
)}`;
this._error = this.supervisor.localize(
"addon.documentation.get_logs",
"error",
extractApiErrorMessage(err)
);
}
}
}

View File

@@ -21,6 +21,7 @@ import { extractSearchParam } from "../../../src/common/url/search-params";
import "../../../src/components/ha-circular-progress";
import {
fetchHassioAddonInfo,
fetchHassioAddonsInfo,
HassioAddonDetails,
} from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
@@ -79,7 +80,7 @@ class HassioAddonDashboard extends LitElement {
const addonTabs: PageNavigation[] = [
{
name: "Info",
translationKey: "addon.panel.info",
path: `/hassio/addon/${this.addon.slug}/info`,
iconPath: mdiInformationVariant,
},
@@ -87,7 +88,7 @@ class HassioAddonDashboard extends LitElement {
if (this.addon.documentation) {
addonTabs.push({
name: "Documentation",
translationKey: "addon.panel.documentation",
path: `/hassio/addon/${this.addon.slug}/documentation`,
iconPath: mdiFileDocument,
});
@@ -96,12 +97,12 @@ class HassioAddonDashboard extends LitElement {
if (this.addon.version) {
addonTabs.push(
{
name: "Configuration",
translationKey: "addon.panel.configuration",
path: `/hassio/addon/${this.addon.slug}/config`,
iconPath: mdiCogs,
},
{
name: "Log",
translationKey: "addon.panel.log",
path: `/hassio/addon/${this.addon.slug}/logs`,
iconPath: mdiMathLog,
}
@@ -113,11 +114,12 @@ class HassioAddonDashboard extends LitElement {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow}
.backPath=${this.addon.version ? "/hassio/dashboard" : "/hassio/store"}
.route=${route}
hassio
.tabs=${addonTabs}
supervisor
>
<span slot="header">${this.addon.name}</span>
<hassio-addon-router
@@ -172,9 +174,16 @@ class HassioAddonDashboard extends LitElement {
protected async firstUpdated(): Promise<void> {
if (this.route.path === "") {
const addon = extractSearchParam("addon");
if (addon) {
navigate(this, `/hassio/addon/${addon}`, true);
const requestedAddon = extractSearchParam("addon");
if (requestedAddon) {
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
const validAddon = addonsInfo.addons
.some((addon) => addon.slug === requestedAddon);
if (!validAddon) {
this._error = this.supervisor.localize("my.error_addon_not_found");
} else {
navigate(this, `/hassio/addon/${requestedAddon}`, true);
}
}
}
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
@@ -190,7 +199,9 @@ class HassioAddonDashboard extends LitElement {
const path: string = pathSplit[pathSplit.length - 1];
if (["uninstall", "install", "update", "start", "stop"].includes(path)) {
fireEvent(this, "supervisor-store-refresh", { store: "supervisor" });
fireEvent(this, "supervisor-collection-refresh", {
collection: "supervisor",
});
}
if (path === "uninstall") {

View File

@@ -25,6 +25,7 @@ import {
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../../src/common/config/version";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { navigate } from "../../../../src/common/navigate";
@@ -57,6 +58,7 @@ import {
fetchHassioStats,
HassioStats,
} from "../../../../src/data/hassio/common";
import { StoreAddon } from "../../../../src/data/supervisor/store";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
@@ -68,6 +70,7 @@ import { bytesToString } from "../../../../src/util/bytes-to-string";
import "../../components/hassio-card-content";
import "../../components/supervisor-metric";
import { showHassioMarkdownDialog } from "../../dialogs/markdown/show-dialog-hassio-markdown";
import { showDialogSupervisorUpdate } from "../../dialogs/update/show-dialog-update";
import { hassioStyle } from "../../resources/hassio-style";
import { addonArchIsSupported } from "../../util/addon";
@@ -77,63 +80,6 @@ const STAGE_ICON = {
deprecated: mdiExclamationThick,
};
const PERMIS_DESC = {
stage: {
title: "Add-on Stage",
description: `Add-ons can have one of three stages:\n\n<ha-svg-icon path="${STAGE_ICON.stable}"></ha-svg-icon> **Stable**: These are add-ons ready to be used in production.\n\n<ha-svg-icon path="${STAGE_ICON.experimental}"></ha-svg-icon> **Experimental**: These may contain bugs, and may be unfinished.\n\n<ha-svg-icon path="${STAGE_ICON.deprecated}"></ha-svg-icon> **Deprecated**: These add-ons will no longer receive any updates.`,
},
rating: {
title: "Add-on Security Rating",
description:
"Home Assistant provides a security rating to each of the add-ons, which indicates the risks involved when using this add-on. The more access an add-on requires on your system, the lower the score, thus raising the possible security risks.\n\nA score is on a scale from 1 to 6. Where 1 is the lowest score (considered the most insecure and highest risk) and a score of 6 is the highest score (considered the most secure and lowest risk).",
},
host_network: {
title: "Host Network",
description:
"Add-ons usually run in their own isolated network layer, which prevents them from accessing the network of the host operating system. In some cases, this network isolation can limit add-ons in providing their services and therefore, the isolation can be lifted by the add-on author, giving the add-on full access to the network capabilities of the host machine. This gives the add-on more networking capabilities but lowers the security, hence, the security rating of the add-on will be lowered when this option is used by the add-on.",
},
homeassistant_api: {
title: "Home Assistant API Access",
description:
"This add-on is allowed to access your running Home Assistant instance directly via the Home Assistant API. This mode handles authentication for the add-on as well, which enables an add-on to interact with Home Assistant without the need for additional authentication tokens.",
},
full_access: {
title: "Full Hardware Access",
description:
"This add-on is given full access to the hardware of your system, by request of the add-on author. Access is comparable to the privileged mode in Docker. Since this opens up possible security risks, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
},
hassio_api: {
title: "Supervisor API Access",
description:
"The add-on was given access to the Supervisor API, by request of the add-on author. By default, the add-on can access general version information of your system. When the add-on requests 'manager' or 'admin' level access to the API, it will gain access to control multiple parts of your Home Assistant system. This permission is indicated by this badge and will impact the security score of the addon negatively.",
},
docker_api: {
title: "Full Docker Access",
description:
"The add-on author has requested the add-on to have management access to the Docker instance running on your system. This mode gives the add-on full access and control to your entire Home Assistant system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
},
host_pid: {
title: "Host Processes Namespace",
description:
"Usually, the processes the add-on runs, are isolated from all other system processes. The add-on author has requested the add-on to have access to the system processes running on the host system instance, and allow the add-on to spawn processes on the host system as well. This mode gives the add-on full access and control to your entire Home Assistant system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
},
apparmor: {
title: "AppArmor",
description:
"AppArmor ('Application Armor') is a Linux kernel security module that restricts add-ons capabilities like network access, raw socket access, and permission to read, write, or execute specific files.\n\nAdd-on authors can provide their security profiles, optimized for the add-on, or request it to be disabled. If AppArmor is disabled, it will raise security risks and therefore, has a negative impact on the security score of the add-on.",
},
auth_api: {
title: "Home Assistant Authentication",
description:
"An add-on can authenticate users against Home Assistant, allowing add-ons to give users the possibility to log into applications running inside add-ons, using their Home Assistant username/password. This badge indicates if the add-on author requests this capability.",
},
ingress: {
title: "Ingress",
description:
"This add-on is using Ingress to embed its interface securely into Home Assistant.",
},
};
@customElement("hassio-addon-info")
class HassioAddonInfo extends LitElement {
@property({ type: Boolean }) public narrow!: boolean;
@@ -148,14 +94,23 @@ class HassioAddonInfo extends LitElement {
@internalProperty() private _error?: string;
private _addonStoreInfo = memoizeOne(
(slug: string, storeAddons: StoreAddon[]) =>
storeAddons.find((addon) => addon.slug === slug)
);
protected render(): TemplateResult {
const addonStoreInfo =
!this.addon.detached && !this.addon.available
? this._addonStoreInfo(this.addon.slug, this.supervisor.store.addons)
: undefined;
const metrics = [
{
description: "Add-on CPU Usage",
description: this.supervisor.localize("addon.dashboard.cpu_usage"),
value: this._metrics?.cpu_percent,
},
{
description: "Add-on RAM Usage",
description: this.supervisor.localize("addon.dashboard.ram_usage"),
value: this._metrics?.memory_percent,
tooltip: `${bytesToString(this._metrics?.memory_usage)}/${bytesToString(
this._metrics?.memory_limit
@@ -165,47 +120,64 @@ class HassioAddonInfo extends LitElement {
return html`
${this.addon.update_available
? html`
<ha-card header="Update available! 🎉">
<ha-card
.header="${this.supervisor.localize(
"common.update_available",
"count",
1
)}🎉"
>
<div class="card-content">
<hassio-card-content
.hass=${this.hass}
.title="${this.addon.name} ${this.addon
.version_latest} is available"
.description="You are currently running version ${this.addon
.version}"
.title="${this.supervisor.localize(
"addon.dashboard.new_update_available",
"name",
this.addon.name,
"version",
this.addon.version_latest
)}"
.description="${this.supervisor.localize(
"common.running_version",
"version",
this.addon.version
)}"
icon=${mdiArrowUpBoldCircle}
iconClass="update"
></hassio-card-content>
${!this.addon.available
${!this.addon.available && addonStoreInfo
? !addonArchIsSupported(
this.supervisor.info.supported_arch,
this.addon.arch
)
? html`
<p>
This add-on is not compatible with the processor of
your device or the operating system you have installed
on your device.
<p class="warning">
${this.supervisor.localize(
"addon.dashboard.not_available_arch"
)}
</p>
`
: html`
<p>
You are running Home Assistant
${this.supervisor.core.version}, to update to this
version of the add-on you need at least version
${this.addon.homeassistant} of Home Assistant
<p class="warning">
${this.supervisor.localize(
"addon.dashboard.not_available_arch",
"core_version_installed",
this.supervisor.core.version,
"core_version_needed",
addonStoreInfo.homeassistant
)}
</p>
`
: ""}
</div>
<div class="card-actions">
<ha-progress-button @click=${this._updateClicked}>
Update
</ha-progress-button>
<mwc-button @click=${this._updateClicked}>
${this.supervisor.localize("common.update")}
</mwc-button>
${this.addon.changelog
? html`
<mwc-button @click=${this._openChangelog}>
Changelog
${this.supervisor.localize("addon.dashboard.changelog")}
</mwc-button>
`
: ""}
@@ -216,12 +188,19 @@ class HassioAddonInfo extends LitElement {
${!this.addon.protected
? html`
<ha-card class="warning">
<h1 class="card-header">Warning: Protection mode is disabled!</h1>
<h1 class="card-header">${this.supervisor.localize(
"addon.dashboard.protection_mode.title"
)}
</h1>
<div class="card-content">
Protection mode on this add-on is disabled! This gives the add-on full access to the entire system, which adds security risks, and could damage your system when used incorrectly. Only disable the protection mode if you know, need AND trust the source of this add-on.
${this.supervisor.localize("addon.dashboard.protection_mode.content")}
</div>
<div class="card-actions protection-enable">
<mwc-button @click=${this._protectionToggled}>Enable Protection mode</mwc-button>
<mwc-button @click=${this._protectionToggled}>
${this.supervisor.localize(
"addon.dashboard.protection_mode.enable"
)}
</mwc-button>
</div>
</div>
</ha-card>
@@ -238,14 +217,18 @@ class HassioAddonInfo extends LitElement {
${this._computeIsRunning
? html`
<ha-svg-icon
title="Add-on is running"
.title=${this.supervisor.localize(
"dashboard.addon_running"
)}
class="running"
.path=${mdiCircle}
></ha-svg-icon>
`
: html`
<ha-svg-icon
title="Add-on is stopped"
.title=${this.supervisor.localize(
"dashboard.addon_stopped"
)}
class="stopped"
.path=${mdiCircle}
></ha-svg-icon>
@@ -259,21 +242,29 @@ class HassioAddonInfo extends LitElement {
? html`
Current version: ${this.addon.version}
<div class="changelog" @click=${this._openChangelog}>
(<span class="changelog-link">changelog</span>)
(<span class="changelog-link">${
this.supervisor.localize("addon.dashboard.changelog")}</span
>)
</div>
`
: html`<span class="changelog-link" @click=${this._openChangelog}
>Changelog</span
>`}
: html`<span class="changelog-link" @click=${this._openChangelog}>${
this.supervisor.localize("addon.dashboard.changelog")
}</span>`}
</div>
<div class="description light-color">
${this.addon.description}.<br />
Visit
<a href="${this.addon.url!}" target="_blank" rel="noreferrer">
${this.addon.name} page</a
>
for details.
${this.supervisor.localize(
"addon.dashboard.visit_addon_page",
"name",
html`<a
href="${this.addon.url!}"
target="_blank"
rel="noreferrer"
>
${this.addon.name}
</a>`
)}
</div>
<div class="addon-container">
<div>
@@ -294,7 +285,9 @@ class HassioAddonInfo extends LitElement {
})}
@click=${this._showMoreInfo}
id="stage"
label="stage"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.stage"
)}
description=""
>
<ha-svg-icon
@@ -320,7 +313,9 @@ class HassioAddonInfo extends LitElement {
<ha-label-badge
@click=${this._showMoreInfo}
id="host_network"
label="host"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.host"
)}
description=""
>
<ha-svg-icon .path=${mdiNetwork}></ha-svg-icon>
@@ -332,7 +327,9 @@ class HassioAddonInfo extends LitElement {
<ha-label-badge
@click=${this._showMoreInfo}
id="full_access"
label="hardware"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.hardware"
)}
description=""
>
<ha-svg-icon .path=${mdiChip}></ha-svg-icon>
@@ -344,7 +341,9 @@ class HassioAddonInfo extends LitElement {
<ha-label-badge
@click=${this._showMoreInfo}
id="homeassistant_api"
label="hass"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.hass"
)}
description=""
>
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
@@ -356,8 +355,12 @@ class HassioAddonInfo extends LitElement {
<ha-label-badge
@click=${this._showMoreInfo}
id="hassio_api"
label="hassio"
.description=${this.addon.hassio_role}
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.hassio"
)}
.description=${this.supervisor.localize(
`addon.dashboard.capability.role.${this.addon.hassio_role}`
) || this.addon.hassio_role}
>
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
</ha-label-badge>
@@ -368,7 +371,9 @@ class HassioAddonInfo extends LitElement {
<ha-label-badge
@click=${this._showMoreInfo}
id="docker_api"
label="docker"
.label=".${this.supervisor.localize(
"addon.dashboard.capability.label.docker"
)}"
description=""
>
<ha-svg-icon .path=${mdiDocker}></ha-svg-icon>
@@ -380,7 +385,9 @@ class HassioAddonInfo extends LitElement {
<ha-label-badge
@click=${this._showMoreInfo}
id="host_pid"
label="host pid"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.host_pid"
)}
description=""
>
<ha-svg-icon .path=${mdiPound}></ha-svg-icon>
@@ -393,7 +400,9 @@ class HassioAddonInfo extends LitElement {
@click=${this._showMoreInfo}
class=${this._computeApparmorClassName}
id="apparmor"
label="apparmor"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.apparmor"
)}
description=""
>
<ha-svg-icon .path=${mdiShield}></ha-svg-icon>
@@ -405,7 +414,9 @@ class HassioAddonInfo extends LitElement {
<ha-label-badge
@click=${this._showMoreInfo}
id="auth_api"
label="auth"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.auth"
)}
description=""
>
<ha-svg-icon .path=${mdiKey}></ha-svg-icon>
@@ -417,7 +428,9 @@ class HassioAddonInfo extends LitElement {
<ha-label-badge
@click=${this._showMoreInfo}
id="ingress"
label="ingress"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.ingress"
)}
description=""
>
<ha-svg-icon
@@ -438,10 +451,14 @@ class HassioAddonInfo extends LitElement {
>
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
Start on boot
${this.supervisor.localize(
"addon.dashboard.option.boot.title"
)}
</span>
<span slot="description">
Make the add-on start during a system boot
${this.supervisor.localize(
"addon.dashboard.option.boot.description"
)}
</span>
<ha-switch
@change=${this._startOnBootToggled}
@@ -454,10 +471,14 @@ class HassioAddonInfo extends LitElement {
? html`
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
Watchdog
${this.supervisor.localize(
"addon.dashboard.option.watchdog.title"
)}
</span>
<span slot="description">
This will start the add-on if it crashes
${this.supervisor.localize(
"addon.dashboard.option.watchdog.description"
)}
</span>
<ha-switch
@change=${this._watchdogToggled}
@@ -472,11 +493,14 @@ class HassioAddonInfo extends LitElement {
? html`
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
Auto update
${this.supervisor.localize(
"addon.dashboard.option.auto_update.title"
)}
</span>
<span slot="description">
Auto update the add-on when there is a new
version available
${this.supervisor.localize(
"addon.dashboard.option.auto_update.description"
)}
</span>
<ha-switch
@change=${this._autoUpdateToggled}
@@ -486,21 +510,22 @@ class HassioAddonInfo extends LitElement {
</ha-settings-row>
`
: ""}
${this.addon.ingress
${!this._computeCannotIngressSidebar && this.addon.ingress
? html`
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
Show in sidebar
${this.supervisor.localize(
"addon.dashboard.option.ingress_panel.title"
)}
</span>
<span slot="description">
${this._computeCannotIngressSidebar
? "This option requires Home Assistant 0.92 or later."
: "Add this add-on to your sidebar"}
${this.supervisor.localize(
"addon.dashboard.option.ingress_panel.description"
)}
</span>
<ha-switch
@change=${this._panelToggled}
.checked=${this.addon.ingress_panel}
.disabled=${this._computeCannotIngressSidebar}
haptic
></ha-switch>
</ha-settings-row>
@@ -510,10 +535,14 @@ class HassioAddonInfo extends LitElement {
? html`
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
Protection mode
${this.supervisor.localize(
"addon.dashboard.option.protected.title"
)}
</span>
<span slot="description">
Blocks elevated system access from the add-on
${this.supervisor.localize(
"addon.dashboard.option.protected.description"
)}
</span>
<ha-switch
@change=${this._protectionToggled}
@@ -531,7 +560,7 @@ class HassioAddonInfo extends LitElement {
${this.addon.state === "started"
? html`<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
Hostname
${this.supervisor.localize("addon.dashboard.hostname")}
</span>
<code slot="description">
${this.addon.hostname}
@@ -551,24 +580,27 @@ class HassioAddonInfo extends LitElement {
</div>
</div>
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
${!this.addon.available
${!this.addon.version && addonStoreInfo && !this.addon.available
? !addonArchIsSupported(
this.supervisor.info.supported_arch,
this.addon.arch
)
? html`
<p class="warning">
This add-on is not compatible with the processor of your
device or the operating system you have installed on your
device.
${this.supervisor.localize(
"addon.dashboard.not_available_arch"
)}
</p>
`
: html`
<p class="warning">
You are running Home Assistant
${this.supervisor.core.version}, to install this add-on you
need at least version ${this.addon.homeassistant} of Home
Assistant
${this.supervisor.localize(
"addon.dashboard.not_available_version",
"core_version_installed",
this.supervisor.core.version,
"core_version_needed",
addonStoreInfo!.homeassistant
)}
</p>
`
: ""}
@@ -582,18 +614,18 @@ class HassioAddonInfo extends LitElement {
class="warning"
@click=${this._stopClicked}
>
Stop
${this.supervisor.localize("addon.dashboard.stop")}
</ha-progress-button>
<ha-progress-button
class="warning"
@click=${this._restartClicked}
>
Restart
${this.supervisor.localize("addon.dashboard.restart")}
</ha-progress-button>
`
: html`
<ha-progress-button @click=${this._startClicked}>
Start
${this.supervisor.localize("addon.dashboard.start")}
</ha-progress-button>
`
: html`
@@ -601,7 +633,7 @@ class HassioAddonInfo extends LitElement {
.disabled=${!this.addon.available}
@click=${this._installClicked}
>
Install
${this.supervisor.localize("addon.dashboard.install")}
</ha-progress-button>
`}
</div>
@@ -616,7 +648,9 @@ class HassioAddonInfo extends LitElement {
rel="noopener"
>
<mwc-button>
Open web UI
${this.supervisor.localize(
"addon.dashboard.open_web_ui"
)}
</mwc-button>
</a>
`
@@ -624,7 +658,9 @@ class HassioAddonInfo extends LitElement {
${this._computeShowIngressUI
? html`
<mwc-button @click=${this._openIngress}>
Open web UI
${this.supervisor.localize(
"addon.dashboard.open_web_ui"
)}
</mwc-button>
`
: ""}
@@ -632,7 +668,7 @@ class HassioAddonInfo extends LitElement {
class="warning"
@click=${this._uninstallClicked}
>
Uninstall
${this.supervisor.localize("addon.dashboard.uninstall")}
</ha-progress-button>
${this.addon.build
? html`
@@ -641,7 +677,7 @@ class HassioAddonInfo extends LitElement {
.hass=${this.hass}
.path="hassio/addons/${this.addon.slug}/rebuild"
>
Rebuild
${this.supervisor.localize("addon.dashboard.rebuild")}
</ha-call-api-button>
`
: ""}`
@@ -701,8 +737,21 @@ class HassioAddonInfo extends LitElement {
private _showMoreInfo(ev): void {
const id = ev.currentTarget.id;
showHassioMarkdownDialog(this, {
title: PERMIS_DESC[id].title,
content: PERMIS_DESC[id].description,
title: this.supervisor.localize(`addon.dashboard.capability.${id}.title`),
content:
id === "stage"
? this.supervisor.localize(
`addon.dashboard.capability.${id}.description`,
"icon_stable",
`<ha-svg-icon path="${STAGE_ICON.stable}"></ha-svg-icon>`,
"icon_experimental",
`<ha-svg-icon path="${STAGE_ICON.experimental}"></ha-svg-icon>`,
"icon_deprecated",
`<ha-svg-icon path="${STAGE_ICON.deprecated}"></ha-svg-icon>`
)
: this.supervisor.localize(
`addon.dashboard.capability.${id}.description`
),
});
}
@@ -755,9 +804,11 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon option, ${extractApiErrorMessage(
err
)}`;
this._error = this.supervisor.localize(
"addon.failed_to_save",
"error",
extractApiErrorMessage(err)
);
}
}
@@ -775,9 +826,11 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon option, ${extractApiErrorMessage(
err
)}`;
this._error = this.supervisor.localize(
"addon.failed_to_save",
"error",
extractApiErrorMessage(err)
);
}
}
@@ -795,9 +848,11 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon option, ${extractApiErrorMessage(
err
)}`;
this._error = this.supervisor.localize(
"addon.failed_to_save",
"error",
extractApiErrorMessage(err)
);
}
}
@@ -815,9 +870,11 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon security option, ${extractApiErrorMessage(
err
)}`;
this._error = this.supervisor.localize(
"addon.failed_to_save",
"error",
extractApiErrorMessage(err)
);
}
}
@@ -835,9 +892,11 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon option, ${extractApiErrorMessage(
err
)}`;
this._error = this.supervisor.localize(
"addon.failed_to_save",
"error",
extractApiErrorMessage(err)
);
}
}
@@ -848,12 +907,14 @@ class HassioAddonInfo extends LitElement {
this.addon.slug
);
showHassioMarkdownDialog(this, {
title: "Changelog",
title: this.supervisor.localize("addon.dashboard.changelog"),
content,
});
} catch (err) {
showAlertDialog(this, {
title: "Failed to get addon changelog",
title: this.supervisor.localize(
"addon.dashboard.action_error.get_changelog"
),
text: extractApiErrorMessage(err),
});
}
@@ -873,7 +934,7 @@ class HassioAddonInfo extends LitElement {
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
showAlertDialog(this, {
title: "Failed to install addon",
title: this.supervisor.localize("addon.dashboard.action_error.install"),
text: extractApiErrorMessage(err),
});
}
@@ -894,7 +955,7 @@ class HassioAddonInfo extends LitElement {
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
showAlertDialog(this, {
title: "Failed to stop addon",
title: this.supervisor.localize("addon.dashboard.action_error.stop"),
text: extractApiErrorMessage(err),
});
}
@@ -915,45 +976,38 @@ class HassioAddonInfo extends LitElement {
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
showAlertDialog(this, {
title: "Failed to restart addon",
title: this.supervisor.localize("addon.dashboard.action_error.restart"),
text: extractApiErrorMessage(err),
});
}
button.progress = false;
}
private async _updateClicked(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.addon.name,
text: "Are you sure you want to update this add-on?",
confirmText: "update add-on",
dismissText: "no",
private async _updateClicked(): Promise<void> {
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(),
});
}
if (!confirmed) {
button.progress = false;
return;
}
this._error = undefined;
try {
await updateHassioAddon(this.hass, this.addon.slug);
const eventdata = {
success: true,
response: undefined,
path: "update",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
showAlertDialog(this, {
title: "Failed to update addon",
text: extractApiErrorMessage(err),
});
}
button.progress = false;
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> {
@@ -966,11 +1020,15 @@ class HassioAddonInfo extends LitElement {
);
if (!validate.valid) {
await showConfirmationDialog(this, {
title: "Failed to start addon - configuration validation failed!",
title: this.supervisor.localize(
"addon.dashboard.action_error.start_invalid_config"
),
text: validate.message.split(" Got ")[0],
confirm: () => this._openConfiguration(),
confirmText: "Go to configuration",
dismissText: "Cancel",
confirmText: this.supervisor.localize(
"addon.dashboard.action_error.go_to_config"
),
dismissText: this.supervisor.localize("common.cancel"),
});
button.progress = false;
return;
@@ -995,7 +1053,7 @@ class HassioAddonInfo extends LitElement {
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
showAlertDialog(this, {
title: "Failed to start addon",
title: this.supervisor.localize("addon.dashboard.action_error.start"),
text: extractApiErrorMessage(err),
});
}
@@ -1033,7 +1091,9 @@ class HassioAddonInfo extends LitElement {
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
showAlertDialog(this, {
title: "Failed to uninstall addon",
title: this.supervisor.localize(
"addon.dashboard.action_error.uninstall"
),
text: extractApiErrorMessage(err),
});
}

View File

@@ -9,6 +9,7 @@ import {
} from "lit-element";
import "../../../../src/components/ha-circular-progress";
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
@@ -18,6 +19,8 @@ import "./hassio-addon-logs";
class HassioAddonLogDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon?: HassioAddonDetails;
protected render(): TemplateResult {
@@ -28,6 +31,7 @@ class HassioAddonLogDashboard extends LitElement {
<div class="content">
<hassio-addon-logs
.hass=${this.hass}
.supervisor=${this.supervisor}
.addon=${this.addon}
></hassio-addon-logs>
</div>

View File

@@ -15,6 +15,7 @@ import {
HassioAddonDetails,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import "../../components/hassio-ansi-to-html";
@@ -24,6 +25,8 @@ import { hassioStyle } from "../../resources/hassio-style";
class HassioAddonLogs extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon!: HassioAddonDetails;
@internalProperty() private _error?: string;
@@ -48,7 +51,9 @@ class HassioAddonLogs extends LitElement {
: ""}
</div>
<div class="card-actions">
<mwc-button @click=${this._refresh}>Refresh</mwc-button>
<mwc-button @click=${this._refresh}>
${this.supervisor.localize("common.refresh")}
</mwc-button>
</div>
</ha-card>
`;
@@ -76,7 +81,11 @@ class HassioAddonLogs extends LitElement {
try {
this._content = await fetchHassioAddonLogs(this.hass, this.addon.slug);
} catch (err) {
this._error = `Failed to get addon logs, ${extractApiErrorMessage(err)}`;
this._error = this.supervisor.localize(
"addon.logs.get_logs",
"error",
extractApiErrorMessage(err)
);
}
}

View File

@@ -44,7 +44,7 @@ class HassioCardContent extends LitElement {
${this.iconImage
? html`
<div class="icon_image ${this.iconClass}">
<img src="${this.iconImage}" title="${this.iconTitle}" />
<img src="${this.iconImage}" .title=${this.iconTitle} />
<div></div>
</div>
`

View File

@@ -26,9 +26,9 @@ class SupervisorMetric extends LitElement {
<span slot="heading">
${this.description}
</span>
<div slot="description" title="${this.tooltip ?? ""}">
<div slot="description" .title=${this.tooltip ?? ""}>
<span class="value">
${roundedValue}%
${roundedValue} %
</span>
<ha-bar
class="${classMap({

View File

@@ -27,17 +27,15 @@ class HassioAddons extends LitElement {
protected render(): TemplateResult {
return html`
<div class="content">
<h1>Add-ons</h1>
<h1>${this.supervisor.localize("dashboard.addons")}</h1>
<div class="card-group">
${!this.supervisor.supervisor.addons?.length
? html`
<ha-card>
<div class="card-content">
You don't have any add-ons installed yet. Head over to
<button class="link" @click=${this._openStore}>
the add-on store
${this.supervisor.localize("dashboard.no_addons")}
</button>
to get started!
</div>
</ha-card>
`
@@ -58,10 +56,16 @@ class HassioAddons extends LitElement {
? mdiArrowUpBoldCircle
: mdiPuzzle}
.iconTitle=${addon.state !== "started"
? "Add-on is stopped"
? this.supervisor.localize(
"dashboard.addon_stopped"
)
: addon.update_available!
? "New version available"
: "Add-on is running"}
? this.supervisor.localize(
"dashboard.addon_new_version"
)
: this.supervisor.localize(
"dashboard.addon_running"
)}
.iconClass=${addon.update_available
? addon.state === "started"
? "update"

View File

@@ -29,13 +29,16 @@ class HassioDashboard extends LitElement {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow}
hassio
main-page
.route=${this.route}
.tabs=${supervisorTabs}
main-page
supervisor
>
<span slot="header">Dashboard</span>
<span slot="header">
${this.supervisor.localize("panel.dashboard")}
</span>
<div class="content">
<hassio-update
.hass=${this.hass}

View File

@@ -10,29 +10,40 @@ import {
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-svg-icon";
import {
extractApiErrorMessage,
HassioResponse,
ignoredStatusCodes,
ignoreSupervisorError,
} from "../../../src/data/hassio/common";
import { HassioHassOSInfo } from "../../../src/data/hassio/host";
import {
HassioHomeAssistantInfo,
HassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { updateCore } from "../../../src/data/supervisor/core";
import {
Supervisor,
supervisorApiWsRequest,
} from "../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { showDialogSupervisorUpdate } from "../dialogs/update/show-dialog-update";
import { hassioStyle } from "../resources/hassio-style";
const computeVersion = (key: string, version: string): string => {
return key === "os" ? version : `${key}-${version}`;
};
@customElement("hassio-update")
export class HassioUpdate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -58,9 +69,12 @@ export class HassioUpdate extends LitElement {
return html`
<div class="content">
<h1>
${updatesAvailable > 1
? "Updates Available 🎉"
: "Update Available 🎉"}
${this.supervisor.localize(
"common.update_available",
"count",
updatesAvailable
)}
🎉
</h1>
<div class="card-group">
${this._renderUpdateCard(
@@ -109,14 +123,30 @@ export class HassioUpdate extends LitElement {
<div class="icon">
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
</div>
<div class="update-heading">${name} ${object.version_latest}</div>
<div class="warning">
You are currently running version ${object.version}
</div>
<div class="update-heading">${name}</div>
<ha-settings-row two-line>
<span slot="heading">
${this.supervisor.localize("common.version")}
</span>
<span slot="description">
${computeVersion(key, object.version!)}
</span>
</ha-settings-row>
<ha-settings-row two-line>
<span slot="heading">
${this.supervisor.localize("common.newest_version")}
</span>
<span slot="description">
${computeVersion(key, object.version_latest!)}
</span>
</ha-settings-row>
</div>
<div class="card-actions">
<a href="${releaseNotesUrl}" target="_blank" rel="noreferrer">
<mwc-button>Release notes</mwc-button>
<mwc-button>
${this.supervisor.localize("common.release_notes")}
</mwc-button>
</a>
<ha-progress-button
.apiPath=${apiPath}
@@ -125,7 +155,7 @@ export class HassioUpdate extends LitElement {
.version=${object.version_latest}
@click=${this._confirmUpdate}
>
Update
${this.supervisor.localize("common.update")}
</ha-progress-button>
</div>
</ha-card>
@@ -134,12 +164,36 @@ export class HassioUpdate extends LitElement {
private async _confirmUpdate(ev): Promise<void> {
const item = ev.currentTarget;
if (item.key === "core") {
showDialogSupervisorUpdate(this, {
supervisor: this.supervisor,
name: "Home Assistant Core",
version: this.supervisor.core.version_latest,
snapshotParams: {
name: `core_${this.supervisor.core.version}`,
folders: ["homeassistant"],
homeassistant: true,
},
updateHandler: async () => this._updateCore(),
});
return;
}
item.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: `Update ${item.name}`,
text: `Are you sure you want to update ${item.name} to version ${item.version}?`,
confirmText: "update",
dismissText: "cancel",
title: this.supervisor.localize(
"confirm.update.title",
"name",
item.name
),
text: this.supervisor.localize(
"confirm.update.text",
"name",
item.name,
"version",
computeVersion(item.key, item.version)
),
confirmText: this.supervisor.localize("common.update"),
dismissText: this.supervisor.localize("common.cancel"),
});
if (!confirmed) {
@@ -147,14 +201,24 @@ export class HassioUpdate extends LitElement {
return;
}
try {
await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath);
fireEvent(this, "supervisor-store-refresh", { store: item.key });
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
await supervisorApiWsRequest(this.hass.connection, {
method: "post",
endpoint: item.apiPath.replace("hassio", ""),
timeout: null,
});
} else {
await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath);
}
fireEvent(this, "supervisor-collection-refresh", {
collection: item.key,
});
} catch (err) {
// Only show an error if the status code was not expected (user behind proxy)
// or no status at all(connection terminated)
if (err.status_code && !ignoredStatusCodes.has(err.status_code)) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
showAlertDialog(this, {
title: "Update failed",
title: this.supervisor.localize("common.error.update_failed"),
text: extractApiErrorMessage(err),
});
}
@@ -162,6 +226,13 @@ export class HassioUpdate extends LitElement {
item.progress = false;
}
private async _updateCore(): Promise<void> {
await updateCore(this.hass);
fireEvent(this, "supervisor-collection-refresh", {
collection: "core",
});
}
static get styles(): CSSResult[] {
return [
haStyle,
@@ -179,9 +250,6 @@ export class HassioUpdate extends LitElement {
margin-bottom: 0.5em;
color: var(--primary-text-color);
}
.warning {
color: var(--secondary-text-color);
}
.card-content {
height: calc(100% - 47px);
box-sizing: border-box;
@@ -189,13 +257,13 @@ export class HassioUpdate extends LitElement {
.card-actions {
text-align: right;
}
.errors {
color: var(--error-color);
padding: 16px;
}
a {
text-decoration: none;
}
ha-settings-row {
padding: 0;
--paper-item-body-two-line-min-height: 32px;
}
`,
];
}

View File

@@ -18,7 +18,6 @@ import {
} from "lit-element";
import { cache } from "lit-html/directives/cache";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-chips";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel";
@@ -35,6 +34,7 @@ import {
updateNetworkInterface,
WifiConfiguration,
} from "../../../../src/data/hassio/network";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
@@ -51,6 +51,8 @@ export class DialogHassioNetwork extends LitElement
implements HassDialog<HassioNetworkDialogParams> {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@internalProperty() private _accessPoints?: AccessPoints;
@internalProperty() private _curTabIndex = 0;
@@ -73,7 +75,8 @@ export class DialogHassioNetwork extends LitElement
this._params = params;
this._dirty = false;
this._curTabIndex = 0;
this._interfaces = params.network.interfaces.sort((a, b) => {
this.supervisor = params.supervisor;
this._interfaces = params.supervisor.network.interfaces.sort((a, b) => {
return a.primary > b.primary ? -1 : 1;
});
this._interface = { ...this._interfaces[this._curTabIndex] };
@@ -104,7 +107,7 @@ export class DialogHassioNetwork extends LitElement
<div slot="heading">
<ha-header-bar>
<span slot="title">
Network settings
${this.supervisor.localize("dialog.network.title")}
</span>
<mwc-icon-button slot="actionItems" dialogAction="cancel">
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
@@ -139,7 +142,13 @@ export class DialogHassioNetwork extends LitElement
? html`
<ha-expansion-panel header="Wi-Fi" outlined>
${this._interface?.wifi?.ssid
? html`<p>Connected to: ${this._interface?.wifi?.ssid}</p>`
? html`<p>
${this.supervisor.localize(
"dialog.network.connected_to",
"ssid",
this._interface?.wifi?.ssid
)}
</p>`
: ""}
<mwc-button
class="scan"
@@ -149,7 +158,7 @@ export class DialogHassioNetwork extends LitElement
${this._scanning
? html`<ha-circular-progress active size="small">
</ha-circular-progress>`
: "Scan for accesspoints"}
: this.supervisor.localize("dialog.network.scan_ap")}
</mwc-button>
${this._accessPoints &&
this._accessPoints.accesspoints &&
@@ -181,7 +190,11 @@ export class DialogHassioNetwork extends LitElement
${this._wifiConfiguration
? html`
<div class="radio-row">
<ha-formfield label="open">
<ha-formfield
.label=${this.supervisor.localize(
"dialog.network.open"
)}
>
<ha-radio
@change=${this._handleRadioValueChangedAp}
.ap=${this._wifiConfiguration}
@@ -193,7 +206,11 @@ export class DialogHassioNetwork extends LitElement
>
</ha-radio>
</ha-formfield>
<ha-formfield label="wep">
<ha-formfield
.label=${this.supervisor.localize(
"dialog.network.wep"
)}
>
<ha-radio
@change=${this._handleRadioValueChangedAp}
.ap=${this._wifiConfiguration}
@@ -203,7 +220,11 @@ export class DialogHassioNetwork extends LitElement
>
</ha-radio>
</ha-formfield>
<ha-formfield label="wpa-psk">
<ha-formfield
.label=${this.supervisor.localize(
"dialog.network.wpa"
)}
>
<ha-radio
@change=${this._handleRadioValueChangedAp}
.ap=${this._wifiConfiguration}
@@ -237,18 +258,21 @@ export class DialogHassioNetwork extends LitElement
: ""}
${this._dirty
? html`<div class="warning">
If you are changing the Wi-Fi, IP or gateway addresses, you might
lose the connection!
${this.supervisor.localize("dialog.network.warning")}
</div>`
: ""}
</div>
<div class="buttons">
<mwc-button label="close" @click=${this.closeDialog}> </mwc-button>
<mwc-button
.label=${this.supervisor.localize("common.cancel")}
@click=${this.closeDialog}
>
</mwc-button>
<mwc-button @click=${this._updateNetwork} .disabled=${!this._dirty}>
${this._processing
? html`<ha-circular-progress active size="small">
</ha-circular-progress>`
: "Save"}
: this.supervisor.localize("common.save")}
</mwc-button>
</div>`;
}
@@ -285,7 +309,9 @@ export class DialogHassioNetwork extends LitElement
outlined
>
<div class="radio-row">
<ha-formfield label="DHCP">
<ha-formfield
.label=${this.supervisor.localize("dialog.network.dhcp")}
>
<ha-radio
@change=${this._handleRadioValueChanged}
.version=${version}
@@ -295,7 +321,9 @@ export class DialogHassioNetwork extends LitElement
>
</ha-radio>
</ha-formfield>
<ha-formfield label="Static">
<ha-formfield
.label=${this.supervisor.localize("dialog.network.static")}
>
<ha-radio
@change=${this._handleRadioValueChanged}
.version=${version}
@@ -305,7 +333,10 @@ export class DialogHassioNetwork extends LitElement
>
</ha-radio>
</ha-formfield>
<ha-formfield label="Disabled" class="warning">
<ha-formfield
.label=${this.supervisor.localize("dialog.network.disabled")}
class="warning"
>
<ha-radio
@change=${this._handleRadioValueChanged}
.version=${version}
@@ -321,7 +352,7 @@ export class DialogHassioNetwork extends LitElement
<paper-input
class="flex-auto"
id="address"
label="IP address/Netmask"
.label=${this.supervisor.localize("dialog.network.ip_netmask")}
.version=${version}
.value=${this._toString(this._interface![version].address)}
@value-changed=${this._handleInputValueChanged}
@@ -330,7 +361,7 @@ export class DialogHassioNetwork extends LitElement
<paper-input
class="flex-auto"
id="gateway"
label="Gateway address"
.label=${this.supervisor.localize("dialog.network.gateway")}
.version=${version}
.value=${this._interface![version].gateway}
@value-changed=${this._handleInputValueChanged}
@@ -339,7 +370,7 @@ export class DialogHassioNetwork extends LitElement
<paper-input
class="flex-auto"
id="nameservers"
label="DNS servers"
.label=${this.supervisor.localize("dialog.network.dns_servers")}
.version=${version}
.value=${this._toString(this._interface![version].nameservers)}
@value-changed=${this._handleInputValueChanged}
@@ -424,7 +455,7 @@ export class DialogHassioNetwork extends LitElement
);
} catch (err) {
showAlertDialog(this, {
title: "Failed to change network settings",
title: this.supervisor.localize("dialog.network.failed_to_change"),
text: extractApiErrorMessage(err),
});
this._processing = false;
@@ -437,10 +468,9 @@ export class DialogHassioNetwork extends LitElement
private async _handleTabActivated(ev: CustomEvent): Promise<void> {
if (this._dirty) {
const confirm = await showConfirmationDialog(this, {
text:
"You have unsaved changes, these will get lost if you change tabs, do you want to continue?",
confirmText: "yes",
dismissText: "no",
text: this.supervisor.localize("dialog.network.unsaved"),
confirmText: this.supervisor.localize("common.yes"),
dismissText: this.supervisor.localize("common.no"),
});
if (!confirm) {
this.requestUpdate("_interface");

View File

@@ -1,9 +1,9 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { NetworkInfo } from "../../../../src/data/hassio/network";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import "./dialog-hassio-network";
export interface HassioNetworkDialogParams {
network: NetworkInfo;
supervisor: Supervisor;
loadData: () => Promise<void>;
}

View File

@@ -22,14 +22,18 @@ import {
fetchHassioDockerRegistries,
removeHassioDockerRegistry,
} from "../../../../src/data/hassio/docker";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { RegistriesDialogParams } from "./show-dialog-registries";
@customElement("dialog-hassio-registries")
class HassioRegistriesDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) private _registries?: {
registry: string;
username: string;
@@ -55,8 +59,8 @@ class HassioRegistriesDialog extends LitElement {
.heading=${createCloseHeading(
this.hass,
this._addingRegistry
? "Add New Docker Registry"
: "Manage Docker Registries"
? this.supervisor.localize("dialog.registries.title_add")
: this.supervisor.localize("dialog.registries.title_manage")
)}
>
<div class="form">
@@ -66,7 +70,9 @@ class HassioRegistriesDialog extends LitElement {
@value-changed=${this._inputChanged}
class="flex-auto"
name="registry"
label="Registry"
.label=${this.supervisor.localize(
"dialog.registries.registry"
)}
required
auto-validate
></paper-input>
@@ -74,7 +80,9 @@ class HassioRegistriesDialog extends LitElement {
@value-changed=${this._inputChanged}
class="flex-auto"
name="username"
label="Username"
.label=${this.supervisor.localize(
"dialog.registries.username"
)}
required
auto-validate
></paper-input>
@@ -82,7 +90,9 @@ class HassioRegistriesDialog extends LitElement {
@value-changed=${this._inputChanged}
class="flex-auto"
name="password"
label="Password"
.label=${this.supervisor.localize(
"dialog.registries.password"
)}
type="password"
required
auto-validate
@@ -94,7 +104,7 @@ class HassioRegistriesDialog extends LitElement {
)}
@click=${this._addNewRegistry}
>
Add registry
${this.supervisor.localize("dialog.registries.add_registry")}
</mwc-button>
`
: html`${this._registries?.length
@@ -103,11 +113,16 @@ class HassioRegistriesDialog extends LitElement {
<mwc-list-item class="option" hasMeta twoline>
<span>${entry.registry}</span>
<span slot="secondary"
>Username: ${entry.username}</span
>${this.supervisor.localize(
"dialog.registries.username"
)}:
${entry.username}</span
>
<mwc-icon-button
.entry=${entry}
title="Remove"
.title=${this.supervisor.localize(
"dialog.registries.remove"
)}
slot="meta"
@click=${this._removeRegistry}
>
@@ -118,11 +133,17 @@ class HassioRegistriesDialog extends LitElement {
})
: html`
<mwc-list-item>
<span>No registries configured</span>
<span
>${this.supervisor.localize(
"dialog.registries.no_registries"
)}</span
>
</mwc-list-item>
`}
<mwc-button @click=${this._addRegistry}>
Add new registry
${this.supervisor.localize(
"dialog.registries.add_new_registry"
)}
</mwc-button> `}
</div>
</ha-dialog>
@@ -134,8 +155,9 @@ class HassioRegistriesDialog extends LitElement {
this[`_${target.name}`] = target.value;
}
public async showDialog(_dialogParams: any): Promise<void> {
public async showDialog(dialogParams: RegistriesDialogParams): Promise<void> {
this._opened = true;
this.supervisor = dialogParams.supervisor;
await this._loadRegistries();
await this.updateComplete;
}
@@ -178,7 +200,7 @@ class HassioRegistriesDialog extends LitElement {
this._addingRegistry = false;
} catch (err) {
showAlertDialog(this, {
title: "Failed to add registry",
title: this.supervisor.localize("dialog.registries.failed_to_add"),
text: extractApiErrorMessage(err),
});
}
@@ -192,7 +214,7 @@ class HassioRegistriesDialog extends LitElement {
await this._loadRegistries();
} catch (err) {
showAlertDialog(this, {
title: "Failed to remove registry",
title: this.supervisor.localize("dialog.registries.failed_to_remove"),
text: extractApiErrorMessage(err),
});
}

View File

@@ -1,10 +1,18 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import "./dialog-hassio-registries";
export const showRegistriesDialog = (element: HTMLElement): void => {
export interface RegistriesDialogParams {
supervisor: Supervisor;
}
export const showRegistriesDialog = (
element: HTMLElement,
dialogParams: RegistriesDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-registries",
dialogImport: () => import("./dialog-hassio-registries"),
dialogParams: {},
dialogParams,
});
};

View File

@@ -17,8 +17,9 @@ import {
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-svg-icon";
import {
fetchHassioAddonsInfo,
@@ -34,27 +35,29 @@ import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
class HassioRepositoriesDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) private _repos: HassioAddonRepository[] = [];
@property({ attribute: false })
private _dialogParams?: HassioRepositoryDialogParams;
@query("#repository_input", true) private _optionInput?: PaperInputElement;
@internalProperty() private _repositories?: HassioAddonRepository[];
@internalProperty() private _dialogParams?: HassioRepositoryDialogParams;
@internalProperty() private _opened = false;
@internalProperty() private _prosessing = false;
@internalProperty() private _error?: string;
public async showDialog(_dialogParams: any): Promise<void> {
this._dialogParams = _dialogParams;
this._repos = _dialogParams.repos;
public async showDialog(
dialogParams: HassioRepositoryDialogParams
): Promise<void> {
this._dialogParams = dialogParams;
this._opened = true;
await this._loadData();
await this.updateComplete;
}
public closeDialog(): void {
this._dialogParams = undefined;
this._opened = false;
this._error = "";
}
@@ -66,14 +69,20 @@ class HassioRepositoriesDialog extends LitElement {
);
protected render(): TemplateResult {
const repositories = this._filteredRepositories(this._repos);
if (!this._dialogParams?.supervisor || this._repositories === undefined) {
return html``;
}
const repositories = this._filteredRepositories(this._repositories);
return html`
<ha-dialog
.open=${this._opened}
@closing=${this.closeDialog}
scrimClickAction
escapeKeyAction
heading="Manage add-on repositories"
.heading=${createCloseHeading(
this.hass,
this._dialogParams!.supervisor.localize("dialog.repositories.title")
)}
>
${this._error ? html`<div class="error">${this._error}</div>` : ""}
<div class="form">
@@ -88,7 +97,9 @@ class HassioRepositoriesDialog extends LitElement {
</paper-item-body>
<mwc-icon-button
.slug=${repo.slug}
title="Remove"
.title=${this._dialogParams!.supervisor.localize(
"dialog.repositories.remove"
)}
@click=${this._removeRepository}
>
<ha-svg-icon .path=${mdiDelete}></ha-svg-icon>
@@ -105,18 +116,23 @@ class HassioRepositoriesDialog extends LitElement {
<paper-input
class="flex-auto"
id="repository_input"
label="Add repository"
.value=${this._dialogParams!.url || ""}
.label=${this._dialogParams!.supervisor.localize(
"dialog.repositories.add"
)}
@keydown=${this._handleKeyAdd}
></paper-input>
<mwc-button @click=${this._addRepository}>
${this._prosessing
? html`<ha-circular-progress active></ha-circular-progress>`
: "Add"}
: this._dialogParams!.supervisor.localize(
"dialog.repositories.add"
)}
</mwc-button>
</div>
</div>
<mwc-button slot="primaryAction" @click="${this.closeDialog}">
Close
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this._dialogParams?.supervisor.localize("common.close")}
</mwc-button>
</ha-dialog>
`;
@@ -147,6 +163,11 @@ class HassioRepositoriesDialog extends LitElement {
ha-paper-dropdown-menu {
display: block;
}
ha-circular-progress {
display: block;
margin: 32px;
text-align: center;
}
`,
];
}
@@ -167,13 +188,25 @@ class HassioRepositoriesDialog extends LitElement {
this._addRepository();
}
private async _loadData(): Promise<void> {
try {
const addonsinfo = await fetchHassioAddonsInfo(this.hass);
this._repositories = addonsinfo.repositories;
fireEvent(this, "supervisor-collection-refresh", { collection: "addon" });
} catch (err) {
this._error = extractApiErrorMessage(err);
}
}
private async _addRepository() {
const input = this._optionInput;
if (!input || !input.value) {
return;
}
this._prosessing = true;
const repositories = this._filteredRepositories(this._repos);
const repositories = this._filteredRepositories(this._repositories!);
const newRepositories = repositories.map((repo) => {
return repo.source;
});
@@ -183,11 +216,7 @@ class HassioRepositoriesDialog extends LitElement {
await setSupervisorOption(this.hass, {
addons_repositories: newRepositories,
});
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
this._repos = addonsInfo.repositories;
await this._dialogParams!.loadData();
await this._loadData();
input.value = "";
} catch (err) {
@@ -198,7 +227,7 @@ class HassioRepositoriesDialog extends LitElement {
private async _removeRepository(ev: Event) {
const slug = (ev.currentTarget as any).slug;
const repositories = this._filteredRepositories(this._repos);
const repositories = this._filteredRepositories(this._repositories!);
const repository = repositories.find((repo) => {
return repo.slug === slug;
});
@@ -217,11 +246,7 @@ class HassioRepositoriesDialog extends LitElement {
await setSupervisorOption(this.hass, {
addons_repositories: newRepositories,
});
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
this._repos = addonsInfo.repositories;
await this._dialogParams!.loadData();
await this._loadData();
} catch (err) {
this._error = extractApiErrorMessage(err);
}

View File

@@ -1,10 +1,10 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { HassioAddonRepository } from "../../../../src/data/hassio/addon";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import "./dialog-hassio-repositories";
export interface HassioRepositoryDialogParams {
repos: HassioAddonRepository[];
loadData: () => Promise<void>;
supervisor: Supervisor;
url?: string;
}
export const showRepositoriesDialog = (

View File

@@ -95,7 +95,7 @@ class HassioSnapshotDialog extends LitElement {
@internalProperty() private _snapshotPassword!: string;
@internalProperty() private _restoreHass: boolean | null | undefined = true;
@internalProperty() private _restoreHass = true;
public async showDialog(params: HassioSnapshotDialogParams) {
this._snapshot = await fetchHassioSnapshotInfo(this.hass, params.slug);
@@ -109,6 +109,9 @@ class HassioSnapshotDialog extends LitElement {
this._dialogParams = params;
this._onboarding = params.onboarding ?? false;
this.supervisor = params.supervisor;
if (!this._snapshot.homeassistant) {
this._restoreHass = false;
}
}
protected render(): TemplateResult {
@@ -134,15 +137,17 @@ class HassioSnapshotDialog extends LitElement {
(${this._computeSize})<br />
${this._formatDatetime(this._snapshot.date)}
</div>
<div>Home Assistant:</div>
<paper-checkbox
.checked=${this._restoreHass}
@change="${(ev: Event) => {
this._restoreHass = (ev.target as PaperCheckboxElement).checked;
}}"
>
Home Assistant ${this._snapshot.homeassistant}
</paper-checkbox>
${this._snapshot.homeassistant
? html`<div>Home Assistant:</div>
<paper-checkbox
.checked=${this._restoreHass}
@change="${(ev: Event) => {
this._restoreHass = (ev.target as PaperCheckboxElement).checked!;
}}"
>
Home Assistant ${this._snapshot.homeassistant}
</paper-checkbox>`
: ""}
${this._folders.length
? html`
<div>Folders:</div>
@@ -334,7 +339,7 @@ class HassioSnapshotDialog extends LitElement {
.map((folder) => folder.slug);
const data: {
homeassistant: boolean | null | undefined;
homeassistant: boolean;
addons: any;
folders: any;
password?: string;

View File

@@ -4,6 +4,7 @@ import {
restartHassioAddon,
} from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
@@ -13,20 +14,25 @@ import { HomeAssistant } from "../../../src/types";
export const suggestAddonRestart = async (
element: LitElement,
hass: HomeAssistant,
supervisor: Supervisor,
addon: HassioAddonDetails
): Promise<void> => {
const confirmed = await showConfirmationDialog(element, {
title: addon.name,
text: "Do you want to restart the add-on with your changes?",
confirmText: "restart add-on",
dismissText: "no",
title: supervisor.localize("common.restart_name", "name", addon.name),
text: supervisor.localize("dialog.restart_addon.text"),
confirmText: supervisor.localize("dialog.restart_addon.confirm_text"),
dismissText: supervisor.localize("common.cancel"),
});
if (confirmed) {
try {
await restartHassioAddon(hass, addon.slug);
} catch (err) {
showAlertDialog(element, {
title: "Failed to restart",
title: supervisor.localize(
"common.failed_to_restart_name",
"name",
addon.name
),
text: extractApiErrorMessage(err),
});
}

View File

@@ -0,0 +1,208 @@
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,
ignoreSupervisorError,
} from "../../../../src/data/hassio/common";
import { createHassioPartialSnapshot } from "../../../../src/data/hassio/snapshot";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { SupervisorDialogSupervisorUpdateParams } from "./show-dialog-update";
@customElement("dialog-supervisor-update")
class DialogSupervisorUpdate extends LitElement {
public hass!: HomeAssistant;
@internalProperty() private _opened = false;
@internalProperty() private _createSnapshot = true;
@internalProperty() private _action: "snapshot" | "update" | null = null;
@internalProperty() private _error?: string;
@internalProperty()
private _dialogParams?: SupervisorDialogSupervisorUpdateParams;
public async showDialog(
params: SupervisorDialogSupervisorUpdateParams
): Promise<void> {
this._opened = true;
this._dialogParams = params;
await this.updateComplete;
}
public closeDialog(): void {
this._action = null;
this._createSnapshot = true;
this._error = undefined;
this._dialogParams = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
public focus(): void {
this.updateComplete.then(() =>
(this.shadowRoot?.querySelector(
"[dialogInitialFocus]"
) as HTMLElement)?.focus()
);
}
protected render(): TemplateResult {
if (!this._dialogParams) {
return html``;
}
return html`
<ha-dialog .open=${this._opened} scrimClickAction escapeKeyAction>
${this._action === null
? html`<slot name="heading">
<h2 id="title" class="header_title">
${this._dialogParams.supervisor.localize(
"confirm.update.title",
"name",
this._dialogParams.name
)}
</h2>
</slot>
<div>
${this._dialogParams.supervisor.localize(
"confirm.update.text",
"name",
this._dialogParams.name,
"version",
this._dialogParams.version
)}
</div>
<ha-settings-row>
<span slot="heading">
${this._dialogParams.supervisor.localize(
"dialog.update.snapshot"
)}
</span>
<span slot="description">
${this._dialogParams.supervisor.localize(
"dialog.update.create_snapshot",
"name",
this._dialogParams.name
)}
</span>
<ha-switch
.checked=${this._createSnapshot}
haptic
@click=${this._toggleSnapshot}
>
</ha-switch>
</ha-settings-row>
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
${this._dialogParams.supervisor.localize("common.cancel")}
</mwc-button>
<mwc-button
.disabled=${this._error !== undefined}
@click=${this._update}
slot="primaryAction"
>
${this._dialogParams.supervisor.localize("common.update")}
</mwc-button>`
: html`<ha-circular-progress alt="Updating" size="large" active>
</ha-circular-progress>
<p class="progress-text">
${this._action === "update"
? this._dialogParams.supervisor.localize(
"dialog.update.updating",
"name",
this._dialogParams.name,
"version",
this._dialogParams.version
)
: this._dialogParams.supervisor.localize(
"dialog.update.snapshotting",
"name",
this._dialogParams.name
)}
</p>`}
${this._error ? html`<p class="error">${this._error}</p>` : ""}
</ha-dialog>
`;
}
private _toggleSnapshot() {
this._createSnapshot = !this._createSnapshot;
}
private async _update() {
if (this._createSnapshot) {
this._action = "snapshot";
try {
await createHassioPartialSnapshot(
this.hass,
this._dialogParams!.snapshotParams
);
} catch (err) {
this._error = extractApiErrorMessage(err);
this._action = null;
return;
}
}
this._action = "update";
try {
await this._dialogParams!.updateHandler!();
} catch (err) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
this._error = extractApiErrorMessage(err);
}
this._action = null;
return;
}
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-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

@@ -3,7 +3,7 @@ import { atLeastVersion } from "../../src/common/config/version";
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
import { fireEvent } from "../../src/common/dom/fire_event";
import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
import { supervisorStore } from "../../src/data/supervisor/supervisor";
import { Supervisor } from "../../src/data/supervisor/supervisor";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import "../../src/layouts/hass-loading-screen";
import { HomeAssistant, Route } from "../../src/types";
@@ -14,6 +14,8 @@ import { SupervisorBaseElement } from "./supervisor-base-element";
export class HassioMain extends SupervisorBaseElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public panel!: HassioPanelInfo;
@property({ type: Boolean }) public narrow!: boolean;
@@ -72,16 +74,6 @@ export class HassioMain extends SupervisorBaseElement {
}
protected render() {
if (!this.supervisor || !this.hass) {
return html`<hass-loading-screen></hass-loading-screen>`;
}
if (
Object.keys(supervisorStore).some((store) => !this.supervisor![store])
) {
return html`<hass-loading-screen></hass-loading-screen>`;
}
return html`
<hassio-router
.hass=${this.hass}

View File

@@ -19,8 +19,12 @@ import {
} from "../../src/panels/my/ha-panel-my";
import { navigate } from "../../src/common/navigate";
import { HomeAssistant, Route } from "../../src/types";
import { Supervisor } from "../../src/data/supervisor/supervisor";
const REDIRECTS: Redirects = {
supervisor: {
redirect: "/hassio/dashboard",
},
supervisor_logs: {
redirect: "/hassio/system",
},
@@ -33,22 +37,27 @@ const REDIRECTS: Redirects = {
supervisor_store: {
redirect: "/hassio/store",
},
supervisor: {
redirect: "/hassio/dashboard",
},
supervisor_addon: {
redirect: "/hassio/addon",
params: {
addon: "string",
},
},
supervisor_add_addon_repository: {
redirect: "/hassio/store",
params: {
repository_url: "url",
},
},
};
@customElement("hassio-my-redirect")
class HassioMyRedirect extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public route!: Route;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public route!: Route;
@internalProperty() public _error?: TemplateResult | string;
@@ -58,15 +67,17 @@ class HassioMyRedirect extends LitElement {
const redirect = REDIRECTS[path];
if (!redirect) {
this._error = html`This redirect is not supported by your Home Assistant
instance. Check the
<a
this._error = this.supervisor.localize(
"my.not_supported",
"link",
html`<a
target="_blank"
rel="noreferrer noopener"
href="https://my.home-assistant.io/faq.html#supported-pages"
>My Home Assistant FAQ</a
>
for the supported redirects and the version they where introduced.`;
${this.supervisor.localize("my.faq_link")}
</a>`
);
return;
}
@@ -74,7 +85,7 @@ class HassioMyRedirect extends LitElement {
try {
url = this._createRedirectUrl(redirect);
} catch (err) {
this._error = "An unknown error occured";
this._error = this.supervisor.localize("my.error");
return;
}

View File

@@ -7,7 +7,10 @@ import {
property,
TemplateResult,
} from "lit-element";
import { Supervisor } from "../../src/data/supervisor/supervisor";
import {
Supervisor,
supervisorCollection,
} from "../../src/data/supervisor/supervisor";
import { HomeAssistant, Route } from "../../src/types";
import "./hassio-panel-router";
@@ -22,6 +25,17 @@ class HassioPanel extends LitElement {
@property({ attribute: false }) public route!: Route;
protected render(): TemplateResult {
if (!this.hass) {
return html`<hass-loading-screen></hass-loading-screen>`;
}
if (
Object.keys(supervisorCollection).some(
(collection) => !this.supervisor[collection]
)
) {
return html`<hass-loading-screen></hass-loading-screen>`;
}
return html`
<hassio-panel-router
.hass=${this.hass}

View File

@@ -23,7 +23,7 @@ class HassioRouter extends HassRouterPage {
protected routerOptions: RouterOptions = {
// Hass.io has a page with tabs, so we route all non-matching routes to it.
defaultPage: "dashboard",
initialLoad: () => this._fetchData(),
initialLoad: () => this._redirectIngress(),
showLoading: true,
routes: {
dashboard: {
@@ -50,7 +50,13 @@ class HassioRouter extends HassRouterPage {
protected updatePageEl(el) {
// the tabs page does its own routing so needs full route.
const route = el.nodeName === "HASSIO-PANEL" ? this.route : this.routeTail;
const hassioPanel = el.nodeName === "HASSIO-PANEL";
const route = hassioPanel ? this.route : this.routeTail;
if (hassioPanel && this.panel.config?.ingress) {
this._redirectIngress();
return;
}
el.hass = this.hass;
el.narrow = this.narrow;
@@ -63,15 +69,14 @@ class HassioRouter extends HassRouterPage {
}
}
private async _fetchData() {
private async _redirectIngress() {
if (this.panel.config && this.panel.config.ingress) {
this._redirectIngress(this.panel.config.ingress);
this.route = {
prefix: "/hassio",
path: `/ingress/${this.panel.config.ingress}`,
};
}
}
private _redirectIngress(addonSlug: string) {
this.route = { prefix: "/hassio", path: `/ingress/${addonSlug}` };
}
}
declare global {

View File

@@ -3,22 +3,22 @@ import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage";
export const supervisorTabs: PageNavigation[] = [
{
name: "Dashboard",
translationKey: "panel.dashboard",
path: `/hassio/dashboard`,
iconPath: mdiViewDashboard,
},
{
name: "Add-on Store",
translationKey: "panel.store",
path: `/hassio/store`,
iconPath: mdiStore,
},
{
name: "Snapshots",
translationKey: "panel.snapshots",
path: `/hassio/snapshots`,
iconPath: mdiBackupRestore,
},
{
name: "System",
translationKey: "panel.system",
path: `/hassio/system`,
iconPath: mdiCogs,
},

View File

@@ -55,8 +55,8 @@ import { hassioStyle } from "../resources/hassio-style";
interface CheckboxItem {
slug: string;
name: string;
checked: boolean;
name?: string;
}
@customElement("hassio-snapshots")
@@ -84,13 +84,12 @@ class HassioSnapshots extends LitElement {
@internalProperty() private _folderList: CheckboxItem[] = [
{
slug: "homeassistant",
name: "Home Assistant configuration",
checked: true,
},
{ slug: "ssl", name: "SSL", checked: true },
{ slug: "share", name: "Share", checked: true },
{ slug: "media", name: "Media", checked: true },
{ slug: "addons/local", name: "Local add-ons", checked: true },
{ slug: "ssl", checked: true },
{ slug: "share", checked: true },
{ slug: "media", checked: true },
{ slug: "addons/local", checked: true },
];
@internalProperty() private _error = "";
@@ -104,13 +103,16 @@ class HassioSnapshots extends LitElement {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow}
hassio
main-page
.route=${this.route}
.tabs=${supervisorTabs}
main-page
supervisor
>
<span slot="header">Snapshots</span>
<span slot="header">
${this.supervisor.localize("panel.snapshots")}
</span>
<ha-button-menu
corner="BOTTOM_START"
slot="toolbar-icon"
@@ -120,50 +122,50 @@ class HassioSnapshots extends LitElement {
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item>
Reload
${this.supervisor.localize("common.reload")}
</mwc-list-item>
${atLeastVersion(this.hass.config.version, 0, 116)
? html`<mwc-list-item>
Upload snapshot
${this.supervisor.localize("snapshot.upload_snapshot")}
</mwc-list-item>`
: ""}
</ha-button-menu>
<div class="content">
<h1>
Create Snapshot
${this.supervisor.localize("snapshot.create_snapshot")}
</h1>
<p class="description">
Snapshots allow you to easily backup and restore all data of your
Home Assistant instance.
${this.supervisor.localize("snapshot.description")}
</p>
<div class="card-group">
<ha-card>
<div class="card-content">
<paper-input
autofocus
label="Name"
.label=${this.supervisor.localize("snapshot.name")}
name="snapshotName"
.value=${this._snapshotName}
@value-changed=${this._handleTextValueChanged}
></paper-input>
Type:
${this.supervisor.localize("snapshot.type")}:
<paper-radio-group
name="snapshotType"
type="${this.supervisor.localize("snapshot.type")}"
.selected=${this._snapshotType}
@selected-changed=${this._handleRadioValueChanged}
>
<paper-radio-button name="full">
Full snapshot
${this.supervisor.localize("snapshot.full_snapshot")}
</paper-radio-button>
<paper-radio-button name="partial">
Partial snapshot
${this.supervisor.localize("snapshot.partial_snapshot")}
</paper-radio-button>
</paper-radio-group>
${this._snapshotType === "full"
? undefined
: html`
Folders:
${this.supervisor.localize("snapshot.folders")}:
${this._folderList.map(
(folder, idx) => html`
<paper-checkbox
@@ -171,11 +173,13 @@ class HassioSnapshots extends LitElement {
.checked=${folder.checked}
@checked-changed=${this._folderChecked}
>
${folder.name}
${this.supervisor.localize(
`snapshot.folder.${folder.slug}`
)}
</paper-checkbox>
`
)}
Add-ons:
${this.supervisor.localize("snapshot.addons")}:
${this._addonList.map(
(addon, idx) => html`
<paper-checkbox
@@ -188,18 +192,18 @@ class HassioSnapshots extends LitElement {
`
)}
`}
Security:
${this.supervisor.localize("snapshot.security")}:
<paper-checkbox
name="snapshotHasPassword"
.checked=${this._snapshotHasPassword}
@checked-changed=${this._handleCheckboxValueChanged}
>
Password protection
${this.supervisor.localize("snapshot.password_protection")}
</paper-checkbox>
${this._snapshotHasPassword
? html`
<paper-input
label="Password"
.label=${this.supervisor.localize("snapshot.password")}
type="password"
name="snapshotPassword"
.value=${this._snapshotPassword}
@@ -214,18 +218,22 @@ class HassioSnapshots extends LitElement {
<div class="card-actions">
<ha-progress-button
@click=${this._createSnapshot}
title="${this.supervisor.info.state !== "running"
? `Creating a snapshot is not possible right now because the system is in ${this.supervisor.info.state} state.`
: ""}"
.title=${this.supervisor.info.state !== "running"
? this.supervisor.localize(
"snapshot.create_blocked_not_running",
"state",
this.supervisor.info.state
)
: ""}
.disabled=${this.supervisor.info.state !== "running"}
>
Create
${this.supervisor.localize("snapshot.create")}
</ha-progress-button>
</div>
</ha-card>
</div>
<h1>Available Snapshots</h1>
<h1>${this.supervisor.localize("snapshot.available_snapshots")}</h1>
<div class="card-group">
${this._snapshots === undefined
? undefined
@@ -233,7 +241,7 @@ class HassioSnapshots extends LitElement {
? html`
<ha-card>
<div class="card-content">
You don't have any snapshots yet.
${this.supervisor.localize("snapshot.no_snapshots")}
</div>
</ha-card>
`
@@ -334,8 +342,12 @@ class HassioSnapshots extends LitElement {
private async _createSnapshot(ev: CustomEvent): Promise<void> {
if (this.supervisor.info.state !== "running") {
await showAlertDialog(this, {
title: "Could not create snapshot",
text: `Creating a snapshot is not possible right now because the system is in ${this.supervisor.info.state} state.`,
title: this.supervisor.localize("snapshot.could_not_create"),
text: this.supervisor.localize(
"snapshot.create_blocked_not_running",
"state",
this.supervisor.info.state
),
});
}
const button = ev.currentTarget as any;
@@ -343,7 +355,7 @@ class HassioSnapshots extends LitElement {
this._error = "";
if (this._snapshotHasPassword && !this._snapshotPassword.length) {
this._error = "Please enter a password.";
this._error = this.supervisor.localize("snapshot.enter_password");
button.progress = false;
return;
}
@@ -392,7 +404,9 @@ class HassioSnapshots extends LitElement {
private _computeDetails(snapshot: HassioSnapshot) {
const type =
snapshot.type === "full" ? "Full snapshot" : "Partial snapshot";
snapshot.type === "full"
? this.supervisor.localize("snapshot.full_snapshot")
: this.supervisor.localize("snapshot.partial_snapshot");
return snapshot.protected ? `${type}, password protected` : type;
}

View File

@@ -6,6 +6,7 @@ import {
PropertyValues,
} from "lit-element";
import { atLeastVersion } from "../../src/common/config/version";
import { computeLocalize } from "../../src/common/translations/localize";
import { fetchHassioAddonsInfo } from "../../src/data/hassio/addon";
import { HassioResponse } from "../../src/data/hassio/common";
import {
@@ -19,27 +20,31 @@ import {
fetchHassioInfo,
fetchHassioSupervisorInfo,
} from "../../src/data/hassio/supervisor";
import { fetchSupervisorStore } from "../../src/data/supervisor/store";
import {
getSupervisorEventCollection,
subscribeSupervisorEvents,
Supervisor,
SupervisorObject,
supervisorStore,
supervisorCollection,
} from "../../src/data/supervisor/supervisor";
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
import { urlSyncMixin } from "../../src/state/url-sync-mixin";
import { HomeAssistant } from "../../src/types";
import { getTranslation } from "../../src/util/common-translation";
declare global {
interface HASSDomEvents {
"supervisor-update": Partial<Supervisor>;
"supervisor-store-refresh": { store: SupervisorObject };
"supervisor-collection-refresh": { collection: SupervisorObject };
}
}
export class SupervisorBaseElement extends urlSyncMixin(
ProvideHassLitMixin(LitElement)
) {
@property({ attribute: false }) public supervisor?: Supervisor;
@property({ attribute: false }) public supervisor: Partial<Supervisor> = {
localize: () => "",
};
@internalProperty() private _unsubs: Record<string, UnsubscribeFunc> = {};
@@ -48,6 +53,13 @@ export class SupervisorBaseElement extends urlSyncMixin(
Collection<unknown>
> = {};
@internalProperty() private _language = "en";
public connectedCallback(): void {
super.connectedCallback();
this._initializeLocalize();
}
public disconnectedCallback() {
super.disconnectedCallback();
Object.keys(this._unsubs).forEach((unsub) => {
@@ -55,97 +67,155 @@ export class SupervisorBaseElement extends urlSyncMixin(
});
}
protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (changedProperties.has("hass")) {
const oldHass = changedProperties.get("hass") as
| HomeAssistant
| undefined;
if (
oldHass !== undefined &&
oldHass.language !== undefined &&
oldHass.language !== this.hass.language
) {
this._language = this.hass.language;
}
}
if (changedProperties.has("_language")) {
if (changedProperties.get("_language") !== this._language) {
this._initializeLocalize();
}
}
if (changedProperties.has("_collections")) {
if (this._collections) {
const unsubs = Object.keys(this._unsubs);
for (const collection of Object.keys(this._collections)) {
if (!unsubs.includes(collection)) {
this._unsubs[collection] = this._collections[
collection
].subscribe((data) =>
this._updateSupervisor({ [collection]: data })
);
}
}
}
}
}
protected _updateSupervisor(obj: Partial<Supervisor>): void {
this.supervisor = { ...this.supervisor!, ...obj };
this.supervisor = { ...this.supervisor, ...obj };
}
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
if (
this._language !== this.hass.language &&
this.hass.language !== undefined
) {
this._language = this.hass.language;
}
this._initializeLocalize();
this._initSupervisor();
}
private async _initializeLocalize() {
const { language, data } = await getTranslation(
null,
this._language,
"/api/hassio/app/static/translations"
);
this.supervisor = {
...this.supervisor,
localize: await computeLocalize(this.constructor.prototype, language, {
[language]: data,
}),
};
}
private async _handleSupervisorStoreRefreshEvent(ev) {
const store = ev.detail.store;
const collection = ev.detail.collection;
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
this._collections[store].refresh();
this._collections[collection].refresh();
return;
}
const response = await this.hass.callApi<HassioResponse<any>>(
"GET",
`hassio${supervisorStore[store]}`
`hassio${supervisorCollection[collection]}`
);
this._updateSupervisor({ [store]: response.data });
this._updateSupervisor({ [collection]: response.data });
}
private async _initSupervisor(): Promise<void> {
this.addEventListener(
"supervisor-store-refresh",
"supervisor-collection-refresh",
this._handleSupervisorStoreRefreshEvent
);
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
Object.keys(supervisorStore).forEach((store) => {
this._unsubs[store] = subscribeSupervisorEvents(
this.hass,
(data) => this._updateSupervisor({ [store]: data }),
store,
supervisorStore[store]
);
if (this._collections[store]) {
this._collections[store].refresh();
Object.keys(supervisorCollection).forEach((collection) => {
if (collection in this._collections) {
this._collections[collection].refresh();
} else {
this._collections[store] = getSupervisorEventCollection(
this._collections[collection] = getSupervisorEventCollection(
this.hass.connection,
store,
supervisorStore[store]
collection,
supervisorCollection[collection]
);
}
});
if (this.supervisor === undefined) {
Object.keys(this._collections).forEach((collection) =>
Object.keys(this._collections).forEach((collection) => {
if (
this.supervisor === undefined ||
this.supervisor[collection] === undefined
) {
this._updateSupervisor({
[collection]: this._collections[collection].state,
})
);
}
return;
});
}
});
} else {
const [
addon,
supervisor,
host,
core,
info,
os,
network,
resolution,
store,
] = await Promise.all([
fetchHassioAddonsInfo(this.hass),
fetchHassioSupervisorInfo(this.hass),
fetchHassioHostInfo(this.hass),
fetchHassioHomeAssistantInfo(this.hass),
fetchHassioInfo(this.hass),
fetchHassioHassOsInfo(this.hass),
fetchNetworkInfo(this.hass),
fetchHassioResolution(this.hass),
fetchSupervisorStore(this.hass),
]);
this._updateSupervisor({
addon,
supervisor,
host,
core,
info,
os,
network,
resolution,
store,
});
this.addEventListener("supervisor-update", (ev) =>
this._updateSupervisor(ev.detail)
);
}
const [
addon,
supervisor,
host,
core,
info,
os,
network,
resolution,
] = await Promise.all([
fetchHassioAddonsInfo(this.hass),
fetchHassioSupervisorInfo(this.hass),
fetchHassioHostInfo(this.hass),
fetchHassioHomeAssistantInfo(this.hass),
fetchHassioInfo(this.hass),
fetchHassioHassOsInfo(this.hass),
fetchNetworkInfo(this.hass),
fetchHassioResolution(this.hass),
]);
this.supervisor = {
addon,
supervisor,
host,
core,
info,
os,
network,
resolution,
};
this.addEventListener("supervisor-update", (ev) =>
this._updateSupervisor(ev.detail)
);
}
}

View File

@@ -30,6 +30,7 @@ import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { bytesToString } from "../../../src/util/bytes-to-string";
import "../components/supervisor-metric";
import { showDialogSupervisorUpdate } from "../dialogs/update/show-dialog-update";
import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-core-info")
@@ -43,11 +44,11 @@ class HassioCoreInfo extends LitElement {
protected render(): TemplateResult | void {
const metrics = [
{
description: "Core CPU Usage",
description: this.supervisor.localize("system.core.cpu_usage"),
value: this._metrics?.cpu_percent,
},
{
description: "Core RAM Usage",
description: this.supervisor.localize("system.core.ram_usage"),
value: this._metrics?.memory_percent,
tooltip: `${bytesToString(this._metrics?.memory_usage)}/${bytesToString(
this._metrics?.memory_limit
@@ -61,7 +62,7 @@ class HassioCoreInfo extends LitElement {
<div>
<ha-settings-row>
<span slot="heading">
Version
${this.supervisor.localize("common.version")}
</span>
<span slot="description">
core-${this.supervisor.core.version}
@@ -69,7 +70,7 @@ class HassioCoreInfo extends LitElement {
</ha-settings-row>
<ha-settings-row>
<span slot="heading">
Newest Version
${this.supervisor.localize("common.newest_version")}
</span>
<span slot="description">
core-${this.supervisor.core.version_latest}
@@ -77,10 +78,10 @@ class HassioCoreInfo extends LitElement {
${this.supervisor.core.update_available
? html`
<ha-progress-button
title="Update the core"
.title=${this.supervisor.localize("common.update")}
@click=${this._coreUpdate}
>
Update
${this.supervisor.localize("common.update")}
</ha-progress-button>
`
: ""}
@@ -104,9 +105,13 @@ class HassioCoreInfo extends LitElement {
slot="primaryAction"
class="warning"
@click=${this._coreRestart}
title="Restart Home Assistant Core"
.title=${this.supervisor.localize(
"common.restart_name",
"name",
"Core"
)}
>
Restart Core
${this.supervisor.localize("common.restart_name", "name", "Core")}
</ha-progress-button>
</div>
</ha-card>
@@ -126,10 +131,18 @@ class HassioCoreInfo extends LitElement {
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: "Restart Home Assistant Core",
text: "Are you sure you want to restart Home Assistant Core",
confirmText: "restart",
dismissText: "cancel",
title: this.supervisor.localize(
"confirm.restart.title",
"name",
"Home Assistant Core"
),
text: this.supervisor.localize(
"confirm.restart.text",
"name",
"Home Assistant Core"
),
confirmText: this.supervisor.localize("common.restart"),
dismissText: this.supervisor.localize("common.cancel"),
});
if (!confirmed) {
@@ -140,42 +153,40 @@ class HassioCoreInfo extends LitElement {
try {
await restartCore(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to restart Home Assistant Core",
text: extractApiErrorMessage(err),
});
if (this.hass.connection.connected) {
showAlertDialog(this, {
title: this.supervisor.localize(
"common.failed_to_restart_name",
"name",
"Home AssistantCore"
),
text: extractApiErrorMessage(err),
});
}
} finally {
button.progress = false;
}
}
private async _coreUpdate(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: "Update Home Assistant Core",
text: `Are you sure you want to update Home Assistant Core to version ${this.supervisor.core.version_latest}?`,
confirmText: "update",
dismissText: "cancel",
private async _coreUpdate(): Promise<void> {
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(),
});
}
if (!confirmed) {
button.progress = false;
return;
}
try {
await updateCore(this.hass);
fireEvent(this, "supervisor-store-refresh", { store: "core" });
} catch (err) {
showAlertDialog(this, {
title: "Failed to update Home Assistant Core",
text: extractApiErrorMessage(err),
});
} finally {
button.progress = false;
}
private async _updateCore(): Promise<void> {
await updateCore(this.hass);
fireEvent(this, "supervisor-collection-refresh", {
collection: "core",
});
}
static get styles(): CSSResult[] {

View File

@@ -21,7 +21,7 @@ import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row";
import {
extractApiErrorMessage,
ignoredStatusCodes,
ignoreSupervisorError,
} from "../../../src/data/hassio/common";
import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware";
import {
@@ -65,7 +65,7 @@ class HassioHostInfo extends LitElement {
const metrics = [
{
description: "Used Space",
description: this.supervisor.localize("system.host.used_space"),
value: this._getUsedSpace(
this.supervisor.host.disk_used,
this.supervisor.host.disk_total
@@ -80,14 +80,13 @@ class HassioHostInfo extends LitElement {
${this.supervisor.host.features.includes("hostname")
? html`<ha-settings-row>
<span slot="heading">
Hostname
${this.supervisor.localize("system.host.hostname")}
</span>
<span slot="description">
${this.supervisor.host.hostname}
</span>
<mwc-button
title="Change the hostname"
label="Change"
.label=${this.supervisor.localize("system.host.change")}
@click=${this._changeHostnameClicked}
>
</mwc-button>
@@ -96,14 +95,13 @@ class HassioHostInfo extends LitElement {
${this.supervisor.host.features.includes("network")
? html` <ha-settings-row>
<span slot="heading">
IP Address
${this.supervisor.localize("system.host.ip_address")}
</span>
<span slot="description">
${primaryIpAddress}
</span>
<mwc-button
title="Change the network"
label="Change"
.label=${this.supervisor.localize("system.host.change")}
@click=${this._changeNetworkClicked}
>
</mwc-button>
@@ -112,18 +110,15 @@ class HassioHostInfo extends LitElement {
<ha-settings-row>
<span slot="heading">
Operating System
${this.supervisor.localize("system.host.operating_system")}
</span>
<span slot="description">
${this.supervisor.host.operating_system}
</span>
${this.supervisor.os.update_available
? html`
<ha-progress-button
title="Update the host OS"
@click=${this._osUpdate}
>
Update
<ha-progress-button @click=${this._osUpdate}>
${this.supervisor.localize("commmon.update")}
</ha-progress-button>
`
: ""}
@@ -131,7 +126,7 @@ class HassioHostInfo extends LitElement {
${!this.supervisor.host.features.includes("hassos")
? html`<ha-settings-row>
<span slot="heading">
Docker version
${this.supervisor.localize("system.host.docker_version")}
</span>
<span slot="description">
${this.supervisor.info.docker}
@@ -141,7 +136,7 @@ class HassioHostInfo extends LitElement {
${this.supervisor.host.deployment
? html`<ha-settings-row>
<span slot="heading">
Deployment
${this.supervisor.localize("system.host.deployment")}
</span>
<span slot="description">
${this.supervisor.host.deployment}
@@ -154,11 +149,13 @@ class HassioHostInfo extends LitElement {
this.supervisor.host.disk_life_time >= 10
? html` <ha-settings-row>
<span slot="heading">
eMMC Lifetime Used
${this.supervisor.localize(
"system.host.emmc_lifetime_used"
)}
</span>
<span slot="description">
${this.supervisor.host.disk_life_time - 10}% -
${this.supervisor.host.disk_life_time}%
${this.supervisor.host.disk_life_time - 10} % -
${this.supervisor.host.disk_life_time} %
</span>
</ha-settings-row>`
: ""}
@@ -177,23 +174,18 @@ class HassioHostInfo extends LitElement {
<div class="card-actions">
${this.supervisor.host.features.includes("reboot")
? html`
<ha-progress-button
title="Reboot the host OS"
class="warning"
@click=${this._hostReboot}
>
Reboot Host
<ha-progress-button class="warning" @click=${this._hostReboot}>
${this.supervisor.localize("system.host.reboot_host")}
</ha-progress-button>
`
: ""}
${this.supervisor.host.features.includes("shutdown")
? html`
<ha-progress-button
title="Shutdown the host OS"
class="warning"
@click=${this._hostShutdown}
>
Shutdown Host
${this.supervisor.localize("system.host.shutdown_host")}
</ha-progress-button>
`
: ""}
@@ -205,14 +197,12 @@ class HassioHostInfo extends LitElement {
<mwc-icon-button slot="trigger">
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item title="Show a list of hardware">
Hardware
<mwc-list-item>
${this.supervisor.localize("system.host.hardware")}
</mwc-list-item>
${this.supervisor.host.features.includes("hassos")
? html`<mwc-list-item
title="Load HassOS configs or updates from USB"
>
Import from USB
? html`<mwc-list-item>
${this.supervisor.localize("system.host.import_from_usb")}
</mwc-list-item>`
: ""}
</ha-button-menu>
@@ -251,12 +241,14 @@ class HassioHostInfo extends LitElement {
try {
const content = await fetchHassioHardwareInfo(this.hass);
showHassioMarkdownDialog(this, {
title: "Hardware",
title: this.supervisor.localize("system.host.hardware"),
content: `<pre>${safeDump(content, { indent: 2 })}</pre>`,
});
} catch (err) {
showAlertDialog(this, {
title: "Failed to get hardware list",
title: this.supervisor.localize(
"system.host.failed_to_get_hardware_list"
),
text: extractApiErrorMessage(err),
});
}
@@ -267,10 +259,10 @@ class HassioHostInfo extends LitElement {
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: "Reboot",
text: "Are you sure you want to reboot the host?",
confirmText: "reboot host",
dismissText: "no",
title: this.supervisor.localize("system.host.reboot_host"),
text: this.supervisor.localize("system.host.confirm_reboot"),
confirmText: this.supervisor.localize("system.host.reboot_host"),
dismissText: this.supervisor.localize("common.cancel"),
});
if (!confirmed) {
@@ -282,9 +274,9 @@ class HassioHostInfo extends LitElement {
await rebootHost(this.hass);
} catch (err) {
// Ignore connection errors, these are all expected
if (err.status_code && !ignoredStatusCodes.has(err.status_code)) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
showAlertDialog(this, {
title: "Failed to reboot",
title: this.supervisor.localize("system.host.failed_to_reboot"),
text: extractApiErrorMessage(err),
});
}
@@ -297,10 +289,10 @@ class HassioHostInfo extends LitElement {
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: "Shutdown",
text: "Are you sure you want to shutdown the host?",
confirmText: "shutdown host",
dismissText: "no",
title: this.supervisor.localize("system.host.shutdown_host"),
text: this.supervisor.localize("system.host.confirm_shutdown"),
confirmText: this.supervisor.localize("system.host.shutdown_host"),
dismissText: this.supervisor.localize("common.cancel"),
});
if (!confirmed) {
@@ -312,9 +304,9 @@ class HassioHostInfo extends LitElement {
await shutdownHost(this.hass);
} catch (err) {
// Ignore connection errors, these are all expected
if (err.status_code && !ignoredStatusCodes.has(err.status_code)) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
showAlertDialog(this, {
title: "Failed to shutdown",
title: this.supervisor.localize("system.host.failed_to_shutdown"),
text: extractApiErrorMessage(err),
});
}
@@ -327,9 +319,19 @@ class HassioHostInfo extends LitElement {
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: "Update",
text: "Are you sure you want to update the OS?",
confirmText: "update os",
title: this.supervisor.localize(
"confirm.update.title",
"name",
"Home Assistant Operating System"
),
text: this.supervisor.localize(
"confirm.update.text",
"name",
"Home Assistant Operating System",
"version",
this.supervisor.os.version_latest
),
confirmText: this.supervisor.localize("common.update"),
dismissText: "no",
});
@@ -340,19 +342,25 @@ class HassioHostInfo extends LitElement {
try {
await updateOS(this.hass);
fireEvent(this, "supervisor-store-refresh", { store: "os" });
fireEvent(this, "supervisor-collection-refresh", { collection: "os" });
} catch (err) {
showAlertDialog(this, {
title: "Failed to update",
text: extractApiErrorMessage(err),
});
if (this.hass.connection.connected) {
showAlertDialog(this, {
title: this.supervisor.localize(
"common.failed_to_update_name",
"name",
"Home Assistant Operating System"
),
text: extractApiErrorMessage(err),
});
}
}
button.progress = false;
}
private async _changeNetworkClicked(): Promise<void> {
showNetworkDialog(this, {
network: this.supervisor.network!,
supervisor: this.supervisor,
loadData: () => this._loadData(),
});
}
@@ -360,19 +368,22 @@ class HassioHostInfo extends LitElement {
private async _changeHostnameClicked(): Promise<void> {
const curHostname: string = this.supervisor.host.hostname;
const hostname = await showPromptDialog(this, {
title: "Change Hostname",
inputLabel: "Please enter a new hostname:",
title: this.supervisor.localize("system.host.change_hostname"),
inputLabel: this.supervisor.localize("system.host.new_hostname"),
inputType: "string",
defaultValue: curHostname,
confirmText: this.supervisor.localize("common.update"),
});
if (hostname && hostname !== curHostname) {
try {
await changeHostOptions(this.hass, { hostname });
fireEvent(this, "supervisor-store-refresh", { store: "host" });
fireEvent(this, "supervisor-collection-refresh", {
collection: "host",
});
} catch (err) {
showAlertDialog(this, {
title: "Setting hostname failed",
title: this.supervisor.localize("system.host.failed_to_set_hostname"),
text: extractApiErrorMessage(err),
});
}
@@ -382,10 +393,14 @@ class HassioHostInfo extends LitElement {
private async _importFromUSB(): Promise<void> {
try {
await configSyncOS(this.hass);
fireEvent(this, "supervisor-store-refresh", { store: "host" });
fireEvent(this, "supervisor-collection-refresh", {
collection: "host",
});
} catch (err) {
showAlertDialog(this, {
title: "Failed to import from USB",
title: this.supervisor.localize(
"system.host.failed_to_import_from_usb"
),
text: extractApiErrorMessage(err),
});
}
@@ -393,7 +408,9 @@ class HassioHostInfo extends LitElement {
private async _loadData(): Promise<void> {
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
fireEvent(this, "supervisor-store-refresh", { store: "network" });
fireEvent(this, "supervisor-collection-refresh", {
collection: "network",
});
} else {
const network = await fetchNetworkInfo(this.hass);
fireEvent(this, "supervisor-update", { network });

View File

@@ -8,6 +8,7 @@ import {
property,
TemplateResult,
} from "lit-element";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card";
@@ -37,54 +38,26 @@ import { documentationUrl } from "../../../src/util/documentation-url";
import "../components/supervisor-metric";
import { hassioStyle } from "../resources/hassio-style";
const UNSUPPORTED_REASON = {
container: {
title: "Containers known to cause issues",
url: "/more-info/unsupported/container",
},
dbus: { title: "DBUS", url: "/more-info/unsupported/dbus" },
docker_configuration: {
title: "Docker Configuration",
url: "/more-info/unsupported/docker_configuration",
},
docker_version: {
title: "Docker Version",
url: "/more-info/unsupported/docker_version",
},
job_conditions: {
title: "Ignored job conditions",
url: "/more-info/unsupported/job_conditions",
},
lxc: { title: "LXC", url: "/more-info/unsupported/lxc" },
network_manager: {
title: "Network Manager",
url: "/more-info/unsupported/network_manager",
},
os: { title: "Operating System", url: "/more-info/unsupported/os" },
privileged: {
title: "Supervisor is not privileged",
url: "/more-info/unsupported/privileged",
},
systemd: { title: "Systemd", url: "/more-info/unsupported/systemd" },
const UNSUPPORTED_REASON_URL = {
container: "/more-info/unsupported/container",
dbus: "/more-info/unsupported/dbus",
docker_configuration: "/more-info/unsupported/docker_configuration",
docker_version: "/more-info/unsupported/docker_version",
job_conditions: "/more-info/unsupported/job_conditions",
lxc: "/more-info/unsupported/lxc",
network_manager: "/more-info/unsupported/network_manager",
os: "/more-info/unsupported/os",
privileged: "/more-info/unsupported/privileged",
systemd: "/more-info/unsupported/systemd",
content_trust: "/more-info/unsupported/content_trust",
};
const UNHEALTHY_REASON = {
privileged: {
title: "Supervisor is not privileged",
url: "/more-info/unsupported/privileged",
},
supervisor: {
title: "Supervisor was not able to update",
url: "/more-info/unhealthy/supervisor",
},
setup: {
title: "Setup of the Supervisor failed",
url: "/more-info/unhealthy/setup",
},
docker: {
title: "The Docker environment is not working properly",
url: "/more-info/unhealthy/docker",
},
const UNHEALTHY_REASON_URL = {
privileged: "/more-info/unsupported/privileged",
supervisor: "/more-info/unhealthy/supervisor",
setup: "/more-info/unhealthy/setup",
docker: "/more-info/unhealthy/docker",
untrusted: "/more-info/unhealthy/untrusted",
};
@customElement("hassio-supervisor-info")
@@ -98,11 +71,11 @@ class HassioSupervisorInfo extends LitElement {
protected render(): TemplateResult | void {
const metrics = [
{
description: "Supervisor CPU Usage",
description: this.supervisor.localize("system.supervisor.cpu_usage"),
value: this._metrics?.cpu_percent,
},
{
description: "Supervisor RAM Usage",
description: this.supervisor.localize("system.supervisor.ram_usage"),
value: this._metrics?.memory_percent,
tooltip: `${bytesToString(this._metrics?.memory_usage)}/${bytesToString(
this._metrics?.memory_limit
@@ -115,7 +88,7 @@ class HassioSupervisorInfo extends LitElement {
<div>
<ha-settings-row>
<span slot="heading">
Version
${this.supervisor.localize("common.version")}
</span>
<span slot="description">
supervisor-${this.supervisor.supervisor.version}
@@ -123,7 +96,7 @@ class HassioSupervisorInfo extends LitElement {
</ha-settings-row>
<ha-settings-row>
<span slot="heading">
Newest Version
${this.supervisor.localize("common.newest_version")}
</span>
<span slot="description">
supervisor-${this.supervisor.supervisor.version_latest}
@@ -131,17 +104,19 @@ class HassioSupervisorInfo extends LitElement {
${this.supervisor.supervisor.update_available
? html`
<ha-progress-button
title="Update the supervisor"
.title=${this.supervisor.localize(
"system.supervisor.update_supervisor"
)}
@click=${this._supervisorUpdate}
>
Update
${this.supervisor.localize("common.update")}
</ha-progress-button>
`
: ""}
</ha-settings-row>
<ha-settings-row>
<span slot="heading">
Channel
${this.supervisor.localize("system.supervisor.channel")}
</span>
<span slot="description">
${this.supervisor.supervisor.channel}
@@ -150,49 +125,65 @@ class HassioSupervisorInfo extends LitElement {
? html`
<ha-progress-button
@click=${this._toggleBeta}
title="Get stable updates for Home Assistant, supervisor and host"
.title=${this.supervisor.localize(
"system.supervisor.leave_beta_description"
)}
>
Leave beta channel
${this.supervisor.localize(
"system.supervisor.leave_beta_action"
)}
</ha-progress-button>
`
: this.supervisor.supervisor.channel === "stable"
? html`
<ha-progress-button
@click=${this._toggleBeta}
title="Get beta updates for Home Assistant (RCs), supervisor and host"
.title=${this.supervisor.localize(
"system.supervisor.join_beta_description"
)}
>
Join beta channel
${this.supervisor.localize(
"system.supervisor.join_beta_action"
)}
</ha-progress-button>
`
: ""}
</ha-settings-row>
${this.supervisor.supervisor.supported
? html` <ha-settings-row three-line>
<span slot="heading">
Share Diagnostics
</span>
<div slot="description" class="diagnostics-description">
Share crash reports and diagnostic information.
<button
class="link"
title="Show more information about this"
@click=${this._diagnosticsInformationDialog}
>
Learn more
</button>
</div>
<ha-switch
haptic
.checked=${this.supervisor.supervisor.diagnostics}
@change=${this._toggleDiagnostics}
></ha-switch>
</ha-settings-row>`
? !atLeastVersion(this.hass.config.version, 2021, 4)
? html` <ha-settings-row three-line>
<span slot="heading">
${this.supervisor.localize(
"system.supervisor.share_diagnostics"
)}
</span>
<div slot="description" class="diagnostics-description">
${this.supervisor.localize(
"system.supervisor.share_diagnostics_description"
)}
<button
class="link"
.title=${this.supervisor.localize("common.show_more")}
@click=${this._diagnosticsInformationDialog}
>
${this.supervisor.localize("common.learn_more")}
</button>
</div>
<ha-switch
haptic
.checked=${this.supervisor.supervisor.diagnostics}
@change=${this._toggleDiagnostics}
></ha-switch>
</ha-settings-row>`
: ""
: html`<div class="error">
You are running an unsupported installation.
${this.supervisor.localize(
"system.supervisor.unsupported_title"
)}
<button
class="link"
title="Learn more about how you can make your system compliant"
.title=${this.supervisor.localize("common.learn_more")}
@click=${this._unsupportedDialog}
>
Learn more
@@ -200,10 +191,12 @@ class HassioSupervisorInfo extends LitElement {
</div>`}
${!this.supervisor.supervisor.healthy
? html`<div class="error">
Your installation is running in an unhealthy state.
${this.supervisor.localize(
"system.supervisor.unhealthy_title"
)}
<button
class="link"
title="Learn more about why your system is marked as unhealthy"
.title=${this.supervisor.localize("common.learn_more")}
@click=${this._unhealthyDialog}
>
Learn more
@@ -227,16 +220,26 @@ class HassioSupervisorInfo extends LitElement {
<div class="card-actions">
<ha-progress-button
@click=${this._supervisorReload}
title="Reload parts of the Supervisor"
.title=${this.supervisor.localize(
"system.supervisor.reload_supervisor"
)}
>
Reload Supervisor
${this.supervisor.localize("system.supervisor.reload_supervisor")}
</ha-progress-button>
<ha-progress-button
class="warning"
@click=${this._supervisorRestart}
title="Restart the Supervisor"
.title=${this.supervisor.localize(
"common.restart_name",
"name",
"Supervisor"
)}
>
Restart Supervisor
${this.supervisor.localize(
"common.restart_name",
"name",
"Supervisor"
)}
</ha-progress-button>
</div>
</ha-card>
@@ -257,23 +260,23 @@ class HassioSupervisorInfo extends LitElement {
if (this.supervisor.supervisor.channel === "stable") {
const confirmed = await showConfirmationDialog(this, {
title: "WARNING",
text: html` Beta releases are for testers and early adopters and can
contain unstable code changes.
title: this.supervisor.localize("system.supervisor.warning"),
text: html`${this.supervisor.localize("system.supervisor.beta_warning")}
<br />
<b>
Make sure you have backups of your data before you activate this
feature.
${this.supervisor.localize("system.supervisor.beta_backup")}
</b>
<br /><br />
This includes beta releases for:
${this.supervisor.localize("system.supervisor.beta_release_items")}
<li>Home Assistant Core</li>
<li>Home Assistant Supervisor</li>
<li>Home Assistant Operating System</li>
<br />
Do you want to join the beta channel?`,
confirmText: "join beta",
dismissText: "no",
${this.supervisor.localize("system.supervisor.join_beta_action")}`,
confirmText: this.supervisor.localize(
"system.supervisor.beta_join_confirm"
),
dismissText: this.supervisor.localize("common.cancel"),
});
if (!confirmed) {
@@ -291,7 +294,9 @@ class HassioSupervisorInfo extends LitElement {
await this._reloadSupervisor();
} catch (err) {
showAlertDialog(this, {
title: "Failed to set supervisor option",
title: this.supervisor.localize(
"system.supervisor.failed_to_set_option"
),
text: extractApiErrorMessage(err),
});
} finally {
@@ -307,7 +312,7 @@ class HassioSupervisorInfo extends LitElement {
await this._reloadSupervisor();
} catch (err) {
showAlertDialog(this, {
title: "Failed to reload the supervisor",
title: this.supervisor.localize("system.supervisor.failed_to_reload"),
text: extractApiErrorMessage(err),
});
} finally {
@@ -317,7 +322,9 @@ class HassioSupervisorInfo extends LitElement {
private async _reloadSupervisor(): Promise<void> {
await reloadSupervisor(this.hass);
fireEvent(this, "supervisor-store-refresh", { store: "supervisor" });
fireEvent(this, "supervisor-collection-refresh", {
collection: "supervisor",
});
}
private async _supervisorRestart(ev: CustomEvent): Promise<void> {
@@ -325,10 +332,18 @@ class HassioSupervisorInfo extends LitElement {
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: "Restart the Supervisor",
text: "Are you sure you want to restart the Supervisor",
confirmText: "restart",
dismissText: "cancel",
title: this.supervisor.localize(
"confirm.restart.title",
"name",
"Supervisor"
),
text: this.supervisor.localize(
"confirm.restart.text",
"name",
"Supervisor"
),
confirmText: this.supervisor.localize("common.restart"),
dismissText: this.supervisor.localize("common.cancel"),
});
if (!confirmed) {
@@ -340,7 +355,11 @@ class HassioSupervisorInfo extends LitElement {
await restartSupervisor(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to restart the supervisor",
title: this.supervisor.localize(
"common.failed_to_restart_name",
"name",
"Supervisor"
),
text: extractApiErrorMessage(err),
});
} finally {
@@ -353,10 +372,20 @@ class HassioSupervisorInfo extends LitElement {
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: "Update Supervisor",
text: `Are you sure you want to update supervisor to version ${this.supervisor.supervisor.version_latest}?`,
confirmText: "update",
dismissText: "cancel",
title: this.supervisor.localize(
"confirm.update.title",
"name",
"Supervisor"
),
text: this.supervisor.localize(
"confirm.update.text",
"name",
"Supervisor",
"version",
this.supervisor.supervisor.version_latest
),
confirmText: this.supervisor.localize("common.update"),
dismissText: this.supervisor.localize("common.cancel"),
});
if (!confirmed) {
@@ -366,10 +395,16 @@ class HassioSupervisorInfo extends LitElement {
try {
await updateSupervisor(this.hass);
fireEvent(this, "supervisor-store-refresh", { store: "supervisor" });
fireEvent(this, "supervisor-collection-refresh", {
collection: "supervisor",
});
} catch (err) {
showAlertDialog(this, {
title: "Failed to update the supervisor",
title: this.supervisor.localize(
"common.failed_to_update_name",
"name",
"Supervisor"
),
text: extractApiErrorMessage(err),
});
} finally {
@@ -379,40 +414,41 @@ class HassioSupervisorInfo extends LitElement {
private async _diagnosticsInformationDialog(): Promise<void> {
await showAlertDialog(this, {
title: "Help Improve Home Assistant",
text: html`Would you want to automatically share crash reports and
diagnostic information when the supervisor encounters unexpected errors?
<br /><br />
This will allow us to fix the problems, the information is only
accessible to the Home Assistant Core team and will not be shared with
others.
<br /><br />
The data does not include any private/sensitive information and you can
disable this in settings at any time you want.`,
title: this.supervisor.localize(
"system.supervisor.share_diagonstics_title"
),
text: this.supervisor.localize(
"system.supervisor.share_diagonstics_description",
"line_break",
html`<br /><br />`
),
});
}
private async _unsupportedDialog(): Promise<void> {
await showAlertDialog(this, {
title: "You are running an unsupported installation",
text: html`Below is a list of issues found with your installation, click
on the links to learn how you can resolve the issues. <br /><br />
title: this.supervisor.localize("system.supervisor.unsupported_title"),
text: html`${this.supervisor.localize(
"system.supervisor.unsupported_description"
)} <br /><br />
<ul>
${this.supervisor.resolution.unsupported.map(
(issue) => html`
(reason) => html`
<li>
${UNSUPPORTED_REASON[issue]
${UNSUPPORTED_REASON_URL[reason]
? html`<a
href="${documentationUrl(
this.hass,
UNSUPPORTED_REASON[issue].url
UNSUPPORTED_REASON_URL[reason]
)}"
target="_blank"
rel="noreferrer"
>
${UNSUPPORTED_REASON[issue].title}
${this.supervisor.localize(
`system.supervisor.unsupported_reason.${reason}`
) || reason}
</a>`
: issue}
: reason}
</li>
`
)}
@@ -422,26 +458,28 @@ class HassioSupervisorInfo extends LitElement {
private async _unhealthyDialog(): Promise<void> {
await showAlertDialog(this, {
title: "Your installation is unhealthy",
text: html`Running an unhealthy installation will cause issues. Below is a
list of issues found with your installation, click on the links to learn
how you can resolve the issues. <br /><br />
title: this.supervisor.localize("system.supervisor.unhealthy_title"),
text: html`${this.supervisor.localize(
"system.supervisor.unhealthy_description"
)} <br /><br />
<ul>
${this.supervisor.resolution.unhealthy.map(
(issue) => html`
(reason) => html`
<li>
${UNHEALTHY_REASON[issue]
${UNHEALTHY_REASON_URL[reason]
? html`<a
href="${documentationUrl(
this.hass,
UNHEALTHY_REASON[issue].url
UNHEALTHY_REASON_URL[reason]
)}"
target="_blank"
rel="noreferrer"
>
${UNHEALTHY_REASON[issue].title}
${this.supervisor.localize(
`system.supervisor.unhealthy_reason.${reason}`
) || reason}
</a>`
: issue}
: reason}
</li>
`
)}
@@ -457,7 +495,9 @@ class HassioSupervisorInfo extends LitElement {
await setSupervisorOption(this.hass, data);
} catch (err) {
showAlertDialog(this, {
title: "Failed to set supervisor option",
title: this.supervisor.localize(
"system.supervisor.failed_to_set_option"
),
text: extractApiErrorMessage(err),
});
}

View File

@@ -16,6 +16,7 @@ import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { fetchHassioLogs } from "../../../src/data/hassio/supervisor";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import "../../../src/layouts/hass-loading-screen";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
@@ -58,6 +59,8 @@ const logProviders: LogProvider[] = [
class HassioSupervisorLog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@internalProperty() private _error?: string;
@internalProperty() private _selectedLogProvider = "supervisor";
@@ -76,7 +79,7 @@ class HassioSupervisorLog extends LitElement {
${this.hass.userData?.showAdvanced
? html`
<paper-dropdown-menu
label="Log Provider"
.label=${this.supervisor.localize("system.log.log_provider")}
@iron-select=${this._setLogProvider}
>
<paper-listbox
@@ -86,9 +89,9 @@ class HassioSupervisorLog extends LitElement {
>
${logProviders.map((provider) => {
return html`
<paper-item provider=${provider.key}
>${provider.name}</paper-item
>
<paper-item provider=${provider.key}>
${provider.name}
</paper-item>
`;
})}
</paper-listbox>
@@ -98,14 +101,13 @@ class HassioSupervisorLog extends LitElement {
<div class="card-content" id="content">
${this._content
? html`<hassio-ansi-to-html
.content=${this._content}
></hassio-ansi-to-html>`
? html`<hassio-ansi-to-html .content=${this._content}>
</hassio-ansi-to-html>`
: html`<hass-loading-screen no-toolbar></hass-loading-screen>`}
</div>
<div class="card-actions">
<ha-progress-button @click=${this._refresh}>
Refresh
${this.supervisor.localize("common.refresh")}
</ha-progress-button>
</div>
</ha-card>
@@ -134,9 +136,13 @@ class HassioSupervisorLog extends LitElement {
this._selectedLogProvider
);
} catch (err) {
this._error = `Failed to get supervisor logs, ${extractApiErrorMessage(
err
)}`;
this._error = this.supervisor.localize(
"system.log.get_logs",
"provider",
this._selectedLogProvider,
"error",
extractApiErrorMessage(err)
);
}
}

View File

@@ -32,13 +32,16 @@ class HassioSystem extends LitElement {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow}
hassio
main-page
.route=${this.route}
.tabs=${supervisorTabs}
main-page
supervisor
>
<span slot="header">System</span>
<span slot="header">
${this.supervisor.localize("panel.system")}
</span>
<div class="content">
<div class="card-group">
<hassio-core-info
@@ -54,7 +57,10 @@ class HassioSystem extends LitElement {
.supervisor=${this.supervisor}
></hassio-host-info>
</div>
<hassio-supervisor-log .hass=${this.hass}></hassio-supervisor-log>
<hassio-supervisor-log
.hass=${this.hass}
.supervisor=${this.supervisor}
></hassio-supervisor-log>
</div>
</hass-tabs-subpage>
`;

View File

@@ -23,6 +23,17 @@
"license": "Apache-2.0",
"dependencies": {
"@braintree/sanitize-url": "^5.0.0",
"@codemirror/commands": "^0.18.0",
"@codemirror/gutter": "^0.18.0",
"@codemirror/highlight": "^0.18.1",
"@codemirror/history": "^0.18.0",
"@codemirror/legacy-modes": "^0.18.0",
"@codemirror/rectangular-selection": "^0.18.0",
"@codemirror/search": "^0.18.0",
"@codemirror/state": "^0.18.0",
"@codemirror/stream-parser": "^0.18.0",
"@codemirror/text": "^0.18.0",
"@codemirror/view": "^0.18.0",
"@formatjs/intl-getcanonicallocales": "^1.4.6",
"@formatjs/intl-pluralrules": "^3.4.10",
"@fullcalendar/common": "5.1.0",
@@ -46,8 +57,8 @@
"@material/mwc-tab": "^0.20.0",
"@material/mwc-tab-bar": "^0.20.0",
"@material/top-app-bar": "=9.0.0-canary.1c156d69d.0",
"@mdi/js": "5.6.55",
"@mdi/svg": "5.6.55",
"@mdi/js": "5.9.55",
"@mdi/svg": "5.9.55",
"@polymer/app-layout": "^3.0.2",
"@polymer/app-route": "^3.0.2",
"@polymer/app-storage": "^3.0.2",
@@ -80,8 +91,6 @@
"@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "3.1.0",
"@thomasloven/round-slider": "0.5.2",
"@types/chromecast-caf-sender": "^1.0.3",
"@types/sortablejs": "^1.10.6",
"@vaadin/vaadin-combo-box": "^5.0.10",
"@vaadin/vaadin-date-picker": "^4.0.7",
"@vibrant/color": "^3.2.1-alpha.1",
@@ -101,7 +110,7 @@
"fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2",
"hls.js": "^0.13.2",
"home-assistant-js-websocket": "^5.8.1",
"home-assistant-js-websocket": "^5.9.0",
"idb-keyval": "^3.2.0",
"intl-messageformat": "^8.3.9",
"js-yaml": "^3.13.1",
@@ -123,6 +132,7 @@
"sortablejs": "^1.10.2",
"superstruct": "^0.10.13",
"tinykeys": "^1.1.1",
"tsparticles": "^1.19.2",
"unfetch": "^4.1.0",
"vis-data": "^7.1.1",
"vis-network": "^8.5.4",
@@ -156,6 +166,7 @@
"@rollup/plugin-replace": "^2.3.2",
"@types/chai": "^4.1.7",
"@types/chromecast-caf-receiver": "^5.0.11",
"@types/chromecast-caf-sender": "^1.0.3",
"@types/codemirror": "^0.0.97",
"@types/hls.js": "^0.12.3",
"@types/js-yaml": "^3.12.1",
@@ -165,6 +176,7 @@
"@types/memoize-one": "4.1.0",
"@types/mocha": "^7.0.2",
"@types/resize-observer-browser": "^0.1.3",
"@types/sortablejs": "^1.10.6",
"@types/webspeechapi": "^0.0.29",
"@typescript-eslint/eslint-plugin": "^4.4.0",
"@typescript-eslint/parser": "^4.4.0",
@@ -177,7 +189,7 @@
"eslint": "^6.8.0",
"eslint-config-airbnb-typescript": "^7.2.1",
"eslint-config-prettier": "^6.10.1",
"eslint-import-resolver-webpack": "^0.12.2",
"eslint-import-resolver-webpack": "^0.13.0",
"eslint-plugin-disable": "^2.0.1",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-lit": "^1.2.0",
@@ -213,16 +225,16 @@
"sinon": "^7.3.1",
"source-map-url": "^0.4.0",
"systemjs": "^6.3.2",
"terser-webpack-plugin": "^5.0.0",
"terser-webpack-plugin": "^5.1.1",
"ts-lit-plugin": "^1.2.1",
"ts-mocha": "^7.0.0",
"typescript": "^4.0.3",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0",
"webpack": "5.1.3",
"webpack-cli": "4.1.0",
"webpack-dev-server": "^3.11.0",
"webpack-manifest-plugin": "3.0.0-rc.0",
"webpack": "^5.24.1",
"webpack-cli": "^4.5.0",
"webpack-dev-server": "^3.11.2",
"webpack-manifest-plugin": "^3.0.0",
"workbox-build": "^5.1.3"
},
"_comment": "Polymer fixed to 3.1 because 3.2 throws on logbook page",

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -12,5 +12,5 @@ yarn install
script/build_frontend
rm -rf dist
python3 setup.py sdist
python3 setup.py -q sdist
python3 -m twine upload dist/* --skip-existing

View File

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

View File

@@ -5,11 +5,16 @@ export const atLeastVersion = (
patch?: number
): boolean => {
const [haMajor, haMinor, haPatch] = version.split(".", 3);
return (
Number(haMajor) > major ||
(Number(haMajor) === major && Number(haMinor) >= minor) ||
(Number(haMajor) === major &&
(patch === undefined
? Number(haMinor) >= minor
: Number(haMinor) > minor)) ||
(patch !== undefined &&
Number(haMajor) === major && Number(haMinor) === minor &&
Number(haMajor) === major &&
Number(haMinor) === minor &&
Number(haPatch) >= patch)
);
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,12 +8,19 @@ export const batteryIcon = (
const battery = Number(batteryState.state);
const battery_charging =
batteryChargingState && batteryChargingState.state === "on";
let icon = "hass:battery";
if (isNaN(battery)) {
return "hass:battery-unknown";
if (batteryState.state === "off") {
icon += "-full";
} else if (batteryState.state === "on") {
icon += "-alert";
} else {
icon += "-unknown";
}
return icon;
}
let icon = "hass:battery";
const batteryRound = Math.round(battery / 10) * 10;
if (battery_charging && battery > 10) {
icon += `-charging-${batteryRound}`;

View File

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

View File

@@ -42,7 +42,7 @@ export const fuzzySequentialMatch = (filter: string, ...words: string[]) => {
export interface ScorableTextItem {
score?: number;
text: string;
filterText: string;
altText?: string;
}
@@ -55,8 +55,8 @@ export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => {
return items
.map((item) => {
item.score = item.altText
? fuzzySequentialMatch(filter, item.text, item.altText)
: fuzzySequentialMatch(filter, item.text);
? fuzzySequentialMatch(filter, item.filterText, item.altText)
: fuzzySequentialMatch(filter, item.filterText);
return item;
})
.filter((item) => item.score !== undefined && item.score > 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 language The language to use when formatting the number
* @param locale The user-selected language and number format, from `hass.locale`
* @param options Intl.NumberFormatOptions to use
*/
export const formatNumber = (
num: string | number,
language: string,
locale?: FrontendTranslationData,
options?: Intl.NumberFormatOptions
): string => {
let format: string | string[] | undefined;
switch (locale?.number_format) {
case NumberFormat.comma_decimal:
format = ["en-US", "en"]; // Use United States with fallback to English formatting 1,234,567.89
break;
case NumberFormat.decimal_comma:
format = ["de", "es", "it"]; // Use German with fallback to Spanish then Italian formatting 1.234.567,89
break;
case NumberFormat.space_comma:
format = ["fr", "sv", "cs"]; // Use French with fallback to Swedish and Czech formatting 1 234 567,89
break;
case NumberFormat.system:
format = undefined;
break;
default:
format = locale?.language;
}
// Polyfill for Number.isNaN, which is more reliable than the global isNaN()
Number.isNaN =
Number.isNaN ||
@@ -16,13 +38,27 @@ export const formatNumber = (
return typeof input === "number" && isNaN(input);
};
if (!Number.isNaN(Number(num)) && Intl) {
return new Intl.NumberFormat(
language,
getDefaultFormatOptions(num, options)
).format(Number(num));
if (
!Number.isNaN(Number(num)) &&
Intl &&
locale?.number_format !== NumberFormat.none
) {
try {
return new Intl.NumberFormat(
format,
getDefaultFormatOptions(num, options)
).format(Number(num));
} catch (error) {
// Don't fail when using "TEST" language
// eslint-disable-next-line no-console
console.error(error);
return new Intl.NumberFormat(
undefined,
getDefaultFormatOptions(num, options)
).format(Number(num));
}
}
return num.toString();
return num ? num.toString() : "";
};
/**

View File

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

View File

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

View File

@@ -15,7 +15,7 @@ export const iconColorCSS = css`
ha-icon[data-domain="media_player"][data-state="on"],
ha-icon[data-domain="media_player"][data-state="paused"],
ha-icon[data-domain="media_player"][data-state="playing"],
ha-icon[data-domain="script"][data-state="running"],
ha-icon[data-domain="script"][data-state="on"],
ha-icon[data-domain="sun"][data-state="above_horizon"],
ha-icon[data-domain="switch"][data-state="on"],
ha-icon[data-domain="timer"][data-state="active"],

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

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

View File

@@ -6,6 +6,7 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
PropertyValues,
@@ -33,7 +34,8 @@ import {
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import { HaComboBox } from "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-combo-box";
interface Device {
name: string;
@@ -107,10 +109,11 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property({ type: Boolean })
private _opened?: boolean;
@property({ type: Boolean }) public disabled?: boolean;
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
@internalProperty() private _opened?: boolean;
@query("ha-combo-box", true) public comboBox!: HaComboBox;
private _init = false;
@@ -239,11 +242,11 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
);
public open() {
this._comboBox?.open();
this.comboBox?.open();
}
public focus() {
this._comboBox?.focus();
this.comboBox?.focus();
}
public hassSubscribe(): UnsubscribeFunc[] {
@@ -266,7 +269,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
(changedProps.has("_opened") && this._opened)
) {
this._init = true;
(this._comboBox as any).items = this._getDevices(
(this.comboBox as any).items = this._getDevices(
this.devices!,
this.areas!,
this.entities!,
@@ -290,6 +293,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
: this.label}
.value=${this._value}
.renderer=${rowRenderer}
.disabled=${this.disabled}
item-value-path="id"
item-id-path="id"
item-label-path="name"

View File

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

View File

@@ -17,6 +17,7 @@ import { forwardHaptic } from "../../data/haptics";
import { HomeAssistant } from "../../types";
import "../ha-icon-button";
import "../ha-switch";
import "../ha-formfield";
const isOn = (stateObj?: HassEntity) =>
stateObj !== undefined &&
@@ -29,6 +30,8 @@ export class HaEntityToggle extends LitElement {
@property() public stateObj?: HassEntity;
@property() public label?: string;
@internalProperty() private _isOn = false;
protected render(): TemplateResult {
@@ -55,15 +58,21 @@ export class HaEntityToggle extends LitElement {
`;
}
const switchTemplate = html`<ha-switch
aria-label=${`Toggle ${computeStateName(this.stateObj)} ${
this._isOn ? "off" : "on"
}`}
.checked=${this._isOn}
.disabled=${UNAVAILABLE_STATES.includes(this.stateObj.state)}
@change=${this._toggleChanged}
></ha-switch>`;
if (!this.label) {
return switchTemplate;
}
return html`
<ha-switch
aria-label=${`Toggle ${computeStateName(this.stateObj)} ${
this._isOn ? "off" : "on"
}`}
.checked=${this._isOn}
.disabled=${UNAVAILABLE_STATES.includes(this.stateObj.state)}
@change=${this._toggleChanged}
></ha-switch>
<ha-formfield .label=${this.label}>${switchTemplate}</ha-formfield>
`;
}

View File

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

View File

@@ -115,7 +115,7 @@ export class StateBadge extends LitElement {
// eslint-disable-next-line
console.warn(errorMessage);
}
// lowest brighntess will be around 50% (that's pretty dark)
// lowest brightness will be around 50% (that's pretty dark)
iconStyle.filter = `brightness(${(brightness + 245) / 5}%)`;
}
}

View File

@@ -84,7 +84,7 @@ class StateInfo extends LitElement {
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.language !== this.hass.language) {
if (!oldHass || oldHass.locale !== this.hass.locale) {
this.rtl = computeRTL(this.hass);
}
}

View File

@@ -0,0 +1,148 @@
import {
customElement,
html,
internalProperty,
LitElement,
property,
query,
TemplateResult,
} from "lit-element";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event";
import { compare } from "../common/string/compare";
import { HassioAddonInfo } from "../data/hassio/addon";
import { fetchHassioSupervisorInfo } from "../data/hassio/supervisor";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types";
import { HaComboBox } from "./ha-combo-box";
const rowRenderer = (
root: HTMLElement,
_owner,
model: { item: HassioAddonInfo }
) => {
if (!root.firstElementChild) {
root.innerHTML = `
<style>
paper-item {
margin: -10px 0;
padding: 0;
}
</style>
<paper-item>
<paper-item-body two-line="">
<div class='name'>[[item.name]]</div>
<div secondary>[[item.slug]]</div>
</paper-item-body>
</paper-item>
`;
}
root.querySelector(".name")!.textContent = model.item.name;
root.querySelector("[secondary]")!.textContent = model.item.slug;
};
@customElement("ha-addon-picker")
class HaAddonPicker extends LitElement {
public hass!: HomeAssistant;
@property() public label?: string;
@property() public value = "";
@internalProperty() private _addons?: HassioAddonInfo[];
@property({ type: Boolean }) public disabled = false;
@query("ha-combo-box") private _comboBox!: HaComboBox;
public open() {
this._comboBox?.open();
}
public focus() {
this._comboBox?.focus();
}
protected firstUpdated() {
this._getAddons();
}
protected render(): TemplateResult {
if (!this._addons) {
return html``;
}
return html`
<ha-combo-box
.hass=${this.hass}
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.addon-picker.addon")
: this.label}
.value=${this._value}
.renderer=${rowRenderer}
.items=${this._addons}
item-value-path="slug"
item-id-path="slug"
item-label-path="name"
@value-changed=${this._addonChanged}
></ha-combo-box>
`;
}
private async _getAddons() {
try {
if (isComponentLoaded(this.hass, "hassio")) {
const supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
this._addons = supervisorInfo.addons.sort((a, b) =>
compare(a.name, b.name)
);
} else {
showAlertDialog(this, {
title: this.hass.localize(
"ui.componencts.addon-picker.error.no_supervisor.title"
),
text: this.hass.localize(
"ui.componencts.addon-picker.error.no_supervisor.description"
),
});
}
} catch (error) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.componencts.addon-picker.error.fetch_addons.title"
),
text: this.hass.localize(
"ui.componencts.addon-picker.error.fetch_addons.description"
),
});
}
}
private get _value() {
return this.value || "";
}
private _addonChanged(ev: PolymerChangedEvent<string>) {
ev.stopPropagation();
const newValue = ev.detail.value;
if (newValue !== this._value) {
this._setValue(newValue);
}
}
private _setValue(value: string) {
this.value = value;
setTimeout(() => {
fireEvent(this, "value-changed", { value });
fireEvent(this, "change");
}, 0);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-addon-picker": HaAddonPicker;
}
}

View File

@@ -0,0 +1,188 @@
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 { documentationUrl } from "../util/documentation-url";
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 {
if (!this.analytics.huuid) {
return html``;
}
const enabled = this.analytics.preferences.base;
return html`
<p>
${this.hass.localize(
"ui.panel.config.core.section.core.analytics.instance_id",
"huuid",
this.analytics.huuid
)}
</p>
<ha-settings-row>
<span slot="prefix">
<ha-checkbox
@change=${this._handleRowCheckboxClick}
.checked=${enabled}
.preference=${"base"}
>
</ha-checkbox>
</span>
<span slot="heading">
${this.hass.localize(
`ui.panel.config.core.section.core.analytics.preference.base.title`
)}
</span>
<span slot="description">
${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}
.disabled=${!enabled}
>
</ha-checkbox>
${!enabled
? 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">
${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">
${preference === "usage"
? 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`
)
: this.hass.localize(
`ui.panel.config.core.section.core.analytics.preference.${preference}.description`
)}
</span>
</ha-settings-row>`
)}
<ha-settings-row>
<span slot="prefix">
<ha-checkbox
@change=${this._handleRowCheckboxClick}
.checked=${this.analytics.preferences.diagnostics}
.preference=${"diagnostics"}
>
</ha-checkbox>
</span>
<span slot="heading">
${this.hass.localize(
`ui.panel.config.core.section.core.analytics.preference.diagnostics.title`
)}
</span>
<span slot="description">
${this.hass.localize(
`ui.panel.config.core.section.core.analytics.preference.diagnostics.description`
)}
</span>
</ha-settings-row>
<p>
<a
.href=${documentationUrl(this.hass, "/integrations/analytics/")}
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.core.section.core.analytics.learn_more"
)}
</a>
</p>
`;
}
private _handleRowCheckboxClick(ev: Event) {
const checkbox = ev.currentTarget as HaCheckbox;
const preference = (checkbox as any).preference;
const preferences = { ...this.analytics.preferences };
if (checkbox.checked) {
if (preferences[preference]) {
return;
}
preferences[preference] = true;
} else {
preferences[preference] = false;
}
fireEvent(this, "analytics-preferences-changed", { preferences });
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
.error {
color: var(--error-color);
}
ha-settings-row {
padding: 0;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-analytics": HaAnalytics;
}
}

View File

@@ -117,6 +117,8 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
@property() public entityFilter?: (entity: EntityRegistryEntry) => boolean;
@property({ type: Boolean }) public disabled?: boolean;
@internalProperty() private _areas?: AreaRegistryEntry[];
@internalProperty() private _devices?: DeviceRegistryEntry[];
@@ -125,7 +127,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
@internalProperty() private _opened?: boolean;
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
@query("vaadin-combo-box-light", true) public comboBox!: HTMLElement;
private _init = false;
@@ -192,10 +194,13 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
}
inputDevices = devices;
inputEntities = entities.filter((entity) => entity.area_id);
} else if (deviceFilter) {
inputDevices = devices;
} else if (entityFilter) {
inputEntities = entities.filter((entity) => entity.area_id);
} else {
if (deviceFilter) {
inputDevices = devices;
}
if (entityFilter) {
inputEntities = entities.filter((entity) => entity.area_id);
}
}
if (includeDomains) {
@@ -314,7 +319,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
(changedProps.has("_opened") && this._opened)
) {
this._init = true;
(this._comboBox as any).items = this._getAreas(
(this.comboBox as any).items = this._getAreas(
this._areas!,
this._devices!,
this._entities!,
@@ -339,6 +344,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
item-label-path="name"
.value=${this._value}
.renderer=${rowRenderer}
.disabled=${this.disabled}
@opened-changed=${this._openedChanged}
@value-changed=${this._areaChanged}
>
@@ -349,6 +355,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
.placeholder=${this.placeholder
? this._area(this.placeholder)?.name
: undefined}
.disabled=${this.disabled}
class="input"
autocapitalize="none"
autocomplete="off"

View File

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

View File

@@ -0,0 +1,163 @@
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";
declare global {
// for fire event
interface HASSDomEvents {
"related-changed": {
value?: FilterValue;
items?: RelatedResult;
filter?: string;
};
}
}
interface FilterValue {
area?: string;
device?: 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;
@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>
</mwc-menu-surface>
`;
}
private _handleClick(): void {
if (this.disabled) {
return;
}
this._open = true;
}
private _onClosed(): void {
this._open = false;
}
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",
(ev.currentTarget as any).comboBox.selectedItem.name
);
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 {
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
import chipStyles from "@material/chips/dist/mdc.chips.min.css";
import { ripple } from "@material/mwc-ripple/ripple-directive";
import {
css,
CSSResult,
@@ -12,6 +11,7 @@ import {
unsafeCSS,
} from "lit-element";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-chip";
declare global {
// for fire event
@@ -20,8 +20,8 @@ declare global {
}
}
@customElement("ha-chips")
export class HaChips extends LitElement {
@customElement("ha-chip-set")
export class HaChipSet extends LitElement {
@property() public items = [];
protected render(): TemplateResult {
@@ -33,18 +33,9 @@ export class HaChips extends LitElement {
${this.items.map(
(item, idx) =>
html`
<div class="mdc-chip" .index=${idx} @click=${this._handleClick}>
<div class="mdc-chip__ripple" .ripple="${ripple()}"></div>
<span role="gridcell">
<span
role="button"
tabindex="0"
class="mdc-chip__primary-action"
>
<span class="mdc-chip__text">${item}</span>
</span>
</span>
</div>
<ha-chip .index=${idx} @click=${this._handleClick}>
${item}
</ha-chip>
`
)}
</div>
@@ -60,9 +51,9 @@ export class HaChips extends LitElement {
static get styles(): CSSResult {
return css`
${unsafeCSS(chipStyles)}
.mdc-chip {
background-color: rgba(var(--rgb-primary-text-color), 0.15);
color: var(--primary-text-color);
ha-chip {
margin: 4px;
}
`;
}
@@ -70,6 +61,6 @@ export class HaChips extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"ha-chips": HaChips;
"ha-chip-set": HaChipSet;
}
}

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

View File

@@ -1,4 +1,4 @@
import { Editor } from "codemirror";
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
import {
customElement,
internalProperty,
@@ -15,32 +15,47 @@ declare global {
}
}
const saveKeyBinding: KeyBinding = {
key: "Mod-s",
run: (view: EditorView) => {
fireEvent(view.dom, "editor-save");
return true;
},
};
@customElement("ha-code-editor")
export class HaCodeEditor extends UpdatingElement {
public codemirror?: Editor;
public codemirror?: EditorView;
@property() public mode?: string;
@property() public mode = "yaml";
@property({ type: Boolean }) public autofocus = false;
@property({ type: Boolean }) public readOnly = false;
@property() public rtl = false;
@property() public error = false;
@internalProperty() private _value = "";
private _loadedCodeMirror?: typeof import("../resources/codemirror");
public set value(value: string) {
this._value = value;
}
public get value(): string {
return this.codemirror ? this.codemirror.getValue() : this._value;
return this.codemirror ? this.codemirror.state.doc.toString() : this._value;
}
public get hasComments(): boolean {
return !!this.shadowRoot!.querySelector("span.cm-comment");
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() {
@@ -48,7 +63,6 @@ export class HaCodeEditor extends UpdatingElement {
if (!this.codemirror) {
return;
}
this.codemirror.refresh();
if (this.autofocus !== false) {
this.codemirror.focus();
}
@@ -62,17 +76,27 @@ export class HaCodeEditor extends UpdatingElement {
}
if (changedProps.has("mode")) {
this.codemirror.setOption("mode", this.mode);
this.codemirror.dispatch({
effects: this._loadedCodeMirror!.langCompartment!.reconfigure(
this._mode
),
});
}
if (changedProps.has("autofocus")) {
this.codemirror.setOption("autofocus", this.autofocus !== false);
if (changedProps.has("readOnly")) {
this.codemirror.dispatch({
effects: this._loadedCodeMirror!.readonlyCompartment!.reconfigure(
this._loadedCodeMirror!.EditorView!.editable.of(!this.readOnly)
),
});
}
if (changedProps.has("_value") && this._value !== this.value) {
this.codemirror.setValue(this._value);
}
if (changedProps.has("rtl")) {
this.codemirror.setOption("gutters", this._calcGutters());
this._setScrollBarDirection();
this.codemirror.dispatch({
changes: {
from: 0,
to: this.codemirror.state.doc.length,
insert: this._value,
},
});
}
if (changedProps.has("error")) {
this.classList.toggle("error-state", this.error);
@@ -85,159 +109,69 @@ export class HaCodeEditor extends UpdatingElement {
this._load();
}
private async _load(): Promise<void> {
const loaded = await loadCodeMirror();
private get _mode() {
return this._loadedCodeMirror!.langs[this.mode];
}
const codeMirror = loaded.codeMirror;
private async _load(): Promise<void> {
this._loadedCodeMirror = await loadCodeMirror();
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot!.innerHTML = `
<style>
${loaded.codeMirrorCss}
.CodeMirror {
height: var(--code-mirror-height, auto);
direction: var(--code-mirror-direction, ltr);
font-family: var(--code-font-family, monospace);
}
.CodeMirror-scroll {
max-height: var(--code-mirror-max-height, --code-mirror-height);
}
:host(.error-state) .CodeMirror-gutters {
shadowRoot!.innerHTML = `<style>
:host(.error-state) div.cm-wrap .cm-gutters {
border-color: var(--error-state-color, red);
}
.CodeMirror-focused .CodeMirror-gutters {
border-right: 2px solid var(--paper-input-container-focus-color, var(--primary-color));
}
.CodeMirror-linenumber {
color: var(--paper-dialog-color, var(--secondary-text-color));
}
.rtl .CodeMirror-vscrollbar {
right: auto;
left: 0px;
}
.rtl-gutter {
width: 20px;
}
.CodeMirror-gutters {
border-right: 1px solid var(--paper-input-container-color, var(--secondary-text-color));
background-color: var(--paper-dialog-background-color, var(--primary-background-color));
transition: 0.2s ease border-right;
}
.cm-s-default.CodeMirror {
background-color: var(--code-editor-background-color, var(--card-background-color));
color: var(--primary-text-color);
}
.cm-s-default .CodeMirror-cursor {
border-left: 1px solid var(--secondary-text-color);
}
.cm-s-default div.CodeMirror-selected, .cm-s-default.CodeMirror-focused div.CodeMirror-selected {
background: rgba(var(--rgb-primary-color), 0.2);
}
.cm-s-default .CodeMirror-line::selection,
.cm-s-default .CodeMirror-line>span::selection,
.cm-s-default .CodeMirror-line>span>span::selection {
background: rgba(var(--rgb-primary-color), 0.2);
}
.cm-s-default .cm-keyword {
color: var(--codemirror-keyword, #6262FF);
}
.cm-s-default .cm-operator {
color: var(--codemirror-operator, #cda869);
}
.cm-s-default .cm-variable-2 {
color: var(--codemirror-variable-2, #690);
}
.cm-s-default .cm-builtin {
color: var(--codemirror-builtin, #9B7536);
}
.cm-s-default .cm-atom {
color: var(--codemirror-atom, #F90);
}
.cm-s-default .cm-number {
color: var(--codemirror-number, #ca7841);
}
.cm-s-default .cm-def {
color: var(--codemirror-def, #8DA6CE);
}
.cm-s-default .cm-string {
color: var(--codemirror-string, #07a);
}
.cm-s-default .cm-string-2 {
color: var(--codemirror-string-2, #bd6b18);
}
.cm-s-default .cm-comment {
color: var(--codemirror-comment, #777);
}
.cm-s-default .cm-variable {
color: var(--codemirror-variable, #07a);
}
.cm-s-default .cm-tag {
color: var(--codemirror-tag, #997643);
}
.cm-s-default .cm-meta {
color: var(--codemirror-meta, var(--primary-text-color));
}
.cm-s-default .cm-attribute {
color: var(--codemirror-attribute, #d6bb6d);
}
.cm-s-default .cm-property {
color: var(--codemirror-property, #905);
}
.cm-s-default .cm-qualifier {
color: var(--codemirror-qualifier, #690);
}
.cm-s-default .cm-variable-3 {
color: var(--codemirror-variable-3, #07a);
}
.cm-s-default .cm-type {
color: var(--codemirror-type, #07a);
}
</style>`;
this.codemirror = codeMirror(shadowRoot, {
value: this._value,
lineNumbers: true,
tabSize: 2,
mode: this.mode,
autofocus: this.autofocus !== false,
viewportMargin: Infinity,
readOnly: this.readOnly,
extraKeys: {
Tab: "indentMore",
"Shift-Tab": "indentLess",
},
gutters: this._calcGutters(),
const container = document.createElement("span");
shadowRoot.appendChild(container);
this.codemirror = new this._loadedCodeMirror.EditorView({
state: this._loadedCodeMirror.EditorState.create({
doc: this._value,
extensions: [
this._loadedCodeMirror.lineNumbers(),
this._loadedCodeMirror.EditorState.allowMultipleSelections.of(true),
this._loadedCodeMirror.history(),
this._loadedCodeMirror.highlightSelectionMatches(),
this._loadedCodeMirror.highlightActiveLine(),
this._loadedCodeMirror.drawSelection(),
this._loadedCodeMirror.rectangularSelection(),
this._loadedCodeMirror.keymap.of([
...this._loadedCodeMirror.defaultKeymap,
...this._loadedCodeMirror.searchKeymap,
...this._loadedCodeMirror.historyKeymap,
...this._loadedCodeMirror.tabKeyBindings,
saveKeyBinding,
] as KeyBinding[]),
this._loadedCodeMirror.langCompartment.of(this._mode),
this._loadedCodeMirror.theme,
this._loadedCodeMirror.Prec.fallback(
this._loadedCodeMirror.highlightStyle
),
this._loadedCodeMirror.readonlyCompartment.of(
this._loadedCodeMirror.EditorView.editable.of(!this.readOnly)
),
this._loadedCodeMirror.EditorView.updateListener.of((update) =>
this._onUpdate(update)
),
],
}),
root: shadowRoot,
parent: container,
});
this._setScrollBarDirection();
this.codemirror!.on("changes", () => this._onChange());
}
private _blockKeyboardShortcuts() {
this.addEventListener("keydown", (ev) => ev.stopPropagation());
}
private _onChange(): void {
private _onUpdate(update: ViewUpdate): void {
if (!update.docChanged) {
return;
}
const newValue = this.value;
if (newValue === this._value) {
return;
@@ -245,16 +179,6 @@ export class HaCodeEditor extends UpdatingElement {
this._value = newValue;
fireEvent(this, "value-changed", { value: this._value });
}
private _calcGutters(): string[] {
return this.rtl ? ["rtl-gutter", "CodeMirror-linenumbers"] : [];
}
private _setScrollBarDirection(): void {
if (this.codemirror) {
this.codemirror.getWrapperElement().classList.toggle("rtl", this.rtl);
}
}
}
declare global {

View File

@@ -10,6 +10,7 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
query,
@@ -67,8 +68,9 @@ export class HaComboBox extends LitElement {
model: { item: any }
) => void;
@property({ type: Boolean })
private _opened?: boolean;
@property({ type: Boolean }) public disabled?: boolean;
@internalProperty() private _opened?: boolean;
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
@@ -84,6 +86,10 @@ export class HaComboBox extends LitElement {
});
}
public get selectedItem() {
return (this._comboBox as any).selectedItem;
}
protected render(): TemplateResult {
return html`
<vaadin-combo-box-light
@@ -95,12 +101,14 @@ export class HaComboBox extends LitElement {
.filteredItems=${this.filteredItems}
.renderer=${this.renderer || defaultRowRenderer}
.allowCustomValue=${this.allowCustomValue}
.disabled=${this.disabled}
@opened-changed=${this._openedChanged}
@filter-changed=${this._filterChanged}
@value-changed=${this._valueChanged}
>
<paper-input
.label=${this.label}
.disabled=${this.disabled}
class="input"
autocapitalize="none"
autocomplete="off"
@@ -145,9 +153,9 @@ export class HaComboBox extends LitElement {
fireEvent(this, ev.type, ev.detail);
}
private _filterChanged(ev: PolymerChangedEvent<boolean>) {
private _filterChanged(ev: PolymerChangedEvent<string>) {
// @ts-ignore
fireEvent(this, ev.type, ev.detail);
fireEvent(this, ev.type, ev.detail, { composed: false });
}
private _valueChanged(ev: PolymerChangedEvent<string>) {

View File

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

View File

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

View File

@@ -21,8 +21,11 @@ export class HaActionSelector extends LitElement {
@property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
protected render() {
return html`<ha-automation-action
.disabled=${this.disabled}
.actions=${this.value || []}
.hass=${this.hass}
></ha-automation-action>`;
@@ -34,6 +37,10 @@ export class HaActionSelector extends LitElement {
display: block;
margin-bottom: 16px;
}
:host([disabled]) ha-automation-action {
opacity: var(--light-disabled-opacity);
pointer-events: none;
}
`;
}
}

View File

@@ -0,0 +1,30 @@
import { customElement, html, LitElement, property } from "lit-element";
import { AddonSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../ha-addon-picker";
@customElement("ha-selector-addon")
export class HaAddonSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: AddonSelector;
@property() public value?: any;
@property() public label?: string;
protected render() {
return html`<ha-addon-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
allow-custom-entity
></ha-addon-picker>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-addon": HaAddonSelector;
}
}

View File

@@ -24,6 +24,8 @@ export class HaAreaSelector extends LitElement {
@internalProperty() public _configEntries?: ConfigEntry[];
@property({ type: Boolean }) public disabled = false;
protected updated(changedProperties) {
if (changedProperties.has("selector")) {
const oldSelector = changedProperties.get("selector");
@@ -50,6 +52,7 @@ export class HaAreaSelector extends LitElement {
.includeDomains=${this.selector.area.entity?.domain
? [this.selector.area.entity.domain]
: undefined}
.disabled=${this.disabled}
></ha-area-picker>`;
}

View File

@@ -19,11 +19,14 @@ export class HaBooleanSelector extends LitElement {
@property() public label?: string;
@property({ type: Boolean }) public disabled = false;
protected render() {
return html` <ha-formfield alignEnd spaceBetween .label=${this.label}>
<ha-switch
.checked=${this.value}
@change=${this._handleChange}
.disabled=${this.disabled}
></ha-switch>
</ha-formfield>`;
}

View File

@@ -23,10 +23,12 @@ export class HaDeviceSelector extends LitElement {
@internalProperty() public _configEntries?: ConfigEntry[];
@property({ type: Boolean }) public disabled = false;
protected updated(changedProperties) {
if (changedProperties.has("selector")) {
const oldSelector = changedProperties.get("selector");
if (oldSelector !== this.selector && this.selector.device.integration) {
if (oldSelector !== this.selector && this.selector.device?.integration) {
this._loadConfigEntries();
}
}
@@ -44,24 +46,25 @@ export class HaDeviceSelector extends LitElement {
.includeDomains=${this.selector.device.entity?.domain
? [this.selector.device.entity.domain]
: undefined}
.disabled=${this.disabled}
allow-custom-entity
></ha-device-picker>`;
}
private _filterDevices(device: DeviceRegistryEntry): boolean {
if (
this.selector.device.manufacturer &&
this.selector.device?.manufacturer &&
device.manufacturer !== this.selector.device.manufacturer
) {
return false;
}
if (
this.selector.device.model &&
this.selector.device?.model &&
device.model !== this.selector.device.model
) {
return false;
}
if (this.selector.device.integration) {
if (this.selector.device?.integration) {
if (
this._configEntries &&
!this._configEntries.some((entry) =>

View File

@@ -25,12 +25,15 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
@property() public label?: string;
@property({ type: Boolean }) public disabled = false;
protected render() {
return html`<ha-entity-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.entityFilter=${(entity) => this._filterEntities(entity)}
.disabled=${this.disabled}
allow-custom-entity
></ha-entity-picker>`;
}
@@ -51,12 +54,12 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
}
private _filterEntities(entity: HassEntity): boolean {
if (this.selector.entity.domain) {
if (this.selector.entity?.domain) {
if (computeStateDomain(entity) !== this.selector.entity.domain) {
return false;
}
}
if (this.selector.entity.device_class) {
if (this.selector.entity?.device_class) {
if (
!entity.attributes.device_class ||
entity.attributes.device_class !== this.selector.entity.device_class
@@ -64,7 +67,7 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
return false;
}
}
if (this.selector.entity.integration) {
if (this.selector.entity?.integration) {
if (
!this._entityPlaformLookup ||
this._entityPlaformLookup[entity.entity_id] !==

View File

@@ -21,8 +21,12 @@ export class HaNumberSelector extends LitElement {
@property() public value?: number;
@property() public placeholder?: number;
@property() public label?: string;
@property({ type: Boolean }) public disabled = false;
protected render() {
return html`${this.label}
${this.selector.number.mode === "slider"
@@ -31,6 +35,7 @@ export class HaNumberSelector extends LitElement {
.max=${this.selector.number.max}
.value=${this._value}
.step=${this.selector.number.step}
.disabled=${this.disabled}
pin
ignore-bar-touch
@change=${this._handleSliderChange}
@@ -42,12 +47,14 @@ export class HaNumberSelector extends LitElement {
.label=${this.selector.number.mode === "slider"
? undefined
: this.label}
.placeholder=${this.placeholder}
.noLabelFloat=${this.selector.number.mode === "slider"}
class=${classMap({ single: this.selector.number.mode === "box" })}
.min=${this.selector.number.min}
.max=${this.selector.number.max}
.value=${this.value}
.step=${this.selector.number.step}
.disabled=${this.disabled}
type="number"
auto-validate
@value-changed=${this._handleInputChange}

View File

@@ -11,8 +11,14 @@ export class HaObjectSelector extends LitElement {
@property() public label?: string;
@property() public placeholder?: string;
@property({ type: Boolean }) public disabled = false;
protected render() {
return html`<ha-yaml-editor
.disabled=${this.disabled}
.placeholder=${this.placeholder}
.defaultValue=${this.value}
@value-changed=${this._handleChange}
></ha-yaml-editor>`;

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