Compare commits

...

158 Commits

Author SHA1 Message Date
Donnie
0527fac2ce Create shortcuts to go directly to specific category in command palette 2020-11-14 15:14:45 -08:00
Donnie
75e5d66966 Change commands to use category labels instead of icons. Fixes several translation issues. 2020-11-14 14:35:03 -08:00
Joakim Sørensen
b9f802939c Add a github option when copying from system health (#7663)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-11-13 18:20:24 +01:00
HomeAssistant Azure
6558c2c065 [ci skip] Translation update 2020-11-13 00:32:24 +00:00
Ian Richardson
37a089c868 Convert ha-cover-controls to TypeScript/LitElement (#7521)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2020-11-12 15:29:16 +01:00
Ian Richardson
f68eff6bb3 Fix state-info icon color and convert to TypeScript/LitElement (#7664) 2020-11-12 15:06:45 +01:00
HomeAssistant Azure
88a525f1a7 [ci skip] Translation update 2020-11-12 00:32:16 +00:00
Bram Kragten
34fddd5940 Merge error 2020-11-11 15:27:33 +01:00
Bram Kragten
0e5d6fe8d8 Merge branch 'master' into dev 2020-11-11 15:26:26 +01:00
Bram Kragten
e1342a0d9d Bumped version to 20201111.0 2020-11-11 15:17:21 +01:00
Joakim Sørensen
0cc2d3aaa7 Check for integration before loading logo (#7653) 2020-11-11 15:16:52 +01:00
Bram Kragten
67814505b3 Fix areas devices picker (#7652) 2020-11-11 14:59:26 +01:00
Bram Kragten
bae29c6d62 Move last changed / last updated out of state table (#7649) 2020-11-11 14:42:27 +01:00
Bram Kragten
a0e67d4c03 Fix fabs (#7650) 2020-11-11 14:13:40 +01:00
Bram Kragten
131bc5fbf7 Fix pin of labeled slider cutoff (#7651) 2020-11-11 14:12:49 +01:00
Bram Kragten
051218e29b Fix view height in edit mode (#7646) 2020-11-11 14:08:53 +01:00
Philip Allgaier
6ace8307d8 Always show "off" button if supported by player (#7389) 2020-11-11 14:00:53 +01:00
Bram Kragten
e84bef44b7 Guard for undefined hass (#7647) 2020-11-11 13:55:45 +01:00
Bram Kragten
3186d762f2 Fix height of tabs subpage (#7648) 2020-11-11 13:10:07 +01:00
Bram Kragten
c97a3b0a56 Fixes for logbook card (#7645) 2020-11-11 13:08:03 +01:00
Zack Barett
78f1bb3b91 Logbook Card (#6976) 2020-11-11 11:49:56 +01:00
Joakim Sørensen
67707fbc90 Don't block system_health if one value is null (#7644) 2020-11-11 11:49:10 +01:00
Donnie
2a57ffa615 Add navigation commands to quick bar commands (#7380)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-11-11 11:46:53 +01:00
Ryan Meek
216fce74f8 Header/sidebar sizing (#7470)
Co-authored-by: Zack Barett <zackbarett@hey.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-11-11 10:47:06 +01:00
HomeAssistant Azure
6cd3e6652a [ci skip] Translation update 2020-11-11 00:33:03 +00:00
Paulus Schoutsen
fe7d79cee6 Load system health translations from backend (#7643) 2020-11-10 23:44:59 +01:00
Nico Hirsch
2f4e7b388b Add dialog backdrop blur theme variable (#7635)
* Add dialog backdrop blur theme var

* Update src/components/ha-dialog.ts

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

* Add backdrop-filter to iron-overlay-backdrop

* Revert change from opacity to rgba

* Add comment "for paper-dialog"

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-11-10 18:01:33 +01:00
kg333
2e289cd152 Add fix to more-info for media players without play/pause state (#7608)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-11-10 18:00:44 +01:00
Sören Beye
21a3dcf06c Further brands-url cleanup (#7640) 2020-11-10 17:59:48 +01:00
Philip Allgaier
7f56add914 Show user friendly attribute names in picker (#7337)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-11-10 15:25:53 +01:00
Sören Beye
88701c6167 Refactor brands url to single location (#7613) 2020-11-10 15:25:06 +01:00
uvjustin
e4ce6117a1 Get regular playlist url properly in ha-hls-player (#7417) 2020-11-10 14:41:14 +01:00
Josh McCarty
cec2a61bdf Use ha-dialog for device-registry-details-dialog (#7500) 2020-11-10 12:22:32 +01:00
Philip Allgaier
8275ac5853 Ensure "next" and "prev" buttons always have ARIA label (#7588)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-11-10 11:39:09 +01:00
Philip Allgaier
b7bcf97365 Minor visual QB command tweaks (#7590) 2020-11-10 11:38:44 +01:00
Nathan Orick
fa28b480f1 Add button to duplicate script (#7511)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-11-10 10:16:42 +01:00
HomeAssistant Azure
4bb95b7396 [ci skip] Translation update 2020-11-10 00:32:22 +00:00
Philip Allgaier
5a9bd73e8b Use clearer dialog button texts + various translation improvements (#7584) 2020-11-09 23:41:22 +01:00
Thomas Lovén
4fe0276914 Replace date picker for entities card (#6899) 2020-11-09 23:27:21 +01:00
Kendell R
5e8bda55b4 Make PR template more discussion-friendly (#7622) 2020-11-09 23:05:44 +01:00
Joakim Sørensen
d09c4898c1 Remove logs and fix dark theme during onboarding restore (#7579) 2020-11-09 22:50:16 +01:00
Joakim Sørensen
6ae67ed299 Add flow for "progress" step (#7592) 2020-11-09 22:45:37 +01:00
Zack Barett
32ff166a74 Entities Card: Add Header & Footer Editor (#6751) 2020-11-09 22:41:59 +01:00
Paulus Schoutsen
8feae04281 Add link to conference (#7636) 2020-11-09 22:41:05 +01:00
Erik Montnemery
129f9c147b Improve user experience when enabling a disabled entity (#7580) 2020-11-09 19:48:24 +01:00
Ian Richardson
6e336dd207 convert ha-cover-tilt-controls to TypeScript/LitElement (#7542) 2020-11-09 18:26:05 +01:00
Paulus Schoutsen
161561c48a Support system health streaming (#7593) 2020-11-09 16:11:01 +01:00
Franck Nijhof
c162e84383 Replace lock bot with GitHub Action (#7633) 2020-11-09 12:49:42 +01:00
Franck Nijhof
dc8d80a6e5 Replace stale bot with GitHub Action (#7634) 2020-11-09 12:46:19 +01:00
HomeAssistant Azure
293f67968c [ci skip] Translation update 2020-11-09 00:32:31 +00:00
HomeAssistant Azure
4dcf26236e [ci skip] Translation update 2020-11-08 00:32:28 +00:00
Philip Allgaier
a0e8d69243 Add "www" to generated tag QR code (#7577)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-11-07 17:27:57 +01:00
Philip Allgaier
33cd9bf516 Ensure all <mwc-fab> set a label for ARIA (#7587) 2020-11-07 17:27:34 +01:00
HomeAssistant Azure
0132797f2f [ci skip] Translation update 2020-11-07 00:32:20 +00:00
Paulus Schoutsen
7e2db0aa4e Add ingress session validation (#7610) 2020-11-06 23:01:29 +01:00
HomeAssistant Azure
cc1d50491b [ci skip] Translation update 2020-11-06 00:32:18 +00:00
Adam Ernst
461b86a04b Fix race condition in translation loading (#7597) 2020-11-05 18:47:09 +01:00
Ian Richardson
9a3a7c28f4 Use shopping list card in panel (#7519) 2020-11-05 16:17:42 +01:00
Philip Allgaier
1c9d0200ca Add "last_changed" and "last_updated" to dev tools state view (#7375) 2020-11-05 16:15:50 +01:00
Philip Allgaier
0037cd2e69 Make thingtalk dialogs translatable (#7574) 2020-11-05 16:15:16 +01:00
HomeAssistant Azure
028ae061da [ci skip] Translation update 2020-11-05 00:32:18 +00:00
HomeAssistant Azure
2e47763ecc [ci skip] Translation update 2020-11-04 00:32:29 +00:00
HomeAssistant Azure
924e4a45d0 [ci skip] Translation update 2020-11-03 00:32:27 +00:00
Bram Kragten
8361b9553b Fix polyfill check (#7575) 2020-11-02 22:06:49 +01:00
Zack Barett
e52be20fba Grid Card: Fix Card Picker (#7562)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-11-02 10:47:24 +01:00
Bram Kragten
da12233ade Add dark mode toggle to gallery cards (#7532) 2020-11-02 10:46:52 +01:00
HomeAssistant Azure
57500f6c97 [ci skip] Translation update 2020-11-02 00:32:22 +00:00
Ryan Meek
199e17d0b1 Use paper-tabs while in edit mode (#7563)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-11-01 22:04:52 +01:00
Ryan Meek
3b91343082 Dark mode header color (#7514)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-11-01 22:04:27 +01:00
HomeAssistant Azure
1753c9163c [ci skip] Translation update 2020-11-01 00:32:29 +00:00
HomeAssistant Azure
89e5953e89 [ci skip] Translation update 2020-10-31 00:33:15 +00:00
Ian Richardson
5bfd25c8c6 Convert ha-climate-state to TypeScript/LitElement (#7544)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2020-10-30 17:04:13 -05:00
Zack Barett
e555b24f50 Calendar: Adds an Update Size and Makes list view start today and adds in local for first day of week (#7541) 2020-10-30 17:03:02 -05:00
Josh McCarty
14db37459f Formats number state with selected language in compute_state_display (#7516) 2020-10-30 22:58:52 +01:00
Zack Barett
1d9779d47c Calendar Panel: Persist Calendars in Local Storage (#7540) 2020-10-30 11:28:19 -05:00
Rob McCann
3dedbc5457 Remove minification from translations to reduce build time by 1.5mins (#7552) 2020-10-30 15:36:52 +01:00
Zack Barett
facb3266c6 Media Browser: Fix error handling (#7538) 2020-10-30 14:55:30 +01:00
Thomas Lovén
01fe5dd2f7 Make sure rebuilt cards show up in panel views (#7535) 2020-10-30 14:48:39 +01:00
dklemm
9b22b1e499 Resolve #7533 Broken styling on stack card titles (#7534) 2020-10-30 14:47:09 +01:00
Ian Richardson
4bc8818145 Remove ha-state-icon (#7545) 2020-10-30 14:45:51 +01:00
Paulus Schoutsen
48ef8c86c2 Add unavailable entity to gauge demo (#7530) 2020-10-30 14:30:27 +01:00
Zack Barett
89f359a52f Calendar: Fix background to match ha-card (#7539) 2020-10-30 14:28:51 +01:00
HomeAssistant Azure
13b8160d74 [ci skip] Translation update 2020-10-30 00:32:19 +00:00
Paulus Schoutsen
f1c16d6674 Mock subscribe template (#7529) 2020-10-29 21:28:41 +01:00
Paulus Schoutsen
76a088e177 Only show header toggle for entities card if title (#7525) 2020-10-29 21:28:34 +01:00
Philip Allgaier
630d8c3bb6 Add last-updated to state info tooltip (#7445) 2020-10-29 20:08:44 +01:00
Bram Kragten
744efa30f2 Bumped version to 20201021.4 2020-10-29 18:33:22 +01:00
Erik Montnemery
bf4a94dc48 Add MQTT as ignorable discovery flow (#7527) 2020-10-29 18:32:19 +01:00
Bram Kragten
ce4ba2f6f1 Fix tooltip creating scrollbar history card (#7528) 2020-10-29 18:31:52 +01:00
Paulus Schoutsen
5b232b5d35 Fix glance card with header if parent does not set block (#7526) 2020-10-29 18:31:36 +01:00
Donnie
35151bbac7 Fix issue with toggles blocking dialog and dialog launching on mobile (#7506)
* Fix issue with some inputs blocking, or incorrectly allowing, keyboard shortcut activation

* Explicitly declare all input types that we can allow alphanumeric overrides

* Do not launch dialog in codemirror targets on mobile devices
2020-10-29 18:31:21 +01:00
Erik Montnemery
f0e959319e Add MQTT as ignorable discovery flow (#7527) 2020-10-29 18:30:20 +01:00
Bram Kragten
d0c4475724 Fix tooltip creating scrollbar history card (#7528) 2020-10-29 18:29:54 +01:00
Paulus Schoutsen
99935f1e59 Fix glance card with header if parent does not set block (#7526) 2020-10-29 18:29:20 +01:00
Paulus Schoutsen
fbb43821ba Add grid card to the gallery (#7524) 2020-10-29 17:56:26 +01:00
Donnie
c7f5c6c1d1 Fix issue with toggles blocking dialog and dialog launching on mobile (#7506)
* Fix issue with some inputs blocking, or incorrectly allowing, keyboard shortcut activation

* Explicitly declare all input types that we can allow alphanumeric overrides

* Do not launch dialog in codemirror targets on mobile devices
2020-10-29 09:18:47 -07:00
Paulus Schoutsen
d26f1fa371 Fix grid card size when square (#7520) 2020-10-29 14:21:49 +01:00
Paulus Schoutsen
c3718ff7dd Add Grid card (#7476) 2020-10-29 10:31:14 +01:00
HomeAssistant Azure
d63493a859 [ci skip] Translation update 2020-10-29 00:32:17 +00:00
Josh McCarty
a72183851a Use ha-dialog for dialog-area-registry-detail (#7508) 2020-10-28 16:00:39 +01:00
Nathan Orick
40b2387667 Allow pressing return to submit on area dialog (#7509) 2020-10-28 15:57:01 +01:00
HomeAssistant Azure
d814aa36a7 [ci skip] Translation update 2020-10-28 00:32:29 +00:00
Philip Allgaier
e37eebe4ad Get rid of the unwanted tooltip copying (final) (#7459) 2020-10-27 20:22:06 +01:00
Philip Allgaier
0baaaefdf8 Get rid of the unwanted tooltip copying (#7408) 2020-10-27 20:22:04 +01:00
Philip Allgaier
58a58906e7 Get rid of the unwanted tooltip copying (final) (#7459) 2020-10-27 20:20:15 +01:00
Bram Kragten
bec0d9b00e Bumped version to 20201021.3 2020-10-27 20:18:52 +01:00
Donnie
e6a4ab789b Add server restart/stop to quick bar command list (#7488) 2020-10-27 20:18:41 +01:00
Donnie
36c1d3230c Change Quick Bar shortcuts to "e" and "c" (#7496)
* Add toggle for disabling quick bar shortcuts

* Change shortcut from Ctrl+P and Ctrl+Shift+P to qe and qc (Quick Entity, Quick Command)

* Remove accidentally included code

* Use tinykeys for handling shortcuts

* Change shortcuts to e and c. And fix small typo.

* Change copy for toggle

* Rename hass property to be for generic shortcuts

* Minor tweaks to address review comments
2020-10-27 20:18:18 +01:00
Donnie
30466ec3fe Add toggle for disabling quick bar shortcuts (#7495)
* Add toggle for disabling quick bar shortcuts

* Remove accidentally included code

* Change copy for toggle

* Rename hass property to be for generic shortcuts
2020-10-27 20:17:53 +01:00
Thomas Lovén
ce414a5ca9 Correctly replace rebuilt badges in view (#7487) 2020-10-27 20:17:34 +01:00
Ryan Meek
e4e6edd573 Fix lovelace background color (#7478) 2020-10-27 20:17:18 +01:00
Erik Montnemery
79927f4dc9 Update translation for MQTT reload (#7475) 2020-10-27 20:17:02 +01:00
Ryan Meek
603b833757 fix edit mode mwc-button size (#7472) 2020-10-27 20:16:42 +01:00
Donnie
ba99d1a10d Add server restart/stop to quick bar command list (#7488) 2020-10-27 20:14:25 +01:00
Donnie
efe97e8f51 Refactor sidebar renders into methods (prep for mwc-list conversion) (#7453) 2020-10-27 20:12:30 +01:00
Donnie
5ec23bb7ab Change Quick Bar shortcuts to "e" and "c" (#7496)
* Add toggle for disabling quick bar shortcuts

* Change shortcut from Ctrl+P and Ctrl+Shift+P to qe and qc (Quick Entity, Quick Command)

* Remove accidentally included code

* Use tinykeys for handling shortcuts

* Change shortcuts to e and c. And fix small typo.

* Change copy for toggle

* Rename hass property to be for generic shortcuts

* Minor tweaks to address review comments
2020-10-27 10:34:51 -07:00
Donnie
9b4d01ab75 Add toggle for disabling quick bar shortcuts (#7495)
* Add toggle for disabling quick bar shortcuts

* Remove accidentally included code

* Change copy for toggle

* Rename hass property to be for generic shortcuts
2020-10-27 10:00:46 -07:00
Nathan Orick
40191a88d4 Allow edit card dialog to be made wider (#7492) 2020-10-27 15:59:32 +01:00
Josh McCarty
a19477d179 Migrate ha-paper-dialog to ha-dialog in system options dialog (#7455) 2020-10-27 15:24:52 +01:00
Erik Montnemery
bf98a78f3d Add custom device action to remove a Tasmota device (#7469) 2020-10-27 14:04:28 +01:00
Florian Gareis
ba4c2fc1bd Replace prompt with showPromptDialog (#7460)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-10-27 14:04:01 +01:00
Charles Garwood
b56e9ef028 Add cancel command button to ozw config panel (#7471) 2020-10-27 14:00:21 +01:00
Philip Allgaier
dbbd34c520 Allow breaking of entity ID in history tooltip into multiple lines (#7482) 2020-10-27 11:14:47 +01:00
J. Nick Koston
ccb69dbdfa Add icons for shade device class (#7493)
The blinds material icon looks more like a shade
then a blind but since its already being used
blind its still better than having it look like
a window.
2020-10-26 20:43:22 -05:00
HomeAssistant Azure
11e555ef6f [ci skip] Translation update 2020-10-27 00:32:18 +00:00
Thomas Lovén
61e17395c9 Correctly replace rebuilt badges in view (#7487) 2020-10-26 20:55:47 +01:00
Ryan Meek
733ce3b6b8 Fix lovelace background color (#7478) 2020-10-26 09:42:57 +01:00
Erik Montnemery
375f143199 Update translation for MQTT reload (#7475) 2020-10-26 09:41:43 +01:00
HomeAssistant Azure
2419f35eb9 [ci skip] Translation update 2020-10-26 00:32:40 +00:00
HomeAssistant Azure
21867c3576 [ci skip] Translation update 2020-10-25 00:32:37 +00:00
J. Nick Koston
28853b28bc Show cover attributes on the more-info card (#7458) 2020-10-24 08:26:18 -05:00
HomeAssistant Azure
e2f27568a5 [ci skip] Translation update 2020-10-24 00:32:12 +00:00
Ryan Meek
98b2b796b0 fix edit mode mwc-button size (#7472) 2020-10-24 00:53:11 +02:00
Philip Allgaier
b8f3fcf00b Allow discovered integration titles to line wrap (#7468) 2020-10-23 16:43:20 +02:00
Philip Allgaier
d3fda9a821 Ensure attribute values are consistently right aligned (#7466) 2020-10-23 16:29:27 +02:00
HomeAssistant Azure
19e69dc13e [ci skip] Translation update 2020-10-23 00:32:22 +00:00
Bram Kragten
48543a2dad Bumped version to 20201021.2 2020-10-23 00:18:49 +02:00
Philip Allgaier
b22f5ae5c2 Add outline color for dark buttons (#7444) 2020-10-23 00:18:26 +02:00
J. Nick Koston
2acb6a28fe Update template time listener phrasing for core changes (#7450) 2020-10-23 00:16:40 +02:00
Donnie
1064cdb79d Fix quick bar dark mode contrast, filter returning all items, no primary text (#7430)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-10-23 00:16:18 +02:00
Bram Kragten
bd7cb1c877 Template dev tools: Print the type of the response and stringify objects (#7439) 2020-10-23 00:15:57 +02:00
Bram Kragten
6c314982dc Pass narrow to masonry view to calc columns (#7454) 2020-10-23 00:15:36 +02:00
Philip Allgaier
d54710f113 Fix capitalization of state attributes (#7448) 2020-10-23 00:15:20 +02:00
J. Nick Koston
1346156ecd Avoid fetching logbook data instead in addition to not displaying it (#7427)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2020-10-23 00:15:04 +02:00
Bram Kragten
a2d9f9b417 Fix ES5 build, fix virtualizer polyfill (#7451) 2020-10-23 00:14:48 +02:00
Bram Kragten
3de78cca2d Fix quickbar debounce (#7426)
* Fix quicbar debounce

* Clear search property when dialog is closed

Co-authored-by: Donnie <donniekarnsinsb@hotmail.com>
2020-10-23 00:14:28 +02:00
J. Nick Koston
5fa7cd9fa9 Update template time listener phrasing for core changes (#7450) 2020-10-23 00:10:51 +02:00
Donnie
a78c00fb41 Fix quick bar dark mode contrast, filter returning all items, no primary text (#7430)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-10-23 00:06:36 +02:00
Philip Allgaier
edc2a03d1c Get rid of the unwanted tooltip copying (#7408) 2020-10-22 23:57:52 +02:00
Bram Kragten
174f8f5823 Template dev tools: Print the type of the response and stringify objects (#7439) 2020-10-22 23:51:33 +02:00
Bram Kragten
9fbc94e8d8 Pass narrow to masonry view to calc columns (#7454) 2020-10-22 23:29:26 +02:00
Philip Allgaier
6aff35196d Fix capitalization of state attributes (#7448) 2020-10-22 23:13:36 +02:00
J. Nick Koston
eceed4ed74 Avoid fetching logbook data instead in addition to not displaying it (#7427)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2020-10-22 22:47:19 +02:00
Bram Kragten
7428731eac Fix ES5 build, fix virtualizer polyfill (#7451) 2020-10-22 22:43:15 +02:00
Philip Allgaier
89b07ea0ae Add outline color for dark buttons (#7444) 2020-10-22 16:24:12 +02:00
Ian Richardson
d16daf0fd9 Add last-updated as a secondaryinfo option to entity rows (#7433) 2020-10-22 12:37:51 +02:00
Bram Kragten
211ab4eea8 Fix quickbar debounce (#7426)
* Fix quicbar debounce

* Clear search property when dialog is closed

Co-authored-by: Donnie <donniekarnsinsb@hotmail.com>
2020-10-22 11:00:36 +02:00
HomeAssistant Azure
dbd53f8d14 [ci skip] Translation update 2020-10-22 00:32:19 +00:00
266 changed files with 11910 additions and 4013 deletions

View File

@@ -18,8 +18,8 @@
<!-- <!--
Describe the big picture of your changes here to communicate to the Describe the big picture of your changes here to communicate to the
maintainers why we should accept this pull request. If it fixes a bug maintainers why we should accept this pull request. If it fixes a bug
or resolves a feature request, be sure to link to that issue in the or resolves a feature request, be sure to link to that issue or discussion
additional information section. in the additional information section.
--> -->
## Type of change ## Type of change
@@ -56,7 +56,7 @@
--> -->
- This PR fixes or closes issue: fixes # - This PR fixes or closes issue: fixes #
- This PR is related to issue: - This PR is related to issue or discussion:
- Link to documentation pull request: - Link to documentation pull request:
## Checklist ## Checklist

27
.github/lock.yml vendored
View File

@@ -1,27 +0,0 @@
# Configuration for Lock Threads - https://github.com/dessant/lock-threads
# Number of days of inactivity before a closed issue or pull request is locked
daysUntilLock: 1
# Skip issues and pull requests created before a given timestamp. Timestamp must
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
skipCreatedBefore: 2020-01-01
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
exemptLabels: []
# Label to add before locking, such as `outdated`. Set to `false` to disable
lockLabel: false
# Comment to post before locking. Set to `false` to disable
lockComment: false
# Assign `resolved` as the reason for locking. Set to `false` to disable
setLockReason: false
# Limit to only `issues` or `pulls`
only: pulls
# Optionally, specify configuration settings just for `issues` or `pulls`
issues:
daysUntilLock: 30

56
.github/stale.yml vendored
View File

@@ -1,56 +0,0 @@
# Configuration for probot-stale - https://github.com/probot/stale
# Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 90
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
daysUntilClose: 7
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
onlyLabels: []
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:
- feature request
- Help wanted
- to do
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: true
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: true
# Set to true to ignore issues with an assignee (defaults to false)
exemptAssignees: false
# Label to use when marking as stale
staleLabel: stale
# Comment to post when marking as stale. Set to `false` to disable
markComment: >
There hasn't been any activity on this issue recently. Due to the high number
of incoming GitHub notifications, we have to clean some of the old issues,
as many of them have already been resolved with the latest updates.
Please make sure to update to the latest Home Assistant version and check
if that solves the issue. Let us know if that works for you by adding a
comment 👍
This issue now has been marked as stale and will be closed if no further
activity occurs. Thank you for your contributions.
# Comment to post when removing the stale label.
# unmarkComment: >
# Your comment here.
# Comment to post when closing a stale Issue or Pull Request.
# closeComment: >
# Your comment here.
# Limit the number of actions per hour, from 1-30. Default is 30
limitPerRun: 30
# Limit to only `issues` or `pulls`
only: issues

20
.github/workflows/lock.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: Lock
# yamllint disable-line rule:truthy
on:
schedule:
- cron: "0 * * * *"
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v2.0.1
with:
github-token: ${{ github.token }}
issue-lock-inactive-days: "30"
issue-exclude-created-before: "2020-10-01T00:00:00Z"
issue-lock-reason: ""
pr-lock-inactive-days: "1"
pr-exclude-created-before: "2020-11-01T00:00:00Z"
pr-lock-reason: ""

42
.github/workflows/stale.yml vendored Normal file
View File

@@ -0,0 +1,42 @@
name: Stale
# yamllint disable-line rule:truthy
on:
schedule:
- cron: "0 * * * *"
jobs:
stale:
runs-on: ubuntu-latest
steps:
- name: 90 days stale policy
uses: actions/stale@v3.0.13
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 90
days-before-close: 7
operations-per-run: 25
remove-stale-when-updated: true
stale-issue-label: "stale"
exempt-issue-labels: "no-stale,Help%20wanted,help-wanted,feature-request,feature%20request"
stale-issue-message: >
There hasn't been any activity on this issue recently. Due to the
high number of incoming GitHub notifications, we have to clean some
of the old issues, as many of them have already been resolved with
the latest updates.
Please make sure to update to the latest Home Assistant version and
check if that solves the issue. Let us know if that works for you by
adding a comment 👍
This issue has now been marked as stale and will be closed if no
further activity occurs. Thank you for your contributions.
stale-pr-label: "stale"
exempt-pr-labels: "no-stale"
stale-pr-message: >
There hasn't been any activity on this pull request recently. This
pull request has been automatically marked as stale because of that
and will be closed if no further activity occurs within 7 days.
Thank you for your contributions.

View File

@@ -55,7 +55,6 @@ module.exports.babelOptions = ({ latestBuild }) => ({
!latestBuild && [ !latestBuild && [
require("@babel/preset-env").default, require("@babel/preset-env").default,
{ {
modules: false,
useBuiltIns: "entry", useBuiltIns: "entry",
corejs: "3.6", corejs: "3.6",
}, },
@@ -71,7 +70,6 @@ module.exports.babelOptions = ({ latestBuild }) => ({
// Only support the syntax, Webpack will handle it. // Only support the syntax, Webpack will handle it.
"@babel/plugin-syntax-import-meta", "@babel/plugin-syntax-import-meta",
"@babel/plugin-syntax-dynamic-import", "@babel/plugin-syntax-dynamic-import",
"@babel/plugin-syntax-top-level-await",
"@babel/plugin-proposal-optional-chaining", "@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-nullish-coalescing-operator", "@babel/plugin-proposal-nullish-coalescing-operator",
[ [

View File

@@ -7,7 +7,6 @@ const gulp = require("gulp");
const fs = require("fs"); const fs = require("fs");
const foreach = require("gulp-foreach"); const foreach = require("gulp-foreach");
const merge = require("gulp-merge-json"); const merge = require("gulp-merge-json");
const minify = require("gulp-jsonminify");
const rename = require("gulp-rename"); const rename = require("gulp-rename");
const transform = require("gulp-json-transform"); const transform = require("gulp-json-transform");
const { mapFiles } = require("../util"); const { mapFiles } = require("../util");
@@ -301,7 +300,6 @@ gulp.task("build-flattened-translations", function () {
return flatten(data); return flatten(data);
}) })
) )
.pipe(minify())
.pipe( .pipe(
rename((filePath) => { rename((filePath) => {
if (filePath.dirname === "core") { if (filePath.dirname === "core") {

View File

@@ -51,9 +51,6 @@ const createWebpackConfig = ({
}), }),
], ],
}, },
experiments: {
topLevelAwait: true,
},
plugins: [ plugins: [
new ManifestPlugin({ new ManifestPlugin({
// Only include the JS of entrypoints // Only include the JS of entrypoints
@@ -98,6 +95,15 @@ const createWebpackConfig = ({
new RegExp(bundle.emptyPackages({ latestBuild }).join("|")), new RegExp(bundle.emptyPackages({ latestBuild }).join("|")),
path.resolve(paths.polymer_dir, "src/util/empty.js") path.resolve(paths.polymer_dir, "src/util/empty.js")
), ),
// We need to change the import of the polyfill for EventTarget, so we replace the polyfill file with our customized one
new webpack.NormalModuleReplacementPlugin(
new RegExp(
require.resolve(
"lit-virtualizer/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js"
)
),
path.resolve(paths.polymer_dir, "src/resources/EventTarget-ponyfill.js")
),
], ],
resolve: { resolve: {
extensions: [".ts", ".js", ".json"], extensions: [".ts", ".js", ".json"],

View File

@@ -7,8 +7,8 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
cards: [ cards: [
{ type: "custom:ha-demo-card" }, { type: "custom:ha-demo-card" },
{ {
cards: [ type: "grid",
{ columns: 4,
cards: [ cards: [
{ {
image: "/assets/teachingbirds/isa_square.jpg", image: "/assets/teachingbirds/isa_square.jpg",
@@ -77,11 +77,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
], ],
type: "picture-elements", type: "picture-elements",
}, },
],
type: "horizontal-stack",
},
{
cards: [
{ {
show_name: false, show_name: false,
type: "picture-entity", type: "picture-entity",
@@ -104,8 +100,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
type: "picture-entity", type: "picture-entity",
state_image: { state_image: {
Mail: "/assets/teachingbirds/mailbox_square.jpg", Mail: "/assets/teachingbirds/mailbox_square.jpg",
"Package and mail": "Package and mail": "/assets/teachingbirds/mailbox_square.jpg",
"/assets/teachingbirds/mailbox_square.jpg",
Empty: "/assets/teachingbirds/mailbox_bw_square.jpg", Empty: "/assets/teachingbirds/mailbox_bw_square.jpg",
Package: "/assets/teachingbirds/mailbox_square.jpg", Package: "/assets/teachingbirds/mailbox_square.jpg",
}, },
@@ -121,11 +116,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
image: "/assets/teachingbirds/trash_bear_bw_square.jpg", image: "/assets/teachingbirds/trash_bear_bw_square.jpg",
entity: "sensor.trash_status", entity: "sensor.trash_status",
}, },
],
type: "horizontal-stack",
},
{
cards: [
{ {
state_image: { state_image: {
Idle: "/assets/teachingbirds/washer_square.jpg", Idle: "/assets/teachingbirds/washer_square.jpg",
@@ -167,15 +158,10 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
entity: "input_boolean.cleaning_day", entity: "input_boolean.cleaning_day",
}, },
], ],
type: "horizontal-stack",
},
],
type: "vertical-stack",
}, },
{ {
type: "vertical-stack", type: "grid",
cards: [ columns: 2,
{
cards: [ cards: [
{ {
graph: "line", graph: "line",
@@ -188,11 +174,6 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
name: "S's room", name: "S's room",
entity: "sensor.temperature_stefan", entity: "sensor.temperature_stefan",
}, },
],
type: "horizontal-stack",
},
{
cards: [
{ {
graph: "line", graph: "line",
type: "sensor", type: "sensor",
@@ -205,9 +186,6 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
entity: "sensor.temperature_downstairs_bathroom", entity: "sensor.temperature_downstairs_bathroom",
}, },
], ],
type: "horizontal-stack",
},
],
}, },
{ {
entities: [ entities: [

View File

@@ -6,4 +6,11 @@ export const mockTemplate = (hass: MockHomeAssistant) => {
body: { message: "Template dev tool does not work in the demo." }, body: { message: "Template dev tool does not work in the demo." },
}) })
); );
hass.mockWS("render_template", (msg, onChange) => {
onChange!({
result: msg.template,
listeners: { all: false, domains: [], entities: [], time: false },
});
return () => {};
});
}; };

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

@@ -5,11 +5,16 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/ha-switch"; import "../../../src/components/ha-switch";
import "../../../src/components/ha-formfield"; import "../../../src/components/ha-formfield";
import "./demo-card"; import "./demo-card";
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
class DemoCards extends PolymerElement { class DemoCards extends PolymerElement {
static get template() { static get template() {
return html` return html`
<style> <style>
#container {
min-height: calc(100vh - 128px);
background: var(--primary-background-color);
}
.cards { .cards {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -24,6 +29,9 @@ class DemoCards extends PolymerElement {
.filters { .filters {
margin-left: 60px; margin-left: 60px;
} }
ha-formfield {
margin-right: 16px;
}
</style> </style>
<app-toolbar> <app-toolbar>
<div class="filters"> <div class="filters">
@@ -31,8 +39,12 @@ class DemoCards extends PolymerElement {
<ha-switch checked="[[_showConfig]]" on-change="_showConfigToggled"> <ha-switch checked="[[_showConfig]]" on-change="_showConfigToggled">
</ha-switch> </ha-switch>
</ha-formfield> </ha-formfield>
<ha-formfield label="Dark theme">
<ha-switch on-change="_darkThemeToggled"> </ha-switch>
</ha-formfield>
</div> </div>
</app-toolbar> </app-toolbar>
<div id="container">
<div class="cards"> <div class="cards">
<template is="dom-repeat" items="[[configs]]"> <template is="dom-repeat" items="[[configs]]">
<demo-card <demo-card
@@ -42,6 +54,7 @@ class DemoCards extends PolymerElement {
></demo-card> ></demo-card>
</template> </template>
</div> </div>
</div>
`; `;
} }
@@ -59,6 +72,12 @@ class DemoCards extends PolymerElement {
_showConfigToggled(ev) { _showConfigToggled(ev) {
this._showConfig = ev.target.checked; this._showConfig = ev.target.checked;
} }
_darkThemeToggled(ev) {
applyThemesOnElement(this.$.container, { themes: {} }, "default", {
dark: ev.target.checked,
});
}
} }
customElements.define("demo-cards", DemoCards); customElements.define("demo-cards", DemoCards);

View File

@@ -3,7 +3,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import "../../../src/state-summary/state-card-content"; import "../../../src/state-summary/state-card-content";
import "./more-info-content"; import "../../../src/dialogs/more-info/more-info-content";
class DemoMoreInfo extends PolymerElement { class DemoMoreInfo extends PolymerElement {
static get template() { static get template() {
@@ -16,15 +16,12 @@ class DemoMoreInfo extends PolymerElement {
ha-card { ha-card {
width: 333px; width: 333px;
padding: 20px 24px;
} }
state-card-content { state-card-content {
display: block; display: block;
padding: 16px; margin-bottom: 16px;
}
more-info-content {
padding: 0 16px;
} }
pre { pre {

View File

@@ -1,73 +0,0 @@
import { HassEntity } from "home-assistant-js-websocket";
import { property, PropertyValues, UpdatingElement } from "lit-element";
import dynamicContentUpdater from "../../../src/common/dom/dynamic_content_updater";
import { stateMoreInfoType } from "../../../src/dialogs/more-info/state_more_info_control";
import "../../../src/dialogs/more-info/controls/more-info-alarm_control_panel";
import "../../../src/dialogs/more-info/controls/more-info-automation";
import "../../../src/dialogs/more-info/controls/more-info-camera";
import "../../../src/dialogs/more-info/controls/more-info-climate";
import "../../../src/dialogs/more-info/controls/more-info-configurator";
import "../../../src/dialogs/more-info/controls/more-info-counter";
import "../../../src/dialogs/more-info/controls/more-info-cover";
import "../../../src/dialogs/more-info/controls/more-info-default";
import "../../../src/dialogs/more-info/controls/more-info-fan";
import "../../../src/dialogs/more-info/controls/more-info-group";
import "../../../src/dialogs/more-info/controls/more-info-humidifier";
import "../../../src/dialogs/more-info/controls/more-info-input_datetime";
import "../../../src/dialogs/more-info/controls/more-info-light";
import "../../../src/dialogs/more-info/controls/more-info-lock";
import "../../../src/dialogs/more-info/controls/more-info-media_player";
import "../../../src/dialogs/more-info/controls/more-info-person";
import "../../../src/dialogs/more-info/controls/more-info-script";
import "../../../src/dialogs/more-info/controls/more-info-sun";
import "../../../src/dialogs/more-info/controls/more-info-timer";
import "../../../src/dialogs/more-info/controls/more-info-vacuum";
import "../../../src/dialogs/more-info/controls/more-info-water_heater";
import "../../../src/dialogs/more-info/controls/more-info-weather";
import { HomeAssistant } from "../../../src/types";
class MoreInfoContent extends UpdatingElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property() public stateObj?: HassEntity;
private _detachedChild?: ChildNode;
protected firstUpdated(): void {
this.style.position = "relative";
this.style.display = "block";
}
// This is not a lit element, but an updating element, so we implement update
protected update(changedProps: PropertyValues): void {
super.update(changedProps);
const stateObj = this.stateObj;
const hass = this.hass;
if (!stateObj || !hass) {
if (this.lastChild) {
this._detachedChild = this.lastChild;
// Detach child to prevent it from doing work.
this.removeChild(this.lastChild);
}
return;
}
if (this._detachedChild) {
this.appendChild(this._detachedChild);
this._detachedChild = undefined;
}
const moreInfoType =
stateObj.attributes && "custom_ui_more_info" in stateObj.attributes
? stateObj.attributes.custom_ui_more_info
: "more-info-" + stateMoreInfoType(stateObj);
dynamicContentUpdater(this, moreInfoType.toUpperCase(), {
hass,
stateObj,
});
}
}
customElements.define("more-info-content", MoreInfoContent);

View File

@@ -6,7 +6,9 @@ export const createMediaPlayerEntities = () => [
media_content_type: "music", media_content_type: "music",
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)", media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
media_artist: "Technohead", media_artist: "Technohead",
supported_features: 64063, // Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media +
// Select Source + Stop + Clear + Play + Shuffle Set + Browse Media
supported_features: 195135,
entity_picture: "/images/album_cover_2.jpg", entity_picture: "/images/album_cover_2.jpg",
media_duration: 300, media_duration: 300,
media_position: 50, media_position: 50,
@@ -14,12 +16,15 @@ export const createMediaPlayerEntities = () => [
// 23 seconds in // 23 seconds in
new Date().getTime() - 23000 new Date().getTime() - 23000
).toISOString(), ).toISOString(),
volume_level: 0.5,
}), }),
getEntity("media_player", "music_playing", "playing", { getEntity("media_player", "music_playing", "playing", {
friendly_name: "Playing The Music", friendly_name: "Playing The Music",
media_content_type: "music", media_content_type: "music",
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)", media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
media_artist: "Technohead", media_artist: "Technohead",
// Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media +
// Select Source + Stop + Clear + Play + Shuffle Set
supported_features: 64063, supported_features: 64063,
entity_picture: "/images/album_cover.jpg", entity_picture: "/images/album_cover.jpg",
media_duration: 300, media_duration: 300,
@@ -28,6 +33,7 @@ export const createMediaPlayerEntities = () => [
// 23 seconds in // 23 seconds in
new Date().getTime() - 23000 new Date().getTime() - 23000
).toISOString(), ).toISOString(),
volume_level: 0.5,
}), }),
getEntity("media_player", "stream_playing", "playing", { getEntity("media_player", "stream_playing", "playing", {
friendly_name: "Playing the Stream", friendly_name: "Playing the Stream",
@@ -35,50 +41,125 @@ export const createMediaPlayerEntities = () => [
media_title: "Epic sax guy 10 hours", media_title: "Epic sax guy 10 hours",
app_name: "YouTube", app_name: "YouTube",
entity_picture: "/images/frenck.jpg", entity_picture: "/images/frenck.jpg",
supported_features: 33, // Pause + Next Track + Play + Browse Media
supported_features: 147489,
}), }),
getEntity("media_player", "living_room", "playing", { getEntity("media_player", "stream_paused", "paused", {
friendly_name: "Pause, No skip, tvshow", friendly_name: "Paused the Stream",
media_content_type: "movie",
media_title: "Epic sax guy 10 hours",
app_name: "YouTube",
entity_picture: "/images/frenck.jpg",
// Pause + Next Track + Play
supported_features: 16417,
}),
getEntity("media_player", "stream_playing_previous", "playing", {
friendly_name: 'Playing the Stream (with "previous" support)',
media_content_type: "movie",
media_title: "Epic sax guy 10 hours",
app_name: "YouTube",
entity_picture: "/images/frenck.jpg",
// Pause + Previous Track + Play
supported_features: 16401,
}),
getEntity("media_player", "tv_playing", "playing", {
friendly_name: "Playing non-skip TV Show",
media_content_type: "tvshow", media_content_type: "tvshow",
media_title: "Chapter 1", media_title: "Chapter 1",
media_series_title: "House of Cards", media_series_title: "House of Cards",
app_name: "Netflix", app_name: "Netflix",
entity_picture: "/images/netflix.jpg", entity_picture: "/images/netflix.jpg",
// Pause
supported_features: 1, supported_features: 1,
}), }),
getEntity("media_player", "sonos_idle", "idle", { getEntity("media_player", "sonos_idle", "idle", {
friendly_name: "Sonos Idle", friendly_name: "Sonos Idle",
// Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media +
// Select Source + Stop + Clear + Play + Shuffle Set
supported_features: 64063, supported_features: 64063,
volume_level: 0.33,
is_volume_muted: true,
}), }),
getEntity("media_player", "theater", "off", { getEntity("media_player", "idle_browse_media", "idle", {
friendly_name: "Idle waiting for Browse Media (e.g. Spotify)",
// Pause + Seek + Volume Set + Previous Track + Next Track + Play Media +
// Select Source + Play + Shuffle Set + Browse Media
supported_features: 182839,
volume_level: 0.79,
}),
getEntity("media_player", "theater_off", "off", {
friendly_name: "TV Off", friendly_name: "TV Off",
// On + Off + Play + Next + Pause
supported_features: 16801,
}),
getEntity("media_player", "theater_on", "on", {
friendly_name: "TV On",
// On + Off + Play + Next + Pause
supported_features: 16801,
}),
getEntity("media_player", "theater_off_static", "off", {
friendly_name: "TV Off (cannot be switched on)",
// Off + Next + Pause
supported_features: 289,
}),
getEntity("media_player", "theater_on_static", "on", {
friendly_name: "TV On (cannot be switched off)",
// On + Next + Pause
supported_features: 161, supported_features: 161,
}), }),
getEntity("media_player", "android_cast", "playing", { getEntity("media_player", "android_cast", "playing", {
friendly_name: "Casting App", friendly_name: "Casting App (no supported features)",
media_title: "Android Screen Casting", media_title: "Android Screen Casting",
app_name: "Screen Mirroring", app_name: "Screen Mirroring",
// supported_features: 21437, }),
getEntity("media_player", "image_display", "playing", {
friendly_name: "Digital Picture Frame",
media_content_type: "image",
media_title: "Famous Painting",
media_artist: "Famous Artist",
entity_picture: "/images/sunflowers.jpg",
// On + Off + Browse Media
supported_features: 131456,
}), }),
getEntity("media_player", "unavailable", "unavailable", { getEntity("media_player", "unavailable", "unavailable", {
friendly_name: "Player Unavailable", friendly_name: "Player Unavailable",
// Pause + Volume Set + Volume Mute + Previous Track + Next Track +
// Play Media + Stop + Play
supported_features: 21437, supported_features: 21437,
}), }),
getEntity("media_player", "unknown", "unknown", { getEntity("media_player", "unknown", "unknown", {
friendly_name: "Player Unknown", friendly_name: "Player Unknown",
// Pause + Volume Set + Volume Mute + Previous Track + Next Track +
// Play Media + Stop + Play
supported_features: 21437, supported_features: 21437,
}), }),
getEntity("media_player", "playing", "playing", {
friendly_name: "Player Playing (no Pause support)",
// Volume Set + Volume Mute + Previous Track + Next Track +
// Play Media + Stop + Play
supported_features: 21436,
volume_level: 1,
}),
getEntity("media_player", "idle", "idle", {
friendly_name: "Player Idle",
// Pause + Volume Set + Volume Mute + Previous Track + Next Track +
// Play Media + Stop + Play
supported_features: 21437,
volume_level: 0,
}),
getEntity("media_player", "receiver_on", "on", { getEntity("media_player", "receiver_on", "on", {
source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"], source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"],
volume_level: 0.63, volume_level: 0.63,
is_volume_muted: false, is_volume_muted: false,
source: "TV", source: "TV",
friendly_name: "Receiver", friendly_name: "Receiver (selectable sources)",
// Volume Set + Volume Mute + On + Off + Select Source + Play + Sound Mode
supported_features: 84364, supported_features: 84364,
}), }),
getEntity("media_player", "receiver_off", "off", { getEntity("media_player", "receiver_off", "off", {
source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"], source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"],
friendly_name: "Receiver", friendly_name: "Receiver (selectable sources)",
// Volume Set + Volume Mute + On + Off + Select Source + Play + Sound Mode
supported_features: 84364, supported_features: 84364,
}), }),
]; ];

View File

@@ -15,6 +15,10 @@ const ENTITIES = [
getEntity("alarm_control_panel", "unavailable", "unavailable", { getEntity("alarm_control_panel", "unavailable", "unavailable", {
friendly_name: "Alarm", friendly_name: "Alarm",
}), }),
getEntity("alarm_control_panel", "alarm_code", "disarmed", {
friendly_name: "Alarm",
code_format: "number",
}),
]; ];
const CONFIGS = [ const CONFIGS = [
@@ -30,7 +34,14 @@ const CONFIGS = [
config: ` config: `
- type: alarm-panel - type: alarm-panel
entity: alarm_control_panel.alarm_armed entity: alarm_control_panel.alarm_armed
title: My Alarm name: My Alarm
`,
},
{
heading: "Code Example",
config: `
- type: alarm-panel
entity: alarm_control_panel.alarm_code
`, `,
}, },
{ {
@@ -83,8 +94,12 @@ class DemoAlarmPanelEntity extends PolymerElement {
public ready() { public ready() {
super.ready(); super.ready();
this._setupDemo();
}
private async _setupDemo() {
const hass = provideHass(this.$.demos); const hass = provideHass(this.$.demos);
hass.updateTranslations(null, "en"); await hass.updateTranslations(null, "en");
hass.addEntities(ENTITIES); hass.addEntities(ENTITIES);
} }
} }

View File

@@ -98,4 +98,4 @@ class DemoButtonEntity extends PolymerElement {
} }
} }
customElements.define("demo-hui-button-card", DemoButtonEntity); customElements.define("demo-hui-entity-button-card", DemoButtonEntity);

View File

@@ -8,6 +8,7 @@ import "../components/demo-cards";
const ENTITIES = [ const ENTITIES = [
getEntity("sensor", "brightness", "12", {}), getEntity("sensor", "brightness", "12", {}),
getEntity("plant", "bonsai", "ok", {}), getEntity("plant", "bonsai", "ok", {}),
getEntity("sensor", "not_working", "unavailable", {}),
getEntity("sensor", "outside_humidity", "54", { getEntity("sensor", "outside_humidity", "54", {
unit_of_measurement: "%", unit_of_measurement: "%",
}), }),
@@ -74,6 +75,13 @@ const CONFIGS = [
entity: plant.bonsai entity: plant.bonsai
`, `,
}, },
{
heading: "Unavailable entity",
config: `
- type: gauge
entity: sensor.not_working
`,
},
]; ];
class DemoGaugeEntity extends PolymerElement { class DemoGaugeEntity extends PolymerElement {

View File

@@ -1,6 +1,8 @@
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */ /* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import { mockTemplate } from "../../../demo/src/stubs/template";
import { provideHass } from "../../../src/fake_data/provide_hass";
import "../components/demo-cards"; import "../components/demo-cards";
const CONFIGS = [ const CONFIGS = [
@@ -254,7 +256,7 @@ const CONFIGS = [
class DemoMarkdown extends PolymerElement { class DemoMarkdown extends PolymerElement {
static get template() { static get template() {
return html` <demo-cards configs="[[_configs]]"></demo-cards> `; return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
} }
static get properties() { static get properties() {
@@ -265,6 +267,12 @@ class DemoMarkdown extends PolymerElement {
}, },
}; };
} }
public ready() {
super.ready();
const hass = provideHass(this.$.demos);
mockTemplate(hass);
}
} }
customElements.define("demo-hui-markdown-card", DemoMarkdown); customElements.define("demo-hui-markdown-card", DemoMarkdown);

View File

@@ -7,40 +7,61 @@ import { createMediaPlayerEntities } from "../data/media_players";
const CONFIGS = [ const CONFIGS = [
{ {
heading: "Paused music", heading: "Paused Music",
config: ` config: `
- type: media-control - type: media-control
entity: media_player.music_paused entity: media_player.music_paused
`, `,
}, },
{ {
heading: "Playing music", heading: "Playing Music",
config: ` config: `
- type: media-control - type: media-control
entity: media_player.music_playing entity: media_player.music_playing
`, `,
}, },
{ {
heading: "Playing stream", heading: "Playing Stream",
config: ` config: `
- type: media-control - type: media-control
entity: media_player.stream_playing entity: media_player.stream_playing
`, `,
}, },
{ {
heading: "Pause, No skip, tvshow", heading: "Paused Stream",
config: ` config: `
- type: media-control - type: media-control
entity: media_player.living_room entity: media_player.stream_paused
`, `,
}, },
{ {
heading: "Screen casting", heading: 'Playing Stream (with "previous" support)',
config: `
- type: media-control
entity: media_player.stream_playing_previous
`,
},
{
heading: "Playing non-skip TV Show",
config: `
- type: media-control
entity: media_player.tv_playing
`,
},
{
heading: "Screen Casting",
config: ` config: `
- type: media-control - type: media-control
entity: media_player.android_cast entity: media_player.android_cast
`, `,
}, },
{
heading: "Digital Picture Frame",
config: `
- type: media-control
entity: media_player.image_display
`,
},
{ {
heading: "Sonos Idle", heading: "Sonos Idle",
config: ` config: `
@@ -48,11 +69,53 @@ const CONFIGS = [
entity: media_player.sonos_idle entity: media_player.sonos_idle
`, `,
}, },
{
heading: "Idle waiting for Browse Media",
config: `
- type: media-control
entity: media_player.idle_browse_media
`,
},
{ {
heading: "Player Off", heading: "Player Off",
config: ` config: `
- type: media-control - type: media-control
entity: media_player.theater entity: media_player.theater_off
`,
},
{
heading: "Player On",
config: `
- type: media-control
entity: media_player.theater_on
`,
},
{
heading: "Player Off (cannot be switched on)",
config: `
- type: media-control
entity: media_player.theater_off_static
`,
},
{
heading: "Player On (cannot be switched off)",
config: `
- type: media-control
entity: media_player.theater_on_static
`,
},
{
heading: "Player Idle",
config: `
- type: media-control
entity: media_player.idle
`,
},
{
heading: "Player Playing",
config: `
- type: media-control
entity: media_player.playing
`, `,
}, },
{ {
@@ -70,14 +133,14 @@ const CONFIGS = [
`, `,
}, },
{ {
heading: "Receiver On", heading: "Receiver On (selectable sources)",
config: ` config: `
- type: media-control - type: media-control
entity: media_player.receiver_on entity: media_player.receiver_on
`, `,
}, },
{ {
heading: "Receiver Off", heading: "Receiver Off (selectable sources)",
config: ` config: `
- type: media-control - type: media-control
entity: media_player.receiver_off entity: media_player.receiver_off

View File

@@ -12,23 +12,45 @@ const CONFIGS = [
- type: entities - type: entities
entities: entities:
- entity: media_player.music_paused - entity: media_player.music_paused
name: Paused music name: Paused Music
- entity: media_player.music_playing - entity: media_player.music_playing
name: Playing music name: Playing Music
- entity: media_player.stream_playing - entity: media_player.stream_playing
name: Paused, no play name: Playing Stream
- entity: media_player.living_room - entity: media_player.stream_paused
name: Pause, No skip, tvshow name: Paused Stream
- entity: media_player.stream_playing_previous
name: Playing Stream (with "previous" support)
- entity: media_player.tv_playing
name: Playing non-skip TV Show
- entity: media_player.android_cast - entity: media_player.android_cast
name: Screen casting name: Screen casting
- entity: media_player.image_display
name: Digital Picture Frame
- entity: media_player.sonos_idle - entity: media_player.sonos_idle
name: Chromcast Idle name: Sonos Idle
- entity: media_player.theater - entity: media_player.idle_browse_media
name: Idle waiting for Browse Media
- entity: media_player.theater_off
name: Player Off name: Player Off
- entity: media_player.theater_on
name: Player On
- entity: media_player.theater_off_static
name: Player Off (cannot be switched on)
- entity: media_player.theater_on_static
name: Player On (cannot be switched off)
- entity: media_player.idle
name: Player Idle
- entity: media_player.playing
name: Player Playing
- entity: media_player.unavailable - entity: media_player.unavailable
name: Player Unavailable name: Player Unavailable
- entity: media_player.unknown - entity: media_player.unknown
name: Player Unknown name: Player Unknown
- entity: media_player.receiver_on
name: Receiver On (selectable sources)
- entity: media_player.receiver_off
name: Receiver Off (selectable sources)
`, `,
}, },
]; ];

View File

@@ -1,6 +1,7 @@
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */ /* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import { mockHistory } from "../../../demo/src/stubs/history";
import { getEntity } from "../../../src/fake_data/entity"; import { getEntity } from "../../../src/fake_data/entity";
import { provideHass } from "../../../src/fake_data/provide_hass"; import { provideHass } from "../../../src/fake_data/provide_hass";
import "../components/demo-cards"; import "../components/demo-cards";
@@ -36,6 +37,10 @@ const ENTITIES = [
battery: 71, battery: 71,
friendly_name: "Home Boy", friendly_name: "Home Boy",
}), }),
getEntity("sensor", "illumination", "23", {
friendly_name: "Illumination",
unit_of_measurement: "lx",
}),
]; ];
const CONFIGS = [ const CONFIGS = [
@@ -89,6 +94,42 @@ const CONFIGS = [
entity: light.bed_light entity: light.bed_light
`, `,
}, },
{
heading: "Default Grid",
config: `
- type: grid
cards:
- type: entity
entity: light.kitchen_lights
- type: entity
entity: light.bed_light
- type: entity
entity: device_tracker.demo_paulus
- type: sensor
entity: sensor.illumination
graph: line
- type: entity
entity: device_tracker.demo_anne_therese
`,
},
{
heading: "Non-square Grid with 2 columns",
config: `
- type: grid
columns: 2
square: false
cards:
- type: entity
entity: light.kitchen_lights
- type: entity
entity: light.bed_light
- type: entity
entity: device_tracker.demo_paulus
- type: sensor
entity: sensor.illumination
graph: line
`,
},
]; ];
class DemoStack extends PolymerElement { class DemoStack extends PolymerElement {
@@ -110,6 +151,7 @@ class DemoStack extends PolymerElement {
const hass = provideHass(this.$.demos); const hass = provideHass(this.$.demos);
hass.updateTranslations(null, "en"); hass.updateTranslations(null, "en");
hass.addEntities(ENTITIES); hass.addEntities(ENTITIES);
mockHistory(hass);
} }
} }

View File

@@ -6,7 +6,7 @@ import { SUPPORT_BRIGHTNESS } from "../../../src/data/light";
import { getEntity } from "../../../src/fake_data/entity"; import { getEntity } from "../../../src/fake_data/entity";
import { provideHass } from "../../../src/fake_data/provide_hass"; import { provideHass } from "../../../src/fake_data/provide_hass";
import "../components/demo-more-infos"; import "../components/demo-more-infos";
import "../components/more-info-content"; import "../../../src/dialogs/more-info/more-info-content";
const ENTITIES = [ const ENTITIES = [
getEntity("light", "bed_light", "on", { getEntity("light", "bed_light", "on", {
@@ -40,8 +40,12 @@ class DemoMoreInfoLight extends PolymerElement {
public ready() { public ready() {
super.ready(); super.ready();
this._setupDemo();
}
private async _setupDemo() {
const hass = provideHass(this); const hass = provideHass(this);
hass.updateTranslations(null, "en"); await hass.updateTranslations(null, "en");
hass.addEntities(ENTITIES); hass.addEntities(ENTITIES);
} }
} }

View File

@@ -13,7 +13,10 @@ import {
fetchHassioAddonInfo, fetchHassioAddonInfo,
HassioAddonDetails, HassioAddonDetails,
} from "../../../src/data/hassio/addon"; } from "../../../src/data/hassio/addon";
import { createHassioSession } from "../../../src/data/hassio/supervisor"; import {
createHassioSession,
validateHassioSession,
} from "../../../src/data/hassio/ingress";
import "../../../src/layouts/hass-loading-screen"; import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-subpage"; import "../../../src/layouts/hass-subpage";
import { HomeAssistant, Route } from "../../../src/types"; import { HomeAssistant, Route } from "../../../src/types";
@@ -35,6 +38,17 @@ class HassioIngressView extends LitElement {
@property({ type: Boolean }) @property({ type: Boolean })
public narrow = false; public narrow = false;
private _sessionKeepAlive?: number;
public disconnectedCallback() {
super.disconnectedCallback();
if (this._sessionKeepAlive) {
clearInterval(this._sessionKeepAlive);
this._sessionKeepAlive = undefined;
}
}
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._addon) { if (!this._addon) {
return html` <hass-loading-screen></hass-loading-screen> `; return html` <hass-loading-screen></hass-loading-screen> `;
@@ -44,6 +58,7 @@ class HassioIngressView extends LitElement {
if (!this.ingressPanel) { if (!this.ingressPanel) {
return html`<hass-subpage return html`<hass-subpage
.hass=${this.hass}
.header=${this._addon.name} .header=${this._addon.name}
.narrow=${this.narrow} .narrow=${this.narrow}
> >
@@ -83,10 +98,7 @@ class HassioIngressView extends LitElement {
} }
private async _fetchData(addonSlug: string) { private async _fetchData(addonSlug: string) {
const createSessionPromise = createHassioSession(this.hass).then( const createSessionPromise = createHassioSession(this.hass);
() => true,
() => false
);
let addon; let addon;
@@ -119,7 +131,11 @@ class HassioIngressView extends LitElement {
return; return;
} }
if (!(await createSessionPromise)) { let session;
try {
session = await createSessionPromise;
} catch (err) {
await showAlertDialog(this, { await showAlertDialog(this, {
text: "Unable to create an Ingress session", text: "Unable to create an Ingress session",
title: addon.name, title: addon.name,
@@ -128,6 +144,17 @@ class HassioIngressView extends LitElement {
return; return;
} }
if (this._sessionKeepAlive) {
clearInterval(this._sessionKeepAlive);
}
this._sessionKeepAlive = window.setInterval(async () => {
try {
await validateHassioSession(this.hass, session);
} catch (err) {
session = await createHassioSession(this.hass);
}
}, 60000);
this._addon = addon; this._addon = addon;
} }

View File

@@ -119,6 +119,7 @@
"roboto-fontface": "^0.10.0", "roboto-fontface": "^0.10.0",
"sortablejs": "^1.10.2", "sortablejs": "^1.10.2",
"superstruct": "^0.10.12", "superstruct": "^0.10.12",
"tinykeys": "^1.1.1",
"unfetch": "^4.1.0", "unfetch": "^4.1.0",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue2-daterange-picker": "^0.5.1", "vue2-daterange-picker": "^0.5.1",
@@ -139,7 +140,6 @@
"@babel/plugin-proposal-optional-chaining": "^7.11.0", "@babel/plugin-proposal-optional-chaining": "^7.11.0",
"@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-import-meta": "^7.10.4",
"@babel/plugin-syntax-top-level-await": "^7.10.4",
"@babel/preset-env": "^7.11.5", "@babel/preset-env": "^7.11.5",
"@babel/preset-typescript": "^7.10.4", "@babel/preset-typescript": "^7.10.4",
"@rollup/plugin-commonjs": "^11.1.0", "@rollup/plugin-commonjs": "^11.1.0",
@@ -177,7 +177,6 @@
"gulp": "^4.0.0", "gulp": "^4.0.0",
"gulp-foreach": "^0.1.0", "gulp-foreach": "^0.1.0",
"gulp-json-transform": "^0.4.6", "gulp-json-transform": "^0.4.6",
"gulp-jsonminify": "^1.1.0",
"gulp-merge-json": "^1.3.1", "gulp-merge-json": "^1.3.1",
"gulp-rename": "^2.0.0", "gulp-rename": "^2.0.0",
"gulp-zopfli-green": "^3.0.1", "gulp-zopfli-green": "^3.0.1",

View File

@@ -13,7 +13,6 @@
"src/panels/iframe/ha-panel-iframe.js", "src/panels/iframe/ha-panel-iframe.js",
"src/panels/logbook/ha-panel-logbook.js", "src/panels/logbook/ha-panel-logbook.js",
"src/panels/map/ha-panel-map.js", "src/panels/map/ha-panel-map.js",
"src/panels/shopping-list/ha-panel-shopping-list.js",
"src/panels/mailbox/ha-panel-mailbox.js", "src/panels/mailbox/ha-panel-mailbox.js",
"hassio/src/entrypoint.js" "hassio/src/entrypoint.js"
], ],

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup( setup(
name="home-assistant-frontend", name="home-assistant-frontend",
version="20201021.1", version="20201111.0",
description="The Home Assistant frontend", description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer", url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors", author="The Home Assistant Authors",

View File

@@ -1,10 +1,4 @@
const expand_hex = (hex: string): string => { import { expandHex } from "./hex";
let result = "";
for (const val of hex) {
result += val + val;
}
return result;
};
const rgb_hex = (component: number): string => { const rgb_hex = (component: number): string => {
const hex = Math.round(Math.min(Math.max(component, 0), 255)).toString(16); const hex = Math.round(Math.min(Math.max(component, 0), 255)).toString(16);
@@ -14,10 +8,7 @@ const rgb_hex = (component: number): string => {
// Conversion between HEX and RGB // Conversion between HEX and RGB
export const hex2rgb = (hex: string): [number, number, number] => { export const hex2rgb = (hex: string): [number, number, number] => {
hex = hex.replace("#", ""); hex = expandHex(hex);
if (hex.length === 3 || hex.length === 4) {
hex = expand_hex(hex);
}
return [ return [
parseInt(hex.substring(0, 2), 16), parseInt(hex.substring(0, 2), 16),

24
src/common/color/hex.ts Normal file
View File

@@ -0,0 +1,24 @@
export const expandHex = (hex: string): string => {
hex = hex.replace("#", "");
if (hex.length === 6) return hex;
let result = "";
for (const val of hex) {
result += val + val;
}
return result;
};
// Blend 2 hex colors: c1 is placed over c2, blend is c1's opacity.
export const hexBlend = (c1: string, c2: string, blend = 50): string => {
let color = "";
c1 = expandHex(c1);
c2 = expandHex(c2);
for (let i = 0; i <= 5; i += 2) {
const h1 = parseInt(c1.substr(i, 2), 16);
const h2 = parseInt(c2.substr(i, 2), 16);
let hex = Math.floor(h2 + (h1 - h2) * (blend / 100)).toString(16);
while (hex.length < 2) hex = "0" + hex;
color += hex;
}
return `#${color}`;
};

View File

@@ -0,0 +1,18 @@
import { isComponentLoaded } from "./is_component_loaded";
import { PageNavigation } from "../../layouts/hass-tabs-subpage";
import { HomeAssistant } from "../../types";
export const canShowPage = (hass: HomeAssistant, page: PageNavigation) => {
return (
(isCore(page) || isLoadedIntegration(hass, page)) &&
!hideAdvancedPage(hass, page)
);
};
const isLoadedIntegration = (hass: HomeAssistant, page: PageNavigation) =>
!page.component || isComponentLoaded(hass, page.component);
const isCore = (page: PageNavigation) => page.core;
const isAdvancedPage = (page: PageNavigation) => page.advancedOnly;
const userWantsAdvanced = (hass: HomeAssistant) => hass.userData?.showAdvanced;
const hideAdvancedPage = (hass: HomeAssistant, page: PageNavigation) =>
isAdvancedPage(page) && !userWantsAdvanced(hass);

View File

@@ -7,6 +7,7 @@ import {
rgb2hex, rgb2hex,
rgb2lab, rgb2lab,
} from "../color/convert-color"; } from "../color/convert-color";
import { hexBlend } from "../color/hex";
import { labBrighten, labDarken } from "../color/lab"; import { labBrighten, labDarken } from "../color/lab";
import { rgbContrast } from "../color/rgb"; import { rgbContrast } from "../color/rgb";
@@ -37,6 +38,13 @@ export const applyThemesOnElement = (
if (themeOptions.dark) { if (themeOptions.dark) {
cacheKey = `${cacheKey}__dark`; cacheKey = `${cacheKey}__dark`;
themeRules = darkStyles; themeRules = darkStyles;
if (themeOptions.primaryColor) {
themeRules["app-header-background-color"] = hexBlend(
themeOptions.primaryColor,
"#121212",
8
);
}
} }
if (themeOptions.primaryColor) { if (themeOptions.primaryColor) {
cacheKey = `${cacheKey}__primary_${themeOptions.primaryColor}`; cacheKey = `${cacheKey}__primary_${themeOptions.primaryColor}`;

View File

@@ -5,6 +5,7 @@ import { formatDateTime } from "../datetime/format_date_time";
import { formatTime } from "../datetime/format_time"; import { formatTime } from "../datetime/format_time";
import { LocalizeFunc } from "../translations/localize"; import { LocalizeFunc } from "../translations/localize";
import { computeStateDomain } from "./compute_state_domain"; import { computeStateDomain } from "./compute_state_domain";
import { numberFormat } from "../string/number-format";
export const computeStateDisplay = ( export const computeStateDisplay = (
localize: LocalizeFunc, localize: LocalizeFunc,
@@ -19,7 +20,9 @@ export const computeStateDisplay = (
} }
if (stateObj.attributes.unit_of_measurement) { if (stateObj.attributes.unit_of_measurement) {
return `${compareState} ${stateObj.attributes.unit_of_measurement}`; return `${numberFormat(compareState, language)} ${
stateObj.attributes.unit_of_measurement
}`;
} }
const domain = computeStateDomain(stateObj); const domain = computeStateDomain(stateObj);

View File

@@ -43,6 +43,7 @@ export const coverIcon = (state?: string, stateObj?: HassEntity): string => {
} }
case "blind": case "blind":
case "curtain": case "curtain":
case "shade":
switch (state) { switch (state) {
case "opening": case "opening":
return "hass:arrow-up-box"; return "hass:arrow-up-box";
@@ -77,3 +78,25 @@ export const coverIcon = (state?: string, stateObj?: HassEntity): string => {
return "hass:window-open"; return "hass:window-open";
} }
}; };
export const computeOpenIcon = (stateObj: HassEntity): string => {
switch (stateObj.attributes.device_class) {
case "awning":
case "door":
case "gate":
return "hass:arrow-expand-horizontal";
default:
return "hass:arrow-up";
}
};
export const computeCloseIcon = (stateObj: HassEntity): string => {
switch (stateObj.attributes.device_class) {
case "awning":
case "door":
case "gate":
return "hass:arrow-collapse-horizontal";
default:
return "hass:arrow-down";
}
};

View File

@@ -42,7 +42,7 @@ export const fuzzySequentialMatch = (filter: string, ...words: string[]) => {
export interface ScorableTextItem { export interface ScorableTextItem {
score?: number; score?: number;
text: string; filterText: string;
altText?: string; altText?: string;
} }
@@ -55,11 +55,11 @@ export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => {
return items return items
.map((item) => { .map((item) => {
item.score = item.altText item.score = item.altText
? fuzzySequentialMatch(filter, item.text, item.altText) ? fuzzySequentialMatch(filter, item.filterText, item.altText)
: fuzzySequentialMatch(filter, item.text); : fuzzySequentialMatch(filter, item.filterText);
return item; return item;
}) })
.filter((item) => item.score === undefined || item.score > 0) .filter((item) => item.score !== undefined && item.score > 0)
.sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) => .sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) =>
scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0 scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0
); );

View File

@@ -0,0 +1,22 @@
/**
* Formats a number based on the specified language 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
*/
export const numberFormat = (
num: string | number,
language: string
): string => {
// Polyfill for Number.isNaN, which is more reliable that the global isNaN()
Number.isNaN =
Number.isNaN ||
function isNaN(input) {
return typeof input === "number" && isNaN(input);
};
if (!Number.isNaN(Number(num)) && Intl) {
return new Intl.NumberFormat(language).format(Number(num));
}
return num.toString();
};

View File

@@ -13,9 +13,12 @@ export interface FormatsType {
time: FormatType; time: FormatType;
} }
if (shouldPolyfill()) { let polyfillLoaded = !shouldPolyfill();
await import("@formatjs/intl-pluralrules/polyfill-locales"); const polyfillProm = polyfillLoaded
} ? undefined
: import("@formatjs/intl-pluralrules/polyfill-locales").then(() => {
polyfillLoaded = true;
});
/** /**
* Adapted from Polymer app-localize-behavior. * Adapted from Polymer app-localize-behavior.
@@ -38,12 +41,16 @@ if (shouldPolyfill()) {
* } * }
*/ */
export const computeLocalize = ( export const computeLocalize = async (
cache: any, cache: any,
language: string, language: string,
resources: Resources, resources: Resources,
formats?: FormatsType formats?: FormatsType
): LocalizeFunc => { ): Promise<LocalizeFunc> => {
if (!polyfillLoaded) {
await polyfillProm;
}
// Everytime any of the parameters change, invalidate the strings cache. // Everytime any of the parameters change, invalidate the strings cache.
cache._localizationCache = {}; cache._localizationCache = {};

View File

@@ -0,0 +1,8 @@
export const copyToClipboard = (str) => {
const el = document.createElement("textarea");
el.value = str;
document.body.appendChild(el);
el.select();
document.execCommand("copy");
document.body.removeChild(el);
};

View File

@@ -1,5 +1,5 @@
import "@material/mwc-icon-button/mwc-icon-button";
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "../ha-icon-button";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-item/paper-item-body";
@@ -38,6 +38,8 @@ import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { PolymerChangedEvent } from "../../polymer-types"; import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "./ha-devices-picker"; import "./ha-devices-picker";
import "../ha-svg-icon";
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
interface DevicesByArea { interface DevicesByArea {
[areaId: string]: AreaDevices; [areaId: string]: AreaDevices;
@@ -62,7 +64,7 @@ const rowRenderer = (
margin: -10px 0; margin: -10px 0;
padding: 0; padding: 0;
} }
ha-icon-button { mwc-icon-button {
float: right; float: right;
} }
.devices { .devices {
@@ -324,36 +326,34 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
autocorrect="off" autocorrect="off"
spellcheck="false" spellcheck="false"
> >
<div class="suffix" slot="suffix">
${this.value ${this.value
? html` ? html`<mwc-icon-button
<ha-icon-button class="clear-button"
aria-label=${this.hass.localize( .label=${this.hass.localize(
"ui.components.device-picker.clear" "ui.components.device-picker.clear"
)} )}
slot="suffix"
class="clear-button"
icon="hass:close"
@click=${this._clearValue} @click=${this._clearValue}
no-ripple no-ripple
> >
Clear <ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</ha-icon-button> </mwc-icon-button> `
`
: ""} : ""}
${areas.length > 0 ${areas.length > 0
? html` ? html`
<ha-icon-button <mwc-icon-button
aria-label=${this.hass.localize( .label=${this.hass.localize(
"ui.components.device-picker.show_devices" "ui.components.device-picker.show_devices"
)} )}
slot="suffix"
class="toggle-button" class="toggle-button"
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
> >
Toggle <ha-svg-icon
</ha-icon-button> .path=${this._opened ? mdiMenuUp : mdiMenuDown}
></ha-svg-icon>
</mwc-icon-button>
` `
: ""} : ""}
</div>
</paper-input> </paper-input>
</vaadin-combo-box-light> </vaadin-combo-box-light>
<mwc-button @click=${this._switchPicker} <mwc-button @click=${this._switchPicker}
@@ -409,10 +409,12 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
static get styles(): CSSResult { static get styles(): CSSResult {
return css` return css`
paper-input > ha-icon-button { .suffix {
width: 24px; display: flex;
height: 24px; }
padding: 2px; mwc-icon-button {
--mdc-icon-button-size: 24px;
padding: 0px 2px;
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
[hidden] { [hidden] {

View File

@@ -88,6 +88,7 @@ class HaChartBase extends mixinBehaviors(
.chartTooltip .beforeBody { .chartTooltip .beforeBody {
text-align: center; text-align: center;
font-weight: 300; font-weight: 300;
word-break: break-all;
} }
.chartLegend li { .chartLegend li {
display: inline-block; display: inline-block;

View File

@@ -19,6 +19,7 @@ import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-svg-icon"; import "../ha-svg-icon";
import "./state-badge"; import "./state-badge";
import { formatAttributeName } from "../../util/hass-attributes-util";
import "@material/mwc-icon-button/mwc-icon-button"; import "@material/mwc-icon-button/mwc-icon-button";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
@@ -35,7 +36,9 @@ const rowRenderer = (root: HTMLElement, _owner, model: { item: string }) => {
<paper-item></paper-item> <paper-item></paper-item>
`; `;
} }
root.querySelector("paper-item")!.textContent = model.item; root.querySelector("paper-item")!.textContent = formatAttributeName(
model.item
);
}; };
@customElement("ha-entity-attribute-picker") @customElement("ha-entity-attribute-picker")
@@ -92,7 +95,7 @@ class HaEntityAttributePicker extends LitElement {
this.hass.localize( this.hass.localize(
"ui.components.entity.entity-attribute-picker.attribute" "ui.components.entity.entity-attribute-picker.attribute"
)} )}
.value=${this._value} .value=${this._value ? formatAttributeName(this._value) : ""}
.disabled=${this.disabled || !this.entityId} .disabled=${this.disabled || !this.entityId}
class="input" class="input"
autocapitalize="none" autocapitalize="none"
@@ -140,7 +143,7 @@ class HaEntityAttributePicker extends LitElement {
} }
private get _value() { private get _value() {
return this.value || ""; return this.value;
} }
private _openedChanged(ev: PolymerChangedEvent<boolean>) { private _openedChanged(ev: PolymerChangedEvent<boolean>) {

View File

@@ -1,25 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { stateIcon } from "../../common/entity/state_icon";
import "../ha-icon";
class HaStateIcon extends PolymerElement {
static get template() {
return html` <ha-icon icon="[[computeIcon(stateObj)]]"></ha-icon> `;
}
static get properties() {
return {
stateObj: {
type: Object,
},
};
}
computeIcon(stateObj) {
return stateIcon(stateObj);
}
}
customElements.define("ha-state-icon", HaStateIcon);

View File

@@ -1,124 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import "@polymer/paper-tooltip/paper-tooltip";
/* eslint-plugin-disable lit */
import LocalizeMixin from "../../mixins/localize-mixin";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { computeStateName } from "../../common/entity/compute_state_name";
import { computeRTL } from "../../common/util/compute_rtl";
import "../ha-relative-time";
import "./state-badge";
class StateInfo extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
${this.styleTemplate} ${this.stateBadgeTemplate} ${this.infoTemplate}
`;
}
static get styleTemplate() {
return html`
<style>
:host {
@apply --paper-font-body1;
min-width: 120px;
white-space: nowrap;
}
state-badge {
float: left;
}
:host([rtl]) state-badge {
float: right;
}
.info {
margin-left: 56px;
}
:host([rtl]) .info {
margin-right: 56px;
margin-left: 0;
text-align: right;
}
.name {
@apply --paper-font-common-nowrap;
color: var(--primary-text-color);
line-height: 40px;
}
.name[in-dialog],
:host([secondary-line]) .name {
line-height: 20px;
}
.time-ago,
.extra-info,
.extra-info > * {
@apply --paper-font-common-nowrap;
color: var(--secondary-text-color);
}
</style>
`;
}
static get stateBadgeTemplate() {
return html` <state-badge state-obj="[[stateObj]]"></state-badge> `;
}
static get infoTemplate() {
return html`
<div class="info">
<div class="name" in-dialog$="[[inDialog]]">
[[computeStateName(stateObj)]]
</div>
<template is="dom-if" if="[[inDialog]]">
<div class="time-ago">
<ha-relative-time
id="last_changed"
hass="[[hass]]"
datetime="[[stateObj.last_changed]]"
></ha-relative-time>
<paper-tooltip animation-delay="0" for="last_changed">
[[localize('ui.dialogs.more_info_control.last_updated')]]:
<ha-relative-time
hass="[[hass]]"
datetime="[[stateObj.last_updated]]"
></ha-relative-time>
</paper-tooltip>
</div>
</template>
<template is="dom-if" if="[[!inDialog]]">
<div class="extra-info"><slot> </slot></div>
</template>
</div>
`;
}
static get properties() {
return {
hass: Object,
stateObj: Object,
inDialog: {
type: Boolean,
value: () => false,
},
rtl: {
type: Boolean,
reflectToAttribute: true,
computed: "computeRTL(hass)",
},
};
}
computeStateName(stateObj) {
return computeStateName(stateObj);
}
computeRTL(hass) {
return computeRTL(hass);
}
}
customElements.define("state-info", StateInfo);

View File

@@ -0,0 +1,158 @@
import "@polymer/paper-tooltip/paper-tooltip";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import type { HassEntity } from "home-assistant-js-websocket";
import { computeStateName } from "../../common/entity/compute_state_name";
import { computeRTL } from "../../common/util/compute_rtl";
import type { HomeAssistant } from "../../types";
import "../ha-relative-time";
import "./state-badge";
@customElement("state-info")
class StateInfo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@property({ type: Boolean }) public inDialog = false;
// property used only in css
@property({ type: Boolean, reflect: true }) public rtl = false;
protected render(): TemplateResult {
if (!this.hass || !this.stateObj) {
return html``;
}
return html`<state-badge
.stateObj=${this.stateObj}
.stateColor=${true}
></state-badge>
<div class="info">
<div class="name" .inDialog=${this.inDialog}>
${computeStateName(this.stateObj)}
</div>
${this.inDialog
? html`<div class="time-ago">
<ha-relative-time
id="last_changed"
.hass=${this.hass}
.datetime=${this.stateObj.last_changed}
></ha-relative-time>
<paper-tooltip animation-delay="0" for="last_changed">
<div>
<div class="row">
<span class="column-name">
${this.hass.localize(
"ui.dialogs.more_info_control.last_changed"
)}:
</span>
<ha-relative-time
.hass=${this.hass}
.datetime=${this.stateObj.last_changed}
></ha-relative-time>
</div>
<div class="row">
<span>
${this.hass.localize(
"ui.dialogs.more_info_control.last_updated"
)}:
</span>
<ha-relative-time
.hass=${this.hass}
.datetime=${this.stateObj.last_updated}
></ha-relative-time>
</div>
</div>
</paper-tooltip>
</div>`
: html`<div class="extra-info"><slot> </slot></div>`}
</div>`;
}
protected updated(changedProps) {
super.updated(changedProps);
if (!changedProps.has("hass")) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.language !== this.hass.language) {
this.rtl = computeRTL(this.hass);
}
}
static get styles(): CSSResult {
return css`
:host {
@apply --paper-font-body1;
min-width: 120px;
white-space: nowrap;
}
state-badge {
float: left;
}
:host([rtl]) state-badge {
float: right;
}
.info {
margin-left: 56px;
}
:host([rtl]) .info {
margin-right: 56px;
margin-left: 0;
text-align: right;
}
.name {
@apply --paper-font-common-nowrap;
color: var(--primary-text-color);
line-height: 40px;
}
.name[in-dialog],
:host([secondary-line]) .name {
line-height: 20px;
}
.time-ago,
.extra-info,
.extra-info > * {
@apply --paper-font-common-nowrap;
color: var(--secondary-text-color);
}
.row {
display: flex;
flex-direction: row;
flex-wrap: no-wrap;
width: 100%;
justify-content: space-between;
margin: 0 2px 4px 0;
}
.row:last-child {
margin-bottom: 0px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"state-info": StateInfo;
}
}

View File

@@ -9,7 +9,9 @@ import {
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { until } from "lit-html/directives/until"; import { until } from "lit-html/directives/until";
import hassAttributeUtil from "../util/hass-attributes-util"; import hassAttributeUtil, {
formatAttributeName,
} from "../util/hass-attributes-util";
let jsYamlPromise: Promise<typeof import("js-yaml")>; let jsYamlPromise: Promise<typeof import("js-yaml")>;
@@ -34,7 +36,7 @@ class HaAttributes extends LitElement {
(attribute) => html` (attribute) => html`
<div class="data-entry"> <div class="data-entry">
<div class="key"> <div class="key">
${attribute.replace(/_/g, " ").replace("id", "ID")} ${formatAttributeName(attribute)}
</div> </div>
<div class="value"> <div class="value">
${this.formatAttribute(attribute)} ${this.formatAttribute(attribute)}
@@ -61,15 +63,16 @@ class HaAttributes extends LitElement {
justify-content: space-between; justify-content: space-between;
} }
.data-entry .value { .data-entry .value {
max-width: 200px; max-width: 50%;
overflow-wrap: break-word; overflow-wrap: break-word;
text-align: right;
} }
.key:first-letter { .key {
text-transform: capitalize; flex-grow: 1;
} }
.attribution { .attribution {
color: var(--secondary-text-color); color: var(--secondary-text-color);
text-align: right; text-align: center;
} }
pre { pre {
font-family: inherit; font-family: inherit;

View File

@@ -11,7 +11,7 @@ export class HaCircularProgress extends CircularProgress {
public alt = "Loading"; public alt = "Loading";
@property() @property()
public size: "small" | "medium" | "large" = "medium"; public size: "tiny" | "small" | "medium" | "large" = "medium";
// @ts-ignore // @ts-ignore
public set density(_) { public set density(_) {
@@ -20,6 +20,8 @@ export class HaCircularProgress extends CircularProgress {
public get density() { public get density() {
switch (this.size) { switch (this.size) {
case "tiny":
return -8;
case "small": case "small":
return -5; return -5;
case "medium": case "medium":

View File

@@ -1,131 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { CLIMATE_PRESET_NONE } from "../data/climate";
import LocalizeMixin from "../mixins/localize-mixin";
/*
* @appliesMixin LocalizeMixin
*/
class HaClimateState extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style>
:host {
display: flex;
flex-direction: column;
justify-content: center;
white-space: nowrap;
}
.target {
color: var(--primary-text-color);
}
.current {
color: var(--secondary-text-color);
}
.state-label {
font-weight: bold;
text-transform: capitalize;
}
.unit {
display: inline-block;
direction: ltr;
}
</style>
<div class="target">
<template is="dom-if" if="[[_hasKnownState(stateObj.state)]]">
<span class="state-label">
[[_localizeState(localize, stateObj)]]
<template is="dom-if" if="[[_renderPreset(stateObj.attributes)]]">
- [[_localizePreset(localize, stateObj.attributes.preset_mode)]]
</template>
</span>
</template>
<div class="unit">[[computeTarget(hass, stateObj)]]</div>
</div>
<template is="dom-if" if="[[currentStatus]]">
<div class="current">
[[localize('ui.card.climate.currently')]]:
<div class="unit">[[currentStatus]]</div>
</div>
</template>
`;
}
static get properties() {
return {
hass: Object,
stateObj: Object,
currentStatus: {
type: String,
computed: "computeCurrentStatus(hass, stateObj)",
},
};
}
computeCurrentStatus(hass, stateObj) {
if (!hass || !stateObj) return null;
if (stateObj.attributes.current_temperature != null) {
return `${stateObj.attributes.current_temperature} ${hass.config.unit_system.temperature}`;
}
if (stateObj.attributes.current_humidity != null) {
return `${stateObj.attributes.current_humidity} %`;
}
return null;
}
computeTarget(hass, stateObj) {
if (!hass || !stateObj) return null;
// We're using "!= null" on purpose so that we match both null and undefined.
if (
stateObj.attributes.target_temp_low != null &&
stateObj.attributes.target_temp_high != null
) {
return `${stateObj.attributes.target_temp_low}-${stateObj.attributes.target_temp_high} ${hass.config.unit_system.temperature}`;
}
if (stateObj.attributes.temperature != null) {
return `${stateObj.attributes.temperature} ${hass.config.unit_system.temperature}`;
}
if (
stateObj.attributes.target_humidity_low != null &&
stateObj.attributes.target_humidity_high != null
) {
return `${stateObj.attributes.target_humidity_low}-${stateObj.attributes.target_humidity_high}%`;
}
if (stateObj.attributes.humidity != null) {
return `${stateObj.attributes.humidity} %`;
}
return "";
}
_hasKnownState(state) {
return state !== "unknown";
}
_localizeState(localize, stateObj) {
const stateString = localize(`component.climate.state._.${stateObj.state}`);
return stateObj.attributes.hvac_action
? `${localize(
`state_attributes.climate.hvac_action.${stateObj.attributes.hvac_action}`
)} (${stateString})`
: stateString;
}
_localizePreset(localize, preset) {
return localize(`state_attributes.climate.preset_mode.${preset}`) || preset;
}
_renderPreset(attributes) {
return (
attributes.preset_mode && attributes.preset_mode !== CLIMATE_PRESET_NONE
);
}
}
customElements.define("ha-climate-state", HaClimateState);

View File

@@ -0,0 +1,139 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { HassEntity } from "home-assistant-js-websocket";
import { CLIMATE_PRESET_NONE } from "../data/climate";
import type { HomeAssistant } from "../types";
@customElement("ha-climate-state")
class HaClimateState extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: HassEntity;
protected render(): TemplateResult {
const currentStatus = this._computeCurrentStatus();
return html`<div class="target">
${this.stateObj.state !== "unknown"
? html`<span class="state-label">
${this._localizeState()}
${this.stateObj.attributes.preset_mode &&
this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE
? html`-
${this.hass.localize(
`state_attributes.climate.preset_mode.${this.stateObj.attributes.preset_mode}`
) || this.stateObj.attributes.preset_mode}`
: ""}
</span>`
: ""}
<div class="unit">${this._computeTarget()}</div>
</div>
${currentStatus
? html`<div class="current">
${this.hass.localize("ui.card.climate.currently")}:
<div class="unit">${currentStatus}</div>
</div>`
: ""}`;
}
private _computeCurrentStatus(): string | undefined {
if (!this.hass || !this.stateObj) {
return undefined;
}
if (this.stateObj.attributes.current_temperature != null) {
return `${this.stateObj.attributes.current_temperature} ${this.hass.config.unit_system.temperature}`;
}
if (this.stateObj.attributes.current_humidity != null) {
return `${this.stateObj.attributes.current_humidity} %`;
}
return undefined;
}
private _computeTarget(): string {
if (!this.hass || !this.stateObj) {
return "";
}
if (
this.stateObj.attributes.target_temp_low != null &&
this.stateObj.attributes.target_temp_high != null
) {
return `${this.stateObj.attributes.target_temp_low}-${this.stateObj.attributes.target_temp_high} ${this.hass.config.unit_system.temperature}`;
}
if (this.stateObj.attributes.temperature != null) {
return `${this.stateObj.attributes.temperature} ${this.hass.config.unit_system.temperature}`;
}
if (
this.stateObj.attributes.target_humidity_low != null &&
this.stateObj.attributes.target_humidity_high != null
) {
return `${this.stateObj.attributes.target_humidity_low}-${this.stateObj.attributes.target_humidity_high}%`;
}
if (this.stateObj.attributes.humidity != null) {
return `${this.stateObj.attributes.humidity} %`;
}
return "";
}
private _localizeState(): string {
const stateString = this.hass.localize(
`component.climate.state._.${this.stateObj.state}`
);
return this.stateObj.attributes.hvac_action
? `${this.hass.localize(
`state_attributes.climate.hvac_action.${this.stateObj.attributes.hvac_action}`
)} (${stateString})`
: stateString;
}
static get styles(): CSSResult {
return css`
:host {
display: flex;
flex-direction: column;
justify-content: center;
white-space: nowrap;
}
.target {
color: var(--primary-text-color);
}
.current {
color: var(--secondary-text-color);
}
.state-label {
font-weight: bold;
text-transform: capitalize;
}
.unit {
display: inline-block;
direction: ltr;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-climate-state": HaClimateState;
}
}

View File

@@ -81,6 +81,7 @@ export class HaCodeEditor extends UpdatingElement {
protected firstUpdated(changedProps: PropertyValues): void { protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
this._blockKeyboardShortcuts();
this._load(); this._load();
} }
@@ -232,6 +233,10 @@ export class HaCodeEditor extends UpdatingElement {
this.codemirror!.on("changes", () => this._onChange()); this.codemirror!.on("changes", () => this._onChange());
} }
private _blockKeyboardShortcuts() {
this.addEventListener("keydown", (ev) => ev.stopPropagation());
}
private _onChange(): void { private _onChange(): void {
const newValue = this.value; const newValue = this.value;
if (newValue === this._value) { if (newValue === this._value) {

View File

@@ -1,126 +0,0 @@
import "./ha-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { UNAVAILABLE } from "../data/entity";
import CoverEntity from "../util/cover-model";
class HaCoverControls extends PolymerElement {
static get template() {
return html`
<style>
.state {
white-space: nowrap;
}
[invisible] {
visibility: hidden !important;
}
</style>
<div class="state">
<ha-icon-button
aria-label="Open cover"
icon="[[computeOpenIcon(stateObj)]]"
on-click="onOpenTap"
invisible$="[[!entityObj.supportsOpen]]"
disabled="[[computeOpenDisabled(stateObj, entityObj)]]"
></ha-icon-button>
<ha-icon-button
aria-label="Stop the cover from moving"
icon="hass:stop"
on-click="onStopTap"
invisible$="[[!entityObj.supportsStop]]"
disabled="[[computeStopDisabled(stateObj)]]"
></ha-icon-button>
<ha-icon-button
aria-label="Close cover"
icon="[[computeCloseIcon(stateObj)]]"
on-click="onCloseTap"
invisible$="[[!entityObj.supportsClose]]"
disabled="[[computeClosedDisabled(stateObj, entityObj)]]"
></ha-icon-button>
</div>
`;
}
static get properties() {
return {
hass: {
type: Object,
},
stateObj: {
type: Object,
},
entityObj: {
type: Object,
computed: "computeEntityObj(hass, stateObj)",
},
};
}
computeEntityObj(hass, stateObj) {
return new CoverEntity(hass, stateObj);
}
computeOpenIcon(stateObj) {
switch (stateObj.attributes.device_class) {
case "awning":
case "door":
case "gate":
return "hass:arrow-expand-horizontal";
default:
return "hass:arrow-up";
}
}
computeCloseIcon(stateObj) {
switch (stateObj.attributes.device_class) {
case "awning":
case "door":
case "gate":
return "hass:arrow-collapse-horizontal";
default:
return "hass:arrow-down";
}
}
computeStopDisabled(stateObj) {
if (stateObj.state === UNAVAILABLE) {
return true;
}
return false;
}
computeOpenDisabled(stateObj, entityObj) {
if (stateObj.state === UNAVAILABLE) {
return true;
}
const assumedState = stateObj.attributes.assumed_state === true;
return (entityObj.isFullyOpen || entityObj.isOpening) && !assumedState;
}
computeClosedDisabled(stateObj, entityObj) {
if (stateObj.state === UNAVAILABLE) {
return true;
}
const assumedState = stateObj.attributes.assumed_state === true;
return (entityObj.isFullyClosed || entityObj.isClosing) && !assumedState;
}
onOpenTap(ev) {
ev.stopPropagation();
this.entityObj.openCover();
}
onCloseTap(ev) {
ev.stopPropagation();
this.entityObj.closeCover();
}
onStopTap(ev) {
ev.stopPropagation();
this.entityObj.stopCover();
}
}
customElements.define("ha-cover-controls", HaCoverControls);

View File

@@ -0,0 +1,135 @@
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import type { HassEntity } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../types";
import { UNAVAILABLE } from "../data/entity";
import CoverEntity from "../util/cover-model";
import "./ha-icon-button";
import { computeCloseIcon, computeOpenIcon } from "../common/entity/cover_icon";
@customElement("ha-cover-controls")
class HaCoverControls extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: HassEntity;
@internalProperty() private _entityObj?: CoverEntity;
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (changedProperties.has("stateObj")) {
this._entityObj = new CoverEntity(this.hass, this.stateObj);
}
}
protected render(): TemplateResult {
if (!this._entityObj) {
return html``;
}
return html`
<div class="state">
<ha-icon-button
class=${classMap({
hidden: !this._entityObj.supportsOpen,
})}
.label=${this.hass.localize(
"ui.dialogs.more_info_control.open_cover"
)}
.icon=${computeOpenIcon(this.stateObj)}
@click=${this._onOpenTap}
.disabled=${this._computeOpenDisabled()}
></ha-icon-button>
<ha-icon-button
class=${classMap({
hidden: !this._entityObj.supportsStop,
})}
.label=${this.hass.localize(
"ui.dialogs.more_info_control.stop_cover"
)}
icon="hass:stop"
@click=${this._onStopTap}
.disabled=${this.stateObj.state === UNAVAILABLE}
></ha-icon-button>
<ha-icon-button
class=${classMap({
hidden: !this._entityObj.supportsClose,
})}
.label=${this.hass.localize(
"ui.dialogs.more_info_control.close_cover"
)}
.icon=${computeCloseIcon(this.stateObj)}
@click=${this._onCloseTap}
.disabled=${this._computeClosedDisabled()}
></ha-icon-button>
</div>
`;
}
private _computeOpenDisabled(): boolean {
if (this.stateObj.state === UNAVAILABLE) {
return true;
}
const assumedState = this.stateObj.attributes.assumed_state === true;
return (
(this._entityObj.isFullyOpen || this._entityObj.isOpening) &&
!assumedState
);
}
private _computeClosedDisabled(): boolean {
if (this.stateObj.state === UNAVAILABLE) {
return true;
}
const assumedState = this.stateObj.attributes.assumed_state === true;
return (
(this._entityObj.isFullyClosed || this._entityObj.isClosing) &&
!assumedState
);
}
private _onOpenTap(ev): void {
ev.stopPropagation();
this._entityObj.openCover();
}
private _onCloseTap(ev): void {
ev.stopPropagation();
this._entityObj.closeCover();
}
private _onStopTap(ev): void {
ev.stopPropagation();
this._entityObj.stopCover();
}
static get styles(): CSSResult {
return css`
.state {
white-space: nowrap;
}
.hidden {
visibility: hidden !important;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-cover-controls": HaCoverControls;
}
}

View File

@@ -1,106 +0,0 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "./ha-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { UNAVAILABLE } from "../data/entity";
import CoverEntity from "../util/cover-model";
class HaCoverTiltControls extends PolymerElement {
static get template() {
return html`
<style include="iron-flex"></style>
<style>
:host {
white-space: nowrap;
}
[invisible] {
visibility: hidden !important;
}
</style>
<ha-icon-button
aria-label="Open cover tilt"
icon="hass:arrow-top-right"
on-click="onOpenTiltTap"
title="Open tilt"
invisible$="[[!entityObj.supportsOpenTilt]]"
disabled="[[computeOpenDisabled(stateObj, entityObj)]]"
></ha-icon-button>
<ha-icon-button
aria-label="Stop cover from moving"
icon="hass:stop"
on-click="onStopTiltTap"
invisible$="[[!entityObj.supportsStopTilt]]"
disabled="[[computeStopDisabled(stateObj)]]"
title="Stop tilt"
></ha-icon-button>
<ha-icon-button
aria-label="Close cover tilt"
icon="hass:arrow-bottom-left"
on-click="onCloseTiltTap"
title="Close tilt"
invisible$="[[!entityObj.supportsCloseTilt]]"
disabled="[[computeClosedDisabled(stateObj, entityObj)]]"
></ha-icon-button>
`;
}
static get properties() {
return {
hass: {
type: Object,
},
stateObj: {
type: Object,
},
entityObj: {
type: Object,
computed: "computeEntityObj(hass, stateObj)",
},
};
}
computeEntityObj(hass, stateObj) {
return new CoverEntity(hass, stateObj);
}
computeStopDisabled(stateObj) {
if (stateObj.state === UNAVAILABLE) {
return true;
}
return false;
}
computeOpenDisabled(stateObj, entityObj) {
if (stateObj.state === UNAVAILABLE) {
return true;
}
const assumedState = stateObj.attributes.assumed_state === true;
return entityObj.isFullyOpenTilt && !assumedState;
}
computeClosedDisabled(stateObj, entityObj) {
if (stateObj.state === UNAVAILABLE) {
return true;
}
const assumedState = stateObj.attributes.assumed_state === true;
return entityObj.isFullyClosedTilt && !assumedState;
}
onOpenTiltTap(ev) {
ev.stopPropagation();
this.entityObj.openCoverTilt();
}
onCloseTiltTap(ev) {
ev.stopPropagation();
this.entityObj.closeCoverTilt();
}
onStopTiltTap(ev) {
ev.stopPropagation();
this.entityObj.stopCoverTilt();
}
}
customElements.define("ha-cover-tilt-controls", HaCoverTiltControls);

View File

@@ -0,0 +1,122 @@
import { HassEntity } from "home-assistant-js-websocket";
import {
LitElement,
property,
internalProperty,
CSSResult,
css,
customElement,
TemplateResult,
html,
PropertyValues,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { UNAVAILABLE } from "../data/entity";
import { HomeAssistant } from "../types";
import CoverEntity from "../util/cover-model";
import "./ha-icon-button";
@customElement("ha-cover-tilt-controls")
class HaCoverTiltControls extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) stateObj!: HassEntity;
@internalProperty() private _entityObj?: CoverEntity;
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (changedProperties.has("stateObj")) {
this._entityObj = new CoverEntity(this.hass, this.stateObj);
}
}
protected render(): TemplateResult {
if (!this._entityObj) {
return html``;
}
return html` <ha-icon-button
class=${classMap({
invisible: !this._entityObj.supportsStop,
})}
label=${this.hass.localize(
"ui.dialogs.more_info_control.open_tilt_cover"
)}
icon="hass:arrow-top-right"
@click=${this._onOpenTiltTap}
.disabled=${this._computeOpenDisabled()}
></ha-icon-button>
<ha-icon-button
class=${classMap({
invisible: !this._entityObj.supportsStop,
})}
label=${this.hass.localize("ui.dialogs.more_info_control.stop_cover")}
icon="hass:stop"
@click=${this._onStopTiltTap}
.disabled=${this.stateObj.state === UNAVAILABLE}
></ha-icon-button>
<ha-icon-button
class=${classMap({
invisible: !this._entityObj.supportsStop,
})}
label=${this.hass.localize(
"ui.dialogs.more_info_control.open_tilt_cover"
)}
icon="hass:arrow-bottom-left"
@click=${this._onCloseTiltTap}
.disabled=${this._computeClosedDisabled()}
></ha-icon-button>`;
}
private _computeOpenDisabled(): boolean {
if (this.stateObj.state === UNAVAILABLE) {
return true;
}
const assumedState = this.stateObj.attributes.assumed_state === true;
return this._entityObj.isFullyOpenTilt && !assumedState;
}
private _computeClosedDisabled(): boolean {
if (this.stateObj.state === UNAVAILABLE) {
return true;
}
const assumedState = this.stateObj.attributes.assumed_state === true;
return this._entityObj.isFullyClosedTilt && !assumedState;
}
private _onOpenTiltTap(ev): void {
ev.stopPropagation();
this._entityObj.openCoverTilt();
}
private _onCloseTiltTap(ev): void {
ev.stopPropagation();
this._entityObj.closeCoverTilt();
}
private _onStopTiltTap(ev): void {
ev.stopPropagation();
this._entityObj.stopCoverTilt();
}
static get styles(): CSSResult {
return css`
:host {
white-space: nowrap;
}
.invisible {
visibility: hidden !important;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-cover-tilt-controls": HaCoverTiltControls;
}
}

View File

@@ -1,127 +1,70 @@
import "@polymer/paper-input/paper-input"; import "@vaadin/vaadin-date-picker/theme/material/vaadin-date-picker";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import {
css,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
@customElement("ha-date-input") const VaadinDatePicker = customElements.get("vaadin-date-picker");
export class HaDateInput extends LitElement {
@property() public year?: string;
@property() public month?: string; export class HaDateInput extends VaadinDatePicker {
constructor() {
super();
@property() public day?: string; this.i18n.formatDate = this._formatISODate;
this.i18n.parseDate = this._parseISODate;
}
@property({ type: Boolean }) public disabled = false; ready() {
super.ready();
static get styles() { const styleEl = document.createElement("style");
return css` styleEl.innerHTML = `
:host { :host {
display: block; width: 12ex;
font-family: var(--paper-font-common-base_-_font-family); margin-top: -6px;
-webkit-font-smoothing: var( --material-body-font-size: 16px;
--paper-font-common-base_-_-webkit-font-smoothing --_material-text-field-input-line-background-color: var(--primary-text-color);
); --_material-text-field-input-line-opacity: 1;
} --material-primary-color: var(--primary-text-color);
paper-input {
width: 30px;
text-align: center;
--paper-input-container-shared-input-style_-_-webkit-appearance: textfield;
--paper-input-container-input_-_-moz-appearance: textfield;
--paper-input-container-shared-input-style_-_appearance: textfield;
--paper-input-container-input-webkit-spinner_-_-webkit-appearance: none;
--paper-input-container-input-webkit-spinner_-_margin: 0;
--paper-input-container-input-webkit-spinner_-_display: none;
}
paper-input#year {
width: 50px;
}
.date-input-wrap {
display: flex;
flex-direction: row;
} }
`; `;
this.shadowRoot.appendChild(styleEl);
this._inputElement.querySelector("[part='toggle-button']").style.display =
"none";
} }
protected render(): TemplateResult { private _formatISODate(d) {
return html` return [
<div class="date-input-wrap"> ("0000" + String(d.year)).slice(-4),
<paper-input ("0" + String(d.month + 1)).slice(-2),
id="year" ("0" + String(d.day)).slice(-2),
type="number" ].join("-");
.value=${this.year}
@change=${this._formatYear}
maxlength="4"
max="9999"
min="0"
.disabled=${this.disabled}
no-label-float
>
<span suffix="" slot="suffix">-</span>
</paper-input>
<paper-input
id="month"
type="number"
.value=${this.month}
@change=${this._formatMonth}
maxlength="2"
max="12"
min="1"
.disabled=${this.disabled}
no-label-float
>
<span suffix="" slot="suffix">-</span>
</paper-input>
<paper-input
id="day"
type="number"
.value=${this.day}
@change=${this._formatDay}
maxlength="2"
max="31"
min="1"
.disabled=${this.disabled}
no-label-float
>
</paper-input>
</div>
`;
} }
private _formatYear() { private _parseISODate(text) {
const yearElement = this.shadowRoot!.getElementById( const parts = text.split("-");
"year" const today = new Date();
) as PaperInputElement; let date;
this.year = yearElement.value!; let month = today.getMonth();
let year = today.getFullYear();
if (parts.length === 3) {
year = parseInt(parts[0]);
if (parts[0].length < 3 && year >= 0) {
year += year < 50 ? 2000 : 1900;
}
month = parseInt(parts[1]) - 1;
date = parseInt(parts[2]);
} else if (parts.length === 2) {
month = parseInt(parts[0]) - 1;
date = parseInt(parts[1]);
} else if (parts.length === 1) {
date = parseInt(parts[0]);
} }
private _formatMonth() { if (date !== undefined) {
const monthElement = this.shadowRoot!.getElementById( return { day: date, month, year };
"month"
) as PaperInputElement;
this.month = ("0" + monthElement.value!).slice(-2);
} }
return undefined;
private _formatDay() {
const dayElement = this.shadowRoot!.getElementById(
"day"
) as PaperInputElement;
this.day = ("0" + dayElement.value!).slice(-2);
}
get value() {
return `${this.year}-${this.month}-${this.day}`;
} }
} }
customElements.define("ha-date-input", HaDateInput as any);
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-date-input": HaDateInput; "ha-date-input": HaDateInput;

View File

@@ -40,6 +40,7 @@ export class HaDialog extends MwcDialog {
.mdc-dialog { .mdc-dialog {
--mdc-dialog-scroll-divider-color: var(--divider-color); --mdc-dialog-scroll-divider-color: var(--divider-color);
z-index: var(--dialog-z-index, 7); z-index: var(--dialog-z-index, 7);
backdrop-filter: var(--dialog-backdrop-filter, none);
} }
.mdc-dialog__actions { .mdc-dialog__actions {
justify-content: var(--justify-action-buttons, flex-end); justify-content: var(--justify-action-buttons, flex-end);

View File

@@ -104,7 +104,6 @@ class HaHLSPlayer extends LitElement {
private async _startHls(): Promise<void> { private async _startHls(): Promise<void> {
const videoEl = this._videoEl; const videoEl = this._videoEl;
const playlist_url = this.url.replace("master_playlist", "playlist");
const useExoPlayerPromise = this._getUseExoPlayer(); const useExoPlayerPromise = this._getUseExoPlayer();
const masterPlaylistPromise = fetch(this.url); const masterPlaylistPromise = fetch(this.url);
@@ -126,13 +125,30 @@ class HaHLSPlayer extends LitElement {
} }
this._useExoPlayer = await useExoPlayerPromise; this._useExoPlayer = await useExoPlayerPromise;
let hevcRegexp: RegExp; const masterPlaylist = await (await masterPlaylistPromise).text();
let masterPlaylist: string;
if (this._useExoPlayer) { // Parse playlist assuming it is a master playlist. Match group 1 is whether hevc, match group 2 is regular playlist url
hevcRegexp = /CODECS=".*?((hev1)|(hvc1))\..*?"/; // See https://tools.ietf.org/html/rfc8216 for HLS spec details
masterPlaylist = await (await masterPlaylistPromise).text(); const playlistRegexp = /#EXT-X-STREAM-INF:.*?(?:CODECS=".*?(?<isHevc>hev1|hvc1)?\..*?".*?)?(?:\n|\r\n)(?<streamUrl>.+)/g;
const match = playlistRegexp.exec(masterPlaylist);
const matchTwice = playlistRegexp.exec(masterPlaylist);
// Get the regular playlist url from the input (master) playlist, falling back to the input playlist if necessary
// This avoids the player having to load and parse the master playlist again before loading the regular playlist
let playlist_url: string;
if (match !== null && matchTwice === null) {
// Only send the regular playlist url if we match exactly once
playlist_url = new URL(match.groups!.streamUrl, this.url).href;
} else {
playlist_url = this.url;
} }
if (this._useExoPlayer && hevcRegexp!.test(masterPlaylist!)) {
// If codec is HEVC and ExoPlayer is supported, use ExoPlayer.
if (
this._useExoPlayer &&
match !== null &&
match.groups!.isHevc !== undefined
) {
this._renderHLSExoPlayer(playlist_url); this._renderHLSExoPlayer(playlist_url);
} else if (hls.isSupported()) { } else if (hls.isSupported()) {
this._renderHLSPolyfill(videoEl, hls, playlist_url); this._renderHLSPolyfill(videoEl, hls, playlist_url);

View File

@@ -9,11 +9,16 @@ import {
import { mdiArrowLeft, mdiArrowRight } from "@mdi/js"; import { mdiArrowLeft, mdiArrowRight } from "@mdi/js";
import "@material/mwc-icon-button/mwc-icon-button"; import "@material/mwc-icon-button/mwc-icon-button";
import "./ha-svg-icon"; import "./ha-svg-icon";
import { HomeAssistant } from "../types";
@customElement("ha-icon-button-arrow-next") @customElement("ha-icon-button-arrow-next")
export class HaIconButtonArrowNext extends LitElement { export class HaIconButtonArrowNext extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property() public label?: string;
@internalProperty() private _icon = mdiArrowRight; @internalProperty() private _icon = mdiArrowRight;
public connectedCallback() { public connectedCallback() {
@@ -29,7 +34,10 @@ export class HaIconButtonArrowNext extends LitElement {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
return html`<mwc-icon-button .disabled=${this.disabled}> return html`<mwc-icon-button
.disabled=${this.disabled}
.label=${this.label || this.hass?.localize("ui.common.next") || "Next"}
>
<ha-svg-icon .path=${this._icon}></ha-svg-icon> <ha-svg-icon .path=${this._icon}></ha-svg-icon>
</mwc-icon-button> `; </mwc-icon-button> `;
} }

View File

@@ -9,11 +9,16 @@ import {
import { mdiArrowLeft, mdiArrowRight } from "@mdi/js"; import { mdiArrowLeft, mdiArrowRight } from "@mdi/js";
import "@material/mwc-icon-button/mwc-icon-button"; import "@material/mwc-icon-button/mwc-icon-button";
import "./ha-svg-icon"; import "./ha-svg-icon";
import { HomeAssistant } from "../types";
@customElement("ha-icon-button-arrow-prev") @customElement("ha-icon-button-arrow-prev")
export class HaIconButtonArrowPrev extends LitElement { export class HaIconButtonArrowPrev extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property() public label?: string;
@internalProperty() private _icon = mdiArrowLeft; @internalProperty() private _icon = mdiArrowLeft;
public connectedCallback() { public connectedCallback() {
@@ -29,9 +34,14 @@ export class HaIconButtonArrowPrev extends LitElement {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
return html`<mwc-icon-button .disabled=${this.disabled}> return html`
<mwc-icon-button
.disabled=${this.disabled}
.label=${this.label || this.hass?.localize("ui.common.back") || "Back"}
>
<ha-svg-icon .path=${this._icon}></ha-svg-icon> <ha-svg-icon .path=${this._icon}></ha-svg-icon>
</mwc-icon-button> `; </mwc-icon-button>
`;
} }
} }

View File

@@ -9,11 +9,16 @@ import {
import { mdiChevronRight, mdiChevronLeft } from "@mdi/js"; import { mdiChevronRight, mdiChevronLeft } from "@mdi/js";
import "@material/mwc-icon-button"; import "@material/mwc-icon-button";
import "./ha-svg-icon"; import "./ha-svg-icon";
import { HomeAssistant } from "../types";
@customElement("ha-icon-button-next") @customElement("ha-icon-button-next")
export class HaIconButtonNext extends LitElement { export class HaIconButtonNext extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property() public label?: string;
@internalProperty() private _icon = mdiChevronRight; @internalProperty() private _icon = mdiChevronRight;
public connectedCallback() { public connectedCallback() {
@@ -29,9 +34,14 @@ export class HaIconButtonNext extends LitElement {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
return html`<mwc-icon-button .disabled=${this.disabled}> return html`
<mwc-icon-button
.disabled=${this.disabled}
.label=${this.label || this.hass?.localize("ui.common.next") || "Next"}
>
<ha-svg-icon .path=${this._icon}></ha-svg-icon> <ha-svg-icon .path=${this._icon}></ha-svg-icon>
</mwc-icon-button> `; </mwc-icon-button>
`;
} }
} }

View File

@@ -9,11 +9,16 @@ import {
import { mdiChevronRight, mdiChevronLeft } from "@mdi/js"; import { mdiChevronRight, mdiChevronLeft } from "@mdi/js";
import "@material/mwc-icon-button/mwc-icon-button"; import "@material/mwc-icon-button/mwc-icon-button";
import "./ha-svg-icon"; import "./ha-svg-icon";
import { HomeAssistant } from "../types";
@customElement("ha-icon-button-prev") @customElement("ha-icon-button-prev")
export class HaIconButtonPrev extends LitElement { export class HaIconButtonPrev extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property() public label?: string;
@internalProperty() private _icon = mdiChevronLeft; @internalProperty() private _icon = mdiChevronLeft;
public connectedCallback() { public connectedCallback() {
@@ -29,9 +34,14 @@ export class HaIconButtonPrev extends LitElement {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
return html`<mwc-icon-button .disabled=${this.disabled}> return html`
<mwc-icon-button
.disabled=${this.disabled}
.label=${this.label || this.hass?.localize("ui.common.back") || "Back"}
>
<ha-svg-icon .path=${this._icon}></ha-svg-icon> <ha-svg-icon .path=${this._icon}></ha-svg-icon>
</mwc-icon-button> `; </mwc-icon-button>
`;
} }
} }

View File

@@ -14,7 +14,7 @@ class HaLabeledSlider extends PolymerElement {
} }
.title { .title {
margin-bottom: 8px; margin: 4px 0 8px;
color: var(--primary-text-color); color: var(--primary-text-color);
} }

View File

@@ -47,7 +47,6 @@ class HaMarkdownElement extends UpdatingElement {
node.host !== document.location.host node.host !== document.location.host
) { ) {
node.target = "_blank"; node.target = "_blank";
node.rel = "noreferrer";
// protect referrer on external links and deny window.opener access for security reasons // protect referrer on external links and deny window.opener access for security reasons
// (see https://mathiasbynens.github.io/rel-noopener/) // (see https://mathiasbynens.github.io/rel-noopener/)

View File

@@ -3,6 +3,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import { getAppKey } from "../data/notify_html5"; import { getAppKey } from "../data/notify_html5";
import { EventsMixin } from "../mixins/events-mixin"; import { EventsMixin } from "../mixins/events-mixin";
import { showPromptDialog } from "../dialogs/generic/show-dialog-box";
import "./ha-switch"; import "./ha-switch";
export const pushSupported = export const pushSupported =
@@ -88,7 +89,14 @@ class HaPushNotificationsToggle extends EventsMixin(PolymerElement) {
browserName = "chrome"; browserName = "chrome";
} }
const name = prompt("What should this device be called ?"); const name = await showPromptDialog(this, {
title: this.hass.localize(
"ui.panel.profile.push_notifications.add_device_prompt.title"
),
inputLabel: this.hass.localize(
"ui.panel.profile.push_notifications.add_device_prompt.input_label"
),
});
if (name == null) { if (name == null) {
this.pushChecked = false; this.pushChecked = false;
return; return;

View File

@@ -202,195 +202,17 @@ class HaSidebar extends LitElement {
private _sortable?; private _sortable?;
protected render() { protected render() {
const hass = this.hass; if (!this.hass) {
if (!hass) {
return html``; return html``;
} }
const [beforeSpacer, afterSpacer] = computePanels( // prettier-ignore
hass.panels,
hass.defaultPanel,
this._panelOrder,
this._hiddenPanels
);
let notificationCount = this._notifications
? this._notifications.length
: 0;
for (const entityId in hass.states) {
if (computeDomain(entityId) === "configurator") {
notificationCount++;
}
}
return html` return html`
<div ${this._renderHeader()}
class="menu" ${this._renderAllPanels()}
@action=${this._handleAction} ${this._renderDivider()}
.actionHandler=${actionHandler({ ${this._renderNotifications()}
hasHold: !this.editMode, ${this._renderUserItem()}
disabled: this.editMode,
})}
>
${!this.narrow
? html`
<mwc-icon-button
.label=${hass.localize("ui.sidebar.sidebar_toggle")}
@action=${this._toggleSidebar}
>
<ha-svg-icon
.path=${hass.dockedSidebar === "docked"
? mdiMenuOpen
: mdiMenu}
></ha-svg-icon>
</mwc-icon-button>
`
: ""}
<div class="title">
${this.editMode
? html`<mwc-button outlined @click=${this._closeEditMode}>
${hass.localize("ui.sidebar.done")}
</mwc-button>`
: "Home Assistant"}
</div>
</div>
<paper-listbox
attr-for-selected="data-panel"
class="ha-scrollbar"
.selected=${hass.panelUrl}
@focusin=${this._listboxFocusIn}
@focusout=${this._listboxFocusOut}
@scroll=${this._listboxScroll}
@keydown=${this._listboxKeydown}
>
${this.editMode
? html`<div id="sortable">
${guard([this._hiddenPanels, this._renderEmptySortable], () =>
this._renderEmptySortable
? ""
: this._renderPanels(beforeSpacer)
)}
</div>`
: this._renderPanels(beforeSpacer)}
<div class="spacer" disabled></div>
${this.editMode && this._hiddenPanels.length
? html`
${this._hiddenPanels.map((url) => {
const panel = this.hass.panels[url];
if (!panel) {
return "";
}
return html`<paper-icon-item
@click=${this._unhidePanel}
class="hidden-panel"
.panel=${url}
>
<ha-icon
slot="item-icon"
.icon=${panel.url_path === this.hass.defaultPanel
? "mdi:view-dashboard"
: panel.icon}
></ha-icon>
<span class="item-text"
>${panel.url_path === this.hass.defaultPanel
? hass.localize("panel.states")
: hass.localize(`panel.${panel.title}`) ||
panel.title}</span
>
<mwc-icon-button class="show-panel">
<ha-svg-icon .path=${mdiPlus}></ha-svg-icon>
</mwc-icon-button>
</paper-icon-item>`;
})}
<div class="spacer" disabled></div>
`
: ""}
${this._renderPanels(afterSpacer)}
${this._externalConfig && this._externalConfig.hasSettingsScreen
? html`
<a
aria-role="option"
aria-label=${hass.localize(
"ui.sidebar.external_app_configuration"
)}
href="#external-app-configuration"
tabindex="-1"
@click=${this._handleExternalAppConfiguration}
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item>
<ha-svg-icon
slot="item-icon"
.path=${mdiCellphoneCog}
></ha-svg-icon>
<span class="item-text">
${hass.localize("ui.sidebar.external_app_configuration")}
</span>
</paper-icon-item>
</a>
`
: ""}
</paper-listbox>
<div class="divider"></div>
<div
class="notifications-container"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item
class="notifications"
aria-role="option"
@click=${this._handleShowNotificationDrawer}
>
<ha-svg-icon slot="item-icon" .path=${mdiBell}></ha-svg-icon>
${!this.expanded && notificationCount > 0
? html`
<span class="notification-badge" slot="item-icon">
${notificationCount}
</span>
`
: ""}
<span class="item-text">
${hass.localize("ui.notification_drawer.title")}
</span>
${this.expanded && notificationCount > 0
? html`
<span class="notification-badge">${notificationCount}</span>
`
: ""}
</paper-icon-item>
</div>
<a
class=${classMap({
profile: true,
// Mimick behavior that paper-listbox provides
"iron-selected": hass.panelUrl === "profile",
})}
href="/profile"
data-panel="panel"
tabindex="-1"
aria-role="option"
aria-label=${hass.localize("panel.profile")}
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item>
<ha-user-badge
slot="item-icon"
.user=${hass.user}
.hass=${hass}
></ha-user-badge>
<span class="item-text">
${hass.user ? hass.user.name : ""}
</span>
</paper-icon-item>
</a>
<div disabled class="bottom-spacer"></div> <div disabled class="bottom-spacer"></div>
<div class="tooltip"></div> <div class="tooltip"></div>
`; `;
@@ -475,6 +297,213 @@ class HaSidebar extends LitElement {
} }
} }
private _renderHeader() {
return html`<div
class="menu"
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: !this.editMode,
disabled: this.editMode,
})}
>
${!this.narrow
? html`
<mwc-icon-button
.label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
@action=${this._toggleSidebar}
>
<ha-svg-icon
.path=${this.hass.dockedSidebar === "docked"
? mdiMenuOpen
: mdiMenu}
></ha-svg-icon>
</mwc-icon-button>
`
: ""}
${this.editMode
? html`<mwc-button outlined @click=${this._closeEditMode}>
${this.hass.localize("ui.sidebar.done")}
</mwc-button>`
: html`<div class="title">Home Assistant</div>`}
</div>`;
}
private _renderAllPanels() {
const [beforeSpacer, afterSpacer] = computePanels(
this.hass.panels,
this.hass.defaultPanel,
this._panelOrder,
this._hiddenPanels
);
// prettier-ignore
return html`
<paper-listbox
attr-for-selected="data-panel"
class="ha-scrollbar"
.selected=${this.hass.panelUrl}
@focusin=${this._listboxFocusIn}
@focusout=${this._listboxFocusOut}
@scroll=${this._listboxScroll}
@keydown=${this._listboxKeydown}
>
${this.editMode
? this._renderPanelsEdit(beforeSpacer)
: this._renderPanels(beforeSpacer)}
${this._renderSpacer()}
${this._renderPanels(afterSpacer)}
${this._renderExternalConfiguration()}
</paper-listbox>
`;
}
private _renderPanelsEdit(beforeSpacer: PanelInfo[]) {
// prettier-ignore
return html`<div id="sortable">
${guard([this._hiddenPanels, this._renderEmptySortable], () =>
this._renderEmptySortable ? "" : this._renderPanels(beforeSpacer)
)}
</div>
${this._renderSpacer()}
${this._renderHiddenPanels()} `;
}
private _renderHiddenPanels() {
return html` ${this._hiddenPanels.length
? html`${this._hiddenPanels.map((url) => {
const panel = this.hass.panels[url];
if (!panel) {
return "";
}
return html`<paper-icon-item
@click=${this._unhidePanel}
class="hidden-panel"
.panel=${url}
>
<ha-icon
slot="item-icon"
.icon=${panel.url_path === this.hass.defaultPanel
? "mdi:view-dashboard"
: panel.icon}
></ha-icon>
<span class="item-text"
>${panel.url_path === this.hass.defaultPanel
? this.hass.localize("panel.states")
: this.hass.localize(`panel.${panel.title}`) ||
panel.title}</span
>
<mwc-icon-button class="show-panel">
<ha-svg-icon .path=${mdiPlus}></ha-svg-icon>
</mwc-icon-button>
</paper-icon-item>`;
})}
${this._renderSpacer()}`
: ""}`;
}
private _renderDivider() {
return html`<div class="divider"></div>`;
}
private _renderSpacer() {
return html`<div class="spacer" disabled></div>`;
}
private _renderNotifications() {
let notificationCount = this._notifications
? this._notifications.length
: 0;
for (const entityId in this.hass.states) {
if (computeDomain(entityId) === "configurator") {
notificationCount++;
}
}
return html` <div
class="notifications-container"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item
class="notifications"
aria-role="option"
@click=${this._handleShowNotificationDrawer}
>
<ha-svg-icon slot="item-icon" .path=${mdiBell}></ha-svg-icon>
${!this.expanded && notificationCount > 0
? html`
<span class="notification-badge" slot="item-icon">
${notificationCount}
</span>
`
: ""}
<span class="item-text">
${this.hass.localize("ui.notification_drawer.title")}
</span>
${this.expanded && notificationCount > 0
? html` <span class="notification-badge">${notificationCount}</span> `
: ""}
</paper-icon-item>
</div>`;
}
private _renderUserItem() {
return html`<a
class=${classMap({
profile: true,
// Mimick behavior that paper-listbox provides
"iron-selected": this.hass.panelUrl === "profile",
})}
href="/profile"
data-panel="panel"
tabindex="-1"
aria-role="option"
aria-label=${this.hass.localize("panel.profile")}
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item>
<ha-user-badge
slot="item-icon"
.user=${this.hass.user}
.hass=${this.hass}
></ha-user-badge>
<span class="item-text">
${this.hass.user ? this.hass.user.name : ""}
</span>
</paper-icon-item>
</a>`;
}
private _renderExternalConfiguration() {
return html`${this._externalConfig && this._externalConfig.hasSettingsScreen
? html`
<a
aria-role="option"
aria-label=${this.hass.localize(
"ui.sidebar.external_app_configuration"
)}
href="#external-app-configuration"
tabindex="-1"
@click=${this._handleExternalAppConfiguration}
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item>
<ha-svg-icon
slot="item-icon"
.path=${mdiCellphoneCog}
></ha-svg-icon>
<span class="item-text">
${this.hass.localize("ui.sidebar.external_app_configuration")}
</span>
</paper-icon-item>
</a>
`
: ""}`;
}
private get _tooltip() { private get _tooltip() {
return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement; return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement;
} }
@@ -725,7 +754,7 @@ class HaSidebar extends LitElement {
-moz-user-select: none; -moz-user-select: none;
border-right: 1px solid var(--divider-color); border-right: 1px solid var(--divider-color);
background-color: var(--sidebar-background-color); background-color: var(--sidebar-background-color);
width: 64px; width: 56px;
} }
:host([expanded]) { :host([expanded]) {
width: 256px; width: 256px;
@@ -737,8 +766,9 @@ class HaSidebar extends LitElement {
} }
.menu { .menu {
height: var(--header-height); height: var(--header-height);
box-sizing: border-box;
display: flex; display: flex;
padding: 0 8.5px; padding: 0 4px;
border-bottom: 1px solid transparent; border-bottom: 1px solid transparent;
white-space: nowrap; white-space: nowrap;
font-weight: 400; font-weight: 400;
@@ -747,11 +777,11 @@ class HaSidebar extends LitElement {
background-color: var(--primary-background-color); background-color: var(--primary-background-color);
font-size: 20px; font-size: 20px;
align-items: center; align-items: center;
padding-left: calc(8.5px + env(safe-area-inset-left)); padding-left: calc(4px + env(safe-area-inset-left));
} }
:host([rtl]) .menu { :host([rtl]) .menu {
padding-left: 8.5px; padding-left: 4px;
padding-right: calc(8.5px + env(safe-area-inset-right)); padding-right: calc(4px + env(safe-area-inset-right));
} }
:host([expanded]) .menu { :host([expanded]) .menu {
width: calc(256px + env(safe-area-inset-left)); width: calc(256px + env(safe-area-inset-left));
@@ -762,25 +792,26 @@ class HaSidebar extends LitElement {
.menu mwc-icon-button { .menu mwc-icon-button {
color: var(--sidebar-icon-color); color: var(--sidebar-icon-color);
} }
:host([expanded]) .menu mwc-icon-button {
margin-right: 23px;
}
:host([expanded][rtl]) .menu mwc-icon-button {
margin-right: 0px;
margin-left: 23px;
}
.title { .title {
margin-left: 19px;
width: 100%; width: 100%;
display: none; display: none;
} }
:host([rtl]) .title {
margin-left: 0;
margin-right: 19px;
}
:host([narrow]) .title { :host([narrow]) .title {
margin: 0;
padding: 0 16px; padding: 0 16px;
} }
:host([expanded]) .title { :host([expanded]) .title {
display: initial; display: initial;
} }
.title mwc-button { :host([expanded]) .menu mwc-button {
margin: 0 8px;
}
.menu mwc-button {
width: 100%; width: 100%;
} }
#sortable, #sortable,
@@ -819,14 +850,14 @@ class HaSidebar extends LitElement {
paper-icon-item { paper-icon-item {
box-sizing: border-box; box-sizing: border-box;
margin: 4px 8px; margin: 4px;
padding-left: 12px; padding-left: 12px;
border-radius: 4px; border-radius: 4px;
--paper-item-min-height: 40px; --paper-item-min-height: 40px;
width: 48px; width: 48px;
} }
:host([expanded]) paper-icon-item { :host([expanded]) paper-icon-item {
width: 240px; width: 248px;
} }
:host([rtl]) paper-icon-item { :host([rtl]) paper-icon-item {
padding-left: auto; padding-left: auto;
@@ -843,9 +874,9 @@ class HaSidebar extends LitElement {
border-radius: 4px; border-radius: 4px;
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 2px;
bottom: 0; bottom: 0;
left: 0; left: 2px;
pointer-events: none; pointer-events: none;
content: ""; content: "";
transition: opacity 15ms linear; transition: opacity 15ms linear;

View File

@@ -99,13 +99,13 @@ export class HaTab extends LitElement {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
text-align: center; text-align: center;
box-sizing: border-box;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 64px; height: var(--header-height);
cursor: pointer; cursor: pointer;
position: relative; position: relative;
outline: none; outline: none;
box-sizing: border-box;
} }
.name { .name {

View File

@@ -34,8 +34,8 @@ export class HaTabs extends PaperTabs {
superStyle!.appendChild( superStyle!.appendChild(
document.createTextNode(` document.createTextNode(`
:host { #selectionBar {
padding-top: .5px; box-sizing: border-box;
} }
.not-visible { .not-visible {
display: none; display: none;

View File

@@ -540,7 +540,9 @@ export class HaMediaPlayerBrowse extends LitElement {
mediaContentType?: string mediaContentType?: string
): Promise<MediaPlayerItem> { ): Promise<MediaPlayerItem> {
this._loading = true; this._loading = true;
const itemData = let itemData: any;
try {
itemData =
this.entityId !== BROWSER_PLAYER this.entityId !== BROWSER_PLAYER
? await browseMediaPlayer( ? await browseMediaPlayer(
this.hass, this.hass,
@@ -549,8 +551,9 @@ export class HaMediaPlayerBrowse extends LitElement {
mediaContentType mediaContentType
) )
: await browseLocalMediaPlayer(this.hass, mediaContentId); : await browseLocalMediaPlayer(this.hass, mediaContentId);
} finally {
this._loading = false; this._loading = false;
}
return itemData; return itemData;
} }

View File

@@ -11,6 +11,7 @@ export const DISCOVERY_SOURCES = [
"ssdp", "ssdp",
"zeroconf", "zeroconf",
"discovery", "discovery",
"mqtt",
]; ];
export const ATTENTION_SOURCES = ["reauth"]; export const ATTENTION_SOURCES = ["reauth"];

View File

@@ -58,8 +58,18 @@ export interface DataEntryFlowStepAbort {
description_placeholders: { [key: string]: string }; description_placeholders: { [key: string]: string };
} }
export interface DataEntryFlowStepProgress {
type: "progress";
flow_id: string;
handler: string;
step_id: string;
progress_action: string;
description_placeholders: { [key: string]: string };
}
export type DataEntryFlowStep = export type DataEntryFlowStep =
| DataEntryFlowStepForm | DataEntryFlowStepForm
| DataEntryFlowStepExternal | DataEntryFlowStepExternal
| DataEntryFlowStepCreateEntry | DataEntryFlowStepCreateEntry
| DataEntryFlowStepAbort; | DataEntryFlowStepAbort
| DataEntryFlowStepProgress;

View File

@@ -20,6 +20,12 @@ export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
original_icon?: string; original_icon?: string;
} }
export interface UpdateEntityRegistryEntryResult {
entity_entry: ExtEntityRegistryEntry;
reload_delay?: number;
require_restart?: boolean;
}
export interface EntityRegistryEntryUpdateParams { export interface EntityRegistryEntryUpdateParams {
name?: string | null; name?: string | null;
icon?: string | null; icon?: string | null;
@@ -72,7 +78,7 @@ export const updateEntityRegistryEntry = (
hass: HomeAssistant, hass: HomeAssistant,
entityId: string, entityId: string,
updates: Partial<EntityRegistryEntryUpdateParams> updates: Partial<EntityRegistryEntryUpdateParams>
): Promise<ExtEntityRegistryEntry> => ): Promise<UpdateEntityRegistryEntryResult> =>
hass.callWS({ hass.callWS({
type: "config/entity_registry/update", type: "config/entity_registry/update",
entity_id: entityId, entity_id: entityId,

View File

@@ -0,0 +1,26 @@
import { HomeAssistant } from "../../types";
import { HassioResponse } from "./common";
import { CreateSessionResponse } from "./supervisor";
export const createHassioSession = async (hass: HomeAssistant) => {
const response = await hass.callApi<HassioResponse<CreateSessionResponse>>(
"POST",
"hassio/ingress/session"
);
document.cookie = `ingress_session=${
response.data.session
};path=/api/hassio_ingress/;SameSite=Strict${
location.protocol === "https:" ? ";Secure" : ""
}`;
return response.data.session;
};
export const validateHassioSession = async (
hass: HomeAssistant,
session: string
) =>
await hass.callApi<HassioResponse<null>>(
"POST",
"hassio/ingress/validate_session",
{ session }
);

View File

@@ -111,18 +111,6 @@ export const fetchHassioLogs = async (
return hass.callApi<string>("GET", `hassio/${provider}/logs`); return hass.callApi<string>("GET", `hassio/${provider}/logs`);
}; };
export const createHassioSession = async (hass: HomeAssistant) => {
const response = await hass.callApi<HassioResponse<CreateSessionResponse>>(
"POST",
"hassio/ingress/session"
);
document.cookie = `ingress_session=${
response.data.session
};path=/api/hassio_ingress/;SameSite=Strict${
location.protocol === "https:" ? ";Secure" : ""
}`;
};
export const setSupervisorOption = async ( export const setSupervisorOption = async (
hass: HomeAssistant, hass: HomeAssistant,
data: SupervisorOptions data: SupervisorOptions

View File

@@ -89,6 +89,7 @@ export interface LovelaceViewConfig {
export interface LovelaceViewElement extends HTMLElement { export interface LovelaceViewElement extends HTMLElement {
hass?: HomeAssistant; hass?: HomeAssistant;
lovelace?: Lovelace; lovelace?: Lovelace;
narrow?: boolean;
index?: number; index?: number;
cards?: Array<LovelaceCard | HuiErrorCard>; cards?: Array<LovelaceCard | HuiErrorCard>;
badges?: LovelaceBadge[]; badges?: LovelaceBadge[];

View File

@@ -18,6 +18,8 @@ import {
} from "@mdi/js"; } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import { UNAVAILABLE_STATES } from "./entity";
import { supportsFeature } from "../common/entity/supports-feature";
export const SUPPORT_PAUSE = 1; export const SUPPORT_PAUSE = 1;
export const SUPPORT_SEEK = 2; export const SUPPORT_SEEK = 2;
@@ -31,7 +33,7 @@ export const SUPPORT_PLAY_MEDIA = 512;
export const SUPPORT_VOLUME_BUTTONS = 1024; export const SUPPORT_VOLUME_BUTTONS = 1024;
export const SUPPORT_SELECT_SOURCE = 2048; export const SUPPORT_SELECT_SOURCE = 2048;
export const SUPPORT_STOP = 4096; export const SUPPORT_STOP = 4096;
export const SUPPORTS_PLAY = 16384; export const SUPPORT_PLAY = 16384;
export const SUPPORT_SELECT_SOUND_MODE = 65536; export const SUPPORT_SELECT_SOUND_MODE = 65536;
export const SUPPORT_BROWSE_MEDIA = 131072; export const SUPPORT_BROWSE_MEDIA = 131072;
export const CONTRAST_RATIO = 4.5; export const CONTRAST_RATIO = 4.5;
@@ -166,6 +168,7 @@ export const computeMediaDescription = (stateObj: HassEntity): string => {
switch (stateObj.attributes.media_content_type) { switch (stateObj.attributes.media_content_type) {
case "music": case "music":
case "image":
secondaryTitle = stateObj.attributes.media_artist; secondaryTitle = stateObj.attributes.media_artist;
break; break;
case "playlist": case "playlist":
@@ -187,3 +190,85 @@ export const computeMediaDescription = (stateObj: HassEntity): string => {
return secondaryTitle; return secondaryTitle;
}; };
export const computeMediaControls = (
stateObj: HassEntity
): ControlButton[] | undefined => {
if (!stateObj) {
return undefined;
}
const state = stateObj.state;
if (UNAVAILABLE_STATES.includes(state)) {
return undefined;
}
if (state === "off") {
return supportsFeature(stateObj, SUPPORT_TURN_ON)
? [
{
icon: "hass:power",
action: "turn_on",
},
]
: undefined;
}
const buttons: ControlButton[] = [];
if (supportsFeature(stateObj, SUPPORT_TURN_OFF)) {
buttons.push({
icon: "hass:power",
action: "turn_off",
});
}
if (
(state === "playing" || state === "paused") &&
supportsFeature(stateObj, SUPPORT_PREVIOUS_TRACK)
) {
buttons.push({
icon: "hass:skip-previous",
action: "media_previous_track",
});
}
if (
(state === "playing" &&
(supportsFeature(stateObj, SUPPORT_PAUSE) ||
supportsFeature(stateObj, SUPPORT_STOP))) ||
((state === "paused" || state === "idle") &&
supportsFeature(stateObj, SUPPORT_PLAY)) ||
(state === "on" &&
(supportsFeature(stateObj, SUPPORT_PLAY) ||
supportsFeature(stateObj, SUPPORT_PAUSE)))
) {
buttons.push({
icon:
state === "on"
? "hass:play-pause"
: state !== "playing"
? "hass:play"
: supportsFeature(stateObj, SUPPORT_PAUSE)
? "hass:pause"
: "hass:stop",
action:
state === "playing" && !supportsFeature(stateObj, SUPPORT_PAUSE)
? "media_stop"
: "media_play_pause",
});
}
if (
(state === "playing" || state === "paused") &&
supportsFeature(stateObj, SUPPORT_NEXT_TRACK)
) {
buttons.push({
icon: "hass:skip-next",
action: "media_next_track",
});
}
return buttons.length > 0 ? buttons : undefined;
};

View File

@@ -21,6 +21,18 @@ export const getDefaultPanel = (hass: HomeAssistant): PanelInfo =>
? hass.panels[hass.defaultPanel] ? hass.panels[hass.defaultPanel]
: hass.panels[DEFAULT_PANEL]; : hass.panels[DEFAULT_PANEL];
export const getPanelNameTranslationKey = (panel: PanelInfo): string => {
if (panel.url_path === "lovelace") {
return "panel.states";
}
if (panel.url_path === "profile") {
return "panel.profile";
}
return `panel.${panel.title}`;
};
export const getPanelTitle = (hass: HomeAssistant): string | undefined => { export const getPanelTitle = (hass: HomeAssistant): string | undefined => {
if (!hass.panels) { if (!hass.panels) {
return undefined; return undefined;
@@ -34,13 +46,20 @@ export const getPanelTitle = (hass: HomeAssistant): string | undefined => {
return undefined; return undefined;
} }
if (panel.url_path === "lovelace") { const translationKey = getPanelNameTranslationKey(panel);
return hass.localize("panel.states");
}
if (panel.url_path === "profile") { return hass.localize(translationKey) || panel.title || undefined;
return hass.localize("panel.profile"); };
}
export const getPanelIcon = (panel: PanelInfo): string | null => {
return hass.localize(`panel.${panel.title}`) || panel.title || undefined; if (!panel.icon) {
switch (panel.component_name) {
case "profile":
return "hass:account";
case "lovelace":
return "hass:view-dashboard";
}
}
return panel.icon;
}; };

View File

@@ -1,24 +1,111 @@
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
export interface HomeAssistantSystemHealthInfo { interface SystemCheckValueDateObject {
version: string; type: "date";
dev: boolean; value: string;
hassio: boolean;
virtualenv: string;
python_version: string;
docker: boolean;
arch: string;
timezone: string;
os_name: string;
} }
interface SystemCheckValueErrorObject {
type: "failed";
error: string;
more_info?: string;
}
interface SystemCheckValuePendingObject {
type: "pending";
}
export type SystemCheckValueObject =
| SystemCheckValueDateObject
| SystemCheckValueErrorObject
| SystemCheckValuePendingObject;
export type SystemCheckValue =
| string
| number
| boolean
| SystemCheckValueObject;
export interface SystemHealthInfo { export interface SystemHealthInfo {
[domain: string]: { [key: string]: string | number | boolean }; [domain: string]: {
manage_url?: string;
info: {
[key: string]: SystemCheckValue;
};
};
} }
export const fetchSystemHealthInfo = ( interface SystemHealthEventInitial {
hass: HomeAssistant type: "initial";
): Promise<SystemHealthInfo> => data: SystemHealthInfo;
hass.callWS({ }
interface SystemHealthEventUpdateSuccess {
type: "update";
success: true;
domain: string;
key: string;
data: SystemCheckValue;
}
interface SystemHealthEventUpdateError {
type: "update";
success: false;
domain: string;
key: string;
error: {
msg: string;
};
}
interface SystemHealthEventFinish {
type: "finish";
}
type SystemHealthEvent =
| SystemHealthEventInitial
| SystemHealthEventUpdateSuccess
| SystemHealthEventUpdateError
| SystemHealthEventFinish;
export const subscribeSystemHealthInfo = (
hass: HomeAssistant,
callback: (info: SystemHealthInfo) => void
) => {
let data = {};
const unsubProm = hass.connection.subscribeMessage<SystemHealthEvent>(
(updateEvent) => {
if (updateEvent.type === "initial") {
data = updateEvent.data;
callback(data);
return;
}
if (updateEvent.type === "finish") {
unsubProm.then((unsub) => unsub());
return;
}
data = {
...data,
[updateEvent.domain]: {
...data[updateEvent.domain],
info: {
...data[updateEvent.domain].info,
[updateEvent.key]: updateEvent.success
? updateEvent.data
: {
error: true,
value: updateEvent.error.msg,
},
},
},
};
callback(data);
},
{
type: "system_health/info", type: "system_health/info",
}); }
);
return unsubProm;
};

10
src/data/tasmota.ts Normal file
View File

@@ -0,0 +1,10 @@
import { HomeAssistant } from "../types";
export const removeTasmotaDeviceEntry = (
hass: HomeAssistant,
deviceId: string
): Promise<void> =>
hass.callWS({
type: "tasmota/device/remove",
device_id: deviceId,
});

View File

@@ -17,7 +17,8 @@ export type TranslationCategory =
| "config" | "config"
| "options" | "options"
| "device_automation" | "device_automation"
| "mfa_setup"; | "mfa_setup"
| "system_health";
export const fetchTranslationPreferences = (hass: HomeAssistant) => export const fetchTranslationPreferences = (hass: HomeAssistant) =>
fetchFrontendUserData(hass.connection, "language"); fetchFrontendUserData(hass.connection, "language");

View File

@@ -1,5 +1,4 @@
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; import "@material/mwc-button/mwc-button";
import "@polymer/paper-input/paper-input";
import { import {
css, css,
CSSResult, CSSResult,
@@ -10,16 +9,16 @@ import {
internalProperty, internalProperty,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import "../../components/dialog/ha-paper-dialog"; import "../../components/ha-dialog";
import "../../components/ha-circular-progress"; import "../../components/ha-circular-progress";
import "../../components/ha-switch"; import "../../components/ha-switch";
import "../../components/ha-formfield"; import "../../components/ha-formfield";
import { fireEvent } from "../../common/dom/fire_event";
import type { HaSwitch } from "../../components/ha-switch"; import type { HaSwitch } from "../../components/ha-switch";
import { import {
getConfigEntrySystemOptions, getConfigEntrySystemOptions,
updateConfigEntrySystemOptions, updateConfigEntrySystemOptions,
} from "../../data/config_entries"; } from "../../data/config_entries";
import type { PolymerChangedEvent } from "../../polymer-types";
import { haStyleDialog } from "../../resources/styles"; import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { ConfigEntrySystemOptionsDialogParams } from "./show-dialog-config-entry-system-options"; import { ConfigEntrySystemOptionsDialogParams } from "./show-dialog-config-entry-system-options";
@@ -35,9 +34,9 @@ class DialogConfigEntrySystemOptions extends LitElement {
@internalProperty() private _params?: ConfigEntrySystemOptionsDialogParams; @internalProperty() private _params?: ConfigEntrySystemOptionsDialogParams;
@internalProperty() private _loading?: boolean; @internalProperty() private _loading = false;
@internalProperty() private _submitting?: boolean; @internalProperty() private _submitting = false;
public async showDialog( public async showDialog(
params: ConfigEntrySystemOptionsDialogParams params: ConfigEntrySystemOptionsDialogParams
@@ -51,7 +50,12 @@ class DialogConfigEntrySystemOptions extends LitElement {
); );
this._loading = false; this._loading = false;
this._disableNewEntities = systemOptions.disable_new_entities; this._disableNewEntities = systemOptions.disable_new_entities;
await this.updateComplete; }
public closeDialog(): void {
this._error = "";
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
protected render(): TemplateResult { protected render(): TemplateResult {
@@ -60,21 +64,17 @@ class DialogConfigEntrySystemOptions extends LitElement {
} }
return html` return html`
<ha-paper-dialog <ha-dialog
with-backdrop open
opened @closed=${this.closeDialog}
@opened-changed="${this._openedChanged}" .heading=${this.hass.localize(
>
<h2>
${this.hass.localize(
"ui.dialogs.config_entry_system_options.title", "ui.dialogs.config_entry_system_options.title",
"integration", "integration",
this.hass.localize( this.hass.localize(`component.${this._params.entry.domain}.title`) ||
`component.${this._params.entry.domain}.title` this._params.entry.domain
) || this._params.entry.domain
)} )}
</h2> >
<paper-dialog-scrollable> <div>
${this._loading ${this._loading
? html` ? html`
<div class="init-spinner"> <div class="init-spinner">
@@ -112,22 +112,22 @@ class DialogConfigEntrySystemOptions extends LitElement {
</ha-formfield> </ha-formfield>
</div> </div>
`} `}
</paper-dialog-scrollable> </div>
${!this._loading
? html`
<div class="paper-dialog-buttons">
<mwc-button <mwc-button
@click="${this._updateEntry}" slot="secondaryAction"
@click=${this.closeDialog}
.disabled=${this._submitting} .disabled=${this._submitting}
> >
${this.hass.localize( ${this.hass.localize("ui.common.cancel")}
"ui.dialogs.config_entry_system_options.update"
)}
</mwc-button> </mwc-button>
</div> <mwc-button
` slot="primaryAction"
: ""} @click="${this._updateEntry}"
</ha-paper-dialog> .disabled=${this._submitting || this._loading}
>
${this.hass.localize("ui.dialogs.config_entry_system_options.update")}
</mwc-button>
</ha-dialog>
`; `;
} }
@@ -154,19 +154,10 @@ class DialogConfigEntrySystemOptions extends LitElement {
} }
} }
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
if (!(ev.detail as any).value) {
this._params = undefined;
}
}
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
haStyleDialog, haStyleDialog,
css` css`
ha-paper-dialog {
max-width: 500px;
}
.init-spinner { .init-spinner {
padding: 50px 100px; padding: 50px 100px;
text-align: center; text-align: center;

View File

@@ -36,6 +36,7 @@ import "./step-flow-external";
import "./step-flow-form"; import "./step-flow-form";
import "./step-flow-loading"; import "./step-flow-loading";
import "./step-flow-pick-handler"; import "./step-flow-pick-handler";
import "./step-flow-progress";
let instance = 0; let instance = 0;
@@ -195,6 +196,14 @@ class DataEntryFlowDialog extends LitElement {
.hass=${this.hass} .hass=${this.hass}
></step-flow-abort> ></step-flow-abort>
` `
: this._step.type === "progress"
? html`
<step-flow-progress
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
></step-flow-progress>
`
: this._devices === undefined || this._areas === undefined : this._devices === undefined || this._areas === undefined
? // When it's a create entry result, we will fetch device & area registry ? // When it's a create entry result, we will fetch device & area registry
html` <step-flow-loading></step-flow-loading> ` html` <step-flow-loading></step-flow-loading> `

View File

@@ -160,4 +160,21 @@ export const showConfigFlowDialog = (
</p> </p>
`; `;
}, },
renderShowFormProgressHeader(hass, step) {
return hass.localize(`component.${step.handler}.title`);
},
renderShowFormProgressDescription(hass, step) {
const description = localizeKey(
hass.localize,
`component.${step.handler}.config.progress.${step.progress_action}`,
step.description_placeholders
);
return description
? html`
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
`
: "";
},
}); });

View File

@@ -7,6 +7,7 @@ import {
DataEntryFlowStepCreateEntry, DataEntryFlowStepCreateEntry,
DataEntryFlowStepExternal, DataEntryFlowStepExternal,
DataEntryFlowStepForm, DataEntryFlowStepForm,
DataEntryFlowStepProgress,
} from "../../data/data_entry_flow"; } from "../../data/data_entry_flow";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
@@ -68,6 +69,16 @@ export interface FlowConfig {
hass: HomeAssistant, hass: HomeAssistant,
step: DataEntryFlowStepCreateEntry step: DataEntryFlowStepCreateEntry
): TemplateResult | ""; ): TemplateResult | "";
renderShowFormProgressHeader(
hass: HomeAssistant,
step: DataEntryFlowStepProgress
): string;
renderShowFormProgressDescription(
hass: HomeAssistant,
step: DataEntryFlowStepProgress
): TemplateResult | "";
} }
export interface DataEntryFlowDialogParams { export interface DataEntryFlowDialogParams {

View File

@@ -110,5 +110,13 @@ export const showOptionsFlowDialog = (
<p>${hass.localize(`ui.dialogs.options_flow.success.description`)}</p> <p>${hass.localize(`ui.dialogs.options_flow.success.description`)}</p>
`; `;
}, },
renderShowFormProgressHeader(_hass, _step) {
return "";
},
renderShowFormProgressDescription(_hass, _step) {
return "";
},
} }
); );

View File

@@ -23,6 +23,7 @@ import { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url"; import { documentationUrl } from "../../util/documentation-url";
import { FlowConfig } from "./show-dialog-data-entry-flow"; import { FlowConfig } from "./show-dialog-data-entry-flow";
import { configFlowContentStyles } from "./styles"; import { configFlowContentStyles } from "./styles";
import { brandsUrl } from "../../util/brands-url";
interface HandlerObj { interface HandlerObj {
name: string; name: string;
@@ -102,7 +103,7 @@ class StepFlowPickHandler extends LitElement {
<img <img
slot="item-icon" slot="item-icon"
loading="lazy" loading="lazy"
src="https://brands.home-assistant.io/_/${handler.slug}/icon.png" src=${brandsUrl(handler.slug, "icon", true)}
referrerpolicy="no-referrer" referrerpolicy="no-referrer"
/> />

View File

@@ -0,0 +1,82 @@
import "@material/mwc-button";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-circular-progress";
import {
DataEntryFlowProgressedEvent,
DataEntryFlowStepProgress,
} from "../../data/data_entry_flow";
import { HomeAssistant } from "../../types";
import { FlowConfig } from "./show-dialog-data-entry-flow";
import { configFlowContentStyles } from "./styles";
@customElement("step-flow-progress")
class StepFlowProgress extends LitElement {
public flowConfig!: FlowConfig;
@property({ attribute: false })
public hass!: HomeAssistant;
@property({ attribute: false })
private step!: DataEntryFlowStepProgress;
protected render(): TemplateResult {
return html`
<h2>
${this.flowConfig.renderShowFormProgressHeader(this.hass, this.step)}
</h2>
<div class="content">
<ha-circular-progress active></ha-circular-progress>
${this.flowConfig.renderShowFormProgressDescription(
this.hass,
this.step
)}
</div>
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this.hass.connection.subscribeEvents<DataEntryFlowProgressedEvent>(
async (ev) => {
if (ev.data.flow_id !== this.step.flow_id) {
return;
}
fireEvent(this, "flow-update", {
stepPromise: this.flowConfig.fetchFlow(this.hass, this.step.flow_id),
});
},
"data_entry_flow_progressed"
);
}
static get styles(): CSSResult[] {
return [
configFlowContentStyles,
css`
.content {
padding: 50px 100px;
text-align: center;
}
ha-circular-progress {
margin-bottom: 16px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"step-flow-progress": StepFlowProgress;
}
}

View File

@@ -1,26 +1,28 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox"; import "@polymer/paper-listbox/paper-listbox";
import "../../components/ha-dialog";
import "../../components/ha-area-picker";
import { import {
css,
CSSResult, CSSResult,
LitElement,
TemplateResult,
css,
customElement, customElement,
html, html,
LitElement,
property,
internalProperty, internalProperty,
TemplateResult, property,
} from "lit-element"; } from "lit-element";
import "../../components/dialog/ha-paper-dialog";
import "../../components/ha-area-picker";
import { computeDeviceName } from "../../data/device_registry";
import { PolymerChangedEvent } from "../../polymer-types";
import { haStyleDialog } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import { DeviceRegistryDetailDialogParams } from "./show-dialog-device-registry-detail"; import { DeviceRegistryDetailDialogParams } from "./show-dialog-device-registry-detail";
import { HomeAssistant } from "../../types";
import { PolymerChangedEvent } from "../../polymer-types";
import { computeDeviceName } from "../../data/device_registry";
import { fireEvent } from "../../common/dom/fire_event";
import { haStyleDialog } from "../../resources/styles";
@customElement("dialog-device-registry-detail") @customElement("dialog-device-registry-detail")
class DialogDeviceRegistryDetail extends LitElement { class DialogDeviceRegistryDetail extends LitElement {
@@ -34,7 +36,7 @@ class DialogDeviceRegistryDetail extends LitElement {
@internalProperty() private _areaId?: string; @internalProperty() private _areaId?: string;
private _submitting?: boolean; @internalProperty() private _submitting?: boolean;
public async showDialog( public async showDialog(
params: DeviceRegistryDetailDialogParams params: DeviceRegistryDetailDialogParams
@@ -46,22 +48,24 @@ class DialogDeviceRegistryDetail extends LitElement {
await this.updateComplete; await this.updateComplete;
} }
public closeDialog(): void {
this._error = "";
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._params) { if (!this._params) {
return html``; return html``;
} }
const device = this._params.device; const device = this._params.device;
return html` return html`
<ha-paper-dialog <ha-dialog
with-backdrop open
opened @closed=${this.closeDialog}
@opened-changed="${this._openedChanged}" .heading=${computeDeviceName(device, this.hass)}
> >
<h2> <div>
${computeDeviceName(device, this.hass)}
</h2>
<paper-dialog-scrollable>
${this._error ? html` <div class="error">${this._error}</div> ` : ""} ${this._error ? html` <div class="error">${this._error}</div> ` : ""}
<div class="form"> <div class="form">
<paper-input <paper-input
@@ -77,13 +81,22 @@ class DialogDeviceRegistryDetail extends LitElement {
@value-changed=${this._areaPicked} @value-changed=${this._areaPicked}
></ha-area-picker> ></ha-area-picker>
</div> </div>
</paper-dialog-scrollable> </div>
<div class="paper-dialog-buttons"> <mwc-button
<mwc-button @click="${this._updateEntry}"> slot="secondaryAction"
@click=${this.closeDialog}
.disabled=${this._submitting}
>
${this.hass.localize("ui.common.cancel")}
</mwc-button>
<mwc-button
slot="primaryAction"
@click="${this._updateEntry}"
.disabled=${this._submitting}
>
${this.hass.localize("ui.panel.config.devices.update")} ${this.hass.localize("ui.panel.config.devices.update")}
</mwc-button> </mwc-button>
</div> </ha-dialog>
</ha-paper-dialog>
`; `;
} }
@@ -113,19 +126,10 @@ class DialogDeviceRegistryDetail extends LitElement {
} }
} }
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
if (!(ev.detail as any).value) {
this._params = undefined;
}
}
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
haStyleDialog, haStyleDialog,
css` css`
ha-paper-dialog {
min-width: 400px;
}
.form { .form {
padding-bottom: 24px; padding-bottom: 24px;
} }

View File

@@ -64,6 +64,10 @@ class MoreInfoCover extends LocalizeMixin(PolymerElement) {
</ha-labeled-slider> </ha-labeled-slider>
</div> </div>
</div> </div>
<ha-attributes
state-obj="[[stateObj]]"
extra-filters="current_position,current_tilt_position"
></ha-attributes>
`; `;
} }

View File

@@ -3,7 +3,7 @@ import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */ /* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "@vaadin/vaadin-date-picker/theme/material/vaadin-date-picker"; import "../../../components/ha-date-input";
import { attributeClassNames } from "../../../common/entity/attribute_class_names"; import { attributeClassNames } from "../../../common/entity/attribute_class_names";
import "../../../components/ha-relative-time"; import "../../../components/ha-relative-time";
import "../../../components/paper-time-input"; import "../../../components/paper-time-input";
@@ -14,12 +14,12 @@ class DatetimeInput extends PolymerElement {
<div class$="[[computeClassNames(stateObj)]]"> <div class$="[[computeClassNames(stateObj)]]">
<template is="dom-if" if="[[doesHaveDate(stateObj)]]" restamp=""> <template is="dom-if" if="[[doesHaveDate(stateObj)]]" restamp="">
<div> <div>
<vaadin-date-picker <ha-date-input
id="dateInput" id="dateInput"
on-value-changed="dateTimeChanged" on-value-changed="dateTimeChanged"
label="Date" label="Date"
value="{{selectedDate}}" value="{{selectedDate}}"
></vaadin-date-picker> ></ha-date-input>
</div> </div>
</template> </template>
<template is="dom-if" if="[[doesHaveTime(stateObj)]]" restamp=""> <template is="dom-if" if="[[doesHaveTime(stateObj)]]" restamp="">

View File

@@ -25,19 +25,12 @@ import "../../../components/ha-svg-icon";
import { showMediaBrowserDialog } from "../../../components/media-player/show-media-browser-dialog"; import { showMediaBrowserDialog } from "../../../components/media-player/show-media-browser-dialog";
import { UNAVAILABLE, UNAVAILABLE_STATES, UNKNOWN } from "../../../data/entity"; import { UNAVAILABLE, UNAVAILABLE_STATES, UNKNOWN } from "../../../data/entity";
import { import {
ControlButton, computeMediaControls,
MediaPickedEvent, MediaPickedEvent,
SUPPORTS_PLAY,
SUPPORT_BROWSE_MEDIA, SUPPORT_BROWSE_MEDIA,
SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE,
SUPPORT_PLAY_MEDIA, SUPPORT_PLAY_MEDIA,
SUPPORT_PREVIOUS_TRACK,
SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOUND_MODE,
SUPPORT_SELECT_SOURCE, SUPPORT_SELECT_SOURCE,
SUPPORT_STOP,
SUPPORT_TURN_OFF,
SUPPORT_TURN_ON,
SUPPORT_VOLUME_BUTTONS, SUPPORT_VOLUME_BUTTONS,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_MUTE,
SUPPORT_VOLUME_SET, SUPPORT_VOLUME_SET,
@@ -57,8 +50,8 @@ class MoreInfoMediaPlayer extends LitElement {
return html``; return html``;
} }
const controls = this._getControls();
const stateObj = this.stateObj; const stateObj = this.stateObj;
const controls = computeMediaControls(stateObj);
return html` return html`
${!controls ${!controls
@@ -254,84 +247,6 @@ class MoreInfoMediaPlayer extends LitElement {
`; `;
} }
private _getControls(): ControlButton[] | undefined {
const stateObj = this.stateObj;
if (!stateObj) {
return undefined;
}
const state = stateObj.state;
if (UNAVAILABLE_STATES.includes(state)) {
return undefined;
}
if (state === "off") {
return supportsFeature(stateObj, SUPPORT_TURN_ON)
? [
{
icon: "hass:power",
action: "turn_on",
},
]
: undefined;
}
if (state === "idle") {
return supportsFeature(stateObj, SUPPORTS_PLAY)
? [
{
icon: "hass:play",
action: "media_play",
},
]
: undefined;
}
const buttons: ControlButton[] = [];
if (supportsFeature(stateObj, SUPPORT_TURN_OFF)) {
buttons.push({
icon: "hass:power",
action: "turn_off",
});
}
if (supportsFeature(stateObj, SUPPORT_PREVIOUS_TRACK)) {
buttons.push({
icon: "hass:skip-previous",
action: "media_previous_track",
});
}
if (
(state === "playing" &&
(supportsFeature(stateObj, SUPPORT_PAUSE) ||
supportsFeature(stateObj, SUPPORT_STOP))) ||
(state === "paused" && supportsFeature(stateObj, SUPPORTS_PLAY))
) {
buttons.push({
icon:
state !== "playing"
? "hass:play"
: supportsFeature(stateObj, SUPPORT_PAUSE)
? "hass:pause"
: "hass:stop",
action: "media_play_pause",
});
}
if (supportsFeature(stateObj, SUPPORT_NEXT_TRACK)) {
buttons.push({
icon: "hass:skip-next",
action: "media_next_track",
});
}
return buttons.length > 0 ? buttons : undefined;
}
private _handleClick(e: MouseEvent): void { private _handleClick(e: MouseEvent): void {
this.hass!.callService( this.hass!.callService(
"media_player", "media_player",

View File

@@ -72,6 +72,12 @@ class MoreInfoSun extends LitElement {
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
} }
ha-relative-time {
display: inline-block;
}
ha-relative-time::first-letter {
text-transform: lowercase;
}
`; `;
} }
} }

View File

@@ -17,14 +17,10 @@ import {
DOMAINS_MORE_INFO_NO_HISTORY, DOMAINS_MORE_INFO_NO_HISTORY,
DOMAINS_WITH_MORE_INFO, DOMAINS_WITH_MORE_INFO,
} from "../../common/const"; } from "../../common/const";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain"; import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name"; import { computeStateName } from "../../common/entity/compute_state_name";
import {
stateMoreInfoType,
importMoreInfoControl,
} from "./state_more_info_control";
import { navigate } from "../../common/navigate"; import { navigate } from "../../common/navigate";
import "../../components/ha-dialog"; import "../../components/ha-dialog";
import "../../components/ha-header-bar"; import "../../components/ha-header-bar";
@@ -38,6 +34,8 @@ import { showConfirmationDialog } from "../generic/show-dialog-box";
import "./ha-more-info-history"; import "./ha-more-info-history";
import "./ha-more-info-logbook"; import "./ha-more-info-logbook";
import "./controls/more-info-default"; import "./controls/more-info-default";
import { CONTINUOUS_DOMAINS } from "../../data/logbook";
import "./more-info-content";
const DOMAINS_NO_INFO = ["camera", "configurator"]; const DOMAINS_NO_INFO = ["camera", "configurator"];
/** /**
@@ -62,8 +60,6 @@ export class MoreInfoDialog extends LitElement {
@internalProperty() private _entityId?: string | null; @internalProperty() private _entityId?: string | null;
@internalProperty() private _moreInfoType?: string;
@internalProperty() private _currTabIndex = 0; @internalProperty() private _currTabIndex = 0;
public showDialog(params: MoreInfoDialogParams) { public showDialog(params: MoreInfoDialogParams) {
@@ -73,18 +69,6 @@ export class MoreInfoDialog extends LitElement {
return; return;
} }
this.large = false; this.large = false;
const stateObj = this.hass.states[this._entityId];
if (!stateObj) {
return;
}
if (stateObj.attributes && "custom_ui_more_info" in stateObj.attributes) {
this._moreInfoType = stateObj.attributes.custom_ui_more_info;
} else {
const type = stateMoreInfoType(stateObj);
importMoreInfoControl(type);
this._moreInfoType = type === "hidden" ? undefined : `more-info-${type}`;
}
} }
public closeDialog() { public closeDialog() {
@@ -169,7 +153,8 @@ export class MoreInfoDialog extends LitElement {
: ""} : ""}
</ha-header-bar> </ha-header-bar>
${DOMAINS_WITH_MORE_INFO.includes(domain) && ${DOMAINS_WITH_MORE_INFO.includes(domain) &&
this._computeShowHistoryComponent(entityId) (this._computeShowHistoryComponent(entityId) ||
this._computeShowLogBookComponent(entityId))
? html` ? html`
<mwc-tab-bar <mwc-tab-bar
.activeIndex=${this._currTabIndex} .activeIndex=${this._currTabIndex}
@@ -208,17 +193,18 @@ export class MoreInfoDialog extends LitElement {
: html`<ha-more-info-history : html`<ha-more-info-history
.hass=${this.hass} .hass=${this.hass}
.entityId=${this._entityId} .entityId=${this._entityId}
></ha-more-info-history> ></ha-more-info-history>`}
<ha-more-info-logbook ${DOMAINS_WITH_MORE_INFO.includes(domain) ||
!this._computeShowLogBookComponent(entityId)
? ""
: html`<ha-more-info-logbook
.hass=${this.hass} .hass=${this.hass}
.entityId=${this._entityId} .entityId=${this._entityId}
></ha-more-info-logbook>`} ></ha-more-info-logbook>`}
${this._moreInfoType <more-info-content
? dynamicElement(this._moreInfoType, { .stateObj=${stateObj}
hass: this.hass, .hass=${this.hass}
stateObj, ></more-info-content>
})
: ""}
${stateObj.attributes.restored ${stateObj.attributes.restored
? html` ? html`
<p> <p>
@@ -264,12 +250,32 @@ export class MoreInfoDialog extends LitElement {
private _computeShowHistoryComponent(entityId) { private _computeShowHistoryComponent(entityId) {
return ( return (
(isComponentLoaded(this.hass, "history") || isComponentLoaded(this.hass, "history") &&
isComponentLoaded(this.hass, "logbook")) &&
!DOMAINS_MORE_INFO_NO_HISTORY.includes(computeDomain(entityId)) !DOMAINS_MORE_INFO_NO_HISTORY.includes(computeDomain(entityId))
); );
} }
private _computeShowLogBookComponent(entityId): boolean {
if (!isComponentLoaded(this.hass, "logbook")) {
return false;
}
const stateObj = this.hass.states[entityId];
if (!stateObj || stateObj.attributes.unit_of_measurement) {
return false;
}
const domain = computeDomain(entityId);
if (
CONTINUOUS_DOMAINS.includes(domain) ||
DOMAINS_MORE_INFO_NO_HISTORY.includes(domain)
) {
return false;
}
return true;
}
private _removeEntity() { private _removeEntity() {
const entityId = this._entityId!; const entityId = this._entityId!;
showConfirmationDialog(this, { showConfirmationDialog(this, {
@@ -279,8 +285,8 @@ export class MoreInfoDialog extends LitElement {
text: this.hass.localize( text: this.hass.localize(
"ui.dialogs.more_info_control.restored.confirm_remove_text" "ui.dialogs.more_info_control.restored.confirm_remove_text"
), ),
confirmText: this.hass.localize("ui.common.yes"), confirmText: this.hass.localize("ui.common.remove"),
dismissText: this.hass.localize("ui.common.no"), dismissText: this.hass.localize("ui.common.cancel"),
confirm: () => { confirm: () => {
removeEntityRegistryEntry(this.hass, entityId); removeEntityRegistryEntry(this.hass, entityId);
}, },

View File

@@ -13,11 +13,7 @@ import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { throttle } from "../../common/util/throttle"; import { throttle } from "../../common/util/throttle";
import "../../components/ha-circular-progress"; import "../../components/ha-circular-progress";
import "../../components/state-history-charts"; import "../../components/state-history-charts";
import { import { getLogbookData, LogbookEntry } from "../../data/logbook";
CONTINUOUS_DOMAINS,
getLogbookData,
LogbookEntry,
} from "../../data/logbook";
import "../../panels/logbook/ha-logbook"; import "../../panels/logbook/ha-logbook";
import { haStyle, haStyleScrollbar } from "../../resources/styles"; import { haStyle, haStyleScrollbar } from "../../resources/styles";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
@@ -44,12 +40,7 @@ export class MoreInfoLogbook extends LitElement {
} }
const stateObj = this.hass.states[this.entityId]; const stateObj = this.hass.states[this.entityId];
if (!stateObj || stateObj.attributes.unit_of_measurement) { if (!stateObj) {
return html``;
}
const domain = computeStateDomain(stateObj);
if (CONTINUOUS_DOMAINS.includes(domain)) {
return html``; return html``;
} }

View File

@@ -0,0 +1,57 @@
import { HassEntity } from "home-assistant-js-websocket";
import { property, PropertyValues, UpdatingElement } from "lit-element";
import { HomeAssistant } from "../../types";
import dynamicContentUpdater from "../../common/dom/dynamic_content_updater";
import { stateMoreInfoType } from "./state_more_info_control";
import { importMoreInfoControl } from "../../panels/lovelace/custom-card-helpers";
class MoreInfoContent extends UpdatingElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property() public stateObj?: HassEntity;
private _detachedChild?: ChildNode;
// This is not a lit element, but an updating element, so we implement update
protected update(changedProps: PropertyValues): void {
super.update(changedProps);
const stateObj = this.stateObj;
const hass = this.hass;
if (!stateObj || !hass) {
if (this.lastChild) {
this._detachedChild = this.lastChild;
// Detach child to prevent it from doing work.
this.removeChild(this.lastChild);
}
return;
}
if (this._detachedChild) {
this.appendChild(this._detachedChild);
this._detachedChild = undefined;
}
let moreInfoType: string | undefined;
if (stateObj.attributes && "custom_ui_more_info" in stateObj.attributes) {
moreInfoType = stateObj.attributes.custom_ui_more_info;
} else {
const type = stateMoreInfoType(stateObj);
importMoreInfoControl(type);
moreInfoType = type === "hidden" ? undefined : `more-info-${type}`;
}
if (!moreInfoType) {
return;
}
dynamicContentUpdater(this, moreInfoType.toUpperCase(), {
hass,
stateObj,
});
}
}
customElements.define("more-info-content", MoreInfoContent);

View File

@@ -25,8 +25,8 @@ export class HuiNotificationDrawer extends EventsMixin(
color: var(--primary-text-color); color: var(--primary-text-color);
border-bottom: 1px solid var(--divider-color); border-bottom: 1px solid var(--divider-color);
background-color: var(--primary-background-color); background-color: var(--primary-background-color);
min-height: 64px; height: var(--header-height);
width: calc(100% - 32px); box-sizing: border-box;
} }
div[main-title] { div[main-title] {
@@ -63,7 +63,10 @@ export class HuiNotificationDrawer extends EventsMixin(
<app-drawer id="drawer" opened="{{open}}" disable-swipe align="start"> <app-drawer id="drawer" opened="{{open}}" disable-swipe align="start">
<app-toolbar> <app-toolbar>
<div main-title>[[localize('ui.notification_drawer.title')]]</div> <div main-title>[[localize('ui.notification_drawer.title')]]</div>
<ha-icon-button-prev on-click="_closeDrawer" aria-label$="[[localize('ui.notification_drawer.close')]]"></ha-icon-button-prev> <ha-icon-button-prev hass="[[hass]]" on-click="_closeDrawer"
title="[[localize('ui.notification_drawer.close')]]"
label="[[localize('ui.notification_drawer.close')]]">
</ha-icon-button-prev>
</app-toolbar> </app-toolbar>
<div class="notifications"> <div class="notifications">
<template is="dom-if" if="[[!_empty(notifications)]]"> <template is="dom-if" if="[[!_empty(notifications)]]">

View File

@@ -36,30 +36,66 @@ import "../../components/ha-header-bar";
import { domainToName } from "../../data/integration"; import { domainToName } from "../../data/integration";
import { haStyleDialog } from "../../resources/styles"; import { haStyleDialog } from "../../resources/styles";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import {
ConfirmationDialogParams,
showConfirmationDialog,
} from "../generic/show-dialog-box";
import { QuickBarParams } from "./show-dialog-quick-bar"; import { QuickBarParams } from "./show-dialog-quick-bar";
import { navigate } from "../../common/navigate";
import { configSections } from "../../panels/config/ha-panel-config";
import { PageNavigation } from "../../layouts/hass-tabs-subpage";
import { canShowPage } from "../../common/config/can_show_page";
import { getPanelNameTranslationKey } from "../../data/panel";
const DEFAULT_SERVER_ICON = "hass:server";
interface QuickBarItem extends ScorableTextItem { interface QuickBarItem extends ScorableTextItem {
icon: string; primaryText: string;
icon?: string;
iconPath?: string;
action(data?: any): void; action(data?: any): void;
} }
export interface CommandItem extends QuickBarItem {
categoryKey: "reload" | "navigation" | "server_control";
categoryText: string;
}
const isCommandItem = (
item: QuickBarItem | CommandItem
): item is CommandItem => {
return (item as CommandItem).categoryKey !== undefined;
};
interface QuickBarNavigationItem extends CommandItem {
path: string;
}
type NavigationInfo = PageNavigation & Pick<QuickBarItem, "primaryText">;
type BaseNavigationCommand = Pick<
QuickBarNavigationItem,
"primaryText" | "path"
>;
@customElement("ha-quick-bar") @customElement("ha-quick-bar")
export class QuickBar extends LitElement { export class QuickBar extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@internalProperty() private _commandItems: QuickBarItem[] = []; @internalProperty() private _commandItems?: CommandItem[];
@internalProperty() private _entityItems: QuickBarItem[] = []; @internalProperty() private _entityItems?: QuickBarItem[];
@internalProperty() private _items?: QuickBarItem[] = []; @internalProperty() private _items?: QuickBarItem[] = [];
@internalProperty() private _filter = ""; @internalProperty() private _filter = "";
@internalProperty() private _search = "";
@internalProperty() private _opened = false; @internalProperty() private _opened = false;
@internalProperty() private _commandMode = false; @internalProperty() private _commandMode = false;
@internalProperty() private _commandTriggered = -1; @internalProperty() private _commandType?: CommandItem["categoryKey"];
@internalProperty() private _done = false; @internalProperty() private _done = false;
@@ -69,8 +105,11 @@ export class QuickBar extends LitElement {
public async showDialog(params: QuickBarParams) { public async showDialog(params: QuickBarParams) {
this._commandMode = params.commandMode || this._toggleIfAlreadyOpened(); this._commandMode = params.commandMode || this._toggleIfAlreadyOpened();
this._commandItems = this._generateCommandItems(); this._commandType = params.commandType;
this._entityItems = this._generateEntityItems(); this._search = this._commandType
? `${this._getCommandCategoryLabel(this._commandType).toLowerCase()} `
: "";
this._initializeItemsIfNeeded();
this._opened = true; this._opened = true;
} }
@@ -79,7 +118,7 @@ export class QuickBar extends LitElement {
this._done = false; this._done = false;
this._focusSet = false; this._focusSet = false;
this._filter = ""; this._filter = "";
this._commandTriggered = -1; this._search = "";
this._items = []; this._items = [];
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
@@ -116,7 +155,7 @@ export class QuickBar extends LitElement {
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.dialogs.quick-bar.filter_placeholder" "ui.dialogs.quick-bar.filter_placeholder"
)} )}
.filter=${this._commandMode ? `>${this._filter}` : this._filter} .filter=${this._commandMode ? `>${this._search}` : this._search}
@keydown=${this._handleInputKeyDown} @keydown=${this._handleInputKeyDown}
@focus=${this._setFocusFirstListItem} @focus=${this._setFocusFirstListItem}
> >
@@ -134,7 +173,6 @@ export class QuickBar extends LitElement {
active active
></ha-circular-progress>` ></ha-circular-progress>`
: html`<mwc-list : html`<mwc-list
activatable
@rangechange=${this._handleRangeChanged} @rangechange=${this._handleRangeChanged}
@keydown=${this._handleListItemKeyDown} @keydown=${this._handleListItemKeyDown}
@selected=${this._handleSelected} @selected=${this._handleSelected}
@@ -155,6 +193,14 @@ export class QuickBar extends LitElement {
`; `;
} }
private _initializeItemsIfNeeded() {
if (this._commandMode) {
this._commandItems = this._commandItems || this._generateCommandItems();
} else {
this._entityItems = this._entityItems || this._generateEntityItems();
}
}
private _handleOpened() { private _handleOpened() {
this._setFilteredItems(); this._setFilteredItems();
this.updateComplete.then(() => { this.updateComplete.then(() => {
@@ -174,34 +220,62 @@ export class QuickBar extends LitElement {
} }
private _renderItem(item: QuickBarItem, index?: number) { private _renderItem(item: QuickBarItem, index?: number) {
return isCommandItem(item)
? this._renderCommandItem(item, index)
: this._renderEntityItem(item, index);
}
private _renderEntityItem(item: QuickBarItem, index?: number) {
return html` return html`
<mwc-list-item <mwc-list-item
.twoline=${Boolean(item.altText)} .twoline=${Boolean(item.altText)}
.item=${item} .item=${item}
index=${ifDefined(index)} index=${ifDefined(index)}
hasMeta graphic="icon"
graphic=${item.altText ? "avatar" : "icon"}
> >
<ha-icon .icon=${item.icon} slot="graphic"></ha-icon> ${item.iconPath
<span>${item.text}</span> ? html`<ha-svg-icon
.path=${item.iconPath}
slot="graphic"
></ha-svg-icon>`
: html`<ha-icon .icon=${item.icon} slot="graphic"></ha-icon>`}
<span>${item.primaryText}</span>
${item.altText ${item.altText
? html` ? html`
<span slot="secondary" class="secondary">${item.altText}</span> <span slot="secondary" class="item-text secondary"
>${item.altText}</span
>
` `
: null} : null}
${this._commandTriggered === index </mwc-list-item>
? html`<ha-circular-progress `;
size="small" }
active
slot="meta" private _renderCommandItem(item: CommandItem, index?: number) {
></ha-circular-progress>` return html`
<mwc-list-item
.twoline=${Boolean(item.altText)}
.item=${item}
index=${ifDefined(index)}
>
<span class="command-category ${item.categoryKey}"
>${item.categoryText}
</span>
<span>${item.primaryText}</span>
${item.altText
? html`
<span slot="secondary" class="item-text secondary"
>${item.altText}</span
>
`
: null} : null}
</mwc-list-item> </mwc-list-item>
`; `;
} }
private async processItemAndCloseDialog(item: QuickBarItem, index: number) { private async processItemAndCloseDialog(item: QuickBarItem, index: number) {
this._commandTriggered = index; this._addSpinnerToCommandItem(index);
await item.action(); await item.action();
this.closeDialog(); this.closeDialog();
@@ -231,21 +305,33 @@ export class QuickBar extends LitElement {
return this.renderRoot.querySelector(`mwc-list-item[index="${index}"]`); return this.renderRoot.querySelector(`mwc-list-item[index="${index}"]`);
} }
private _addSpinnerToCommandItem(index: number): void {
const spinner = document.createElement("ha-circular-progress");
spinner.size = "small";
spinner.slot = "meta";
spinner.active = true;
this._getItemAtIndex(index)?.appendChild(spinner);
}
private _handleSearchChange(ev: CustomEvent): void { private _handleSearchChange(ev: CustomEvent): void {
const newFilter = ev.detail.value; const newFilter = ev.detail.value;
const oldCommandMode = this._commandMode; const oldCommandMode = this._commandMode;
if (newFilter.startsWith(">")) { if (newFilter.startsWith(">")) {
this._commandMode = true; this._commandMode = true;
this._debouncedSetFilter(newFilter.substring(1)); this._search = newFilter.substring(1);
} else { } else {
this._commandMode = false; this._commandMode = false;
this._debouncedSetFilter(newFilter); this._search = newFilter;
} }
this._debouncedSetFilter(this._search);
if (oldCommandMode !== this._commandMode) { if (oldCommandMode !== this._commandMode) {
this._items = undefined; this._items = undefined;
this._focusSet = false; this._focusSet = false;
this._initializeItemsIfNeeded();
} }
} }
@@ -273,37 +359,171 @@ export class QuickBar extends LitElement {
} }
} }
private _generateCommandItems(): QuickBarItem[] { private _generateEntityItems(): QuickBarItem[] {
return [...this._generateReloadCommands()].sort((a, b) => return Object.keys(this.hass.states)
compare(a.text.toLowerCase(), b.text.toLowerCase()) .map((entityId) => {
const primaryText = computeStateName(this.hass.states[entityId]);
return {
primaryText,
filterText: primaryText,
altText: entityId,
icon: domainIcon(computeDomain(entityId), this.hass.states[entityId]),
action: () => fireEvent(this, "hass-more-info", { entityId }),
};
})
.sort((a, b) =>
compare(a.primaryText.toLowerCase(), b.primaryText.toLowerCase())
); );
} }
private _generateReloadCommands(): QuickBarItem[] { private _generateCommandItems(): CommandItem[] {
return [
...this._generateReloadCommands(),
...this._generateServerControlCommands(),
...this._generateNavigationCommands(),
].sort((a, b) =>
compare(a.filterText.toLowerCase(), b.filterText.toLowerCase())
);
}
private _generateReloadCommands(): CommandItem[] {
const reloadableDomains = componentsWithService(this.hass, "reload").sort(); const reloadableDomains = componentsWithService(this.hass, "reload").sort();
return reloadableDomains.map((domain) => ({ return reloadableDomains.map((domain) => {
text: const categoryKey = "reload";
const categoryText = this._getCommandCategoryLabel(categoryKey);
const primaryText =
this.hass.localize(`ui.dialogs.quick-bar.commands.reload.${domain}`) || this.hass.localize(`ui.dialogs.quick-bar.commands.reload.${domain}`) ||
this.hass.localize( this.hass.localize(
"ui.dialogs.quick-bar.commands.reload.reload", "ui.dialogs.quick-bar.commands.reload.reload",
"domain", "domain",
domainToName(this.hass.localize, domain) domainToName(this.hass.localize, domain)
), );
return {
primaryText,
filterText: `${categoryText} ${primaryText}`,
icon: domainIcon(domain), icon: domainIcon(domain),
action: () => this.hass.callService(domain, "reload"), action: () => this.hass.callService(domain, "reload"),
})); categoryKey,
categoryText,
};
});
} }
private _generateEntityItems(): QuickBarItem[] { private _generateServerControlCommands(): CommandItem[] {
return Object.keys(this.hass.states) const serverActions = ["restart", "stop"];
.map((entityId) => ({
text: computeStateName(this.hass.states[entityId]), return serverActions.map((action) => {
altText: entityId, const categoryKey = "server_control";
icon: domainIcon(computeDomain(entityId), this.hass.states[entityId]), const categoryText = this._getCommandCategoryLabel(categoryKey);
action: () => fireEvent(this, "hass-more-info", { entityId }), const primaryText = this.hass.localize(
})) "ui.dialogs.quick-bar.commands.server_control.perform_action",
.sort((a, b) => compare(a.text.toLowerCase(), b.text.toLowerCase())); "action",
this.hass.localize(
`ui.dialogs.quick-bar.commands.server_control.${action}`
)
);
return this._generateConfirmationCommand(
{
primaryText,
filterText: `${categoryText} ${primaryText}`,
categoryKey,
categoryText,
icon: DEFAULT_SERVER_ICON,
action: () => this.hass.callService("homeassistant", action),
},
this.hass.localize("ui.dialogs.generic.ok")
);
});
}
private _generateNavigationCommands(): CommandItem[] {
const panelItems = this._generateNavigationPanelCommands();
const sectionItems = this._generateNavigationConfigSectionCommands();
return this._finalizeNavigationCommands([...panelItems, ...sectionItems]);
}
private _generateNavigationPanelCommands(): BaseNavigationCommand[] {
return Object.keys(this.hass.panels).map((panelKey) => {
const panel = this.hass.panels[panelKey];
const translationKey = getPanelNameTranslationKey(panel);
const primaryText =
this.hass.localize(translationKey) || panel.title || panel.url_path;
return {
primaryText,
path: `/${panel.url_path}`,
};
});
}
private _generateNavigationConfigSectionCommands(): BaseNavigationCommand[] {
const items: NavigationInfo[] = [];
for (const sectionKey of Object.keys(configSections)) {
for (const page of configSections[sectionKey]) {
if (canShowPage(this.hass, page)) {
if (page.component) {
const info = this._getNavigationInfoFromConfig(page);
if (info) {
items.push(info);
}
}
}
}
}
return items;
}
private _getNavigationInfoFromConfig(
page: PageNavigation
): NavigationInfo | undefined {
if (page.component) {
const caption = this.hass.localize(
`ui.dialogs.quick-bar.commands.navigation.${page.component}`
);
return { ...page, primaryText: caption };
}
return undefined;
}
private _generateConfirmationCommand(
item: CommandItem,
confirmText: ConfirmationDialogParams["confirmText"]
): CommandItem {
return {
...item,
action: () =>
showConfirmationDialog(this, {
confirmText,
confirm: item.action,
}),
};
}
private _finalizeNavigationCommands(
items: BaseNavigationCommand[]
): CommandItem[] {
return items.map((item) => {
const categoryKey = "navigation";
const categoryText = this._getCommandCategoryLabel(categoryKey);
return {
...item,
categoryKey,
categoryText,
filterText: `${categoryText} ${item.primaryText}`,
action: () => navigate(this, item.path),
};
});
} }
private _toggleIfAlreadyOpened() { private _toggleIfAlreadyOpened() {
@@ -322,6 +542,10 @@ export class QuickBar extends LitElement {
fuzzyFilterSort<QuickBarItem>(filter.trimLeft(), items) fuzzyFilterSort<QuickBarItem>(filter.trimLeft(), items)
); );
private _getCommandCategoryLabel = (
categoryKey: CommandItem["categoryKey"]
) => this.hass.localize(`ui.dialogs.quick-bar.commands.types.${categoryKey}`);
static get styles() { static get styles() {
return [ return [
haStyleDialog, haStyleDialog,
@@ -345,8 +569,35 @@ export class QuickBar extends LitElement {
} }
} }
ha-icon,
ha-svg-icon {
margin-left: 20px;
}
ha-svg-icon.prefix { ha-svg-icon.prefix {
margin: 8px; margin: 8px;
color: var(--primary-text-color);
}
span.command-category {
font-weight: bold;
padding: 3px;
display: inline-flex;
border-radius: 6px;
color: black;
}
span.command-category.reload {
background: pink;
}
span.command-category.navigation {
background: lightblue;
}
span.command-category.server_control {
background: orange;
color: white;
} }
.uni-virtualizer-host { .uni-virtualizer-host {

View File

@@ -1,8 +1,10 @@
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { CommandItem } from "./ha-quick-bar";
export interface QuickBarParams { export interface QuickBarParams {
entityFilter?: string; entityFilter?: string;
commandMode?: boolean; commandMode?: boolean;
commandType?: CommandItem["categoryKey"];
} }
export const loadQuickBar = () => export const loadQuickBar = () =>

View File

@@ -53,9 +53,12 @@ export const provideHass = (
} = {}; } = {};
const entities = {}; const entities = {};
function updateTranslations(fragment: null | string, language?: string) { async function updateTranslations(
fragment: null | string,
language?: string
) {
const lang = language || getLocalLanguage(); const lang = language || getLocalLanguage();
getTranslation(fragment, lang).then((translation) => { const translation = await getTranslation(fragment, lang);
const resources = { const resources = {
[lang]: { [lang]: {
...(hass().resources && hass().resources[lang]), ...(hass().resources && hass().resources[lang]),
@@ -64,8 +67,7 @@ export const provideHass = (
}; };
hass().updateHass({ hass().updateHass({
resources, resources,
localize: computeLocalize(elements[0], lang, resources), localize: await computeLocalize(elements[0], lang, resources),
});
}); });
} }

View File

@@ -17,6 +17,8 @@
--primary-text-color: #e1e1e1; --primary-text-color: #e1e1e1;
--secondary-text-color: #9b9b9b; --secondary-text-color: #9b9b9b;
--disabled-text-color: #6f6f6f; --disabled-text-color: #6f6f6f;
--mdc-theme-surface: #1e1e1e;
--ha-card-background: #1e1e1e;
} }
} }
.content { .content {

View File

@@ -9,9 +9,12 @@ import {
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import "./hass-subpage"; import "./hass-subpage";
import { HomeAssistant } from "../types";
@customElement("hass-error-screen") @customElement("hass-error-screen")
class HassErrorScreen extends LitElement { class HassErrorScreen extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public toolbar = true; @property({ type: Boolean }) public toolbar = true;
@property() public error?: string; @property() public error?: string;
@@ -21,6 +24,7 @@ class HassErrorScreen extends LitElement {
${this.toolbar ${this.toolbar
? html`<div class="toolbar"> ? html`<div class="toolbar">
<ha-icon-button-arrow-prev <ha-icon-button-arrow-prev
.hass=${this.hass}
@click=${this._handleBack} @click=${this._handleBack}
></ha-icon-button-arrow-prev> ></ha-icon-button-arrow-prev>
</div>` </div>`
@@ -50,7 +54,7 @@ class HassErrorScreen extends LitElement {
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 20px; font-size: 20px;
height: 65px; height: var(--header-height);
padding: 0 16px; padding: 0 16px;
pointer-events: none; pointer-events: none;
background-color: var(--app-header-background-color); background-color: var(--app-header-background-color);

View File

@@ -39,6 +39,7 @@ class HassLoadingScreen extends LitElement {
` `
: html` : html`
<ha-icon-button-arrow-prev <ha-icon-button-arrow-prev
.hass=${this.hass}
@click=${this._handleBack} @click=${this._handleBack}
></ha-icon-button-arrow-prev> ></ha-icon-button-arrow-prev>
`} `}
@@ -66,7 +67,7 @@ class HassLoadingScreen extends LitElement {
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 20px; font-size: 20px;
height: 65px; height: var(--header-height);
padding: 0 16px; padding: 0 16px;
pointer-events: none; pointer-events: none;
background-color: var(--app-header-background-color); background-color: var(--app-header-background-color);

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