Compare commits

...

193 Commits

Author SHA1 Message Date
Thomas Lovén c448bbc3b4 Allow multiple states in state condition editor 2020-11-08 22:06:40 +01: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
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 58a58906e7 Get rid of the unwanted tooltip copying (final) (#7459) 2020-10-27 20:20:15 +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
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
Bram Kragten 07fc9b98cc Bump webpack (#7423) 2020-10-21 23:39:05 +02:00
Bram Kragten 33582c0448 Bumped version to 20201021.1 2020-10-21 23:35:57 +02:00
Bram Kragten 611202c905 Fix no focus on first item when switching mode (#7421) 2020-10-21 18:50:08 +02:00
Bram Kragten e553f35a68 Merge branch 'master' into dev 2020-10-21 18:41:18 +02:00
Bram Kragten 673649a603 Bumped version to 20201021.0 2020-10-21 18:36:59 +02:00
Bram Kragten c4ed743370 Fix mwc list items icon color (#7420) 2020-10-21 18:21:58 +02:00
Joakim Sørensen 682fa0d3eb Haos update button (#7419) 2020-10-21 10:30:41 -05:00
On Freund 30f34eee22 Add context to event trigger (#7182)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-10-21 17:26:45 +02:00
Bram Kragten eab76bf85b Fix position edit card dialog (#7418) 2020-10-21 15:26:08 +02:00
Donnie bcf405bf9d Fix Firefox quick bar issue by allowing Ctrl+P to toggle modes (#7413)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-10-21 14:19:16 +02:00
Ryan Meek 3c4b0d4a74 Compact header (#7369)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-10-21 14:18:33 +02:00
Zack Barett fb9bd0eb7d Fix prettier that keeps messing with merging dev (#7412) 2020-10-21 13:14:10 +02:00
Ian Richardson 7e2dc04123 Fix icon for unavailable buttons (#7416) 2020-10-21 13:12:28 +02:00
Bram Kragten 54ec37994c Improve performance of quick bar (#7359)
Co-authored-by: Donnie <donniekarnsinsb@hotmail.com>
2020-10-21 13:07:44 +02:00
Gilson Marquato Júnior 4a5935ee36 Add onClick listener to dismiss toast notification. (#7268) 2020-10-21 13:03:31 +02:00
Joakim Sørensen 01b9a07320 Change logic for the new version handling (#7405) 2020-10-21 12:40:06 +02:00
Zack Barett 0fcf0dcd18 Fix Yarn lock (#7411) 2020-10-20 19:37:26 -05:00
HomeAssistant Azure 80481f142a [ci skip] Translation update 2020-10-21 00:32:23 +00:00
Philip Allgaier 2be08ce7ab Light hui & more-info card fixes (#7397) 2020-10-20 23:52:10 +02:00
Bram Kragten 37eb5af3d4 Pass updated cards and badges to view element (#7407) 2020-10-20 23:51:15 +02:00
Philip Allgaier 8c8151be92 Add entity filter to history panel (#7401) 2020-10-20 23:02:59 +02:00
Philip Allgaier baf31d1c1e Fix alignments in integration card (#7404) 2020-10-20 22:59:20 +02:00
Donnie af2250835a Only admins can launch quick bar (#7388)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2020-10-20 22:24:48 +02:00
Donnie 6f2a759ba3 Add scoring and sorting to sequence matcher (#7367)
* Replace sequence matcher with VS Code's score-based implementation

* Remove everything not related to fuzzyScore and matchSubstring

* Fix bug when filter length <= 3

* Add licensing and credit to Microsoft

* Remove unnecessary character codes

* Remove old sequence matcher, update tests, fix issue with not finding best score in list of words

* Remove unnecessary sequence precheck, refactor client api to remove array

* Fix issue with score sorting not implemented correctly and thus not actually sorting by score

* Update src/common/string/filter/sequence-matching.ts

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

* Remove unnecessary string return from fuzzy matcher. Clean up code

* Remove globals from filter. Move sorting logic into matcher

* Update function description, make score property optional.

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-10-20 09:01:16 -07:00
Jaroslav Hanslík 5065901196 Modified icons of binary sensors: gas, problem, safety, smoke (#7403) 2020-10-20 17:43:01 +02:00
Philip Allgaier 41b59e6e11 Bump round-slider to 0.5.2 (#7399) 2020-10-20 08:58:35 -05:00
onagurna 43afdaadc6 Fix syntax error (#7402) 2020-10-20 15:09:27 +02:00
Tomasz 83c5151792 bump node-vibrant to 3.1.6 (#7400) 2020-10-20 12:42:51 +02:00
HomeAssistant Azure 0880ab67c6 [ci skip] Translation update 2020-10-20 00:32:22 +00:00
Donnie c0b2143c7c Quick Bar dark mode, ignore leading white space (#7387) 2020-10-19 22:10:44 +02:00
Philip Allgaier c1de162c99 Visual alignment between PR 7364 & 7220 (#7396) 2020-10-19 22:09:15 +02:00
Philip Allgaier a7ef8aba68 Add dialog-box warning support (#7356) 2020-10-19 22:08:35 +02:00
Philip Allgaier 3ee4c11a99 Move error log <ha-card> + color in log entries (#7382) 2020-10-19 22:05:19 +02:00
Philip Allgaier 990ae10dc2 Detect Lovelace resource type based on file extension (#7354) 2020-10-19 22:03:59 +02:00
Philip Allgaier 52b2fd046b Improved automation & script menus + show errors in toast (#7371)
* Improved automation & script menus + show errors in toast

* Changes from review

* Re-added old error display

* Toast back to default duration + remove action
2020-10-19 19:55:36 +02:00
J. Nick Koston 9f41f80a91 Add support for displaying time listeners (#7220) 2020-10-19 19:43:04 +02:00
Joakim Sørensen eec4a91ad8 Fixes for snapshot upload during onboarding (#7390)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-10-19 15:24:25 +02:00
Joakim Sørensen 7c51001c3c Move valid so we don't cache an empty element for add-on config (#7394) 2020-10-19 15:23:13 +02:00
Daniel Martin Gonzalez a4ea4b1f5f Add Timer to Helpers (#7366) 2020-10-19 14:45:20 +02:00
Philip Allgaier 19fc37539e Fix alignment / padding of ha-switch (#7349) 2020-10-19 14:44:54 +02:00
Philip Allgaier ce7acb0feb Clean up general config page code (#7383) 2020-10-19 14:40:01 +02:00
Philip Allgaier 105b7678b8 Make error texts of device automation picker translatable (#7370) 2020-10-19 14:30:40 +02:00
Philip Allgaier b67575586e Add option to copy system info into clipboard (#7323) 2020-10-19 14:29:10 +02:00
Ing. Jaroslav Šafka 3dc6898673 Enable volume up/down for media player (#7376)
Always show volume up/down buttons in media player
if SUPPORT_VOLUME_BUTTONS is supported.
And slider if supported

Previous impl. shows slider or up/down buttons.
2020-10-19 11:16:20 +02:00
Philip Allgaier a73754c1b5 Use ha-card for dev tool "Services" + visual tweaks (#7364) 2020-10-19 11:15:12 +02:00
Charles Garwood 1ebf1c00d6 Initial OZW Node Config Panel (#7377) 2020-10-19 11:13:55 +02:00
HomeAssistant Azure 7dac7d757e [ci skip] Translation update 2020-10-19 00:32:45 +00:00
HomeAssistant Azure b1f3192b95 [ci skip] Translation update 2020-10-18 00:32:21 +00:00
Donnie 16984d18bb Refactor quick bar to use a common interface for future commands and easier sorting (#7368) 2020-10-17 15:48:48 -07:00
Philip Allgaier e603893d77 Fix navigation links for "script/edit" (#7363) 2020-10-17 17:28:05 -05:00
Bram Kragten a7998b30c6 Fix hls player (#7362) 2020-10-17 23:43:00 +02:00
Philip Allgaier 3277a4e8c3 Minor tweaks for when media player has no items (#7374) 2020-10-17 23:34:11 +02:00
Philip Allgaier 7e769d0e14 Make <ha-card> use <h1> for header (#7373)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2020-10-17 23:22:56 +02:00
Philip Allgaier 713e0579f8 Entity registry settings: Remove "Override" string + use domain icon as fallback (#7320) 2020-10-17 22:46:34 +02:00
Philip Allgaier 6e130cc020 Properly wrap integration title / device names (#7355) 2020-10-17 22:28:40 +02:00
Villhellm eb036a12d9 add help button to tags config panel (#7278)
* add help button to tags config panel

* Update src/panels/config/tags/ha-config-tags.ts

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

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-10-17 22:21:03 +02:00
Joakim Sørensen 534d1f5055 Add dialog and links for unsupported supervisor installation (#7332) 2020-10-17 22:18:17 +02:00
Daniel Martin Gonzalez cbef909657 Add Counter to Helpers (#7346)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-10-17 22:15:37 +02:00
Philip Allgaier 874f3b32b3 Harmonize the font sizes on area and device page (#7357) 2020-10-17 22:10:52 +02:00
Bram Kragten 2fd017cf73 Fix more info group (#7345)
Co-authored-by: Zack Barett <arnett.zackary@gmail.com>
2020-10-17 22:05:04 +02:00
Bram Kragten 5740b018a7 Prevent old more info control to be created (#7324) 2020-10-17 22:04:44 +02:00
Tomasz 288bf6805a Sort persistent notifications ascending (#7195)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-10-17 22:03:33 +02:00
HomeAssistant Azure 02d37a369a [ci skip] Translation update 2020-10-17 00:32:29 +00:00
Philip Allgaier 1d316c3258 Remove query caching to get YAML editor toggle working again (#7365) 2020-10-16 23:27:00 +02:00
Joakim Sørensen a56ce62f1a Add docker registry management (#7269)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-10-16 17:25:23 +02:00
Zack Barett c268f42851 Entity Picker: undo fuzzy algorithm (#7360) 2020-10-16 09:45:58 -05:00
HomeAssistant Azure 7251e802ab [ci skip] Translation update 2020-10-16 00:32:27 +00:00
Matt 5b1a2d10c2 Add button that dismisses all notifications (#7223) 2020-10-15 16:22:47 -05:00
Charles Garwood 2dd7f292b1 Add product picture to ozw node dashboard (#7203) 2020-10-15 16:20:54 -05:00
Santobert 213c53e307 Add the options dark_mode_filter and dark_mode_image to the picture elements card (#7304) 2020-10-15 16:16:15 -05:00
Bram Kragten ce07dfd8ac Little clean up in data table (#7352) 2020-10-15 18:46:55 +02:00
Zack Barett c1dba462e8 Lovelace Cards: Update size calcs and add height fixes for horizontal stacks (#7177) 2020-10-15 17:46:29 +02:00
Philip Allgaier 47f0d74812 New "clickable" property for <ha-data-table> (#7351) 2020-10-15 15:57:05 +02:00
Ryan Meek ce80285f8d Header styling & paper-tabs improvements (#7238)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-10-15 15:08:53 +02:00
Philip Allgaier d2dd1a43dd Fix attribute picker suffix (#7348) 2020-10-15 14:56:10 +02:00
Zack Barett 12d73fe90d Add Friendly Name to the Entity Picker + FuzzySeq Algo (#7291) 2020-10-14 20:20:21 -05:00
HomeAssistant Azure c2741638b2 [ci skip] Translation update 2020-10-15 00:32:32 +00:00
Philip Allgaier 4a7fb3d509 Use attribute picker for sec. info in weather card editor (#7335) 2020-10-15 00:57:30 +02:00
Bram Kragten f6ff652ca4 Fix es5 build (#7319) 2020-10-14 22:20:39 +02:00
Donnie 6165cb0f83 Make enter key execute first filtered item in Quick Bar (#7288)
* Make enter key execute first filtered item in quick open dialog

* Add support for arrow up and down moving in and out of list

* Add support for typing letters even when arrowing around the list

* Address review comments and clean up the single-character keyboard event logic

* Address comments. Move activated index along with arrow keys

* Add loading spinner and remove memoization
2020-10-14 19:01:44 +02:00
Bram Kragten 1f361b7b10 Update entity picker (#7343) 2020-10-14 17:01:23 +02:00
Tomasz 5269ff978b New component: ha-expansion-panel (#6789)
Co-authored-by: Zack Barett <zackbarett@hey.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-10-14 16:28:46 +02:00
Bram Kragten 55595493a9 Update config.yml 2020-10-14 13:57:48 +02:00
Bram Kragten ad3ff0aba7 Delete FEATURE_REQUEST.md 2020-10-14 12:04:24 +02:00
Bram Kragten ce48546cef Move feature requests to discussions 2020-10-14 12:04:07 +02:00
HomeAssistant Azure 35b3bc995e [ci skip] Translation update 2020-10-14 00:32:30 +00:00
Donnie 63f60019d1 Add friendly name to quick bar list and filter (#7306)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-10-13 17:26:12 -05:00
Philip Allgaier 0d741b6275 Add warning to badge preview in "Panel Mode" (#7331) 2020-10-13 13:49:02 +02:00
uvjustin 0df9080bbb Check if video el still exists before exoplayer resize in ha-hls-player (#7317)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-10-13 12:15:30 +02:00
Donnie ddcf89e6a2 Add tests for fuzzy sequence matcher (#7328) 2020-10-13 11:50:36 +02:00
Donnie 5de225d5d4 Fix: Quick Bar not launching on windows (#7293) 2020-10-13 11:23:18 +02:00
Donnie 5cddb482f1 Fix issue with enter key not executing selected item in quick bar list (#7283) 2020-10-13 11:16:46 +02:00
HomeAssistant Azure c000d724de [ci skip] Translation update 2020-10-13 00:32:16 +00:00
Philip Allgaier 504055f331 Add documentation link to "customize" dialog (#7321) 2020-10-12 22:09:34 +02:00
Philip Allgaier 7f6880f40e Show disabled entities for a config_entry by default + number of hidden entities (#7300)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-10-12 22:00:21 +02:00
Zack Barett 02e4e3c892 Weather Card: add icons instead of text (#7305)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-10-12 11:52:17 -05:00
Bram Kragten b5b1849ab3 Bumped version to 20201001.2 2020-10-08 15:12:18 +02:00
Bram Kragten 0e10c81025 Fix cloud webhooks (#7261) 2020-10-08 15:12:05 +02:00
Bram Kragten cce7ad449a Set hass when creating card (#7187) 2020-10-05 16:04:25 +02:00
Bram Kragten d437dd5919 Bumped version to 20201001.1 2020-10-05 16:00:40 +02:00
Bram Kragten f1980730d2 Fix Apple not showing cards (#7232) 2020-10-05 16:00:27 +02:00
Zack Barett 47773e9cae Fix entities Card toggle (#7192) 2020-10-05 16:00:12 +02:00
Ryan Meek 60969b0916 Styling fixes for hui-masonry-view (#7226) 2020-10-05 15:59:53 +02:00
Zack Barett ecc7925d03 Warning Element: Fix Overflow in Entity Row (#7193) 2020-10-05 15:59:38 +02:00
Zack Barett 6d3010dcc7 Logbook: Fix for no state obj (#7191) 2020-10-05 15:59:19 +02:00
J. Nick Koston 0164bafbf1 Fix reversal in power icon (#7188) 2020-10-05 15:58:57 +02:00
Bram Kragten 98a64e3114 Merge pull request #7185 from home-assistant/dev 2020-10-01 16:05:08 +02:00
Bram Kragten 6ef3d091e1 Merge pull request #7174 from home-assistant/dev 2020-09-30 17:28:42 +02:00
Bram Kragten b612c0e0d6 Merge pull request #7168 from home-assistant/dev 2020-09-30 11:45:31 +02:00
295 changed files with 12171 additions and 5453 deletions
+3 -1
View File
@@ -75,6 +75,7 @@
"object-curly-newline": 0,
"default-case": 0,
"wc/no-self-class": 0,
"no-shadow": 0,
"@typescript-eslint/camelcase": 0,
"@typescript-eslint/ban-ts-comment": 0,
"@typescript-eslint/no-use-before-define": 0,
@@ -82,7 +83,8 @@
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/no-unused-vars": 0,
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/explicit-module-boundary-types": 0
"@typescript-eslint/explicit-module-boundary-types": 0,
"@typescript-eslint/no-shadow": ["error"]
},
"plugins": ["disable", "import", "lit", "prettier", "@typescript-eslint"],
"processor": "disable/disable"
-26
View File
@@ -1,26 +0,0 @@
---
name: Request a feature for the UI, Frontend or Lovelace
about: Request an new feature for the Home Assistant frontend.
labels: feature request
---
<!--
DO NOT DELETE ANY TEXT from this template!
Otherwise, your request may be closed without comment.
-->
## The request
<!--
Describe to our maintainers, the feature you would like to be added.
Please be clear and concise and, if possible, provide a screenshot or mockup.
-->
## The alternatives
<!--
Are you currently using, or have you considered alternatives?
If so, could you please describe those?
-->
## Additional information
+3
View File
@@ -1,5 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Request a feature for the UI, Frontend or Lovelace
url: https://github.com/home-assistant/frontend/discussions/category_choices
about: Request an new feature for the Home Assistant frontend.
- name: Report a bug that is NOT related to the UI, Frontend or Lovelace
url: https://github.com/home-assistant/core/issues
about: This is the issue tracker for our frontend. Please report other issues with the backend repository.
+1 -1
View File
@@ -26,4 +26,4 @@ A complete guide can be found at the following [link](https://www.home-assistant
Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects.
We use [BrowserStack](https://www.browserstack.com) to test Home Assistant on a large variation of devices.
We use [BrowserStack](https://www.browserstack.com) to test Home Assistant on a large variety of devices.
+5 -2
View File
@@ -54,7 +54,10 @@ module.exports.babelOptions = ({ latestBuild }) => ({
presets: [
!latestBuild && [
require("@babel/preset-env").default,
{ modules: false, useBuiltIns: "entry", corejs: 3 },
{
useBuiltIns: "entry",
corejs: "3.6",
},
],
require("@babel/preset-typescript").default,
].filter(Boolean),
@@ -66,7 +69,7 @@ module.exports.babelOptions = ({ latestBuild }) => ({
],
// Only support the syntax, Webpack will handle it.
"@babel/plugin-syntax-import-meta",
"@babel/syntax-dynamic-import",
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-nullish-coalescing-operator",
[
-2
View File
@@ -7,7 +7,6 @@ const gulp = require("gulp");
const fs = require("fs");
const foreach = require("gulp-foreach");
const merge = require("gulp-merge-json");
const minify = require("gulp-jsonminify");
const rename = require("gulp-rename");
const transform = require("gulp-json-transform");
const { mapFiles } = require("../util");
@@ -301,7 +300,6 @@ gulp.task("build-flattened-translations", function () {
return flatten(data);
})
)
.pipe(minify())
.pipe(
rename((filePath) => {
if (filePath.dirname === "core") {
+26 -3
View File
@@ -29,7 +29,7 @@ const createWebpackConfig = ({
module: {
rules: [
{
test: /\.js$|\.ts$/,
test: /\.m?js$|\.ts$/,
exclude: bundle.babelExclude(),
use: {
loader: "babel-loader",
@@ -45,10 +45,8 @@ const createWebpackConfig = ({
optimization: {
minimizer: [
new TerserPlugin({
cache: true,
parallel: true,
extractComments: true,
sourceMap: true,
terserOptions: bundle.terserOptions(latestBuild),
}),
],
@@ -97,6 +95,15 @@ const createWebpackConfig = ({
new RegExp(bundle.emptyPackages({ latestBuild }).join("|")),
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: {
extensions: [".ts", ".js", ".json"],
@@ -108,6 +115,22 @@ const createWebpackConfig = ({
}
return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`;
},
environment: {
// The environment supports arrow functions ('() => { ... }').
arrowFunction: latestBuild,
// The environment supports BigInt as literal (123n).
bigIntLiteral: false,
// The environment supports const and let for variable declarations.
const: latestBuild,
// The environment supports destructuring ('{ a, b } = obj').
destructuring: latestBuild,
// The environment supports an async import() function to import EcmaScript modules.
dynamicImport: latestBuild,
// The environment supports 'for of' iteration ('for (const x of array) { ... }').
forOf: latestBuild,
// The environment supports ECMAScript Module syntax to import ECMAScript modules (import ... from '...').
module: latestBuild,
},
chunkFilename:
isProdBuild && !isStatsBuild
? "chunk.[chunkhash].js"
+2 -2
View File
@@ -30,7 +30,7 @@ class HcLayout extends LitElement {
<ha-card>
<div class="layout">
<img class="hero" src="/images/google-nest-hub.png" />
<div class="card-header">
<h1 class="card-header">
Home Assistant Cast${this.subtitle ? ` ${this.subtitle}` : ""}
${this.auth
? html`
@@ -44,7 +44,7 @@ class HcLayout extends LitElement {
</div>
`
: ""}
</div>
</h1>
<slot></slot>
</div>
</ha-card>
+160 -182
View File
@@ -7,205 +7,183 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
cards: [
{ type: "custom:ha-demo-card" },
{
type: "grid",
columns: 4,
cards: [
{
cards: [
image: "/assets/teachingbirds/isa_square.jpg",
type: "picture-entity",
show_name: false,
tap_action: {
action: "more-info",
},
entity: "sensor.presence_isa",
},
{
image: "/assets/teachingbirds/Stefan_square.jpg",
type: "picture-entity",
show_name: false,
tap_action: {
action: "more-info",
},
entity: "sensor.presence_stefan",
},
{
image: "/assets/teachingbirds/background_square.png",
elements: [
{
image: "/assets/teachingbirds/isa_square.jpg",
type: "picture-entity",
show_name: false,
state_image: {
on: "/assets/teachingbirds/radiator_on.jpg",
off: "/assets/teachingbirds/radiator_off.jpg",
},
type: "image",
style: {
width: "100%",
top: "50%",
left: "50%",
},
tap_action: {
action: "more-info",
},
entity: "sensor.presence_isa",
entity: "switch.stefan_radiator_3",
},
{
image: "/assets/teachingbirds/Stefan_square.jpg",
type: "picture-entity",
show_name: false,
tap_action: {
action: "more-info",
style: {
top: "90%",
left: "50%",
},
entity: "sensor.presence_stefan",
},
{
image: "/assets/teachingbirds/background_square.png",
elements: [
{
state_image: {
on: "/assets/teachingbirds/radiator_on.jpg",
off: "/assets/teachingbirds/radiator_off.jpg",
},
type: "image",
style: {
width: "100%",
top: "50%",
left: "50%",
},
tap_action: {
action: "more-info",
},
entity: "switch.stefan_radiator_3",
},
{
style: {
top: "90%",
left: "50%",
},
type: "state-label",
entity: "sensor.temperature_stefan",
},
],
type: "picture-elements",
},
{
image: "/assets/teachingbirds/background_square.png",
elements: [
{
style: {
"--mdc-icon-size": "100%",
top: "50%",
left: "50%",
},
type: "icon",
tap_action: {
action: "navigate",
navigation_path: "/lovelace/home_info",
},
icon: "mdi:car",
},
],
type: "picture-elements",
},
],
type: "horizontal-stack",
},
{
cards: [
{
show_name: false,
type: "picture-entity",
name: "Alarm",
image: "/assets/teachingbirds/House_square.jpg",
entity: "alarm_control_panel.house",
},
{
name: "Roomba",
image: "/assets/teachingbirds/roomba_square.jpg",
show_name: false,
type: "picture-entity",
state_image: {
"Not Today": "/assets/teachingbirds/roomba_bw_square.jpg",
},
entity: "input_select.roomba_mode",
},
{
show_name: false,
type: "picture-entity",
state_image: {
Mail: "/assets/teachingbirds/mailbox_square.jpg",
"Package and mail":
"/assets/teachingbirds/mailbox_square.jpg",
Empty: "/assets/teachingbirds/mailbox_bw_square.jpg",
Package: "/assets/teachingbirds/mailbox_square.jpg",
},
entity: "sensor.mailbox",
},
{
show_name: false,
state_image: {
"Put out": "/assets/teachingbirds/trash_square.jpg",
"Take in": "/assets/teachingbirds/trash_square.jpg",
},
type: "picture-entity",
image: "/assets/teachingbirds/trash_bear_bw_square.jpg",
entity: "sensor.trash_status",
},
],
type: "horizontal-stack",
},
{
cards: [
{
state_image: {
Idle: "/assets/teachingbirds/washer_square.jpg",
Running: "/assets/teachingbirds/laundry_running_square.jpg",
Clean: "/assets/teachingbirds/laundry_clean_2_square.jpg",
},
entity: "input_select.washing_machine_status",
type: "picture-entity",
show_name: false,
name: "Washer",
},
{
state_image: {
Idle: "/assets/teachingbirds/dryer_square.jpg",
Running: "/assets/teachingbirds/clothes_drying_square.jpg",
Clean: "/assets/teachingbirds/folded_clothes_square.jpg",
},
entity: "input_select.dryer_status",
type: "picture-entity",
show_name: false,
name: "Dryer",
},
{
image: "/assets/teachingbirds/guests_square.jpg",
type: "picture-entity",
show_name: false,
tap_action: {
action: "toggle",
},
entity: "input_boolean.guest_mode",
},
{
image: "/assets/teachingbirds/cleaning_square.jpg",
type: "picture-entity",
show_name: false,
tap_action: {
action: "toggle",
},
entity: "input_boolean.cleaning_day",
},
],
type: "horizontal-stack",
},
],
type: "vertical-stack",
},
{
type: "vertical-stack",
cards: [
{
cards: [
{
graph: "line",
type: "sensor",
entity: "sensor.temperature_bedroom",
},
{
graph: "line",
type: "sensor",
name: "S's room",
type: "state-label",
entity: "sensor.temperature_stefan",
},
],
type: "horizontal-stack",
type: "picture-elements",
},
{
cards: [
image: "/assets/teachingbirds/background_square.png",
elements: [
{
graph: "line",
type: "sensor",
entity: "sensor.temperature_passage",
},
{
graph: "line",
type: "sensor",
name: "Laundry",
entity: "sensor.temperature_downstairs_bathroom",
style: {
"--mdc-icon-size": "100%",
top: "50%",
left: "50%",
},
type: "icon",
tap_action: {
action: "navigate",
navigation_path: "/lovelace/home_info",
},
icon: "mdi:car",
},
],
type: "horizontal-stack",
type: "picture-elements",
},
{
show_name: false,
type: "picture-entity",
name: "Alarm",
image: "/assets/teachingbirds/House_square.jpg",
entity: "alarm_control_panel.house",
},
{
name: "Roomba",
image: "/assets/teachingbirds/roomba_square.jpg",
show_name: false,
type: "picture-entity",
state_image: {
"Not Today": "/assets/teachingbirds/roomba_bw_square.jpg",
},
entity: "input_select.roomba_mode",
},
{
show_name: false,
type: "picture-entity",
state_image: {
Mail: "/assets/teachingbirds/mailbox_square.jpg",
"Package and mail": "/assets/teachingbirds/mailbox_square.jpg",
Empty: "/assets/teachingbirds/mailbox_bw_square.jpg",
Package: "/assets/teachingbirds/mailbox_square.jpg",
},
entity: "sensor.mailbox",
},
{
show_name: false,
state_image: {
"Put out": "/assets/teachingbirds/trash_square.jpg",
"Take in": "/assets/teachingbirds/trash_square.jpg",
},
type: "picture-entity",
image: "/assets/teachingbirds/trash_bear_bw_square.jpg",
entity: "sensor.trash_status",
},
{
state_image: {
Idle: "/assets/teachingbirds/washer_square.jpg",
Running: "/assets/teachingbirds/laundry_running_square.jpg",
Clean: "/assets/teachingbirds/laundry_clean_2_square.jpg",
},
entity: "input_select.washing_machine_status",
type: "picture-entity",
show_name: false,
name: "Washer",
},
{
state_image: {
Idle: "/assets/teachingbirds/dryer_square.jpg",
Running: "/assets/teachingbirds/clothes_drying_square.jpg",
Clean: "/assets/teachingbirds/folded_clothes_square.jpg",
},
entity: "input_select.dryer_status",
type: "picture-entity",
show_name: false,
name: "Dryer",
},
{
image: "/assets/teachingbirds/guests_square.jpg",
type: "picture-entity",
show_name: false,
tap_action: {
action: "toggle",
},
entity: "input_boolean.guest_mode",
},
{
image: "/assets/teachingbirds/cleaning_square.jpg",
type: "picture-entity",
show_name: false,
tap_action: {
action: "toggle",
},
entity: "input_boolean.cleaning_day",
},
],
},
{
type: "grid",
columns: 2,
cards: [
{
graph: "line",
type: "sensor",
entity: "sensor.temperature_bedroom",
},
{
graph: "line",
type: "sensor",
name: "S's room",
entity: "sensor.temperature_stefan",
},
{
graph: "line",
type: "sensor",
entity: "sensor.temperature_passage",
},
{
graph: "line",
type: "sensor",
name: "Laundry",
entity: "sensor.temperature_downstairs_bathroom",
},
],
},
+7
View File
@@ -6,4 +6,11 @@ export const mockTemplate = (hass: MockHomeAssistant) => {
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 () => {};
});
};
+27 -8
View File
@@ -5,11 +5,16 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/ha-switch";
import "../../../src/components/ha-formfield";
import "./demo-card";
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
class DemoCards extends PolymerElement {
static get template() {
return html`
<style>
#container {
min-height: calc(100vh - 128px);
background: var(--primary-background-color);
}
.cards {
display: flex;
flex-wrap: wrap;
@@ -24,6 +29,9 @@ class DemoCards extends PolymerElement {
.filters {
margin-left: 60px;
}
ha-formfield {
margin-right: 16px;
}
</style>
<app-toolbar>
<div class="filters">
@@ -31,16 +39,21 @@ class DemoCards extends PolymerElement {
<ha-switch checked="[[_showConfig]]" on-change="_showConfigToggled">
</ha-switch>
</ha-formfield>
<ha-formfield label="Dark theme">
<ha-switch on-change="_darkThemeToggled"> </ha-switch>
</ha-formfield>
</div>
</app-toolbar>
<div class="cards">
<template is="dom-repeat" items="[[configs]]">
<demo-card
config="[[item]]"
show-config="[[_showConfig]]"
hass="[[hass]]"
></demo-card>
</template>
<div id="container">
<div class="cards">
<template is="dom-repeat" items="[[configs]]">
<demo-card
config="[[item]]"
show-config="[[_showConfig]]"
hass="[[hass]]"
></demo-card>
</template>
</div>
</div>
`;
}
@@ -59,6 +72,12 @@ class DemoCards extends PolymerElement {
_showConfigToggled(ev) {
this._showConfig = ev.target.checked;
}
_darkThemeToggled(ev) {
applyThemesOnElement(this.$.container, { themes: {} }, "default", {
dark: ev.target.checked,
});
}
}
customElements.define("demo-cards", DemoCards);
+3 -6
View File
@@ -3,7 +3,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/ha-card";
import "../../../src/state-summary/state-card-content";
import "./more-info-content";
import "../../../src/dialogs/more-info/more-info-content";
class DemoMoreInfo extends PolymerElement {
static get template() {
@@ -16,15 +16,12 @@ class DemoMoreInfo extends PolymerElement {
ha-card {
width: 333px;
padding: 20px 24px;
}
state-card-content {
display: block;
padding: 16px;
}
more-info-content {
padding: 0 16px;
margin-bottom: 16px;
}
pre {
@@ -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);
+17 -2
View File
@@ -15,6 +15,10 @@ const ENTITIES = [
getEntity("alarm_control_panel", "unavailable", "unavailable", {
friendly_name: "Alarm",
}),
getEntity("alarm_control_panel", "alarm_code", "disarmed", {
friendly_name: "Alarm",
code_format: "number",
}),
];
const CONFIGS = [
@@ -30,7 +34,14 @@ const CONFIGS = [
config: `
- type: alarm-panel
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() {
super.ready();
this._setupDemo();
}
private async _setupDemo() {
const hass = provideHass(this.$.demos);
hass.updateTranslations(null, "en");
await hass.updateTranslations(null, "en");
hass.addEntities(ENTITIES);
}
}
@@ -98,4 +98,4 @@ class DemoButtonEntity extends PolymerElement {
}
}
customElements.define("demo-hui-button-card", DemoButtonEntity);
customElements.define("demo-hui-entity-button-card", DemoButtonEntity);
+8
View File
@@ -8,6 +8,7 @@ import "../components/demo-cards";
const ENTITIES = [
getEntity("sensor", "brightness", "12", {}),
getEntity("plant", "bonsai", "ok", {}),
getEntity("sensor", "not_working", "unavailable", {}),
getEntity("sensor", "outside_humidity", "54", {
unit_of_measurement: "%",
}),
@@ -74,6 +75,13 @@ const CONFIGS = [
entity: plant.bonsai
`,
},
{
heading: "Unavailable entity",
config: `
- type: gauge
entity: sensor.not_working
`,
},
];
class DemoGaugeEntity extends PolymerElement {
+9 -1
View File
@@ -1,6 +1,8 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
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";
const CONFIGS = [
@@ -254,7 +256,7 @@ const CONFIGS = [
class DemoMarkdown extends PolymerElement {
static get template() {
return html` <demo-cards configs="[[_configs]]"></demo-cards> `;
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
}
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);
+42
View File
@@ -1,6 +1,7 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { mockHistory } from "../../../demo/src/stubs/history";
import { getEntity } from "../../../src/fake_data/entity";
import { provideHass } from "../../../src/fake_data/provide_hass";
import "../components/demo-cards";
@@ -36,6 +37,10 @@ const ENTITIES = [
battery: 71,
friendly_name: "Home Boy",
}),
getEntity("sensor", "illumination", "23", {
friendly_name: "Illumination",
unit_of_measurement: "lx",
}),
];
const CONFIGS = [
@@ -89,6 +94,42 @@ const CONFIGS = [
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 {
@@ -110,6 +151,7 @@ class DemoStack extends PolymerElement {
const hass = provideHass(this.$.demos);
hass.updateTranslations(null, "en");
hass.addEntities(ENTITIES);
mockHistory(hass);
}
}
+6 -2
View File
@@ -6,7 +6,7 @@ import { SUPPORT_BRIGHTNESS } from "../../../src/data/light";
import { getEntity } from "../../../src/fake_data/entity";
import { provideHass } from "../../../src/fake_data/provide_hass";
import "../components/demo-more-infos";
import "../components/more-info-content";
import "../../../src/dialogs/more-info/more-info-content";
const ENTITIES = [
getEntity("light", "bed_light", "on", {
@@ -40,8 +40,12 @@ class DemoMoreInfoLight extends PolymerElement {
public ready() {
super.ready();
this._setupDemo();
}
private async _setupDemo() {
const hass = provideHass(this);
hass.updateTranslations(null, "en");
await hass.updateTranslations(null, "en");
hass.addEntities(ENTITIES);
}
}
@@ -23,9 +23,9 @@ import { hassioStyle } from "../resources/hassio-style";
class HassioAddonRepositoryEl extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public repo!: HassioAddonRepository;
@property({ attribute: false }) public repo!: HassioAddonRepository;
@property() public addons!: HassioAddonInfo[];
@property({ attribute: false }) public addons!: HassioAddonInfo[];
@property() public filter!: string;
@@ -78,18 +78,18 @@ class HassioAddonRepositoryEl extends LitElement {
.title=${addon.name}
.description=${addon.description}
.available=${addon.available}
.icon=${addon.installed && addon.installed !== addon.version
.icon=${addon.installed && addon.update_available
? mdiArrowUpBoldCircle
: mdiPuzzle}
.iconTitle=${addon.installed
? addon.installed !== addon.version
? addon.update_available
? "New version available"
: "Add-on is installed"
: addon.available
? "Add-on is not installed"
: "Add-on is not available on your system"}
.iconClass=${addon.installed
? addon.installed !== addon.version
? addon.update_available
? "update"
: "installed"
: !addon.available
@@ -104,7 +104,7 @@ class HassioAddonRepositoryEl extends LitElement {
: undefined}
.showTopbar=${addon.installed || !addon.available}
.topbarClass=${addon.installed
? addon.installed !== addon.version
? addon.update_available
? "update"
: "installed"
: !addon.available
@@ -11,6 +11,7 @@ import {
PropertyValues,
} from "lit-element";
import { html, TemplateResult } from "lit-html";
import { atLeastVersion } from "../../../src/common/config/version";
import "../../../src/common/search/search-input";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-svg-icon";
@@ -24,6 +25,7 @@ import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-tabs-subpage";
import { HomeAssistant, Route } from "../../../src/types";
import { showRegistriesDialog } from "../dialogs/registries/show-dialog-registries";
import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories";
import { supervisorTabs } from "../hassio-tabs";
import "./hassio-addon-repository";
@@ -113,6 +115,12 @@ class HassioAddonStore extends LitElement {
<mwc-list-item>
Reload
</mwc-list-item>
${this.hass.userData?.showAdvanced &&
atLeastVersion(this.hass.config.version, 0, 117)
? html`<mwc-list-item>
Registries
</mwc-list-item>`
: ""}
</ha-button-menu>
${repos.length === 0
? html`<hass-loading-screen no-toolbar></hass-loading-screen>`
@@ -157,6 +165,9 @@ class HassioAddonStore extends LitElement {
case 1:
this.refreshData();
break;
case 2:
this._manageRegistries();
break;
}
}
@@ -173,6 +184,10 @@ class HassioAddonStore extends LitElement {
});
}
private async _manageRegistries() {
showRegistriesDialog(this);
}
private async _loadData() {
try {
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
@@ -39,13 +39,11 @@ class HassioAddonConfig extends LitElement {
@property({ type: Boolean }) private _configHasChanged = false;
@property({ type: Boolean }) private _valid = true;
@query("ha-yaml-editor", true) private _editor!: HaYamlEditor;
protected render(): TemplateResult {
const editor = this._editor;
// If editor not rendered, don't show the error.
const valid = editor ? editor.isValid : true;
return html`
<h1>${this.addon.name}</h1>
<ha-card header="Configuration">
@@ -54,7 +52,7 @@ class HassioAddonConfig extends LitElement {
@value-changed=${this._configChanged}
></ha-yaml-editor>
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
${valid ? "" : html` <div class="errors">Invalid YAML</div> `}
${this._valid ? "" : html` <div class="errors">Invalid YAML</div> `}
</div>
<div class="card-actions">
<ha-progress-button class="warning" @click=${this._resetTapped}>
@@ -62,7 +60,7 @@ class HassioAddonConfig extends LitElement {
</ha-progress-button>
<ha-progress-button
@click=${this._saveTapped}
.disabled=${!this._configHasChanged || !valid}
.disabled=${!this._configHasChanged || !this._valid}
>
Save
</ha-progress-button>
@@ -78,9 +76,9 @@ class HassioAddonConfig extends LitElement {
}
}
private _configChanged(): void {
private _configChanged(ev): void {
this._configHasChanged = true;
this.requestUpdate();
this._valid = ev.detail.isValid;
}
private async _resetTapped(ev: CustomEvent): Promise<void> {
@@ -135,7 +135,7 @@ class HassioAddonInfo extends LitElement {
protected render(): TemplateResult {
return html`
${this._computeUpdateAvailable
${this.addon.update_available
? html`
<ha-card header="Update available! 🎉">
<div class="card-content">
@@ -178,7 +178,7 @@ class HassioAddonInfo extends LitElement {
${!this.addon.protected
? html`
<ha-card class="warning">
<div class="card-header">Warning: Protection mode is disabled!</div>
<h1 class="card-header">Warning: Protection mode is disabled!</h1>
<div class="card-content">
Protection mode on this add-on is disabled! This gives the add-on full access to the entire system, which adds security risks, and could damage your system when used incorrectly. Only disable the protection mode if you know, need AND trust the source of this add-on.
</div>
@@ -609,15 +609,6 @@ class HassioAddonInfo extends LitElement {
return this.addon?.state === "started";
}
private get _computeUpdateAvailable(): boolean | "" {
return (
this.addon &&
!this.addon.detached &&
this.addon.version &&
this.addon.version !== this.addon.version_latest
);
}
private get _pathWebui(): string | null {
return (
this.addon.webui &&
+1 -1
View File
@@ -50,7 +50,7 @@ class HassioCardContent extends LitElement {
`
: html`
<ha-svg-icon
class=${this.iconClass}
class=${this.iconClass!}
.path=${this.icon}
.title=${this.iconTitle}
></ha-svg-icon>
@@ -1,4 +1,3 @@
import "../../../src/components/ha-file-upload";
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiFolderUpload } from "@mdi/js";
import "@polymer/iron-input/iron-input";
@@ -12,13 +11,15 @@ import {
} from "lit-element";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/ha-circular-progress";
import "../../../src/components/ha-file-upload";
import "../../../src/components/ha-svg-icon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import {
HassioSnapshot,
uploadSnapshot,
} from "../../../src/data/hassio/snapshot";
import { HomeAssistant } from "../../../src/types";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import { HomeAssistant } from "../../../src/types";
declare global {
interface HASSDomEvents {
@@ -65,7 +66,7 @@ export class HassioUploadSnapshot extends LitElement {
} catch (err) {
showAlertDialog(this, {
title: "Upload failed",
text: err.toString(),
text: extractApiErrorMessage(err),
confirmText: "ok",
});
} finally {
+5 -6
View File
@@ -52,22 +52,21 @@ class HassioAddons extends LitElement {
.title=${addon.name}
.description=${addon.description}
available
.showTopbar=${addon.installed !== addon.version}
.showTopbar=${addon.update_available}
topbarClass="update"
.icon=${addon.installed !== addon.version
.icon=${addon.update_available!
? mdiArrowUpBoldCircle
: mdiPuzzle}
.iconTitle=${addon.state !== "started"
? "Add-on is stopped"
: addon.installed !== addon.version
: addon.update_available!
? "New version available"
: "Add-on is running"}
.iconClass=${addon.installed &&
addon.installed !== addon.version
.iconClass=${addon.update_available
? addon.state === "started"
? "update"
: "update stopped"
: addon.installed && addon.state === "started"
: addon.state === "started"
? "running"
: "stopped"}
.iconImage=${atLeastVersion(
+34 -44
View File
@@ -5,11 +5,11 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card";
import "../../../src/components/ha-svg-icon";
@@ -35,29 +35,30 @@ import { hassioStyle } from "../resources/hassio-style";
export class HassioUpdate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public hassInfo: HassioHomeAssistantInfo;
@property({ attribute: false }) public hassInfo?: HassioHomeAssistantInfo;
@property({ attribute: false }) public hassOsInfo?: HassioHassOSInfo;
@property() public supervisorInfo: HassioSupervisorInfo;
@property({ attribute: false }) public supervisorInfo?: HassioSupervisorInfo;
@internalProperty() private _error?: string;
private _pendingUpdates = memoizeOne(
(
core?: HassioHomeAssistantInfo,
supervisor?: HassioSupervisorInfo,
os?: HassioHassOSInfo
): number => {
return [core, supervisor, os].filter(
(value) => !!value && value?.update_available
).length;
}
);
protected render(): TemplateResult {
const updatesAvailable: number = [
const updatesAvailable = this._pendingUpdates(
this.hassInfo,
this.supervisorInfo,
this.hassOsInfo,
].filter((value) => {
return (
!!value &&
(value.version_latest
? value.version !== value.version_latest
: value.version_latest
? value.version !== value.version_latest
: false)
);
}).length;
this.hassOsInfo
);
if (!updatesAvailable) {
return html``;
@@ -65,9 +66,6 @@ export class HassioUpdate extends LitElement {
return html`
<div class="content">
${this._error
? html` <div class="error">Error: ${this._error}</div> `
: ""}
<h1>
${updatesAvailable > 1
? "Updates Available 🎉"
@@ -76,26 +74,24 @@ export class HassioUpdate extends LitElement {
<div class="card-group">
${this._renderUpdateCard(
"Home Assistant Core",
this.hassInfo.version,
this.hassInfo.version_latest,
this.hassInfo!,
"hassio/homeassistant/update",
`https://${
this.hassInfo.version_latest.includes("b") ? "rc" : "www"
}.home-assistant.io/latest-release-notes/`,
mdiHomeAssistant
this.hassInfo?.version_latest.includes("b") ? "rc" : "www"
}.home-assistant.io/latest-release-notes/`
)}
${this._renderUpdateCard(
"Supervisor",
this.supervisorInfo.version,
this.supervisorInfo.version_latest,
this.supervisorInfo!,
"hassio/supervisor/update",
`https://github.com//home-assistant/hassio/releases/tag/${this.supervisorInfo.version_latest}`
`https://github.com//home-assistant/hassio/releases/tag/${
this.supervisorInfo!.version_latest
}`
)}
${this.hassOsInfo
? this._renderUpdateCard(
"Operating System",
this.hassOsInfo.version,
this.hassOsInfo.version_latest,
this.hassOsInfo,
"hassio/os/update",
`https://github.com//home-assistant/hassos/releases/tag/${this.hassOsInfo.version_latest}`
)
@@ -107,28 +103,22 @@ export class HassioUpdate extends LitElement {
private _renderUpdateCard(
name: string,
curVersion: string,
lastVersion: string,
object: HassioHomeAssistantInfo | HassioSupervisorInfo | HassioHassOSInfo,
apiPath: string,
releaseNotesUrl: string,
icon?: string
releaseNotesUrl: string
): TemplateResult {
if (!lastVersion || lastVersion === curVersion) {
if (!object.update_available) {
return html``;
}
return html`
<ha-card>
<div class="card-content">
${icon
? html`
<div class="icon">
<ha-svg-icon .path=${icon}></ha-svg-icon>
</div>
`
: ""}
<div class="update-heading">${name} ${lastVersion}</div>
<div class="icon">
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
</div>
<div class="update-heading">${name} ${object.version_latest}</div>
<div class="warning">
You are currently running version ${curVersion}
You are currently running version ${object.version}
</div>
</div>
<div class="card-actions">
@@ -138,7 +128,7 @@ export class HassioUpdate extends LitElement {
<ha-progress-button
.apiPath=${apiPath}
.name=${name}
.version=${lastVersion}
.version=${object.version_latest}
@click=${this._confirmUpdate}
>
Update
@@ -0,0 +1,245 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-icon-button/mwc-icon-button";
import "@material/mwc-list/mwc-list-item";
import { mdiDelete } from "@mdi/js";
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
} from "lit-element";
import "../../../../src/components/ha-circular-progress";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-svg-icon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import {
addHassioDockerRegistry,
fetchHassioDockerRegistries,
removeHassioDockerRegistry,
} from "../../../../src/data/hassio/docker";
import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
@customElement("dialog-hassio-registries")
class HassioRegistriesDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) private _registries?: {
registry: string;
username: string;
}[];
@internalProperty() private _registry?: string;
@internalProperty() private _username?: string;
@internalProperty() private _password?: string;
@internalProperty() private _opened = false;
@internalProperty() private _addingRegistry = false;
protected render(): TemplateResult {
return html`
<ha-dialog
.open=${this._opened}
@closing=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this._addingRegistry
? "Add New Docker Registry"
: "Manage Docker Registries"
)}
>
<div class="form">
${this._addingRegistry
? html`
<paper-input
@value-changed=${this._inputChanged}
class="flex-auto"
name="registry"
label="Registry"
required
auto-validate
></paper-input>
<paper-input
@value-changed=${this._inputChanged}
class="flex-auto"
name="username"
label="Username"
required
auto-validate
></paper-input>
<paper-input
@value-changed=${this._inputChanged}
class="flex-auto"
name="password"
label="Password"
type="password"
required
auto-validate
></paper-input>
<mwc-button
?disabled=${Boolean(
!this._registry || !this._username || !this._password
)}
@click=${this._addNewRegistry}
>
Add registry
</mwc-button>
`
: html`${this._registries?.length
? this._registries.map((entry) => {
return html`
<mwc-list-item class="option" hasMeta twoline>
<span>${entry.registry}</span>
<span slot="secondary"
>Username: ${entry.username}</span
>
<mwc-icon-button
.entry=${entry}
title="Remove"
slot="meta"
@click=${this._removeRegistry}
>
<ha-svg-icon .path=${mdiDelete}></ha-svg-icon>
</mwc-icon-button>
</mwc-list-item>
`;
})
: html`
<mwc-list-item>
<span>No registries configured</span>
</mwc-list-item>
`}
<mwc-button @click=${this._addRegistry}>
Add new registry
</mwc-button> `}
</div>
</ha-dialog>
`;
}
private _inputChanged(ev: Event) {
const target = ev.currentTarget as PaperInputElement;
this[`_${target.name}`] = target.value;
}
public async showDialog(_dialogParams: any): Promise<void> {
this._opened = true;
await this._loadRegistries();
await this.updateComplete;
}
public closeDialog(): void {
this._addingRegistry = false;
this._opened = false;
}
public focus(): void {
this.updateComplete.then(() =>
(this.shadowRoot?.querySelector(
"[dialogInitialFocus]"
) as HTMLElement)?.focus()
);
}
private async _loadRegistries(): Promise<void> {
const registries = await fetchHassioDockerRegistries(this.hass);
this._registries = Object.keys(registries!.registries).map((key) => ({
registry: key,
username: registries.registries[key].username,
}));
}
private _addRegistry(): void {
this._addingRegistry = true;
}
private async _addNewRegistry(): Promise<void> {
const data = {};
data[this._registry!] = {
username: this._username,
password: this._password,
};
try {
await addHassioDockerRegistry(this.hass, data);
await this._loadRegistries();
this._addingRegistry = false;
} catch (err) {
showAlertDialog(this, {
title: "Failed to add registry",
text: extractApiErrorMessage(err),
});
}
}
private async _removeRegistry(ev: Event): Promise<void> {
const entry = (ev.currentTarget as any).entry;
try {
await removeHassioDockerRegistry(this.hass, entry.registry);
await this._loadRegistries();
} catch (err) {
showAlertDialog(this, {
title: "Failed to remove registry",
text: extractApiErrorMessage(err),
});
}
}
static get styles(): CSSResult[] {
return [
haStyle,
haStyleDialog,
css`
ha-dialog.button-left {
--justify-action-buttons: flex-start;
}
paper-icon-item {
cursor: pointer;
}
.form {
color: var(--primary-text-color);
}
.option {
border: 1px solid var(--divider-color);
border-radius: 4px;
margin-top: 4px;
}
mwc-button {
margin-left: 8px;
}
mwc-icon-button {
color: var(--error-color);
margin: -10px;
}
mwc-list-item {
cursor: default;
}
mwc-list-item span[slot="secondary"] {
color: var(--secondary-text-color);
}
ha-paper-dropdown-menu {
display: block;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-registries": HassioRegistriesDialog;
}
}
@@ -0,0 +1,13 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "./dialog-hassio-registries";
export const showRegistriesDialog = (element: HTMLElement): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-registries",
dialogImport: () =>
import(
/* webpackChunkName: "dialog-hassio-registries" */ "./dialog-hassio-registries"
),
dialogParams: {},
});
};
@@ -1,6 +1,7 @@
import "@material/mwc-button";
import { mdiClose, mdiDelete, mdiDownload, mdiHistory } from "@mdi/js";
import { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-checkbox/paper-checkbox";
import type { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-input/paper-input";
import {
css,
+3 -3
View File
@@ -25,13 +25,13 @@ class HassioPanelRouter extends HassRouterPage {
@property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public supervisorInfo: HassioSupervisorInfo;
@property({ attribute: false }) public supervisorInfo?: HassioSupervisorInfo;
@property({ attribute: false }) public hassioInfo!: HassioInfo;
@property({ attribute: false }) public hostInfo: HassioHostInfo;
@property({ attribute: false }) public hostInfo?: HassioHostInfo;
@property({ attribute: false }) public hassInfo: HassioHomeAssistantInfo;
@property({ attribute: false }) public hassInfo?: HassioHomeAssistantInfo;
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
+3 -3
View File
@@ -66,15 +66,15 @@ class HassioRouter extends HassRouterPage {
},
};
@internalProperty() private _supervisorInfo: HassioSupervisorInfo;
@internalProperty() private _supervisorInfo?: HassioSupervisorInfo;
@internalProperty() private _hostInfo: HassioHostInfo;
@internalProperty() private _hostInfo?: HassioHostInfo;
@internalProperty() private _hassioInfo?: HassioInfo;
@internalProperty() private _hassOsInfo?: HassioHassOSInfo;
@internalProperty() private _hassInfo: HassioHomeAssistantInfo;
@internalProperty() private _hassInfo?: HassioHomeAssistantInfo;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
+32 -6
View File
@@ -13,7 +13,10 @@ import {
fetchHassioAddonInfo,
HassioAddonDetails,
} 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-subpage";
import { HomeAssistant, Route } from "../../../src/types";
@@ -35,6 +38,17 @@ class HassioIngressView extends LitElement {
@property({ type: Boolean })
public narrow = false;
private _sessionKeepAlive?: number;
public disconnectedCallback() {
super.disconnectedCallback();
if (this._sessionKeepAlive) {
clearInterval(this._sessionKeepAlive);
this._sessionKeepAlive = undefined;
}
}
protected render(): TemplateResult {
if (!this._addon) {
return html` <hass-loading-screen></hass-loading-screen> `;
@@ -83,10 +97,7 @@ class HassioIngressView extends LitElement {
}
private async _fetchData(addonSlug: string) {
const createSessionPromise = createHassioSession(this.hass).then(
() => true,
() => false
);
const createSessionPromise = createHassioSession(this.hass);
let addon;
@@ -119,7 +130,11 @@ class HassioIngressView extends LitElement {
return;
}
if (!(await createSessionPromise)) {
let session;
try {
session = await createSessionPromise;
} catch (err) {
await showAlertDialog(this, {
text: "Unable to create an Ingress session",
title: addon.name,
@@ -128,6 +143,17 @@ class HassioIngressView extends LitElement {
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;
}
+2 -2
View File
@@ -108,8 +108,8 @@ class HassioHostInfo extends LitElement {
<span slot="description">
${this.hostInfo.operating_system}
</span>
${this.hostInfo.version !== this.hostInfo.version_latest &&
this.hostInfo.features.includes("hassos")
${this.hostInfo.features.includes("hassos") &&
this.hassOsInfo.update_available
? html`
<ha-progress-button
title="Update the host OS"
+67 -15
View File
@@ -7,18 +7,21 @@ import {
property,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-switch";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { HassioHostInfo as HassioHostInfoType } from "../../../src/data/hassio/host";
import { fetchHassioResolution } from "../../../src/data/hassio/resolution";
import {
fetchHassioSupervisorInfo,
HassioSupervisorInfo as HassioSupervisorInfoType,
reloadSupervisor,
setSupervisorOption,
SupervisorOptions,
updateSupervisor,
fetchHassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor";
import {
showAlertDialog,
@@ -26,14 +29,42 @@ import {
} from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { documentationUrl } from "../../../src/util/documentation-url";
import { hassioStyle } from "../resources/hassio-style";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
const ISSUES = {
container: {
title: "Containers known to cause issues",
url: "/more-info/unsupported/container",
},
dbus: { title: "DBUS", url: "/more-info/unsupported/dbus" },
docker_configuration: {
title: "Docker Configuration",
url: "/more-info/unsupported/docker_configuration",
},
docker_version: {
title: "Docker Version",
url: "/more-info/unsupported/docker_version",
},
lxc: { title: "LXC", url: "/more-info/unsupported/lxc" },
network_manager: {
title: "Network Manager",
url: "/more-info/unsupported/network_manager",
},
os: { title: "Operating System", url: "/more-info/unsupported/os" },
privileged: {
title: "Supervisor is not privileged",
url: "/more-info/unsupported/privileged",
},
systemd: { title: "Systemd", url: "/more-info/unsupported/systemd" },
};
@customElement("hassio-supervisor-info")
class HassioSupervisorInfo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public supervisorInfo!: HassioSupervisorInfoType;
@property({ attribute: false })
public supervisorInfo!: HassioSupervisorInfoType;
@property() public hostInfo!: HassioHostInfoType;
@@ -56,7 +87,7 @@ class HassioSupervisorInfo extends LitElement {
<span slot="description">
${this.supervisorInfo.version_latest}
</span>
${this.supervisorInfo.version !== this.supervisorInfo.version_latest
${this.supervisorInfo.update_available
? html`
<ha-progress-button
title="Update the supervisor"
@@ -118,18 +149,13 @@ class HassioSupervisorInfo extends LitElement {
</ha-settings-row>`
: html`<div class="error">
You are running an unsupported installation.
<a
href="https://github.com/home-assistant/architecture/blob/master/adr/${this.hostInfo.features.includes(
"hassos"
)
? "0015-home-assistant-os.md"
: "0014-home-assistant-supervised.md"}"
target="_blank"
rel="noreferrer"
<button
class="link"
title="Learn more about how you can make your system compliant"
@click=${this._unsupportedDialog}
>
Learn More
</a>
Learn more
</button>
</div>`}
</div>
<div class="card-actions">
@@ -181,7 +207,7 @@ class HassioSupervisorInfo extends LitElement {
};
await setSupervisorOption(this.hass, data);
await reloadSupervisor(this.hass);
this.supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
fireEvent(this, "hass-api-called", { success: true, response: null });
} catch (err) {
showAlertDialog(this, {
title: "Failed to set supervisor option",
@@ -249,6 +275,32 @@ class HassioSupervisorInfo extends LitElement {
});
}
private async _unsupportedDialog(): Promise<void> {
const resolution = await fetchHassioResolution(this.hass);
await showAlertDialog(this, {
title: "You are running an unsupported installation",
text: html`Below is a list of issues found with your installation, click
on the links to learn how you can resolve the issues. <br /><br />
<ul>
${resolution.unsupported.map(
(issue) => html`
<li>
${ISSUES[issue]
? html`<a
href="${documentationUrl(this.hass, ISSUES[issue].url)}"
target="_blank"
rel="noreferrer"
>
${ISSUES[issue].title}
</a>`
: issue}
</li>
`
)}
</ul>`,
});
}
private async _toggleDiagnostics(): Promise<void> {
try {
const data: SupervisorOptions = {
+2 -4
View File
@@ -21,11 +21,11 @@ import { fetchHassioStats, HassioStats } from "../../../src/data/hassio/common";
import { HassioHostInfo } from "../../../src/data/hassio/host";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { bytesToString } from "../../../src/util/bytes-to-string";
import {
getValueInPercentage,
roundWithOneDecimal,
} from "../../../src/util/calculate";
import { bytesToString } from "../../../src/util/bytes-to-string";
import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-system-metrics")
@@ -65,9 +65,7 @@ class HassioSystemMetrics extends LitElement {
{
description: "Used Space",
value: this._getUsedSpace(this.hostInfo),
tooltip: `${
this.hostInfo.disk_used
} GB/${this.hostInfo.disk_total} GB`,
tooltip: `${this.hostInfo.disk_used} GB/${this.hostInfo.disk_total} GB`,
},
];
+9 -8
View File
@@ -22,7 +22,8 @@
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
"license": "Apache-2.0",
"dependencies": {
"@formatjs/intl-pluralrules": "^1.5.8",
"@formatjs/intl-getcanonicallocales": "^1.4.6",
"@formatjs/intl-pluralrules": "^3.4.10",
"@fullcalendar/common": "5.1.0",
"@fullcalendar/core": "5.1.0",
"@fullcalendar/daygrid": "5.1.0",
@@ -77,7 +78,7 @@
"@polymer/paper-toast": "^3.0.1",
"@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "3.1.0",
"@thomasloven/round-slider": "0.5.0",
"@thomasloven/round-slider": "0.5.2",
"@types/chromecast-caf-sender": "^1.0.3",
"@types/sortablejs": "^1.10.6",
"@vaadin/vaadin-combo-box": "^5.0.10",
@@ -109,7 +110,7 @@
"marked": "^1.1.1",
"mdn-polyfills": "^5.16.0",
"memoize-one": "^5.0.2",
"node-vibrant": "^3.1.5",
"node-vibrant": "^3.1.6",
"proxy-polyfill": "^0.3.1",
"punycode": "^2.1.1",
"qrcode": "^1.4.4",
@@ -118,6 +119,7 @@
"roboto-fontface": "^0.10.0",
"sortablejs": "^1.10.2",
"superstruct": "^0.10.12",
"tinykeys": "^1.1.1",
"unfetch": "^4.1.0",
"vue": "^2.6.11",
"vue2-daterange-picker": "^0.5.1",
@@ -175,7 +177,6 @@
"gulp": "^4.0.0",
"gulp-foreach": "^0.1.0",
"gulp-json-transform": "^0.4.6",
"gulp-jsonminify": "^1.1.0",
"gulp-merge-json": "^1.3.1",
"gulp-rename": "^2.0.0",
"gulp-zopfli-green": "^3.0.1",
@@ -202,15 +203,15 @@
"sinon": "^7.3.1",
"source-map-url": "^0.4.0",
"systemjs": "^6.3.2",
"terser-webpack-plugin": "^3.0.6",
"terser-webpack-plugin": "^5.0.0",
"ts-lit-plugin": "^1.2.1",
"ts-mocha": "^7.0.0",
"typescript": "^4.0.3",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0",
"webpack": "5.0.0-rc.3",
"webpack-cli": "4.0.0-rc.0",
"webpack-dev-server": "^3.10.3",
"webpack": "5.1.3",
"webpack-cli": "4.1.0",
"webpack-dev-server": "^3.11.0",
"webpack-manifest-plugin": "3.0.0-rc.0",
"workbox-build": "^5.1.3"
},
-1
View File
@@ -13,7 +13,6 @@
"src/panels/iframe/ha-panel-iframe.js",
"src/panels/logbook/ha-panel-logbook.js",
"src/panels/map/ha-panel-map.js",
"src/panels/shopping-list/ha-panel-shopping-list.js",
"src/panels/mailbox/ha-panel-mailbox.js",
"hassio/src/entrypoint.js"
],
+1 -1
View File
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20201001.0",
version="20201021.1",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors",
+2 -11
View File
@@ -1,10 +1,4 @@
const expand_hex = (hex: string): string => {
let result = "";
for (const val of hex) {
result += val + val;
}
return result;
};
import { expandHex } from "./hex";
const rgb_hex = (component: number): string => {
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
export const hex2rgb = (hex: string): [number, number, number] => {
hex = hex.replace("#", "");
if (hex.length === 3 || hex.length === 4) {
hex = expand_hex(hex);
}
hex = expandHex(hex);
return [
parseInt(hex.substring(0, 2), 16),
+24
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}`;
};
@@ -7,6 +7,7 @@ import {
rgb2hex,
rgb2lab,
} from "../color/convert-color";
import { hexBlend } from "../color/hex";
import { labBrighten, labDarken } from "../color/lab";
import { rgbContrast } from "../color/rgb";
@@ -37,6 +38,13 @@ export const applyThemesOnElement = (
if (themeOptions.dark) {
cacheKey = `${cacheKey}__dark`;
themeRules = darkStyles;
if (themeOptions.primaryColor) {
themeRules["app-header-background-color"] = hexBlend(
themeOptions.primaryColor,
"#121212",
8
);
}
}
if (themeOptions.primaryColor) {
cacheKey = `${cacheKey}__primary_${themeOptions.primaryColor}`;
+1 -4
View File
@@ -10,10 +10,7 @@ export const dynamicElement = directive(
let element = part.value as HTMLElement | undefined;
if (
element !== undefined &&
tag.toUpperCase() === (element as HTMLElement).tagName
) {
if (tag === element?.localName) {
if (properties) {
Object.entries(properties).forEach(([key, value]) => {
element![key] = value;
+1 -1
View File
@@ -23,7 +23,7 @@ export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => {
case "problem":
case "safety":
case "smoke":
return is_off ? "hass:shield-check" : "hass:alert";
return is_off ? "hass:check-circle" : "hass:alert-circle";
case "heat":
return is_off ? "hass:thermometer" : "hass:fire";
case "light":
+4 -1
View File
@@ -5,6 +5,7 @@ import { formatDateTime } from "../datetime/format_date_time";
import { formatTime } from "../datetime/format_time";
import { LocalizeFunc } from "../translations/localize";
import { computeStateDomain } from "./compute_state_domain";
import { numberFormat } from "../string/number-format";
export const computeStateDisplay = (
localize: LocalizeFunc,
@@ -19,7 +20,9 @@ export const computeStateDisplay = (
}
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);
+1
View File
@@ -43,6 +43,7 @@ export const coverIcon = (state?: string, stateObj?: HassEntity): string => {
}
case "blind":
case "curtain":
case "shade":
switch (state) {
case "opening":
return "hass:arrow-up-box";
+3 -7
View File
@@ -51,18 +51,14 @@ class SearchInput extends LitElement {
@value-changed=${this._filterInputChanged}
.noLabelFloat=${this.noLabelFloat}
>
<ha-svg-icon
.path=${mdiMagnify}
slot="prefix"
class="prefix"
></ha-svg-icon>
<slot name="prefix" slot="prefix">
<ha-svg-icon class="prefix" .path=${mdiMagnify}></ha-svg-icon>
</slot>
${this.filter &&
html`
<mwc-icon-button
slot="suffix"
class="suffix"
@click=${this._clearSearch}
alt="Clear"
title="Clear"
>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
+244
View File
@@ -0,0 +1,244 @@
// MIT License
// Copyright (c) 2015 - present Microsoft Corporation
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/
/**
* An inlined enum containing useful character codes (to be used with String.charCodeAt).
* Please leave the const keyword such that it gets inlined when compiled to JavaScript!
*/
export enum CharCode {
Null = 0,
/**
* The `\b` character.
*/
Backspace = 8,
/**
* The `\t` character.
*/
Tab = 9,
/**
* The `\n` character.
*/
LineFeed = 10,
/**
* The `\r` character.
*/
CarriageReturn = 13,
Space = 32,
/**
* The `!` character.
*/
ExclamationMark = 33,
/**
* The `"` character.
*/
DoubleQuote = 34,
/**
* The `#` character.
*/
Hash = 35,
/**
* The `$` character.
*/
DollarSign = 36,
/**
* The `%` character.
*/
PercentSign = 37,
/**
* The `&` character.
*/
Ampersand = 38,
/**
* The `'` character.
*/
SingleQuote = 39,
/**
* The `(` character.
*/
OpenParen = 40,
/**
* The `)` character.
*/
CloseParen = 41,
/**
* The `*` character.
*/
Asterisk = 42,
/**
* The `+` character.
*/
Plus = 43,
/**
* The `,` character.
*/
Comma = 44,
/**
* The `-` character.
*/
Dash = 45,
/**
* The `.` character.
*/
Period = 46,
/**
* The `/` character.
*/
Slash = 47,
Digit0 = 48,
Digit1 = 49,
Digit2 = 50,
Digit3 = 51,
Digit4 = 52,
Digit5 = 53,
Digit6 = 54,
Digit7 = 55,
Digit8 = 56,
Digit9 = 57,
/**
* The `:` character.
*/
Colon = 58,
/**
* The `;` character.
*/
Semicolon = 59,
/**
* The `<` character.
*/
LessThan = 60,
/**
* The `=` character.
*/
Equals = 61,
/**
* The `>` character.
*/
GreaterThan = 62,
/**
* The `?` character.
*/
QuestionMark = 63,
/**
* The `@` character.
*/
AtSign = 64,
A = 65,
B = 66,
C = 67,
D = 68,
E = 69,
F = 70,
G = 71,
H = 72,
I = 73,
J = 74,
K = 75,
L = 76,
M = 77,
N = 78,
O = 79,
P = 80,
Q = 81,
R = 82,
S = 83,
T = 84,
U = 85,
V = 86,
W = 87,
X = 88,
Y = 89,
Z = 90,
/**
* The `[` character.
*/
OpenSquareBracket = 91,
/**
* The `\` character.
*/
Backslash = 92,
/**
* The `]` character.
*/
CloseSquareBracket = 93,
/**
* The `^` character.
*/
Caret = 94,
/**
* The `_` character.
*/
Underline = 95,
/**
* The ``(`)`` character.
*/
BackTick = 96,
a = 97,
b = 98,
c = 99,
d = 100,
e = 101,
f = 102,
g = 103,
h = 104,
i = 105,
j = 106,
k = 107,
l = 108,
m = 109,
n = 110,
o = 111,
p = 112,
q = 113,
r = 114,
s = 115,
t = 116,
u = 117,
v = 118,
w = 119,
x = 120,
y = 121,
z = 122,
/**
* The `{` character.
*/
OpenCurlyBrace = 123,
/**
* The `|` character.
*/
Pipe = 124,
/**
* The `}` character.
*/
CloseCurlyBrace = 125,
/**
* The `~` character.
*/
Tilde = 126,
}
+463
View File
@@ -0,0 +1,463 @@
/* eslint-disable no-console */
// MIT License
// Copyright (c) 2015 - present Microsoft Corporation
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { CharCode } from "./char-code";
const _debug = false;
export interface Match {
start: number;
end: number;
}
const _maxLen = 128;
function initTable() {
const table: number[][] = [];
const row: number[] = [0];
for (let i = 1; i <= _maxLen; i++) {
row.push(-i);
}
for (let i = 0; i <= _maxLen; i++) {
const thisRow = row.slice(0);
thisRow[0] = -i;
table.push(thisRow);
}
return table;
}
function isSeparatorAtPos(value: string, index: number): boolean {
if (index < 0 || index >= value.length) {
return false;
}
const code = value.charCodeAt(index);
switch (code) {
case CharCode.Underline:
case CharCode.Dash:
case CharCode.Period:
case CharCode.Space:
case CharCode.Slash:
case CharCode.Backslash:
case CharCode.SingleQuote:
case CharCode.DoubleQuote:
case CharCode.Colon:
case CharCode.DollarSign:
return true;
default:
return false;
}
}
function isWhitespaceAtPos(value: string, index: number): boolean {
if (index < 0 || index >= value.length) {
return false;
}
const code = value.charCodeAt(index);
switch (code) {
case CharCode.Space:
case CharCode.Tab:
return true;
default:
return false;
}
}
function isUpperCaseAtPos(pos: number, word: string, wordLow: string): boolean {
return word[pos] !== wordLow[pos];
}
function isPatternInWord(
patternLow: string,
patternPos: number,
patternLen: number,
wordLow: string,
wordPos: number,
wordLen: number
): boolean {
while (patternPos < patternLen && wordPos < wordLen) {
if (patternLow[patternPos] === wordLow[wordPos]) {
patternPos += 1;
}
wordPos += 1;
}
return patternPos === patternLen; // pattern must be exhausted
}
enum Arrow {
Top = 0b1,
Diag = 0b10,
Left = 0b100,
}
/**
* A tuple of three values.
* 0. the score
* 1. the matches encoded as bitmask (2^53)
* 2. the offset at which matching started
*/
export type FuzzyScore = [number, number, number];
interface FilterGlobals {
_matchesCount: number;
_topMatch2: number;
_topScore: number;
_wordStart: number;
_firstMatchCanBeWeak: boolean;
_table: number[][];
_scores: number[][];
_arrows: Arrow[][];
}
function initGlobals(): FilterGlobals {
return {
_matchesCount: 0,
_topMatch2: 0,
_topScore: 0,
_wordStart: 0,
_firstMatchCanBeWeak: false,
_table: initTable(),
_scores: initTable(),
_arrows: <Arrow[][]>initTable(),
};
}
export function fuzzyScore(
pattern: string,
patternLow: string,
patternStart: number,
word: string,
wordLow: string,
wordStart: number,
firstMatchCanBeWeak: boolean
): FuzzyScore | undefined {
const globals = initGlobals();
const patternLen = pattern.length > _maxLen ? _maxLen : pattern.length;
const wordLen = word.length > _maxLen ? _maxLen : word.length;
if (
patternStart >= patternLen ||
wordStart >= wordLen ||
patternLen - patternStart > wordLen - wordStart
) {
return undefined;
}
// Run a simple check if the characters of pattern occur
// (in order) at all in word. If that isn't the case we
// stop because no match will be possible
if (
!isPatternInWord(
patternLow,
patternStart,
patternLen,
wordLow,
wordStart,
wordLen
)
) {
return undefined;
}
let row = 1;
let column = 1;
let patternPos = patternStart;
let wordPos = wordStart;
let hasStrongFirstMatch = false;
// There will be a match, fill in tables
for (
row = 1, patternPos = patternStart;
patternPos < patternLen;
row++, patternPos++
) {
for (
column = 1, wordPos = wordStart;
wordPos < wordLen;
column++, wordPos++
) {
const score = _doScore(
pattern,
patternLow,
patternPos,
patternStart,
word,
wordLow,
wordPos
);
if (patternPos === patternStart && score > 1) {
hasStrongFirstMatch = true;
}
globals._scores[row][column] = score;
const diag =
globals._table[row - 1][column - 1] + (score > 1 ? 1 : score);
const top = globals._table[row - 1][column] + -1;
const left = globals._table[row][column - 1] + -1;
if (left >= top) {
// left or diag
if (left > diag) {
globals._table[row][column] = left;
globals._arrows[row][column] = Arrow.Left;
} else if (left === diag) {
globals._table[row][column] = left;
globals._arrows[row][column] = Arrow.Left || Arrow.Diag;
} else {
globals._table[row][column] = diag;
globals._arrows[row][column] = Arrow.Diag;
}
} else if (top > diag) {
globals._table[row][column] = top;
globals._arrows[row][column] = Arrow.Top;
} else if (top === diag) {
globals._table[row][column] = top;
globals._arrows[row][column] = Arrow.Top || Arrow.Diag;
} else {
globals._table[row][column] = diag;
globals._arrows[row][column] = Arrow.Diag;
}
}
}
if (_debug) {
printTables(pattern, patternStart, word, wordStart, globals);
}
if (!hasStrongFirstMatch && !firstMatchCanBeWeak) {
return undefined;
}
globals._matchesCount = 0;
globals._topScore = -100;
globals._wordStart = wordStart;
globals._firstMatchCanBeWeak = firstMatchCanBeWeak;
_findAllMatches2(
row - 1,
column - 1,
patternLen === wordLen ? 1 : 0,
0,
false,
globals
);
if (globals._matchesCount === 0) {
return undefined;
}
return [globals._topScore, globals._topMatch2, wordStart];
}
function _doScore(
pattern: string,
patternLow: string,
patternPos: number,
patternStart: number,
word: string,
wordLow: string,
wordPos: number
) {
if (patternLow[patternPos] !== wordLow[wordPos]) {
return -1;
}
if (wordPos === patternPos - patternStart) {
// common prefix: `foobar <-> foobaz`
// ^^^^^
if (pattern[patternPos] === word[wordPos]) {
return 7;
}
return 5;
}
if (
isUpperCaseAtPos(wordPos, word, wordLow) &&
(wordPos === 0 || !isUpperCaseAtPos(wordPos - 1, word, wordLow))
) {
// hitting upper-case: `foo <-> forOthers`
// ^^ ^
if (pattern[patternPos] === word[wordPos]) {
return 7;
}
return 5;
}
if (
isSeparatorAtPos(wordLow, wordPos) &&
(wordPos === 0 || !isSeparatorAtPos(wordLow, wordPos - 1))
) {
// hitting a separator: `. <-> foo.bar`
// ^
return 5;
}
if (
isSeparatorAtPos(wordLow, wordPos - 1) ||
isWhitespaceAtPos(wordLow, wordPos - 1)
) {
// post separator: `foo <-> bar_foo`
// ^^^
return 5;
}
return 1;
}
function printTable(
table: number[][],
pattern: string,
patternLen: number,
word: string,
wordLen: number
): string {
function pad(s: string, n: number, _pad = " ") {
while (s.length < n) {
s = _pad + s;
}
return s;
}
let ret = ` | |${word
.split("")
.map((c) => pad(c, 3))
.join("|")}\n`;
for (let i = 0; i <= patternLen; i++) {
if (i === 0) {
ret += " |";
} else {
ret += `${pattern[i - 1]}|`;
}
ret +=
table[i]
.slice(0, wordLen + 1)
.map((n) => pad(n.toString(), 3))
.join("|") + "\n";
}
return ret;
}
function printTables(
pattern: string,
patternStart: number,
word: string,
wordStart: number,
globals: FilterGlobals
): void {
pattern = pattern.substr(patternStart);
word = word.substr(wordStart);
console.log(
printTable(globals._table, pattern, pattern.length, word, word.length)
);
console.log(
printTable(globals._arrows, pattern, pattern.length, word, word.length)
);
console.log(
printTable(globals._scores, pattern, pattern.length, word, word.length)
);
}
function _findAllMatches2(
row: number,
column: number,
total: number,
matches: number,
lastMatched: boolean,
globals: FilterGlobals
): void {
if (globals._matchesCount >= 10 || total < -25) {
// stop when having already 10 results, or
// when a potential alignment as already 5 gaps
return;
}
let simpleMatchCount = 0;
while (row > 0 && column > 0) {
const score = globals._scores[row][column];
const arrow = globals._arrows[row][column];
if (arrow === Arrow.Left) {
// left -> no match, skip a word character
column -= 1;
if (lastMatched) {
total -= 5; // new gap penalty
} else if (matches !== 0) {
total -= 1; // gap penalty after first match
}
lastMatched = false;
simpleMatchCount = 0;
} else if (arrow && Arrow.Diag) {
if (arrow && Arrow.Left) {
// left
_findAllMatches2(
row,
column - 1,
matches !== 0 ? total - 1 : total, // gap penalty after first match
matches,
lastMatched,
globals
);
}
// diag
total += score;
row -= 1;
column -= 1;
lastMatched = true;
// match -> set a 1 at the word pos
matches += 2 ** (column + globals._wordStart);
// count simple matches and boost a row of
// simple matches when they yield in a
// strong match.
if (score === 1) {
simpleMatchCount += 1;
if (row === 0 && !globals._firstMatchCanBeWeak) {
// when the first match is a weak
// match we discard it
return;
}
} else {
// boost
total += 1 + simpleMatchCount * (score - 1);
simpleMatchCount = 0;
}
} else {
return;
}
}
total -= column >= 3 ? 9 : column * 3; // late start penalty
// dynamically keep track of the current top score
// and insert the current best score at head, the rest at tail
globals._matchesCount += 1;
if (total > globals._topScore) {
globals._topScore = total;
globals._topMatch2 = matches;
}
}
// #endregion
@@ -0,0 +1,66 @@
import { fuzzyScore } from "./filter";
/**
* Determine whether a sequence of letters exists in another string,
* in that order, allowing for skipping. Ex: "chdr" exists in "chandelier")
*
* @param {string} filter - Sequence of letters to check for
* @param {string} word - Word to check for sequence
*
* @return {number} Score representing how well the word matches the filter. Return of 0 means no match.
*/
export const fuzzySequentialMatch = (filter: string, ...words: string[]) => {
let topScore = 0;
for (const word of words) {
const scores = fuzzyScore(
filter,
filter.toLowerCase(),
0,
word,
word.toLowerCase(),
0,
true
);
if (!scores) {
continue;
}
// The VS Code implementation of filter treats a score of "0" as just barely a match
// But we will typically use this matcher in a .filter(), which interprets 0 as a failure.
// By shifting all scores up by 1, we allow "0" matches, while retaining score precedence
const score = scores[0] + 1;
if (score > topScore) {
topScore = score;
}
}
return topScore;
};
export interface ScorableTextItem {
score?: number;
text: string;
altText?: string;
}
type FuzzyFilterSort = <T extends ScorableTextItem>(
filter: string,
items: T[]
) => T[];
export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => {
return items
.map((item) => {
item.score = item.altText
? fuzzySequentialMatch(filter, item.text, item.altText)
: fuzzySequentialMatch(filter, item.text);
return item;
})
.filter((item) => item.score !== undefined && item.score > 0)
.sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) =>
scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0
);
};
+22
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();
};
-29
View File
@@ -1,29 +0,0 @@
/**
* Determine whether a sequence of letters exists in another string,
* in that order, allowing for skipping. Ex: "chdr" exists in "chandelier")
*
* filter => sequence of letters
* word => Word to check for sequence
*
* return true if word contains sequence. Otherwise false.
*/
export const fuzzySequentialMatch = (filter: string, word: string) => {
if (filter === "") {
return true;
}
for (let i = 0; i <= filter.length; i++) {
const pos = word.indexOf(filter[0]);
if (pos < 0) {
return false;
}
const newWord = word.substring(pos + 1);
const newFilter = filter.substring(1);
return fuzzySequentialMatch(newFilter, newWord);
}
return true;
};
+13 -5
View File
@@ -1,4 +1,5 @@
import IntlMessageFormat from "intl-messageformat";
import { shouldPolyfill } from "@formatjs/intl-pluralrules/should-polyfill";
import { Resources } from "../../types";
export type LocalizeFunc = (key: string, ...args: any[]) => string;
@@ -12,9 +13,12 @@ export interface FormatsType {
time: FormatType;
}
if (!Intl.PluralRules) {
import("@formatjs/intl-pluralrules/polyfill-locales");
}
let polyfillLoaded = !shouldPolyfill();
const polyfillProm = polyfillLoaded
? undefined
: import("@formatjs/intl-pluralrules/polyfill-locales").then(() => {
polyfillLoaded = true;
});
/**
* Adapted from Polymer app-localize-behavior.
@@ -37,12 +41,16 @@ if (!Intl.PluralRules) {
* }
*/
export const computeLocalize = (
export const computeLocalize = async (
cache: any,
language: string,
resources: Resources,
formats?: FormatsType
): LocalizeFunc => {
): Promise<LocalizeFunc> => {
if (!polyfillLoaded) {
await polyfillProm;
}
// Everytime any of the parameters change, invalidate the strings cache.
cache._localizationCache = {};
+12 -7
View File
@@ -94,6 +94,8 @@ export class HaDataTable extends LitElement {
@property({ type: Boolean }) public selectable = false;
@property({ type: Boolean }) public clickable = false;
@property({ type: Boolean }) public hasFab = false;
@property({ type: Boolean, attribute: "auto-height" })
@@ -327,12 +329,13 @@ export class HaDataTable extends LitElement {
<div
aria-rowindex=${index}
role="row"
.rowId="${row[this.id]}"
.rowId=${row[this.id]}
@click=${this._handleRowClick}
class="mdc-data-table__row ${classMap({
"mdc-data-table__row--selected": this._checkedRows.includes(
String(row[this.id])
),
clickable: this.clickable,
})}"
aria-selected=${ifDefined(
this._checkedRows.includes(String(row[this.id]))
@@ -350,6 +353,7 @@ export class HaDataTable extends LitElement {
<ha-checkbox
class="mdc-data-table__row-checkbox"
@change=${this._handleRowCheckboxClick}
.rowId=${row[this.id]}
.disabled=${row.selectable === false}
.checked=${this._checkedRows.includes(
String(row[this.id])
@@ -457,9 +461,7 @@ export class HaDataTable extends LitElement {
);
private _handleHeaderClick(ev: Event) {
const columnId = ((ev.target as HTMLElement).closest(
".mdc-data-table__header-cell"
) as any).columnId;
const columnId = (ev.currentTarget as any).columnId;
if (!this.columns[columnId].sortable) {
return;
}
@@ -493,8 +495,8 @@ export class HaDataTable extends LitElement {
}
private _handleRowCheckboxClick(ev: Event) {
const checkbox = ev.target as HaCheckbox;
const rowId = (checkbox.closest(".mdc-data-table__row") as any).rowId;
const checkbox = ev.currentTarget as HaCheckbox;
const rowId = (checkbox as any).rowId;
if (checkbox.checked) {
if (this._checkedRows.includes(rowId)) {
@@ -512,7 +514,7 @@ export class HaDataTable extends LitElement {
if (target.tagName === "HA-CHECKBOX") {
return;
}
const rowId = (target.closest(".mdc-data-table__row") as any).rowId;
const rowId = (ev.currentTarget as any).rowId;
fireEvent(this, "row-click", { id: rowId }, { bubbles: false });
}
@@ -886,6 +888,9 @@ export class HaDataTable extends LitElement {
.forceLTR {
direction: ltr;
}
.clickable {
cursor: pointer;
}
`;
}
}
@@ -9,9 +9,17 @@ import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
@customElement("ha-device-action-picker")
class HaDeviceActionPicker extends HaDeviceAutomationPicker<DeviceAction> {
protected NO_AUTOMATION_TEXT = "No actions";
protected get NO_AUTOMATION_TEXT() {
return this.hass.localize(
"ui.panel.config.devices.automation.actions.no_actions"
);
}
protected UNKNOWN_AUTOMATION_TEXT = "Unknown action";
protected get UNKNOWN_AUTOMATION_TEXT() {
return this.hass.localize(
"ui.panel.config.devices.automation.actions.unknown_action"
);
}
constructor() {
super(
@@ -33,16 +33,24 @@ export abstract class HaDeviceAutomationPicker<
@property() public value?: T;
protected NO_AUTOMATION_TEXT = "No automations";
protected UNKNOWN_AUTOMATION_TEXT = "Unknown automation";
@internalProperty() private _automations: T[] = [];
// Trigger an empty render so we start with a clean DOM.
// paper-listbox does not like changing things around.
@internalProperty() private _renderEmpty = false;
protected get NO_AUTOMATION_TEXT() {
return this.hass.localize(
"ui.panel.config.devices.automation.actions.no_actions"
);
}
protected get UNKNOWN_AUTOMATION_TEXT() {
return this.hass.localize(
"ui.panel.config.devices.automation.actions.unknown_action"
);
}
private _localizeDeviceAutomation: (
hass: HomeAssistant,
automation: T
@@ -11,9 +11,17 @@ import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
class HaDeviceConditionPicker extends HaDeviceAutomationPicker<
DeviceCondition
> {
protected NO_AUTOMATION_TEXT = "No conditions";
protected get NO_AUTOMATION_TEXT() {
return this.hass.localize(
"ui.panel.config.devices.automation.conditions.no_conditions"
);
}
protected UNKNOWN_AUTOMATION_TEXT = "Unknown condition";
protected get UNKNOWN_AUTOMATION_TEXT() {
return this.hass.localize(
"ui.panel.config.devices.automation.conditions.unknown_condition"
);
}
constructor() {
super(
@@ -9,9 +9,17 @@ import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
@customElement("ha-device-trigger-picker")
class HaDeviceTriggerPicker extends HaDeviceAutomationPicker<DeviceTrigger> {
protected NO_AUTOMATION_TEXT = "No triggers";
protected get NO_AUTOMATION_TEXT() {
return this.hass.localize(
"ui.panel.config.devices.automation.triggers.no_triggers"
);
}
protected UNKNOWN_AUTOMATION_TEXT = "Unknown trigger";
protected get UNKNOWN_AUTOMATION_TEXT() {
return this.hass.localize(
"ui.panel.config.devices.automation.triggers.unknown_trigger"
);
}
constructor() {
super(
+2 -1
View File
@@ -88,6 +88,7 @@ class HaChartBase extends mixinBehaviors(
.chartTooltip .beforeBody {
text-align: center;
font-weight: 300;
word-break: break-all;
}
.chartLegend li {
display: inline-block;
@@ -278,7 +279,7 @@ class HaChartBase extends mixinBehaviors(
this.set(["tooltip", "title"], title);
if (tooltip.beforeBody) {
this.set(["tooltip", "beforeBody"], tooltip.beforeBody.join("\n"));
this.set(["tooltip", "beforeBody"], tooltip.beforeBody.join("\n"));
}
const bodyLines = tooltip.body.map((n) => n.lines);
@@ -1,3 +1,4 @@
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
@@ -16,8 +17,9 @@ import {
import { fireEvent } from "../../common/dom/fire_event";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import "../ha-icon-button";
import "../ha-svg-icon";
import "./state-badge";
import "@material/mwc-icon-button/mwc-icon-button";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
@@ -80,6 +82,7 @@ class HaEntityAttributePicker extends LitElement {
.value=${this._value}
.allowCustomValue=${this.allowCustomValue}
.renderer=${rowRenderer}
attr-for-value="bind-value"
@opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged}
>
@@ -97,33 +100,35 @@ class HaEntityAttributePicker extends LitElement {
autocorrect="off"
spellcheck="false"
>
${this.value
? html`
<ha-icon-button
aria-label=${this.hass.localize(
"ui.components.entity.entity-picker.clear"
)}
slot="suffix"
class="clear-button"
icon="hass:close"
@click=${this._clearValue}
no-ripple
>
Clear
</ha-icon-button>
`
: ""}
<div class="suffix" slot="suffix">
${this.value
? html`
<mwc-icon-button
.label=${this.hass.localize(
"ui.components.entity.entity-picker.clear"
)}
class="clear-button"
tabindex="-1"
@click=${this._clearValue}
no-ripple
>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
`
: ""}
<ha-icon-button
aria-label=${this.hass.localize(
"ui.components.entity.entity-attribute-picker.show_attributes"
)}
slot="suffix"
class="toggle-button"
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
>
Toggle
</ha-icon-button>
<mwc-icon-button
.label=${this.hass.localize(
"ui.components.entity.entity-attribute-picker.show_attributes"
)}
class="toggle-button"
tabindex="-1"
>
<ha-svg-icon
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
></ha-svg-icon>
</mwc-icon-button>
</div>
</paper-input>
</vaadin-combo-box-light>
`;
@@ -159,7 +164,10 @@ class HaEntityAttributePicker extends LitElement {
static get styles(): CSSResult {
return css`
paper-input > ha-icon-button {
.suffix {
display: flex;
}
mwc-icon-button {
--mdc-icon-button-size: 24px;
padding: 0px 2px;
color: var(--secondary-text-color);
+49 -32
View File
@@ -1,3 +1,5 @@
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
@@ -20,7 +22,7 @@ import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import "../ha-icon-button";
import "../ha-svg-icon";
import "./state-badge";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
@@ -101,6 +103,8 @@ export class HaEntityPicker extends LitElement {
private _initedStates = false;
private _states: HassEntity[] = [];
private _getStates = memoizeOne(
(
_opened: boolean,
@@ -166,7 +170,7 @@ export class HaEntityPicker extends LitElement {
protected updated(changedProps: PropertyValues) {
if (!this._initedStates || (changedProps.has("_opened") && this._opened)) {
const states = this._getStates(
this._states = this._getStates(
this._opened,
this.hass,
this.includeDomains,
@@ -174,7 +178,7 @@ export class HaEntityPicker extends LitElement {
this.entityFilter,
this.includeDeviceClasses
);
(this._comboBox as any).items = states;
(this._comboBox as any).filteredItems = this._states;
this._initedStates = true;
}
}
@@ -192,6 +196,7 @@ export class HaEntityPicker extends LitElement {
.renderer=${rowRenderer}
@opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged}
@filter-changed=${this._filterChanged}
>
<paper-input
.autofocus=${this.autofocus}
@@ -206,35 +211,35 @@ export class HaEntityPicker extends LitElement {
autocorrect="off"
spellcheck="false"
>
${this.value && !this.hideClearIcon
? html`
<ha-icon-button
aria-label=${this.hass.localize(
"ui.components.entity.entity-picker.clear"
)}
slot="suffix"
class="clear-button"
icon="hass:close"
tabindex="-1"
@click=${this._clearValue}
no-ripple
>
Clear
</ha-icon-button>
`
: ""}
<div class="suffix" slot="suffix">
${this.value && !this.hideClearIcon
? html`
<mwc-icon-button
.label=${this.hass.localize(
"ui.components.entity.entity-picker.clear"
)}
class="clear-button"
tabindex="-1"
@click=${this._clearValue}
no-ripple
>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
`
: ""}
<ha-icon-button
aria-label=${this.hass.localize(
"ui.components.entity.entity-picker.show_entities"
)}
slot="suffix"
class="toggle-button"
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
tabindex="-1"
>
Toggle
</ha-icon-button>
<mwc-icon-button
.label=${this.hass.localize(
"ui.components.entity.entity-picker.show_entities"
)}
class="toggle-button"
tabindex="-1"
>
<ha-svg-icon
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
></ha-svg-icon>
</mwc-icon-button>
</div>
</paper-input>
</vaadin-combo-box-light>
`;
@@ -260,6 +265,15 @@ export class HaEntityPicker extends LitElement {
}
}
private _filterChanged(ev: CustomEvent): void {
const filterString = ev.detail.value.toLowerCase();
(this._comboBox as any).filteredItems = this._states.filter(
(state) =>
state.entity_id.toLowerCase().includes(filterString) ||
computeStateName(state).toLowerCase().includes(filterString)
);
}
private _setValue(value: string) {
this.value = value;
setTimeout(() => {
@@ -270,7 +284,10 @@ export class HaEntityPicker extends LitElement {
static get styles(): CSSResult {
return css`
paper-input > ha-icon-button {
.suffix {
display: flex;
}
mwc-icon-button {
--mdc-icon-button-size: 24px;
padding: 0px 2px;
color: var(--secondary-text-color);
-25
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);
+13 -1
View File
@@ -11,11 +11,14 @@ import {
} from "lit-element";
import { ifDefined } from "lit-html/directives/if-defined";
import { styleMap } from "lit-html/directives/style-map";
import { computeActiveState } from "../../common/entity/compute_active_state";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { stateIcon } from "../../common/entity/state_icon";
import { iconColorCSS } from "../../common/style/icon_color_css";
import type { HomeAssistant } from "../../types";
import "../ha-icon";
export class StateBadge extends LitElement {
@@ -37,7 +40,13 @@ export class StateBadge extends LitElement {
protected render(): TemplateResult {
const stateObj = this.stateObj;
if (!stateObj || !this._showIcon) {
if (!stateObj) {
return html`<div class="missing">
<ha-icon icon="hass:alert"></ha-icon>
</div>`;
}
if (!this._showIcon) {
return html``;
}
@@ -140,6 +149,9 @@ export class StateBadge extends LitElement {
ha-icon {
transition: color 0.3s ease-in-out, filter 0.3s ease-in-out;
}
.missing {
color: #fce588;
}
${iconColorCSS}
`;
+33 -5
View File
@@ -59,6 +59,19 @@ class StateInfo extends LocalizeMixin(PolymerElement) {
@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;
}
</style>
`;
}
@@ -81,11 +94,26 @@ class StateInfo extends LocalizeMixin(PolymerElement) {
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>
<div>
<div class="row">
<span class="column-name">
[[localize('ui.dialogs.more_info_control.last_changed')]]:
</span>
<ha-relative-time
hass="[[hass]]"
datetime="[[stateObj.last_changed]]"
></ha-relative-time>
</div>
<div class="row">
<span>
[[localize('ui.dialogs.more_info_control.last_updated')]]:
</span>
<ha-relative-time
hass="[[hass]]"
datetime="[[stateObj.last_updated]]"
></ha-relative-time>
</div>
</div>
</paper-tooltip>
</div>
</template>
+3 -2
View File
@@ -34,7 +34,7 @@ class HaAttributes extends LitElement {
(attribute) => html`
<div class="data-entry">
<div class="key">
${attribute.replace(/_/g, " ").replace("id", "ID")}
${attribute.replace(/_/g, " ").replace(/\bid\b/g, "ID")}
</div>
<div class="value">
${this.formatAttribute(attribute)}
@@ -63,13 +63,14 @@ class HaAttributes extends LitElement {
.data-entry .value {
max-width: 200px;
overflow-wrap: break-word;
text-align: right;
}
.key:first-letter {
text-transform: capitalize;
}
.attribution {
color: var(--secondary-text-color);
text-align: right;
text-align: center;
}
pre {
font-family: inherit;
+6 -3
View File
@@ -50,9 +50,12 @@ export class HaCard extends LitElement {
font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, 24px);
letter-spacing: -0.012em;
line-height: 32px;
padding: 24px 16px 16px;
line-height: 48px;
padding: 12px 16px 16px;
display: block;
margin-block-start: 0px;
margin-block-end: 0px;
font-weight: normal;
}
:host ::slotted(.card-content:not(:first-child)),
@@ -75,7 +78,7 @@ export class HaCard extends LitElement {
protected render(): TemplateResult {
return html`
${this.header
? html` <div class="card-header">${this.header}</div> `
? html`<h1 class="card-header">${this.header}</h1>`
: html``}
<slot></slot>
`;
-131
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);
+139
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;
}
}
+5
View File
@@ -81,6 +81,7 @@ export class HaCodeEditor extends UpdatingElement {
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this._blockKeyboardShortcuts();
this._load();
}
@@ -232,6 +233,10 @@ export class HaCodeEditor extends UpdatingElement {
this.codemirror!.on("changes", () => this._onChange());
}
private _blockKeyboardShortcuts() {
this.addEventListener("keydown", (ev) => ev.stopPropagation());
}
private _onChange(): void {
const newValue = this.value;
if (newValue === this._value) {
+119
View File
@@ -0,0 +1,119 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
query,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-svg-icon";
import { mdiChevronDown } from "@mdi/js";
import { classMap } from "lit-html/directives/class-map";
@customElement("ha-expansion-panel")
class HaExpansionPanel extends LitElement {
@property({ type: Boolean, reflect: true }) expanded = false;
@property({ type: Boolean, reflect: true }) outlined = false;
@query(".container") private _container!: HTMLDivElement;
protected render(): TemplateResult {
return html`
<div class="summary" @click=${this._toggleContainer}>
<slot name="title"></slot>
<ha-svg-icon
.path=${mdiChevronDown}
class="summary-icon ${classMap({ expanded: this.expanded })}"
></ha-svg-icon>
</div>
<div
class="container ${classMap({ expanded: this.expanded })}"
@transitionend=${this._handleTransitionEnd}
>
<slot></slot>
</div>
`;
}
private _handleTransitionEnd() {
this._container.style.removeProperty("height");
}
private _toggleContainer(): void {
const scrollHeight = this._container.scrollHeight;
this._container.style.height = `${scrollHeight}px`;
if (this.expanded) {
setTimeout(() => {
this._container.style.height = "0px";
}, 0);
}
this.expanded = !this.expanded;
fireEvent(this, "expanded-changed", { expanded: this.expanded });
}
static get styles(): CSSResult {
return css`
:host {
display: block;
}
:host([outlined]) {
box-shadow: none;
border-width: 1px;
border-style: solid;
border-color: var(
--ha-card-border-color,
var(--divider-color, #e0e0e0)
);
border-radius: var(--ha-card-border-radius, 4px);
}
.summary {
display: flex;
padding: 0px 16px;
min-height: 48px;
align-items: center;
cursor: pointer;
overflow: hidden;
}
.summary-icon {
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
margin-left: auto;
}
.summary-icon.expanded {
transform: rotate(180deg);
}
.container {
overflow: hidden;
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
height: 0px;
}
.container.expanded {
height: auto;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-expansion-panel": HaExpansionPanel;
}
// for fire event
interface HASSDomEvents {
"expanded-changed": {
expanded: boolean;
};
}
}
+5 -1
View File
@@ -38,7 +38,8 @@ class HaHLSPlayer extends LitElement {
@property({ type: Boolean, attribute: "allow-exoplayer" })
public allowExoPlayer = false;
@query("video", true) private _videoEl!: HTMLVideoElement;
// don't cache this, as we remove it on disconnects
@query("video") private _videoEl!: HTMLVideoElement;
@internalProperty() private _attached = false;
@@ -154,6 +155,9 @@ class HaHLSPlayer extends LitElement {
}
private _resizeExoPlayer = () => {
if (!this._videoEl) {
return;
}
const rect = this._videoEl.getBoundingClientRect();
this.hass!.auth.external!.fireMessage({
type: "exoplayer/resize",
+2 -3
View File
@@ -14,8 +14,8 @@ class HaLabeledSlider extends PolymerElement {
}
.title {
margin-bottom: 16px;
color: var(--secondary-text-color);
margin-bottom: 8px;
color: var(--primary-text-color);
}
.slider-container {
@@ -43,7 +43,6 @@ class HaLabeledSlider extends PolymerElement {
step="[[step]]"
pin="[[pin]]"
disabled="[[disabled]]"
disabled="[[disabled]]"
value="{{value}}"
></ha-slider>
</div>
+2 -3
View File
@@ -6,9 +6,9 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
internalProperty,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../common/dom/fire_event";
@@ -98,8 +98,7 @@ class HaMenuButton extends LitElement {
return;
}
this.style.visibility =
newNarrow || this._alwaysVisible ? "initial" : "hidden";
this.style.display = newNarrow || this._alwaysVisible ? "initial" : "none";
if (!newNarrow) {
this._hasNotifications = false;
@@ -3,6 +3,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { getAppKey } from "../data/notify_html5";
import { EventsMixin } from "../mixins/events-mixin";
import { showPromptDialog } from "../dialogs/generic/show-dialog-box";
import "./ha-switch";
export const pushSupported =
@@ -88,7 +89,14 @@ class HaPushNotificationsToggle extends EventsMixin(PolymerElement) {
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) {
this.pushChecked = false;
return;
+223 -189
View File
@@ -202,195 +202,17 @@ class HaSidebar extends LitElement {
private _sortable?;
protected render() {
const hass = this.hass;
if (!hass) {
if (!this.hass) {
return html``;
}
const [beforeSpacer, afterSpacer] = computePanels(
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++;
}
}
// prettier-ignore
return html`
<div
class="menu"
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: !this.editMode,
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>
${this._renderHeader()}
${this._renderAllPanels()}
${this._renderDivider()}
${this._renderNotifications()}
${this._renderUserItem()}
<div disabled class="bottom-spacer"></div>
<div class="tooltip"></div>
`;
@@ -475,6 +297,215 @@ 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>
`
: ""}
<div class="title">
${this.editMode
? html`<mwc-button outlined @click=${this._closeEditMode}>
${this.hass.localize("ui.sidebar.done")}
</mwc-button>`
: "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() {
return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement;
}
@@ -728,6 +759,7 @@ class HaSidebar extends LitElement {
width: 64px;
}
:host([expanded]) {
width: 256px;
width: calc(256px + env(safe-area-inset-left));
}
:host([rtl]) {
@@ -735,8 +767,7 @@ class HaSidebar extends LitElement {
border-left: 1px solid var(--divider-color);
}
.menu {
box-sizing: border-box;
height: 65px;
height: var(--header-height);
display: flex;
padding: 0 8.5px;
border-bottom: 1px solid transparent;
@@ -781,7 +812,7 @@ class HaSidebar extends LitElement {
display: initial;
}
.title mwc-button {
width: 100%;
width: 90%;
}
#sortable,
.hidden-panel {
@@ -793,7 +824,10 @@ class HaSidebar extends LitElement {
display: flex;
flex-direction: column;
box-sizing: border-box;
height: calc(100% - 196px - env(safe-area-inset-bottom));
height: calc(100% - var(--header-height) - 132px);
height: calc(
100% - var(--header-height) - 132px - env(safe-area-inset-bottom)
);
overflow-x: hidden;
background: none;
margin-left: env(safe-area-inset-left);
+4 -3
View File
@@ -21,6 +21,7 @@ class HaSlider extends PaperSliderClass {
.pin > .slider-knob > .slider-knob-inner {
font-size: var(--ha-slider-pin-font-size, 10px);
line-height: normal;
cursor: pointer;
}
.disabled.ring > .slider-knob > .slider-knob-inner {
@@ -69,9 +70,9 @@ class HaSlider extends PaperSliderClass {
transform: scale(1) translate(0, -10px);
}
.slider-input {
width: 54px;
}
.slider-input {
width: 54px;
}
`)
);
}
+100
View File
@@ -0,0 +1,100 @@
import "@polymer/paper-tabs/paper-tabs";
import type { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button";
import type { PaperTabElement } from "@polymer/paper-tabs/paper-tab";
import type { PaperTabsElement } from "@polymer/paper-tabs/paper-tabs";
import { customElement } from "lit-element";
import { Constructor } from "../types";
const PaperTabs = customElements.get("paper-tabs") as Constructor<
PaperTabsElement
>;
let subTemplate: HTMLTemplateElement;
@customElement("ha-tabs")
export class HaTabs extends PaperTabs {
private _firstTabWidth = 0;
private _lastTabWidth = 0;
private _lastLeftHiddenState = false;
static get template(): HTMLTemplateElement {
if (!subTemplate) {
subTemplate = (PaperTabs as any).template.cloneNode(true);
const superStyle = subTemplate.content.querySelector("style");
// Add "noink" attribute for scroll buttons to disable animation.
subTemplate.content
.querySelectorAll("paper-icon-button")
.forEach((arrow: PaperIconButtonElement) => {
arrow.setAttribute("noink", "");
});
superStyle!.appendChild(
document.createTextNode(`
:host {
padding-top: .5px;
}
.not-visible {
display: none;
}
paper-icon-button {
width: 24px;
height: 48px;
padding: 0;
margin: 0;
}
`)
);
}
return subTemplate;
}
// Get first and last tab's width for _affectScroll
public _tabChanged(tab: PaperTabElement, old: PaperTabElement): void {
super._tabChanged(tab, old);
const tabs = this.querySelectorAll("paper-tab:not(.hide-tab)");
if (tabs.length > 0) {
this._firstTabWidth = tabs[0].clientWidth;
this._lastTabWidth = tabs[tabs.length - 1].clientWidth;
}
// Scroll active tab into view if needed.
const selected = this.querySelector(".iron-selected");
if (selected) {
selected.scrollIntoView();
}
}
/**
* Modify _affectScroll so that when the scroll arrows appear
* while scrolling and the tab container shrinks we can counteract
* the jump in tab position so that the scroll still appears smooth.
*/
public _affectScroll(dx: number): void {
if (this._firstTabWidth === 0 || this._lastTabWidth === 0) {
return;
}
this.$.tabsContainer.scrollLeft += dx;
const scrollLeft = this.$.tabsContainer.scrollLeft;
this._leftHidden = scrollLeft - this._firstTabWidth < 0;
this._rightHidden =
scrollLeft + this._lastTabWidth > this._tabContainerScrollSize;
if (this._lastLeftHiddenState !== this._leftHidden) {
this._lastLeftHiddenState = this._leftHidden;
this.$.tabsContainer.scrollLeft += this._leftHidden ? -23 : 23;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-tabs": HaTabs;
}
}
@@ -378,6 +378,7 @@ export class HaMediaPlayerBrowse extends LitElement {
: html`
<div class="container">
${this.hass.localize("ui.components.media-browser.no_items")}
<br />
${currentItem.media_content_id ===
"media-source://media_source/local/."
? html`<br />${this.hass.localize(
@@ -398,7 +399,7 @@ export class HaMediaPlayerBrowse extends LitElement {
<br />
${this.hass.localize(
"ui.components.media-browser.local_media_files"
)}.`
)}`
: ""}
</div>
`}
@@ -539,17 +540,20 @@ export class HaMediaPlayerBrowse extends LitElement {
mediaContentType?: string
): Promise<MediaPlayerItem> {
this._loading = true;
const itemData =
this.entityId !== BROWSER_PLAYER
? await browseMediaPlayer(
this.hass,
this.entityId,
mediaContentId,
mediaContentType
)
: await browseLocalMediaPlayer(this.hass, mediaContentId);
this._loading = false;
let itemData: any;
try {
itemData =
this.entityId !== BROWSER_PLAYER
? await browseMediaPlayer(
this.hass,
this.entityId,
mediaContentId,
mediaContentType
)
: await browseLocalMediaPlayer(this.hass, mediaContentId);
} finally {
this._loading = false;
}
return itemData;
}
+19 -9
View File
@@ -24,10 +24,14 @@ class HaUserPicker extends LitElement {
@property() public label?: string;
@property() public value?: string;
@property() public noUserLabel?: string;
@property() public value = "";
@property() public users?: User[];
@property({ type: Boolean }) public disabled = false;
private _sortedUsers = memoizeOne((users?: User[]) => {
if (!users) {
return [];
@@ -40,15 +44,19 @@ class HaUserPicker extends LitElement {
protected render(): TemplateResult {
return html`
<paper-dropdown-menu-light .label=${this.label}>
<paper-dropdown-menu-light
.label=${this.label}
.disabled=${this.disabled}
>
<paper-listbox
slot="dropdown-content"
.selected=${this._value}
.selected=${this.value}
attr-for-selected="data-user-id"
@iron-select=${this._userChanged}
>
<paper-icon-item data-user-id="">
No user
${this.noUserLabel ||
this.hass?.localize("ui.components.user-picker.no_user")}
</paper-icon-item>
${this._sortedUsers(this.users).map(
(user) => html`
@@ -67,10 +75,6 @@ class HaUserPicker extends LitElement {
`;
}
private get _value() {
return this.value || "";
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
if (this.users === undefined) {
@@ -83,7 +87,7 @@ class HaUserPicker extends LitElement {
private _userChanged(ev) {
const newValue = ev.detail.item.dataset.userId;
if (newValue !== this._value) {
if (newValue !== this.value) {
this.value = ev.detail.value;
setTimeout(() => {
fireEvent(this, "value-changed", { value: newValue });
@@ -111,3 +115,9 @@ class HaUserPicker extends LitElement {
}
customElements.define("ha-user-picker", HaUserPicker);
declare global {
interface HTMLElementTagNameMap {
"ha-user-picker": HaUserPicker;
}
}
+169
View File
@@ -0,0 +1,169 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../common/dom/fire_event";
import type { PolymerChangedEvent } from "../../polymer-types";
import type { HomeAssistant } from "../../types";
import { fetchUsers, User } from "../../data/user";
import "./ha-user-picker";
import { mdiClose } from "@mdi/js";
import memoizeOne from "memoize-one";
import { guard } from "lit-html/directives/guard";
@customElement("ha-users-picker")
class HaUsersPickerLight extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property() public value?: string[];
@property({ attribute: "picked-user-label" })
public pickedUserLabel?: string;
@property({ attribute: "pick-user-label" })
public pickUserLabel?: string;
@property({ attribute: false })
public users?: User[];
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
if (this.users === undefined) {
fetchUsers(this.hass!).then((users) => {
this.users = users;
});
}
}
protected render(): TemplateResult {
if (!this.hass || !this.users) {
return html``;
}
const notSelectedUsers = this._notSelectedUsers(this.users, this.value);
return html`
${guard([notSelectedUsers], () =>
this.value?.map(
(user_id, idx) => html`
<div>
<ha-user-picker
.label=${this.pickedUserLabel}
.noUserLabel=${this.hass?.localize(
"ui.components.user-picker.remove_user"
)}
.index=${idx}
.hass=${this.hass}
.value=${user_id}
.users=${this._notSelectedUsersAndSelected(
user_id,
this.users,
notSelectedUsers
)}
@value-changed=${this._userChanged}
></ha-user-picker>
<mwc-icon-button .userId=${user_id} @click=${this._removeUser}>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
</div>
`
)
)}
<ha-user-picker
.noUserLabel=${this.pickUserLabel ||
this.hass?.localize("ui.components.user-picker.add_user")}
.hass=${this.hass}
.users=${notSelectedUsers}
.disabled=${!notSelectedUsers?.length}
@value-changed=${this._addUser}
></ha-user-picker>
`;
}
private _notSelectedUsers = memoizeOne(
(users?: User[], currentUsers?: string[]) =>
currentUsers
? users?.filter(
(user) => !user.system_generated && !currentUsers.includes(user.id)
)
: users?.filter((user) => !user.system_generated)
);
private _notSelectedUsersAndSelected = (
userId: string,
users?: User[],
notSelected?: User[]
) => {
const selectedUser = users?.find((user) => user.id === userId);
if (selectedUser) {
return notSelected ? [...notSelected, selectedUser] : [selectedUser];
}
return notSelected;
};
private get _currentUsers() {
return this.value || [];
}
private async _updateUsers(users) {
this.value = users;
fireEvent(this, "value-changed", {
value: users,
});
}
private _userChanged(event: PolymerChangedEvent<string>) {
event.stopPropagation();
const index = (event.currentTarget as any).index;
const newValue = event.detail.value;
const newUsers = [...this._currentUsers];
if (newValue === "") {
newUsers.splice(index, 1);
} else {
newUsers.splice(index, 1, newValue);
}
this._updateUsers(newUsers);
}
private async _addUser(event: PolymerChangedEvent<string>) {
event.stopPropagation();
const toAdd = event.detail.value;
(event.currentTarget as any).value = "";
if (!toAdd) {
return;
}
const currentUsers = this._currentUsers;
if (currentUsers.includes(toAdd)) {
return;
}
this._updateUsers([...currentUsers, toAdd]);
}
private _removeUser(event) {
const userId = (event.currentTarget as any).userId;
this._updateUsers(this._currentUsers.filter((user) => user !== userId));
}
static get styles(): CSSResult {
return css`
:host {
display: block;
}
div {
display: flex;
align-items: center;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-users-picker": HaUsersPickerLight;
}
}
+8 -1
View File
@@ -109,10 +109,17 @@ export interface TemplateTrigger {
value_template: string;
}
export interface ContextConstraint {
context_id?: string;
parent_id?: string;
user_id?: string | string[];
}
export interface EventTrigger {
platform: "event";
event_type: string;
event_data: any;
event_data?: any;
context?: ContextConstraint;
}
export type Trigger =
+1
View File
@@ -11,6 +11,7 @@ export const DISCOVERY_SOURCES = [
"ssdp",
"zeroconf",
"discovery",
"mqtt",
];
export const ATTENTION_SOURCES = ["reauth"];
+51
View File
@@ -0,0 +1,51 @@
import { HomeAssistant } from "../types";
export interface Counter {
id: string;
name: string;
icon?: string;
initial?: number;
restore?: boolean;
minimum?: number;
maximum?: number;
step?: number;
}
export interface CounterMutableParams {
name: string;
icon: string;
initial: number;
restore: boolean;
minimum: number;
maximum: number;
step: number;
}
export const fetchCounter = (hass: HomeAssistant) =>
hass.callWS<Counter[]>({ type: "counter/list" });
export const createCounter = (
hass: HomeAssistant,
values: CounterMutableParams
) =>
hass.callWS<Counter>({
type: "counter/create",
...values,
});
export const updateCounter = (
hass: HomeAssistant,
id: string,
updates: Partial<CounterMutableParams>
) =>
hass.callWS<Counter>({
type: "counter/update",
counter_id: id,
...updates,
});
export const deleteCounter = (hass: HomeAssistant, id: string) =>
hass.callWS({
type: "counter/delete",
counter_id: id,
});
+51 -58
View File
@@ -2,78 +2,71 @@ import { HomeAssistant } from "../../types";
import { hassioApiResultExtractor, HassioResponse } from "./common";
export interface HassioAddonInfo {
name: string;
slug: string;
description: string;
repository: "core" | "local" | string;
version: string;
state: "none" | "started" | "stopped";
installed: string | undefined;
detached: boolean;
advanced: boolean;
available: boolean;
build: boolean;
advanced: boolean;
url: string | null;
description: string;
detached: boolean;
icon: boolean;
installed: boolean;
logo: boolean;
name: string;
repository: "core" | "local" | string;
slug: string;
stage: "stable" | "experimental" | "deprecated";
state: "started" | "stopped" | null;
update_available: boolean;
url: string | null;
version_latest: string;
version: string;
}
export interface HassioAddonDetails extends HassioAddonInfo {
name: string;
slug: string;
description: string;
long_description: null | string;
auto_update: boolean;
url: null | string;
detached: boolean;
documentation: boolean;
available: boolean;
arch: "armhf" | "aarch64" | "i386" | "amd64";
machine: any;
homeassistant: string;
version_latest: string;
boot: "auto" | "manual";
build: boolean;
options: Record<string, unknown>;
network: null | Record<string, number>;
network_description: null | Record<string, string>;
host_network: boolean;
host_pid: boolean;
host_ipc: boolean;
host_dbus: boolean;
privileged: any;
apparmor: "disable" | "default" | "profile";
devices: string[];
auto_uart: boolean;
icon: boolean;
logo: boolean;
stage: "stable" | "experimental" | "deprecated";
changelog: boolean;
hassio_api: boolean;
hassio_role: "default" | "homeassistant" | "manager" | "admin";
startup: "initialize" | "system" | "services" | "application" | "once";
homeassistant_api: boolean;
auth_api: boolean;
full_access: boolean;
protected: boolean;
rating: "1-6";
stdin: boolean;
webui: null | string;
gpio: boolean;
kernel_modules: boolean;
devicetree: boolean;
docker_api: boolean;
audio: boolean;
arch: "armhf" | "aarch64" | "i386" | "amd64";
audio_input: null | string;
audio_output: null | string;
services_role: string[];
audio: boolean;
auth_api: boolean;
auto_uart: boolean;
auto_update: boolean;
boot: "auto" | "manual";
changelog: boolean;
devices: string[];
devicetree: boolean;
discovery: string[];
ip_address: string;
ingress: boolean;
ingress_panel: boolean;
docker_api: boolean;
documentation: boolean;
full_access: boolean;
gpio: boolean;
hassio_api: boolean;
hassio_role: "default" | "homeassistant" | "manager" | "admin";
homeassistant_api: boolean;
homeassistant: string;
host_dbus: boolean;
host_ipc: boolean;
host_network: boolean;
host_pid: boolean;
ingress_entry: null | string;
ingress_panel: boolean;
ingress_url: null | string;
ingress: boolean;
ip_address: string;
kernel_modules: boolean;
long_description: null | string;
machine: any;
network_description: null | Record<string, string>;
network: null | Record<string, number>;
options: Record<string, unknown>;
privileged: any;
protected: boolean;
rating: "1-6";
services_role: string[];
slug: string;
startup: "initialize" | "system" | "services" | "application" | "once";
stdin: boolean;
watchdog: null | boolean;
webui: null | string;
}
export interface HassioAddonsInfo {
+2 -2
View File
@@ -22,8 +22,8 @@ export const hassioApiResultExtractor = <T>(response: HassioResponse<T>) =>
export const extractApiErrorMessage = (error: any): string => {
return typeof error === "object"
? typeof error.body === "object"
? error.body.message || "Unknown error, see logs"
: error.body || "Unknown error, see logs"
? error.body.message || "Unknown error, see supervisor logs"
: error.body || error.message || "Unknown error, see supervisor logs"
: error;
};
+36
View File
@@ -0,0 +1,36 @@
import { HomeAssistant } from "../../types";
import { hassioApiResultExtractor, HassioResponse } from "./common";
interface HassioDockerRegistries {
[key: string]: { username: string; password?: string };
}
export const fetchHassioDockerRegistries = async (hass: HomeAssistant) => {
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioDockerRegistries>>(
"GET",
"hassio/docker/registries"
)
);
};
export const addHassioDockerRegistry = async (
hass: HomeAssistant,
data: HassioDockerRegistries
) => {
await hass.callApi<HassioResponse<HassioDockerRegistries>>(
"POST",
"hassio/docker/registries",
data
);
};
export const removeHassioDockerRegistry = async (
hass: HomeAssistant,
registry: string
) => {
await hass.callApi<HassioResponse<void>>(
"DELETE",
`hassio/docker/registries/${registry}`
);
};
+16 -5
View File
@@ -1,14 +1,25 @@
import { HomeAssistant } from "../../types";
import { hassioApiResultExtractor, HassioResponse } from "./common";
export type HassioHostInfo = any;
export type HassioHostInfo = {
chassis: string;
cpe: string;
deployment: string;
disk_free: number;
disk_total: number;
disk_used: number;
features: string[];
hostname: string;
kernel: string;
operating_system: string;
};
export interface HassioHassOSInfo {
version: string;
version_cli: string;
board: string;
boot: string;
update_available: boolean;
version_latest: string;
version_cli_latest: string;
board: "ova" | "rpi";
version: string;
}
export const fetchHassioHostInfo = async (hass: HomeAssistant) => {
+26
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 }
);
+15
View File
@@ -0,0 +1,15 @@
import { HomeAssistant } from "../../types";
import { hassioApiResultExtractor, HassioResponse } from "./common";
export interface HassioResolution {
unsupported: string[];
}
export const fetchHassioResolution = async (hass: HomeAssistant) => {
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioResolution>>(
"GET",
"hassio/resolution/info"
)
);
};
+41 -16
View File
@@ -1,19 +1,56 @@
import { HomeAssistant, PanelInfo } from "../../types";
import { HassioAddonInfo, HassioAddonRepository } from "./addon";
import { hassioApiResultExtractor, HassioResponse } from "./common";
export type HassioHomeAssistantInfo = any;
export type HassioSupervisorInfo = any;
export type HassioHomeAssistantInfo = {
arch: string;
audio_input: string | null;
audio_output: string | null;
boot: boolean;
image: string;
ip_address: string;
machine: string;
port: number;
ssl: boolean;
update_available: boolean;
version_latest: string;
version: string;
wait_boot: number;
watchdog: boolean;
};
export type HassioSupervisorInfo = {
addons: HassioAddonInfo[];
addons_repositories: HassioAddonRepository[];
arch: string;
channel: string;
debug: boolean;
debug_block: boolean;
diagnostics: boolean | null;
healthy: boolean;
ip_address: string;
logging: string;
supported: boolean;
timezone: string;
update_available: boolean;
version: string;
version_latest: string;
wait_boot: number;
};
export type HassioInfo = {
arch: string;
channel: string;
docker: string;
hassos?: string;
features: string[];
hassos: null;
homeassistant: string;
hostname: string;
logging: string;
maching: string;
machine: string;
operating_system: string;
supervisor: string;
supported: boolean;
supported_arch: string[];
timezone: string;
};
@@ -74,18 +111,6 @@ export const fetchHassioLogs = async (
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 (
hass: HomeAssistant,
data: SupervisorOptions
+5 -2
View File
@@ -85,11 +85,14 @@ export const fetchRecent = (
export const fetchDate = (
hass: HomeAssistant,
startTime: Date,
endTime: Date
endTime: Date,
entityId
): Promise<HassEntity[][]> => {
return hass.callApi(
"GET",
`history/period/${startTime.toISOString()}?end_time=${endTime.toISOString()}&minimal_response`
`history/period/${startTime.toISOString()}?end_time=${endTime.toISOString()}&minimal_response${
entityId ? `&filter_entity_id=${entityId}` : ``
}`
);
};
+2 -1
View File
@@ -33,7 +33,7 @@ export interface LovelaceResource {
}
export interface LovelaceResourcesMutableParams {
res_type: "css" | "js" | "module" | "html";
res_type: LovelaceResource["type"];
url: string;
}
@@ -89,6 +89,7 @@ export interface LovelaceViewConfig {
export interface LovelaceViewElement extends HTMLElement {
hass?: HomeAssistant;
lovelace?: Lovelace;
narrow?: boolean;
index?: number;
cards?: Array<LovelaceCard | HuiErrorCard>;
badges?: LovelaceBadge[];
+21
View File
@@ -63,6 +63,16 @@ export interface OZWNetworkStatistics {
retries: number;
}
export interface OZWDeviceConfig {
label: string;
type: string;
value: string | number;
parameter: number;
min: number;
max: number;
help: string;
}
export const nodeQueryStages = [
"ProtocolInfo",
"Probe",
@@ -180,6 +190,17 @@ export const fetchOZWNodeMetadata = (
node_id: node_id,
});
export const fetchOZWNodeConfig = (
hass: HomeAssistant,
ozw_instance: number,
node_id: number
): Promise<OZWDeviceConfig[]> =>
hass.callWS({
type: "ozw/get_config_parameters",
ozw_instance: ozw_instance,
node_id: node_id,
});
export const refreshNodeInfo = (
hass: HomeAssistant,
ozw_instance: number,
+10
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,
});
+46
View File
@@ -2,6 +2,7 @@ import {
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
import { HomeAssistant } from "../types";
export type TimerEntity = HassEntityBase & {
attributes: HassEntityAttributeBase & {
@@ -9,3 +10,48 @@ export type TimerEntity = HassEntityBase & {
remaining: string;
};
};
export interface DurationDict {
hours?: number | string;
minutes?: number | string;
seconds?: number | string;
}
export interface Timer {
id: string;
name: string;
icon?: string;
duration?: string | number | DurationDict;
}
export interface TimerMutableParams {
name: string;
icon: string;
duration: string | number | DurationDict;
}
export const fetchTimer = (hass: HomeAssistant) =>
hass.callWS<Timer[]>({ type: "timer/list" });
export const createTimer = (hass: HomeAssistant, values: TimerMutableParams) =>
hass.callWS<Timer>({
type: "timer/create",
...values,
});
export const updateTimer = (
hass: HomeAssistant,
id: string,
updates: Partial<TimerMutableParams>
) =>
hass.callWS<Timer>({
type: "timer/update",
timer_id: id,
...updates,
});
export const deleteTimer = (hass: HomeAssistant, id: string) =>
hass.callWS({
type: "timer/delete",
timer_id: id,
});
+38 -19
View File
@@ -1,6 +1,14 @@
import { SVGTemplateResult, svg, html, TemplateResult, css } from "lit-element";
import {
mdiGauge,
mdiWaterPercent,
mdiWeatherFog,
mdiWeatherRainy,
mdiWeatherWindy,
} from "@mdi/js";
import { css, html, svg, SVGTemplateResult, TemplateResult } from "lit-element";
import { styleMap } from "lit-html/directives/style-map";
import "../components/ha-icon";
import "../components/ha-svg-icon";
import type { HomeAssistant, WeatherEntity } from "../types";
import { roundWithOneDecimal } from "../util/calculate";
@@ -25,6 +33,15 @@ export const weatherIcons = {
exceptional: "hass:alert-circle-outline",
};
export const weatherAttrIcons = {
humidity: mdiWaterPercent,
wind_bearing: mdiWeatherWindy,
wind_speed: mdiWeatherWindy,
pressure: mdiGauge,
visibility: mdiWeatherFog,
precipitation: mdiWeatherRainy,
};
const cloudyStates = new Set<string>([
"partlycloudy",
"cloudy",
@@ -110,6 +127,7 @@ export const getWeatherUnit = (
return lengthUnit === "km" ? "hPa" : "inHg";
case "wind_speed":
return `${lengthUnit}/h`;
case "visibility":
case "length":
return lengthUnit;
case "precipitation":
@@ -125,7 +143,7 @@ export const getWeatherUnit = (
export const getSecondaryWeatherAttribute = (
hass: HomeAssistant,
stateObj: WeatherEntity
): string | undefined => {
): TemplateResult | undefined => {
const extrema = getWeatherExtrema(hass, stateObj);
if (extrema) {
@@ -149,17 +167,22 @@ export const getSecondaryWeatherAttribute = (
return undefined;
}
return `
${hass!.localize(
`ui.card.weather.attributes.${attribute}`
)} ${roundWithOneDecimal(value)} ${getWeatherUnit(hass!, attribute)}
const weatherAttrIcon = weatherAttrIcons[attribute];
return html`
${weatherAttrIcon
? html`
<ha-svg-icon class="attr-icon" .path=${weatherAttrIcon}></ha-svg-icon>
`
: hass!.localize(`ui.card.weather.attributes.${attribute}`)}
${roundWithOneDecimal(value)} ${getWeatherUnit(hass!, attribute)}
`;
};
const getWeatherExtrema = (
hass: HomeAssistant,
stateObj: WeatherEntity
): string | undefined => {
): TemplateResult | undefined => {
if (!stateObj.attributes.forecast?.length) {
return undefined;
}
@@ -189,22 +212,18 @@ const getWeatherExtrema = (
const unit = getWeatherUnit(hass!, "temperature");
return `
${
tempHigh
? `
return html`
${tempHigh
? `
${tempHigh} ${unit}
`
: ""
}
: ""}
${tempLow && tempHigh ? " / " : ""}
${
tempLow
? `
${tempLow
? `
${tempLow} ${unit}
`
: ""
}
: ""}
`;
};
+1
View File
@@ -9,6 +9,7 @@ interface TemplateListeners {
all: boolean;
domains: string[];
entities: string[];
time: boolean;
}
export const subscribeRenderTemplate = (
@@ -1,5 +1,4 @@
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-input/paper-input";
import "@material/mwc-button/mwc-button";
import {
css,
CSSResult,
@@ -10,16 +9,16 @@ import {
internalProperty,
TemplateResult,
} from "lit-element";
import "../../components/dialog/ha-paper-dialog";
import "../../components/ha-dialog";
import "../../components/ha-circular-progress";
import "../../components/ha-switch";
import "../../components/ha-formfield";
import { fireEvent } from "../../common/dom/fire_event";
import type { HaSwitch } from "../../components/ha-switch";
import {
getConfigEntrySystemOptions,
updateConfigEntrySystemOptions,
} from "../../data/config_entries";
import type { PolymerChangedEvent } from "../../polymer-types";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { ConfigEntrySystemOptionsDialogParams } from "./show-dialog-config-entry-system-options";
@@ -35,9 +34,9 @@ class DialogConfigEntrySystemOptions extends LitElement {
@internalProperty() private _params?: ConfigEntrySystemOptionsDialogParams;
@internalProperty() private _loading?: boolean;
@internalProperty() private _loading = false;
@internalProperty() private _submitting?: boolean;
@internalProperty() private _submitting = false;
public async showDialog(
params: ConfigEntrySystemOptionsDialogParams
@@ -51,7 +50,12 @@ class DialogConfigEntrySystemOptions extends LitElement {
);
this._loading = false;
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 {
@@ -60,21 +64,17 @@ class DialogConfigEntrySystemOptions extends LitElement {
}
return html`
<ha-paper-dialog
with-backdrop
opened
@opened-changed="${this._openedChanged}"
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${this.hass.localize(
"ui.dialogs.config_entry_system_options.title",
"integration",
this.hass.localize(`component.${this._params.entry.domain}.title`) ||
this._params.entry.domain
)}
>
<h2>
${this.hass.localize(
"ui.dialogs.config_entry_system_options.title",
"integration",
this.hass.localize(
`component.${this._params.entry.domain}.title`
) || this._params.entry.domain
)}
</h2>
<paper-dialog-scrollable>
<div>
${this._loading
? html`
<div class="init-spinner">
@@ -112,22 +112,22 @@ class DialogConfigEntrySystemOptions extends LitElement {
</ha-formfield>
</div>
`}
</paper-dialog-scrollable>
${!this._loading
? html`
<div class="paper-dialog-buttons">
<mwc-button
@click="${this._updateEntry}"
.disabled=${this._submitting}
>
${this.hass.localize(
"ui.dialogs.config_entry_system_options.update"
)}
</mwc-button>
</div>
`
: ""}
</ha-paper-dialog>
</div>
<mwc-button
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._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[] {
return [
haStyleDialog,
css`
ha-paper-dialog {
max-width: 500px;
}
.init-spinner {
padding: 50px 100px;
text-align: center;
+4
View File
@@ -70,6 +70,7 @@ class DialogBox extends LitElement {
<p
class=${classMap({
"no-bottom-padding": Boolean(this._params.prompt),
warning: Boolean(this._params.warning),
})}
>
${this._params.text}
@@ -180,6 +181,9 @@ class DialogBox extends LitElement {
/* Place above other dialogs */
--dialog-z-index: 104;
}
.warning {
color: var(--warning-color);
}
`,
];
}
+1
View File
@@ -5,6 +5,7 @@ interface BaseDialogParams {
confirmText?: string;
text?: string | TemplateResult;
title?: string;
warning?: boolean;
}
export interface AlertDialogParams extends BaseDialogParams {

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