Compare commits

...

93 Commits

Author SHA1 Message Date
Bram Kragten
51938fb51f 20220203.0 (#11533)
* Only upload wheels to PyPI (#11514)

* Make sure we load data in update card (#11516)

* Guard load diagnostics (#11518)

* Design home - Fix GitHub Links (#11519)

* Add filtering to system log card and error log card (#11166)

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

* Handle unknown toggle state (#11522)

* Fix dialog heading aria label (#11524)

Co-authored-by: Zack Barett <arnett.zackary@gmail.com>

* Use css to hide hint in quickbar (#11527)

* Revert "Mobile click accessibility" (#11526)

* Clear old src when disconnected so we can't fetch it with the wrong t… (#11528)

* Add name of integration to diagnostics when more than 1 (#11523)

* Bumped version to 20220203.0

Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: fpro1212 <75439345+fpro1212@users.noreply.github.com>
Co-authored-by: Kuba Wolanin <hi@kubawolanin.com>
Co-authored-by: Zack Barett <arnett.zackary@gmail.com>
2022-02-03 20:52:49 +01:00
Bram Kragten
890ad9a1c8 Bumped version to 20220203.0 2022-02-03 20:27:33 +01:00
Bram Kragten
8466ef371a Add name of integration to diagnostics when more than 1 (#11523) 2022-02-03 13:18:48 -06:00
Bram Kragten
4e55460799 Clear old src when disconnected so we can't fetch it with the wrong t… (#11528) 2022-02-03 07:54:26 -08:00
Bram Kragten
5fde6e659d Revert "Mobile click accessibility" (#11526) 2022-02-03 16:33:24 +01:00
Bram Kragten
148bb99d89 Use css to hide hint in quickbar (#11527) 2022-02-03 09:28:59 -06:00
Bram Kragten
0540bae707 Fix dialog heading aria label (#11524)
Co-authored-by: Zack Barett <arnett.zackary@gmail.com>
2022-02-03 14:54:37 +00:00
Bram Kragten
0c6f647f53 Handle unknown toggle state (#11522) 2022-02-03 15:43:41 +01:00
Kuba Wolanin
3aca67d511 Add filtering to system log card and error log card (#11166)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-02-03 11:15:27 +01:00
fpro1212
0e41a408e7 Design home - Fix GitHub Links (#11519) 2022-02-03 10:03:26 +01:00
Paulus Schoutsen
19e1eaf2d7 Guard load diagnostics (#11518) 2022-02-03 09:59:04 +01:00
Joakim Sørensen
5e80a2b465 Make sure we load data in update card (#11516) 2022-02-03 09:56:38 +01:00
Marc Mueller
866a57cde4 Only upload wheels to PyPI (#11514) 2022-02-02 09:57:25 -08:00
Bram Kragten
c85236e251 Merge pull request #11512 from home-assistant/dev 2022-02-02 14:47:08 +01:00
Bram Kragten
a88da0e39a Merge branch 'master' into dev 2022-02-02 14:30:14 +01:00
Bram Kragten
21a8fac477 Bumped version to 20220202.0 2022-02-02 14:28:17 +01:00
Zack Barett
ca5ce04a38 Scene to have history (#11510) 2022-02-01 16:42:21 -06:00
Bram Kragten
7c4b9a0410 20220201.0 (#11508)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Thomas Lovén <thomasloven@gmail.com>
Co-authored-by: Zack Barett <arnett.zackary@gmail.com>
Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
Co-authored-by: Philip Allgaier <mail@spacegaier.de>
Co-authored-by: Yosi Levy <37745463+yosilevy@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Patrick ZAJDA <patrick@zajda.fr>
Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
2022-02-01 11:18:14 -06:00
Bram Kragten
de6f06ea6d Bumped version to 20220201.0 2022-02-01 18:06:05 +01:00
Bram Kragten
bbc8e323e8 Add start point to device energy graph (#11507) 2022-02-01 18:02:43 +01:00
Bram Kragten
89b6863ae3 Add alert to OZW and legacy Z-Wave panels (#11506) 2022-02-01 15:21:51 +00:00
Bram Kragten
3f1850e9eb unset error when navigating away in media browser (#11505) 2022-02-01 09:16:24 -06:00
Bram Kragten
54d6b5b6f3 Handle config flow errors better (#11499)
* Handle config flow errors better

* Use body for error message

* Update dialog-data-entry-flow.ts
2022-02-01 11:22:14 +01:00
Steve Repsher
fb55ab197f Mobile click accessibility (#11447) 2022-02-01 00:07:49 +01:00
Zack Barett
cc2db9a761 Place System Dashboards at the top with a colored icon in the Dashboard Configuration (#11500) 2022-02-01 00:06:29 +01:00
Paulus Schoutsen
58ba3e5c22 Some fixes for media panel (#11485) 2022-01-31 12:17:06 -06:00
Bram Kragten
182ffccd0c Remove interpolation from history graph (#11498) 2022-01-31 18:07:00 +01:00
Patrick ZAJDA
ce99d14ee0 Add missing em dash for non-disabled entities and devices (#11493) 2022-01-31 17:01:24 +01:00
Yosi Levy
8ce160b9ce Energy setup wizard missing localization entries (#11469) 2022-01-31 10:32:31 +01:00
Paulus Schoutsen
fe33714c8b Bump Lit (#11481) 2022-01-31 10:29:13 +01:00
J. Nick Koston
afbe85625c Add gate to the list of device classes to pick for overrides (#11487) 2022-01-30 21:37:13 -06:00
Yosi Levy
cb47ee7721 Energy setup wizard missing localization entries (#11469) 2022-01-29 11:57:04 -06:00
Zack Barett
5caa256f1b Fix Safari Battery Percent on device page (#11480) 2022-01-29 09:47:22 +01:00
Zack Barett
c66dfb84f9 Fix Quick bar having false text (#11474) 2022-01-29 09:47:06 +01:00
Philip Allgaier
df1d703e4e Adjust device translations to handle device vs service consistently (#11472) 2022-01-29 09:46:32 +01:00
Marc Mueller
ce0ced0b6a Move to setup.cfg and config for build-system (#11484) 2022-01-28 21:18:17 -08:00
Zack Barett
730e9b144d When refreshing updates, notify user when finished (#11464)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-01-28 13:03:45 -06:00
Thomas Lovén
69ff8dd0c4 Use ha-slider in ha-form-integer (#11471) 2022-01-28 10:17:29 -06:00
Paulus Schoutsen
389a100b46 Fix storing Google pin (#11470) 2022-01-28 09:55:34 +01:00
Paulus Schoutsen
9fee7a2829 Merge pull request #11462 from home-assistant/dev 2022-01-27 10:42:12 -08:00
Paulus Schoutsen
a91897821a Bumped version to 20220127.0 2022-01-27 10:22:19 -08:00
Bram Kragten
815a2a07ff Fix mobile styling quickbar (#11459) 2022-01-27 09:58:41 -08:00
Bram Kragten
b8d3eb76ac Set frontendVersion sooner (#11460) 2022-01-27 09:57:15 -08:00
Bram Kragten
ba75c2e7af Little cleanup (#11461) 2022-01-27 09:56:50 -08:00
Bram Kragten
f04b844223 Check if energy integration is enabled (#11458) 2022-01-27 15:27:40 +00:00
Joakim Sørensen
242bad0a29 Use documentationUrl instead of manifest for core integrations (#11450) 2022-01-27 15:49:55 +01:00
Franck Nijhof
8b20b2b63c Adjust values in Energy Dashboard row (#11452) 2022-01-27 15:39:20 +01:00
Philip Allgaier
e0c8efc5e6 Fix Lovelace view edit mode "Done" button styling (#11449) 2022-01-27 09:44:05 +01:00
Paulus Schoutsen
f59c30ac04 Fix discovery name (#11445) 2022-01-26 21:14:28 +01:00
Bram Kragten
e4b9c08b45 Add padding between control buttons and progress bar 2022-01-26 20:32:50 +01:00
Bram Kragten
04e63eefe2 media_class of integrations should be app (#11444) 2022-01-26 20:02:56 +01:00
Bram Kragten
a064ca0856 Merge pull request #11443 from home-assistant/dev 2022-01-26 18:38:18 +01:00
Bram Kragten
6044ea92ad Merge branch 'master' into dev 2022-01-26 18:09:18 +01:00
Bram Kragten
17e8215420 Bumped version to 20220126.0 2022-01-26 18:07:08 +01:00
Philip Allgaier
a4ae1bee79 Sort all elements on the area page (#11338) 2022-01-26 17:06:12 +00:00
Philip Allgaier
7d335d7d85 Convert ha-climate-control ot Lit and add tooltips to buttons (#10921)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2022-01-26 16:50:50 +00:00
Zack Barett
7c194d8910 Update Media Browser to styling from Mockup (#11424)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-01-26 17:42:25 +01:00
Mattias Persson
a92100bb0a Add viewport initial-scale for iOS devices (#11330) 2022-01-26 16:57:23 +01:00
Erik Montnemery
303af611d1 Remove unused keys from hassAttributeUtil (#10944)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-01-26 15:56:10 +00:00
Robin Wittebol
559b6e9d5b Apply header bottom border and fix header height (#10986) 2022-01-26 16:26:44 +01:00
David F. Mulcahey
75a95ff675 Fix ZHA device reconfiguration dialog (#11016) 2022-01-26 16:19:40 +01:00
Philip Allgaier
3024ee43f9 Fix entity config page rendering for disabled entities (#11439) 2022-01-26 14:54:38 +00:00
Philip Allgaier
b34b92fa87 Give the design page menu some space (#11441) 2022-01-26 15:48:34 +01:00
Philip Allgaier
1832ed0a48 Ensure tag QR code gets shown for each dialog opening (#11438) 2022-01-26 10:13:20 +00:00
Paulus Schoutsen
f398692e75 Improve cloud dashboard (#11422)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2022-01-26 10:00:50 +00:00
Joakim Sørensen
68bee4dd58 Change more_updates base translation (#11437) 2022-01-26 10:31:14 +01:00
Joakim Sørensen
f1297e1f36 Update styling for show more updates (#11435) 2022-01-26 10:30:19 +01:00
Joakim Sørensen
953e3e060b Add version for service devices (#11436) 2022-01-26 10:29:34 +01:00
Bram Kragten
c37f660718 Update translations 2022-01-26 09:54:48 +01:00
Philip Allgaier
02754369a6 Update history and logbook panel path when making selections (#11428) 2022-01-26 09:39:29 +01:00
Philip Allgaier
0df9e9932f Fix 24:XX time issue in Chrome (#11426) 2022-01-26 09:37:09 +01:00
Paulus Schoutsen
eddb392ad0 Always show QR code, and with white bg (#11434) 2022-01-26 00:17:23 -08:00
Philip Allgaier
e8ba349447 Fix border-radius for progress button success and error (#11432) 2022-01-25 19:39:23 -06:00
Bram Kragten
5be22d46ab Don't add quickbar to history (#11429) 2022-01-25 19:46:17 +00:00
Philip Allgaier
ffaff30b46 Fix various supervisor tooltip and aria-label issues (#10878) 2022-01-25 16:36:35 +00:00
Paulus Schoutsen
069f08b55e Bumped version to 20211229.1 2022-01-10 15:30:53 -08:00
Bram Kragten
204ccf8b40 Wait with navigate until history.back is done (#11152)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-01-10 15:30:48 -08:00
Bram Kragten
0ab8f8fd7c Merge pull request #11043 from home-assistant/dev 2021-12-29 10:51:08 +01:00
Bram Kragten
9b0b2c5b71 Merge pull request #11033 from home-assistant/dev 2021-12-27 20:59:22 +01:00
Bram Kragten
0800c702fb Merge pull request #10981 from home-assistant/dev 2021-12-20 14:01:20 +01:00
Bram Kragten
b7bd7c1065 Merge pull request #10930 from home-assistant/dev 2021-12-15 13:48:42 +01:00
Bram Kragten
61bae5da64 Merge pull request #10880 from home-assistant/dev 2021-12-12 13:49:22 +01:00
Bram Kragten
bdd13db8cf Merge pull request #10869 from home-assistant/dev 2021-12-11 17:32:55 +01:00
Paulus Schoutsen
cdc3d11181 Merge pull request #10846 from home-assistant/dev 2021-12-09 14:05:30 -08:00
Paulus Schoutsen
8f729e2a95 Merge pull request #10818 from home-assistant/dev 2021-12-06 15:21:37 -08:00
Paulus Schoutsen
bc9195f7d5 20211203.0 (#10788)
* Fix thingktalk dialog (#10600)

* Add picture uploader to area (#10544)

* Update image-cropper-dialog.ts

* WebRTC fix for Safari (#10602)

* Update MDI to v6.5.95 (#10618)

* Remove deprecated icons (#10622)

* Improve startup experience by removing AppBar skeleton (#10569)

* Correct ZHA LQI sort in device children dialog (#10616)

* Remove add-on store tab (#10624)

* Add markers-updated to ha-locations-editor (#10601)

* Use ha-form for onboarding-create-user (#10604)

* Fix datatable checkbox width (#10631)

* Move updates (#10626)

* Add correct button label to "no_state" statistics fix dialog (#10628)

* Update Lovelace Cast app ID (#10592)

* Cast fixes (#10598)

* Remove customize UI (#10632)

* Show updates on dashboard for dev (#10637)

* Area Card (#10141)

Co-authored-by: Philip Allgaier <mail@spacegaier.de>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Bumped version to 20211117.0

* Fix back button color (#10650)

* Fix active tab (#10654)

* Remove ha-alert actionText (#10646)

* Use ha-formfield around backup checkbox (#10653)

* Simplify launch screen svg (#10643)

* Always render groups/areas in a single column (#10655)

* Send error message to sender (#10660)

* Add frequency device class for sensor (#10621)

* Fix color over slotted image in ha-alert (#10652)

* Make ha-chip-set slot-able (#10647)

* Remove core note on update page (#10661)

* Add iconColor to ha-config-navigation entries (#10658)

* Use white for icons with backgound (#10672)

* Fix color overlay in ha-alert content (#10674)

* Add scenes and scripts as buttons in footer of area cards (#10673)

* Add scenes and scripts as chips in footer of area cards

* Remove unused chips config type

* Update src/panels/lovelace/common/generate-lovelace-config.ts

Co-authored-by: Zack Barett <arnett.zackary@gmail.com>

* Fix typing

Co-authored-by: Zack Barett <arnett.zackary@gmail.com>

* Fix dark main-content and split gallery demo (#10675)

* Make "Show more" show everything starting from yesterday (#10533)

* Use component to ensure relative-time in Glance card gets updated (#10666)

* Limit setting up supervisor subscriptions to the supervisor panel (#10680)

* Remove first part of the update description (#10669)

* Fixing typo in #10626 (#10686)

* Bumped version to 20211123.0

* Update background colors of navigation icons (#10691)

* Render update card on add-on page (#10681)

* Fix addon slug (#10693)

* Improve device information when via device is unknown (#10685)

* Don't make button disabled on error (#10699)

* Use app-header-text-color (#10711)

* Finish up config changes (#10710)

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

* Fix typo on config page + adjust icon color (#10713)

* Add ha-faded (#10651)

* Use `ha-icon-button` in `ha-icon-overflow-menu` (#10692)

* Prevent errors in `more-info-climate` if no modes are provided despite support flags (#10694)

* Make "Energy distribution today" translatable (#10696)

* Default to yaml editing when there are multiple states in condition (#10481)

* Filter out disabled entities in the statistics dev tools (#10677)

* Convert cover UI to Lit + ensure proper tilt rendering (#10671)

* Fixed ellipsis usage on graph legend entries. (#10707)

* Ensure required translations are loaded in safe-mode (#10709)

* Ensure markdown card input is a string (#10705)

* Fix chip text color variable overrides (#10722)

* Ensure `conditional` rows getting `state_color` value (#10708)

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

* Fixed invalid hour handling in AMPM mode (#10717)

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

* Installation type property during onboarding was misspelled (#10721)

* Dashboard tweaks (#10729)

* Tweak how scenes behave in generated lovelace (#10730)

* Bumped version to 20211130.0

* Improve hls stream view error handling (#10714)

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

* Move companion app config from sidebar to configuration dashboard (#10733)

* Move companion app config from sidebar to configuration dashboard

* Remove translation refrence

* Fix typo (#10734)

* Revert 10711 (#10736)

* Use backend for day month stats in energy dashboard (#10728)

* Handle 0 updates and show back on supervisor panels (#10744)

* Hide ha-icon-next if narrow (#10746)

* Change the area of scenes in editor (#10731)

* Fix faded element in change log (#10737)

* Updated text (#10747)

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

* Focus Add-ons & Backups in config panel when clicking Supervisor in sidebar (#10745)

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

* Add SmartStart/QR scan support for Z-Wave JS (#10726)

* Show disabled entity names on the device page (#10743)

* Show disabled entity names on the device page

* Update src/panels/config/devices/device-detail/ha-device-entities-card.ts

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

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

* Bumped version to 20211201.0

* Fix pointer/more-info inconsistencies for entity rows (#10025)

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

* Make graph colors themable (#10698)

* Use puzzle for addons and blur entries on click (#10755)

* Fix create backup checkbox (#10756)

* Use unit system definitions for weather units (#10657)

* handle ha-radio and ha-checkbox in ha-formfield (#10759)

* Fix SU sidebar issues (#10757)

* Use add-ons for mobile header (#10760)

* Hide updates for dev as well (#10761)

* Remove thingtalk cleanup create new automation dialog (#10748)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Add missing translation (#10769)

* Update hui-graph-header-footer.ts (#10476)

* Group entities in area card by domain (#10767)

* Group entities in area card by domain

* Update hui-area-card.ts

* Update

* Add background color when no image

* Add camera support

* exclude unavailable states

* Update hui-area-card.ts

* Use chips for button rows (#10770)

* Bumped version to 20211202.0

* Show add devices fab on devices page for ZJS (#10771)

* Add default icons for button entities (#10774)

* Remove handling of the supervisor panel from the sidebar (#10773)

* Tweak ZJS dashboard (#10772)

* Guard for non numeric states (#10775)

Co-authored-by: Joakim Sørensen <joasoe@gmail.com>

* Use correct styling for cloud certificate dialog (#10782)

* Allow overriding device class (#10777)

* Restore flex alignment for select and input-select rows (#10783)

* Add support for local only users (#10784)

Co-authored-by: Joakim Sørensen <joasoe@gmail.com>

* Differentiate between assigned and targeting scene/automations/script (#10781)

* Add provisioned device overview to zwave js (#10785)

* Use groupBy (#10786)

* Ensure we always have an active theme name (fixes dark theme issues) (#10780)

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

* safari doesnt support overflow-wrap: anywhere

* Fix entity marker (#10787)

* Bumped version to 20211203.0

Co-authored-by: Allen Porter <allen@thebends.org>
Co-authored-by: Michael Irigoyen <michael@irigoyen.dev>
Co-authored-by: Lasse Rosenow <10547444+LasseRosenow@users.noreply.github.com>
Co-authored-by: David F. Mulcahey <david.mulcahey@me.com>
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
Co-authored-by: Philip Allgaier <mail@spacegaier.de>
Co-authored-by: Zack Barett <arnett.zackary@gmail.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
Co-authored-by: Laszlo Magyar <lmagyar1973@gmail.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Nathan Orick <cnathanorick@gmail.com>
Co-authored-by: Luca Cavalli <lcavalli@users.noreply.github.com>
Co-authored-by: amitfin <amittein@gmail.com>
Co-authored-by: Matthias de Baat <hello@matthiasdebaat.com>
Co-authored-by: rianadon <ryanadolf123@gmail.com>
Co-authored-by: Carlos Garcia Saura <CarlosGS@users.noreply.github.com>
2021-12-03 09:36:27 -08:00
Bram Kragten
7f1a321075 Merge pull request #10590 from home-assistant/dev 2021-11-09 22:06:29 +01:00
Bram Kragten
72b9f8636d Merge pull request #10578 from home-assistant/dev 2021-11-08 18:54:44 +01:00
Bram Kragten
c9cd316c0c Play dummy media to prevent app from closing (#10531) 2021-11-08 13:04:22 +01:00
Bram Kragten
6cf3580fb4 Merge pull request #10506 from home-assistant/dev 2021-11-03 11:02:34 +01:00
Bram Kragten
5d91aefb55 Merge pull request #10453 from home-assistant/dev 2021-10-28 20:24:05 +02:00
Bram Kragten
e3c0530941 Merge pull request #10426 from home-assistant/dev 2021-10-27 21:16:47 +02:00
Paulus Schoutsen
2c9223ed80 Merge pull request #10415 from home-assistant/dev (#10415)
* Use MWC components for ha-form (#10120)

* Dont create icon for supervisor (#10191)

* Fix import (#10206)

* Add "gas" device_class to customize (and sort existing ones) (#10196)

* Make zone names readable on map in dark mode (#10195)

* Tweak ha-form (#10194)

* Extract black/white row into component (#10212)

* Extract black/white row into component

* Remove unused import

* Fix dirty check/leaving automation editor (#10211)

* Add selector demo to gallery (#10213)

* Fix icon overlay for person badges (#10201)

* Convert iframe panel to Lit (#10216)

* Allow disabling an ha-form (#10218)

* Fix alarm panel badge (#10221)

* Add missing validation text (#10225)

* Apply flat polyfill globally (#10222)

* Add ha-bar to gallery (#10242)

* Handle text overflow for tabs (#10239)

* Remove "battery" device class from fixed icon list (#10246)

* Add ha-chip to gallery (#10252)

* Add netlify build script for gallery (#10253)

* Add ha-label-badge to gallery (#10248)

* Use correct build url (#10258)

* Remove "Hass.io" from translation (#10257)

* Update demo template (#10256)

* Add WebRTC stream player (#10193)

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

* Add tamper device class for binary sensor (#10268)

* Fix missing translatable energy texts (#10230)

* Consolidate all icon button logic into `<ha-icon-button>` + ensure tooltip (#9230)

* Fix sizing / positioning error for trace graph node with subsequent branches (#10049)

* Initial support for entity category (#10266)

* Add support for device configuration URL (#10251)

* Add support for device configuration URL

* Lint

* Tweak text

* Bump mdc/mwc to 0.25.2 (#10271)

* Bumped version to 20211014.0

* Warn if iframe won't be able to load the website (#10217)

* Disable ha-form while submitting entry flow (#10290)

* Convert all warning classes to ha-alert (#10289)

* ABC automation types + use MWC (#10287)

* Add "capitalize" option to `hui-timestamp-display` (#10280)

* Add additional binary device classes to inversion list (#10152)

* Fix energy onboarding `add_solar_production` button (#10275) (#10286)

* Unify default dashboard name (#10254)

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

* Fix icon buttons in Safari (#10293)

* Only render badge value if there is no icon and no image (#10310)

* Update MDI to v6.3.95 (#10313)

* Rename `stream_type` to `frontend_stream_type` (#10298)

* Fix translation key energy distribution solar (#10316)

* Prevent mwc-list-item from opening up quick-bar (#10317)

* Remove element resize hook (#10300)

* Improve WebRTC stream error handling and cleanup (#10302)

* Fix formatting of weather extrema temperatures (#10306)

* Ensure current active dark modes gets used for manually set themes (#10307)

* Add views dropdown and footer actions to the "move to view" dialog (#10172)

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

* Icon Picker (#10161)

* Use maxLiveSyncPlaybackRate in ha-hls-player (#10323)

* Revise grid neutrality energy dashboard card, modify energy dashboard presentation to match (#10054)

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

* Fix `ha-icon-button` in `ha-file-upload` (#10328)

* Use error for protection mode alert (#10315)

* Change unsupported reason container to software (#10325)

* Migrate all paper checkbox elements to mwc (#10329)

* Migrate all paper-radio elements to mwc-radio (#10327)

* Correct grid neutrality card tooltip, make consistent with new colors (#10326)

* Fix select options for add-on config (#10330)

* Migrate all paper dialogs to mwc (#10333)

* Stack gas and solar sources (#10244)

* Set default value when enabling optional value (#10247)

* Fix overflow icon color in backup dialog (#10331)

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

* Convert default state icons (#10223)

* Convert default state icons

* update

* Update cast/src/launcher/layout/hc-cast.ts

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

* Update ha-config-core.js

* Update

* Finish

* Add siren icon

* FIx

* Add curtain icons

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

* Use secondary-text-color for trailing icon (#10340)

* Use svg icons for default panels (#10342)

* Tweak icon picker a bit (#10319)

* Add support for `no-state` and `entity-no-longer-available` statistic… (#10345)

* Change dark mode input fill color (#10341)

* Replace paper progress with mwc-linear-progess (#10339)

* Bumped version to 20211020.0

* Add auto slider/box mode to number entity (#10272)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Correct automation editor event action translation (#10355)

* Convert cloud account config to Lit (#10350)

* Restore proper state badge image behavior (#10369)

* Add to do list support to markdown (#10129)

* Catch error if input_datetime state is incorrect (#10237)

* Update MDI to v6.4.95 (#10389)

* Remove deprecated icons that where replaced (#10371)

* Make all automation type pickers use natural width to be able to show… (#10391)

* Trim device name from entities on device page (#10285)

* Update markdown card to allow word to be broken (#10387)

* Fix Full Calendar Background color (#10373)

* Add additional properties to zwave_js device info panel (#10132)

* Fix various `slugify()` issues + add tests (#10383)

* Add stopPropagation to move click handlers (#10379)

* Use ha-chip for alarm control panel card (#10393)

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

* Fix timezone issues with date formatting for ES5 (#10370)

* Add automation editor to gallery (#10392)

* Use ha-chip instead of ha-label-badge for add-on capabilities (#10398)

* Do not close edit dialog when more info is escaped (#10249)

* Ensure Sortable is recreated when card editors are reopened (#10382)

* Ensure explicit `false` values from customize form get stored (#10381)

* Add running device class to binary sensor (#10400)

* Ensure consistent card look on device config page (#10386)

* Add "Keep me logged in" checkbox within login flow (#10226)

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

* Update delay label (#10284)

* Introduced ha-icon-overflow-menu component (#10352)

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

* Use ha-alert to warn about logs from custom integrations (#10396)

* Add support for hiding current weather in forecast card (#10267)

* Allow configuration_url to point to an internal panel (#10395)

* Bump Lit (#10409)

* Bump format js (#10405)

* Bump codemirror (#10404)

* Bump and patch material elements (#10406)

* Add blueprint scripts (#9504)

* Make device classes in logbook translatable (#10376)

* Improve device info add to Lovelace (#10413)

* Add navigation option from more-info to history (#9717)

* Move entities to center column on device page (#10412)

* Bumped version to 20211026.0

* Shrink new section titles in more-info dialog a bit (#10414)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
Co-authored-by: Philip Allgaier <mail@spacegaier.de>
Co-authored-by: Jack Wilsdon <jack.wilsdon@gmail.com>
Co-authored-by: Josh McCarty <josh@joshmccarty.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Allen Porter <allen.porter@gmail.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: chriss158 <edgi@arcor.de>
Co-authored-by: Kyle Niewiada <aav7fl@users.noreply.github.com>
Co-authored-by: MartinT <44962077+MartinTuroci@users.noreply.github.com>
Co-authored-by: Michael Irigoyen <michael@irigoyen.dev>
Co-authored-by: Allen Porter <allen@thebends.org>
Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
Co-authored-by: uvjustin <46082645+uvjustin@users.noreply.github.com>
Co-authored-by: Will Adler <will@wtadler.com>
Co-authored-by: Rogério Ribeiro <zroger499@gmail.com>
Co-authored-by: Zack Barett <arnett.zackary@gmail.com>
Co-authored-by: Raman Gupta <7243222+raman325@users.noreply.github.com>
Co-authored-by: Nathan Orick <cnathanorick@gmail.com>
Co-authored-by: Tobias Kündig <tobias@offline.ch>
Co-authored-by: Marc Hörsken <mback2k@users.noreply.github.com>
2021-10-26 13:35:46 -07:00
146 changed files with 18206 additions and 5480 deletions

View File

@@ -41,7 +41,7 @@ jobs:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
- name: Build and release package
run: |
python3 -m pip install twine
python3 -m pip install twine build
export TWINE_USERNAME="__token__"
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"

View File

@@ -1,5 +1,4 @@
include README.md
include LICENSE.md
graft hass_frontend
graft hass_frontend_es5
recursive-exclude * *.py[co]

View File

@@ -26,11 +26,11 @@ module.exports = {
},
version() {
const version = fs
.readFileSync(path.resolve(paths.polymer_dir, "setup.py"), "utf8")
.match(/\d{8}\.\d+/);
.readFileSync(path.resolve(paths.polymer_dir, "setup.cfg"), "utf8")
.match(/version\W+=\W(\d{8}\.\d)/);
if (!version) {
throw Error("Version not found");
}
return version[0];
return version[1];
},
};

View File

@@ -188,6 +188,7 @@ class HaGallery extends LitElement {
.sidebar details {
margin-top: 1em;
margin-left: 1em;
}
.sidebar summary {

View File

@@ -17,7 +17,7 @@ We want to make it as easy for designers to contribute as it is for developers.
- Meet us at <a href="https://discord.gg/BPBc8rZ9" rel="noopener noreferrer" target="_blank">devs_ux Discord</a>. Feel free to share your designs, user test or strategic ideas.
- Start designing with our <a href="https://www.figma.com/community/file/967153512097289521/Home-Assistant-DesignKit" rel="noopener noreferrer" target="_blank">Figma DesignKit</a>.
- Find the lates UX <a href="https://github.com/home-assistant/frontend/labels/ux" rel="noopener noreferrer" target="_blank">discussions</a> and <a href="https://github.com/home-assistant/frontend/discussions?discussions_q=label%3Aux" rel="noopener noreferrer" target="_blank">issues</a> on GitHub. Everyone can start a new issue or discussion!
- Find the lates UX <a href="https://github.com/home-assistant/frontend/discussions?discussions_q=label%3Aux" rel="noopener noreferrer" target="_blank">discussions</a> and <a href="https://github.com/home-assistant/frontend/labels/ux" rel="noopener noreferrer" target="_blank">issues</a> on GitHub. Everyone can start a new issue or discussion!
## Developers

View File

@@ -114,7 +114,7 @@ class HassioAddonConfig extends LitElement {
<div class="card-menu">
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
<ha-icon-button
.label=${this.hass.localize("common.menu")}
.label=${this.supervisor.localize("common.menu")}
.path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>

View File

@@ -191,7 +191,7 @@ export class HassioBackups extends LitElement {
@action=${this._handleAction}
>
<ha-icon-button
.label=${this.hass.localize("common.menu")}
.label=${this.supervisor?.localize("common.menu")}
.path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>

View File

@@ -17,27 +17,27 @@ export class DialogHassioBackupUpload
{
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _params?: HassioBackupUploadDialogParams;
@state() private _dialogParams?: HassioBackupUploadDialogParams;
public async showDialog(
params: HassioBackupUploadDialogParams
dialogParams: HassioBackupUploadDialogParams
): Promise<void> {
this._params = params;
this._dialogParams = dialogParams;
await this.updateComplete;
}
public closeDialog(): void {
if (this._params && !this._params.onboarding) {
if (this._params.reloadBackup) {
this._params.reloadBackup();
if (this._dialogParams && !this._dialogParams.onboarding) {
if (this._dialogParams.reloadBackup) {
this._dialogParams.reloadBackup();
}
}
this._params = undefined;
this._dialogParams = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._params) {
if (!this._dialogParams) {
return html``;
}
@@ -47,14 +47,20 @@ export class DialogHassioBackupUpload
scrimClickAction
escapeKeyAction
hideActions
.heading=${true}
.heading=${this.hass?.localize(
"ui.panel.page-onboarding.restore.upload_backup"
) || "Upload backup"}
@closed=${this.closeDialog}
>
<div slot="heading">
<ha-header-bar>
<span slot="title"> Upload backup </span>
<span slot="title"
>${this.hass?.localize(
"ui.panel.page-onboarding.restore.upload_backup"
) || "Upload backup"}</span
>
<ha-icon-button
.label=${this.hass?.localize("common.close") || "close"}
.label=${this.hass?.localize("ui.common.close") || "Close"}
.path=${mdiClose}
slot="actionItems"
dialogAction="cancel"
@@ -71,7 +77,7 @@ export class DialogHassioBackupUpload
private _backupUploaded(ev) {
const backup = ev.detail.backup;
this._params?.showBackup(backup.slug);
this._dialogParams?.showBackup(backup.slug);
this.closeDialog();
}

View File

@@ -48,9 +48,9 @@ class HassioBackupDialog
@query("supervisor-backup-content")
private _backupContent!: SupervisorBackupContent;
public async showDialog(params: HassioBackupDialogParams) {
this._backup = await fetchHassioBackupInfo(this.hass, params.slug);
this._dialogParams = params;
public async showDialog(dialogParams: HassioBackupDialogParams) {
this._backup = await fetchHassioBackupInfo(this.hass, dialogParams.slug);
this._dialogParams = dialogParams;
this._restoringBackup = false;
}
@@ -71,13 +71,13 @@ class HassioBackupDialog
open
scrimClickAction
@closed=${this.closeDialog}
.heading=${true}
.heading=${this._backup.name}
>
<div slot="heading">
<ha-header-bar>
<span slot="title">${this._backup.name}</span>
<ha-icon-button
.label=${this.hass?.localize("common.close") || "close"}
.label=${this.hass?.localize("ui.common.close") || "Close"}
.path=${mdiClose}
slot="actionItems"
dialogAction="cancel"
@@ -114,12 +114,20 @@ class HassioBackupDialog
@closed=${stopPropagation}
>
<ha-icon-button
.label=${this.hass!.localize("common.menu")}
.label=${this.hass!.localize("ui.common.menu") || "Menu"}
.path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>
<mwc-list-item>Download Backup</mwc-list-item>
<mwc-list-item class="error">Delete Backup</mwc-list-item>
<mwc-list-item
>${this._dialogParams.supervisor?.localize(
"backup.download_backup"
)}</mwc-list-item
>
<mwc-list-item class="error"
>${this._dialogParams.supervisor?.localize(
"backup.delete_backup_title"
)}</mwc-list-item
>
</ha-button-menu>`
: ""}
</ha-dialog>

View File

@@ -30,8 +30,8 @@ class HassioCreateBackupDialog extends LitElement {
@query("supervisor-backup-content")
private _backupContent!: SupervisorBackupContent;
public showDialog(params: HassioCreateBackupDialogParams) {
this._dialogParams = params;
public showDialog(dialogParams: HassioCreateBackupDialogParams) {
this._dialogParams = dialogParams;
this._creatingBackup = false;
}
@@ -57,7 +57,7 @@ class HassioCreateBackupDialog extends LitElement {
)}
>
${this._creatingBackup
? html` <ha-circular-progress active></ha-circular-progress>`
? html`<ha-circular-progress active></ha-circular-progress>`
: html`<supervisor-backup-content
.hass=${this.hass}
.supervisor=${this._dialogParams.supervisor}

View File

@@ -39,8 +39,8 @@ class HassioHardwareDialog extends LitElement {
@state() private _filter?: string;
public showDialog(params: HassioHardwareDialogParams) {
this._dialogParams = params;
public showDialog(dialogParams: HassioHardwareDialogParams) {
this._dialogParams = dialogParams;
}
public closeDialog() {
@@ -65,14 +65,16 @@ class HassioHardwareDialog extends LitElement {
scrimClickAction
hideActions
@closed=${this.closeDialog}
.heading=${true}
.heading=${this._dialogParams.supervisor.localize(
"dialog.hardware.title"
)}
>
<div class="header" slot="heading">
<h2>
${this._dialogParams.supervisor.localize("dialog.hardware.title")}
</h2>
<ha-icon-button
.label=${this.hass.localize("common.close")}
.label=${this._dialogParams.supervisor.localize("common.close")}
.path=${mdiClose}
dialogAction="close"
></ha-icon-button>

View File

@@ -94,7 +94,7 @@ export class DialogHassioNetwork
open
scrimClickAction
escapeKeyAction
.heading=${true}
.heading=${this.supervisor.localize("dialog.network.title")}
hideActions
@closed=${this.closeDialog}
>
@@ -104,7 +104,7 @@ export class DialogHassioNetwork
${this.supervisor.localize("dialog.network.title")}
</span>
<ha-icon-button
.label=${this.hass.localize("common.close")}
.label=${this.supervisor.localize("common.close")}
.path=${mdiClose}
slot="actionItems"
dialogAction="cancel"

View File

@@ -186,7 +186,7 @@ class HassioHostInfo extends LitElement {
<ha-button-menu corner="BOTTOM_START">
<ha-icon-button
.label=${this.hass.localize("common.menu")}
.label=${this.supervisor.localize("common.menu")}
.path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>

View File

@@ -33,8 +33,12 @@ import {
extractApiErrorMessage,
ignoreSupervisorError,
} from "../../../src/data/hassio/common";
import { updateOS } from "../../../src/data/hassio/host";
import { updateSupervisor } from "../../../src/data/hassio/supervisor";
import { fetchHassioHassOsInfo, updateOS } from "../../../src/data/hassio/host";
import {
fetchHassioHomeAssistantInfo,
fetchHassioSupervisorInfo,
updateSupervisor,
} from "../../../src/data/hassio/supervisor";
import { updateCore } from "../../../src/data/supervisor/core";
import { StoreAddon } from "../../../src/data/supervisor/store";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
@@ -212,11 +216,22 @@ class UpdateAvailableCard extends LitElement {
: "addon";
this._updateType = updateType as updateType;
if (updateType === "addon") {
if (!this.addonSlug) {
this.addonSlug = pathPart;
}
this._loadAddonData();
switch (updateType) {
case "addon":
if (!this.addonSlug) {
this.addonSlug = pathPart;
}
this._loadAddonData();
break;
case "core":
this._loadCoreData();
break;
case "supervisor":
this._loadSupervisorData();
break;
case "os":
this._loadOsData();
break;
}
}
@@ -308,6 +323,42 @@ class UpdateAvailableCard extends LitElement {
}
}
private async _loadSupervisorData() {
try {
const supervisor = await fetchHassioSupervisorInfo(this.hass);
fireEvent(this, "supervisor-update", { supervisor });
} catch (err) {
showAlertDialog(this, {
title: this._updateType,
text: extractApiErrorMessage(err),
});
}
}
private async _loadCoreData() {
try {
const core = await fetchHassioHomeAssistantInfo(this.hass);
fireEvent(this, "supervisor-update", { core });
} catch (err) {
showAlertDialog(this, {
title: this._updateType,
text: extractApiErrorMessage(err),
});
}
}
private async _loadOsData() {
try {
const os = await fetchHassioHassOsInfo(this.hass);
fireEvent(this, "supervisor-update", { os });
} catch (err) {
showAlertDialog(this, {
title: this._updateType,
text: extractApiErrorMessage(err),
});
}
}
private async _update() {
this._error = undefined;
this._updating = true;

View File

@@ -110,7 +110,7 @@
"js-yaml": "^4.1.0",
"leaflet": "^1.7.1",
"leaflet-draw": "^1.0.4",
"lit": "^2.0.2",
"lit": "^2.1.2",
"lit-vaadin-helpers": "^0.2.1",
"marked": "^3.0.2",
"memoize-one": "^5.2.1",
@@ -168,6 +168,7 @@
"@types/leaflet-draw": "^1",
"@types/marked": "^2",
"@types/mocha": "^8",
"@types/qrcode": "^1.4.2",
"@types/sortablejs": "^1",
"@types/webspeechapi": "^0.0.29",
"@typescript-eslint/eslint-plugin": "^4.32.0",
@@ -235,10 +236,10 @@
"resolutions": {
"@polymer/polymer": "patch:@polymer/polymer@3.4.1#./.yarn/patches/@polymer/polymer/pr-5569.patch",
"@webcomponents/webcomponentsjs": "^2.2.10",
"lit": "^2.0.2",
"lit-html": "2.0.1",
"lit-element": "3.0.1",
"@lit/reactive-element": "1.0.1"
"lit": "^2.1.2",
"lit-html": "2.1.2",
"lit-element": "3.1.2",
"@lit/reactive-element": "1.2.1"
},
"main": "src/home-assistant.js",
"husky": {

3
pyproject.toml Normal file
View File

@@ -0,0 +1,3 @@
[build-system]
requires = ["setuptools~=60.5", "wheel~=0.37.1"]
build-backend = "setuptools.build_meta"

View File

@@ -11,6 +11,6 @@ yarn install
script/build_frontend
rm -rf dist
python3 setup.py -q sdist
python3 -m twine upload dist/* --skip-existing
rm -rf dist home_assistant_frontend.egg-info
python3 -m build
python3 -m twine upload dist/*.whl --skip-existing

View File

@@ -50,14 +50,14 @@ async function main(args) {
return;
}
const setup = fs.readFileSync("setup.py", "utf8");
const setup = fs.readFileSync("setup.cfg", "utf8");
const version = setup.match(/\d{8}\.\d+/)[0];
const newVersion = method(version);
console.log("Current version:", version);
console.log("New version:", newVersion);
fs.writeFileSync("setup.py", setup.replace(version, newVersion), "utf-8");
fs.writeFileSync("setup.cfg", setup.replace(version, newVersion), "utf-8");
if (!commit) {
return;

21
setup.cfg Normal file
View File

@@ -0,0 +1,21 @@
[metadata]
name = home-assistant-frontend
version = 20220203.0
author = The Home Assistant Authors
author_email = hello@home-assistant.io
license = Apache-2.0
platforms = any
description = The Home Assistant frontend
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/home-assistant/frontend
[options]
packages = find:
zip_safe = False
include_package_data = True
python_requires = >= 3.4.0
[options.packages.find]
include =
hass_frontend*

View File

@@ -1,14 +1,7 @@
from setuptools import setup, find_packages
"""
Entry point for setuptools. Required for editable installs.
TODO: Remove file after updating to pip 21.3
"""
from setuptools import setup
setup(
name="home-assistant-frontend",
version="20220124.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/frontend",
author="The Home Assistant Authors",
author_email="hello@home-assistant.io",
license="Apache-2.0",
packages=find_packages(include=["hass_frontend", "hass_frontend.*"]),
include_package_data=True,
zip_safe=False,
)
setup()

View File

@@ -184,6 +184,7 @@ export const DOMAINS_WITH_MORE_INFO = [
"person",
"remote",
"script",
"scene",
"sun",
"timer",
"vacuum",
@@ -234,7 +235,7 @@ export const DOMAINS_INPUT_ROW = [
];
/** Domains that should have the history hidden in the more info dialog. */
export const DOMAINS_MORE_INFO_NO_HISTORY = ["camera", "configurator", "scene"];
export const DOMAINS_MORE_INFO_NO_HISTORY = ["camera", "configurator"];
/** States that we consider "off". */
export const STATES_OFF = ["closed", "locked", "off"];

View File

@@ -13,14 +13,19 @@ export const formatDateTime = (dateObj: Date, locale: FrontendLocaleData) =>
const formatDateTimeMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
year: "numeric",
month: "long",
day: "numeric",
hour: useAmPm(locale) ? "numeric" : "2-digit",
minute: "2-digit",
hour12: useAmPm(locale),
})
new Intl.DateTimeFormat(
locale.language === "en" && !useAmPm(locale)
? "en-u-hc-h23"
: locale.language,
{
year: "numeric",
month: "long",
day: "numeric",
hour: useAmPm(locale) ? "numeric" : "2-digit",
minute: "2-digit",
hour12: useAmPm(locale),
}
)
);
// August 9, 2021, 8:23:15 AM
@@ -31,15 +36,20 @@ export const formatDateTimeWithSeconds = (
const formatDateTimeWithSecondsMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
year: "numeric",
month: "long",
day: "numeric",
hour: useAmPm(locale) ? "numeric" : "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: useAmPm(locale),
})
new Intl.DateTimeFormat(
locale.language === "en" && !useAmPm(locale)
? "en-u-hc-h23"
: locale.language,
{
year: "numeric",
month: "long",
day: "numeric",
hour: useAmPm(locale) ? "numeric" : "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: useAmPm(locale),
}
)
);
// 9/8/2021, 8:23 AM
@@ -50,12 +60,17 @@ export const formatDateTimeNumeric = (
const formatDateTimeNumericMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "2-digit",
hour12: useAmPm(locale),
})
new Intl.DateTimeFormat(
locale.language === "en" && !useAmPm(locale)
? "en-u-hc-h23"
: locale.language,
{
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "2-digit",
hour12: useAmPm(locale),
}
)
);

View File

@@ -13,11 +13,16 @@ export const formatTime = (dateObj: Date, locale: FrontendLocaleData) =>
const formatTimeMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
hour: "numeric",
minute: "2-digit",
hour12: useAmPm(locale),
})
new Intl.DateTimeFormat(
locale.language === "en" && !useAmPm(locale)
? "en-u-hc-h23"
: locale.language,
{
hour: "numeric",
minute: "2-digit",
hour12: useAmPm(locale),
}
)
);
// 9:15:24 PM || 21:15:24
@@ -28,12 +33,17 @@ export const formatTimeWithSeconds = (
const formatTimeWithSecondsMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
hour: useAmPm(locale) ? "numeric" : "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: useAmPm(locale),
})
new Intl.DateTimeFormat(
locale.language === "en" && !useAmPm(locale)
? "en-u-hc-h23"
: locale.language,
{
hour: useAmPm(locale) ? "numeric" : "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: useAmPm(locale),
}
)
);
// Tuesday 7:00 PM || Tuesday 19:00
@@ -42,10 +52,15 @@ export const formatTimeWeekday = (dateObj: Date, locale: FrontendLocaleData) =>
const formatTimeWeekdayMem = memoizeOne(
(locale: FrontendLocaleData) =>
new Intl.DateTimeFormat(locale.language, {
weekday: "long",
hour: useAmPm(locale) ? "numeric" : "2-digit",
minute: "2-digit",
hour12: useAmPm(locale),
})
new Intl.DateTimeFormat(
locale.language === "en" && !useAmPm(locale)
? "en-u-hc-h23"
: locale.language,
{
weekday: "long",
hour: useAmPm(locale) ? "numeric" : "2-digit",
minute: "2-digit",
hour12: useAmPm(locale),
}
)
);

View File

@@ -120,6 +120,7 @@ export const computeStateDisplay = (
if (
domain === "button" ||
domain === "input_button" ||
domain === "scene" ||
(domain === "sensor" && stateObj.attributes.device_class === "timestamp")
) {
return formatDateTime(new Date(compareState), locale);

View File

@@ -1,2 +1,10 @@
export const clamp = (value: number, min: number, max: number) =>
Math.min(Math.max(value, min), max);
// Variant that only applies the clamping to a border if the border is defined
export const conditionalClamp = (value: number, min?: number, max?: number) => {
let result: number;
result = min ? Math.max(value, min) : value;
result = max ? Math.min(value, max) : value;
return result;
};

View File

@@ -77,7 +77,7 @@ export const computeLocalize = async (
await loadPolyfillLocales(language);
// Everytime any of the parameters change, invalidate the strings cache.
// Every time any of the parameters change, invalidate the strings cache.
cache._localizationCache = {};
return (key, ...args) => {

View File

@@ -68,6 +68,7 @@ export class HaProgressButton extends LitElement {
--mdc-theme-primary: white;
background-color: var(--success-color);
transition: none;
border-radius: 4px;
}
mwc-button[raised].success {
@@ -79,6 +80,7 @@ export class HaProgressButton extends LitElement {
--mdc-theme-primary: white;
background-color: var(--error-color);
transition: none;
border-radius: 4px;
}
mwc-button[raised].error {

View File

@@ -183,12 +183,7 @@ class StateHistoryChartLine extends LitElement {
prevValues = datavalues;
};
const addDataSet = (
nameY: string,
step = false,
fill = false,
color?: string
) => {
const addDataSet = (nameY: string, fill = false, color?: string) => {
if (!color) {
color = getGraphColorByIndex(colorIndex, computedStyles);
colorIndex++;
@@ -198,7 +193,7 @@ class StateHistoryChartLine extends LitElement {
fill: fill ? "origin" : false,
borderColor: color,
backgroundColor: color + "7F",
stepped: step ? "before" : false,
stepped: "before",
pointRadius: 0,
data: [],
});
@@ -239,14 +234,12 @@ class StateHistoryChartLine extends LitElement {
addDataSet(
`${this.hass.localize("ui.card.climate.current_temperature", {
name: name,
})}`,
true
})}`
);
if (hasHeat) {
addDataSet(
`${this.hass.localize("ui.card.climate.heating", { name: name })}`,
true,
true,
computedStyles.getPropertyValue("--state-climate-heat-color")
);
// The "heating" series uses steppedArea to shade the area below the current
@@ -256,7 +249,6 @@ class StateHistoryChartLine extends LitElement {
addDataSet(
`${this.hass.localize("ui.card.climate.cooling", { name: name })}`,
true,
true,
computedStyles.getPropertyValue("--state-climate-cool-color")
);
// The "cooling" series uses steppedArea to shade the area below the current
@@ -268,22 +260,19 @@ class StateHistoryChartLine extends LitElement {
`${this.hass.localize("ui.card.climate.target_temperature_mode", {
name: name,
mode: this.hass.localize("ui.card.climate.high"),
})}`,
true
})}`
);
addDataSet(
`${this.hass.localize("ui.card.climate.target_temperature_mode", {
name: name,
mode: this.hass.localize("ui.card.climate.low"),
})}`,
true
})}`
);
} else {
addDataSet(
`${this.hass.localize("ui.card.climate.target_temperature_entity", {
name: name,
})}`,
true
})}`
);
}
@@ -318,14 +307,12 @@ class StateHistoryChartLine extends LitElement {
addDataSet(
`${this.hass.localize("ui.card.humidifier.target_humidity_entity", {
name: name,
})}`,
true
})}`
);
addDataSet(
`${this.hass.localize("ui.card.humidifier.on_entity", {
name: name,
})}`,
true,
true
);
@@ -337,9 +324,7 @@ class StateHistoryChartLine extends LitElement {
pushData(new Date(entityState.last_changed), series);
});
} else {
// Only interpolate for sensors
const isStep = domain !== "sensor";
addDataSet(name, isStep);
addDataSet(name);
let lastValue: number;
let lastDate: Date;

View File

@@ -14,9 +14,9 @@ import {
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { formatAttributeName } from "../../data/entity_attributes";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import { formatAttributeName } from "../../util/hass-attributes-util";
import "../ha-icon-button";
import "../ha-svg-icon";
import "./state-badge";

View File

@@ -12,7 +12,7 @@ import { property, state } from "lit/decorators";
import { STATES_OFF } from "../../common/const";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { UNAVAILABLE, UNAVAILABLE_STATES } from "../../data/entity";
import { UNAVAILABLE, UNAVAILABLE_STATES, UNKNOWN } from "../../data/entity";
import { forwardHaptic } from "../../data/haptics";
import { HomeAssistant } from "../../types";
import "../ha-formfield";
@@ -39,21 +39,26 @@ export class HaEntityToggle extends LitElement {
return html` <ha-switch disabled></ha-switch> `;
}
if (this.stateObj.attributes.assumed_state) {
if (
this.stateObj.attributes.assumed_state ||
this.stateObj.state === UNKNOWN
) {
return html`
<ha-icon-button
.label=${`Turn ${computeStateName(this.stateObj)} off`}
.path=${mdiFlashOff}
.disabled=${this.stateObj.state === UNAVAILABLE}
@click=${this._turnOff}
?state-active=${!this._isOn}
class=${!this._isOn && this.stateObj.state !== UNKNOWN
? "state-active"
: ""}
></ha-icon-button>
<ha-icon-button
.label=${`Turn ${computeStateName(this.stateObj)} on`}
.path=${mdiFlash}
.disabled=${this.stateObj.state === UNAVAILABLE}
@click=${this._turnOn}
?state-active=${this._isOn}
class=${this._isOn ? "state-active" : ""}
></ha-icon-button>
`;
}
@@ -63,7 +68,7 @@ export class HaEntityToggle extends LitElement {
this._isOn ? "off" : "on"
}`}
.checked=${this._isOn}
.disabled=${UNAVAILABLE_STATES.includes(this.stateObj.state)}
.disabled=${this.stateObj.state === UNAVAILABLE}
@change=${this._toggleChanged}
></ha-switch>`;
@@ -156,10 +161,11 @@ export class HaEntityToggle extends LitElement {
min-width: 38px;
}
ha-icon-button {
--mdc-icon-button-size: 40px;
color: var(--ha-icon-button-inactive-color, var(--primary-text-color));
transition: color 0.5s;
}
ha-icon-button[state-active] {
ha-icon-button.state-active {
color: var(--ha-icon-button-active-color, var(--primary-color));
}
ha-switch {

View File

@@ -1,12 +1,14 @@
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { haStyle } from "../resources/styles";
import { HomeAssistant } from "../types";
import hassAttributeUtil, {
import {
formatAttributeName,
formatAttributeValue,
} from "../util/hass-attributes-util";
STATE_ATTRIBUTES,
} from "../data/entity_attributes";
import { haStyle } from "../resources/styles";
import { HomeAssistant } from "../types";
import "./ha-expansion-panel";
@customElement("ha-attributes")
@@ -25,7 +27,7 @@ class HaAttributes extends LitElement {
}
const attributes = this.computeDisplayAttributes(
Object.keys(hassAttributeUtil.LOGIC_STATE_ATTRIBUTES).concat(
STATE_ATTRIBUTES.concat(
this.extraFilters ? this.extraFilters.split(",") : []
)
);

View File

@@ -1,141 +0,0 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { EventsMixin } from "../mixins/events-mixin";
import "./ha-icon";
import "./ha-icon-button";
/*
* @appliesMixin EventsMixin
*/
class HaClimateControl extends EventsMixin(PolymerElement) {
static get template() {
return html`
<style include="iron-flex iron-flex-alignment"></style>
<style>
/* local DOM styles go here */
:host {
@apply --layout-flex;
@apply --layout-horizontal;
@apply --layout-justified;
}
.in-flux#target_temperature {
color: var(--error-color);
}
#target_temperature {
@apply --layout-self-center;
font-size: 200%;
direction: ltr;
}
.control-buttons {
font-size: 200%;
text-align: right;
}
ha-icon-button {
--mdc-icon-size: 32px;
}
</style>
<!-- local DOM goes here -->
<div id="target_temperature">[[value]] [[units]]</div>
<div class="control-buttons">
<div>
<ha-icon-button on-click="incrementValue">
<ha-icon icon="hass:chevron-up"></ha-icon>
</ha-icon-button>
</div>
<div>
<ha-icon-button on-click="decrementValue">
<ha-icon icon="hass:chevron-down"></ha-icon>
</ha-icon-button>
</div>
</div>
`;
}
static get properties() {
return {
value: {
type: Number,
observer: "valueChanged",
},
units: {
type: String,
},
min: {
type: Number,
},
max: {
type: Number,
},
step: {
type: Number,
value: 1,
},
};
}
temperatureStateInFlux(inFlux) {
this.$.target_temperature.classList.toggle("in-flux", inFlux);
}
_round(val) {
// round value to precision derived from step
// insired by https://github.com/soundar24/roundSlider/blob/master/src/roundslider.js
const s = this.step.toString().split(".");
return s[1] ? parseFloat(val.toFixed(s[1].length)) : Math.round(val);
}
incrementValue() {
const newval = this._round(this.value + this.step);
if (this.value < this.max) {
this.last_changed = Date.now();
this.temperatureStateInFlux(true);
}
if (newval <= this.max) {
// If no initial target_temp
// this forces control to start
// from the min configured instead of 0
if (newval <= this.min) {
this.value = this.min;
} else {
this.value = newval;
}
} else {
this.value = this.max;
}
}
decrementValue() {
const newval = this._round(this.value - this.step);
if (this.value > this.min) {
this.last_changed = Date.now();
this.temperatureStateInFlux(true);
}
if (newval >= this.min) {
this.value = newval;
} else {
this.value = this.min;
}
}
valueChanged() {
// when the last_changed timestamp is changed,
// trigger a potential event fire in
// the future, as long as last changed is far enough in the
// past.
if (this.last_changed) {
window.setTimeout(() => {
const now = Date.now();
if (now - this.last_changed >= 2000) {
this.fire("change");
this.temperatureStateInFlux(false);
this.last_changed = null;
}
}, 2010);
}
}
}
customElements.define("ha-climate-control", HaClimateControl);

View File

@@ -0,0 +1,138 @@
import { mdiChevronDown, mdiChevronUp } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { conditionalClamp } from "../common/number/clamp";
import { HomeAssistant } from "../types";
import "./ha-icon";
import "./ha-icon-button";
@customElement("ha-climate-control")
class HaClimateControl extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public value!: number;
@property() public unit = "";
@property() public min?: number;
@property() public max?: number;
@property() public step = 1;
private _lastChanged?: number;
@query("#target_temperature") private _targetTemperature!: HTMLElement;
protected render(): TemplateResult {
return html`
<div id="target_temperature">${this.value} ${this.unit}</div>
<div class="control-buttons">
<div>
<ha-icon-button
.path=${mdiChevronUp}
.label=${this.hass.localize(
"ui.components.climate-control.temperature_up"
)}
@click=${this._incrementValue}
>
</ha-icon-button>
</div>
<div>
<ha-icon-button
.path=${mdiChevronDown}
.label=${this.hass.localize(
"ui.components.climate-control.temperature_down"
)}
@click=${this._decrementValue}
>
</ha-icon-button>
</div>
</div>
`;
}
protected updated(changedProperties) {
if (changedProperties.has("value")) {
this._valueChanged();
}
}
private _temperatureStateInFlux(inFlux) {
this._targetTemperature.classList.toggle("in-flux", inFlux);
}
private _round(value) {
// Round value to precision derived from step.
// Inspired by https://github.com/soundar24/roundSlider/blob/master/src/roundslider.js
const s = this.step.toString().split(".");
return s[1] ? parseFloat(value.toFixed(s[1].length)) : Math.round(value);
}
private _incrementValue() {
const newValue = this._round(this.value + this.step);
this._processNewValue(newValue);
}
private _decrementValue() {
const newValue = this._round(this.value - this.step);
this._processNewValue(newValue);
}
private _processNewValue(value) {
const newValue = conditionalClamp(value, this.min, this.max);
if (this.value !== newValue) {
this.value = newValue;
this._lastChanged = Date.now();
this._temperatureStateInFlux(true);
}
}
private _valueChanged() {
// When the last_changed timestamp is changed,
// trigger a potential event fire in the future,
// as long as last_changed is far enough in the past.
if (this._lastChanged) {
window.setTimeout(() => {
const now = Date.now();
if (now - this._lastChanged! >= 2000) {
fireEvent(this, "change");
this._temperatureStateInFlux(false);
this._lastChanged = undefined;
}
}, 2010);
}
}
static get styles(): CSSResultGroup {
return css`
:host {
display: flex;
justify-content: space-between;
}
.in-flux {
color: var(--error-color);
}
#target_temperature {
align-self: center;
font-size: 28px;
direction: ltr;
}
.control-buttons {
font-size: 24px;
text-align: right;
}
ha-icon-button {
--mdc-icon-size: 32px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-climate-control": HaClimateControl;
}
}

View File

@@ -1,6 +1,5 @@
import "@material/mwc-textfield";
import type { TextField } from "@material/mwc-textfield";
import "@material/mwc-slider";
import type { Slider } from "@material/mwc-slider";
import {
css,
@@ -14,6 +13,7 @@ import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { HaCheckbox } from "../ha-checkbox";
import { HaFormElement, HaFormIntegerData, HaFormIntegerSchema } from "./types";
import "../ha-slider";
@customElement("ha-form-integer")
export class HaFormInteger extends LitElement implements HaFormElement {
@@ -54,15 +54,16 @@ export class HaFormInteger extends LitElement implements HaFormElement {
></ha-checkbox>
`
: ""}
<mwc-slider
discrete
<ha-slider
pin
ignore-bar-touch
.value=${this._value}
.min=${this.schema.valueMin}
.max=${this.schema.valueMax}
.disabled=${this.disabled ||
(this.data === undefined && this.schema.optional)}
@change=${this._valueChanged}
></mwc-slider>
></ha-slider>
</div>
</div>
`;
@@ -168,7 +169,7 @@ export class HaFormInteger extends LitElement implements HaFormElement {
.flex {
display: flex;
}
mwc-slider {
ha-slider {
flex: 1;
}
mwc-textfield {

View File

@@ -104,7 +104,7 @@ export class HaForm extends LitElement implements HaFormElement {
return css`
.root {
margin-bottom: -24px;
overflow: auto;
overflow: clip visible;
}
.root > * {
display: block;

View File

@@ -17,6 +17,7 @@ import {
import { Selector } from "../data/selector";
import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types";
import { documentationUrl } from "../util/documentation-url";
import "./ha-checkbox";
import "./ha-icon-button";
import "./ha-selector/ha-selector";
@@ -230,7 +231,12 @@ export class HaServiceControl extends LitElement {
<p>${serviceData?.description}</p>
${this._manifest
? html` <a
href=${this._manifest.documentation}
href=${this._manifest.is_built_in
? documentationUrl(
this.hass,
`/integrations/${this._manifest.domain}`
)
: this._manifest.documentation}
title=${this.hass.localize(
"ui.components.service-control.integration_doc"
)}

View File

@@ -1,9 +1,13 @@
import "../ha-header-bar";
import { mdiArrowLeft, mdiClose } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event";
import { computeRTLDirection } from "../../common/util/compute_rtl";
import type {
MediaPickedEvent,
MediaPlayerBrowseAction,
MediaPlayerItem,
} from "../../data/media-player";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
@@ -16,6 +20,8 @@ import { MediaPlayerBrowseDialogParams } from "./show-media-browser-dialog";
class DialogMediaPlayerBrowse extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _currentItem?: MediaPlayerItem;
@state() private _navigateIds?: MediaPlayerItemId[];
@state() private _params?: MediaPlayerBrowseDialogParams;
@@ -33,11 +39,12 @@ class DialogMediaPlayerBrowse extends LitElement {
public closeDialog() {
this._params = undefined;
this._navigateIds = undefined;
this._currentItem = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._params) {
if (!this._params || !this._navigateIds) {
return html``;
}
@@ -48,8 +55,40 @@ class DialogMediaPlayerBrowse extends LitElement {
escapeKeyAction
hideActions
flexContent
.heading=${!this._currentItem
? this.hass.localize(
"ui.components.media-browser.media-player-browser"
)
: this._currentItem.title}
@closed=${this.closeDialog}
>
<ha-header-bar slot="heading">
${this._navigateIds.length > 1
? html`
<ha-icon-button
slot="navigationIcon"
.path=${mdiArrowLeft}
@click=${this._goBack}
></ha-icon-button>
`
: ""}
<span slot="title">
${!this._currentItem
? this.hass.localize(
"ui.components.media-browser.media-player-browser"
)
: this._currentItem.title}
</span>
<ha-icon-button
.label=${this.hass.localize("ui.dialogs.generic.close")}
.path=${mdiClose}
dialogAction="close"
slot="actionItems"
class="header_button"
dir=${computeRTLDirection(this.hass)}
></ha-icon-button>
</ha-header-bar>
<ha-media-player-browse
dialog
.hass=${this.hass}
@@ -64,8 +103,14 @@ class DialogMediaPlayerBrowse extends LitElement {
`;
}
private _mediaBrowsed(ev) {
private _goBack() {
this._navigateIds = this._navigateIds?.slice(0, -1);
this._currentItem = undefined;
}
private _mediaBrowsed(ev: { detail: HASSDomEvents["media-browsed"] }) {
this._navigateIds = ev.detail.ids;
this._currentItem = ev.detail.current;
}
private _mediaPicked(ev: HASSDomEvent<MediaPickedEvent>): void {
@@ -89,7 +134,7 @@ class DialogMediaPlayerBrowse extends LitElement {
}
ha-media-player-browse {
--media-browser-max-height: 100vh;
--media-browser-max-height: calc(100vh - 65px);
}
@media (min-width: 800px) {
@@ -101,10 +146,17 @@ class DialogMediaPlayerBrowse extends LitElement {
}
ha-media-player-browse {
position: initial;
--media-browser-max-height: 100vh - 72px;
--media-browser-max-height: 100vh - 137px;
width: 700px;
}
}
ha-header-bar {
--mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--mdc-theme-surface);
flex-shrink: 0;
border-bottom: 1px solid var(--divider-color, rgba(0, 0, 0, 0.12));
}
`,
];
}

File diff suppressed because it is too large Load Diff

View File

@@ -51,11 +51,13 @@ export interface CloudStatusLoggedIn {
google_registered: boolean;
google_entities: EntityFilter;
google_domains: string[];
alexa_registered: boolean;
alexa_entities: EntityFilter;
prefs: CloudPreferences;
remote_domain: string | undefined;
remote_connected: boolean;
remote_certificate: undefined | CertificateInformation;
http_use_ssl: boolean;
}
export type CloudStatus = CloudStatusNotLoggedIn | CloudStatusLoggedIn;

View File

@@ -104,18 +104,19 @@ export const localizeConfigFlowTitle = (
localize: LocalizeFunc,
flow: DataEntryFlowProgress
) => {
const placeholders = flow.context.title_placeholders || {};
const placeholderKeys = Object.keys(placeholders);
if (placeholderKeys.length === 0) {
if (
!flow.context.title_placeholders ||
Object.keys(flow.context.title_placeholders).length === 0
) {
return domainToName(localize, flow.handler);
}
const args: string[] = [];
placeholderKeys.forEach((key) => {
args.push(key);
args.push(placeholders[key]);
});
return localize(`component.${flow.handler}.config.flow_title`, ...args) ||
"name" in placeholders
? placeholders.name
: domainToName(localize, flow.handler);
return (
localize(
`component.${flow.handler}.config.flow_title`,
flow.context.title_placeholders
) ||
("name" in flow.context.title_placeholders
? flow.context.title_placeholders.name
: domainToName(localize, flow.handler))
);
};

View File

@@ -1,5 +1,6 @@
import { Connection, createCollection } from "home-assistant-js-websocket";
import { computeStateName } from "../common/entity/compute_state_name";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import { debounce } from "../common/util/debounce";
import { HomeAssistant } from "../types";
import { EntityRegistryEntry } from "./entity_registry";
@@ -54,7 +55,13 @@ export const computeDeviceName = (
device.name_by_user ||
device.name ||
(entities && fallbackDeviceName(hass, entities)) ||
hass.localize("ui.panel.config.devices.unnamed_device");
hass.localize(
"ui.panel.config.devices.unnamed_device",
"type",
hass.localize(
`ui.panel.config.devices.type.${device.entry_type || "device"}`
)
);
export const devicesInArea = (devices: DeviceRegistryEntry[], areaId: string) =>
devices.filter((device) => device.area_id === areaId);
@@ -99,3 +106,8 @@ export const subscribeDeviceRegistry = (
conn,
onChange
);
export const sortDeviceRegistryByName = (entries: DeviceRegistryEntry[]) =>
entries.sort((entry1, entry2) =>
caseInsensitiveStringCompare(entry1.name || "", entry2.name || "")
);

View File

@@ -0,0 +1,105 @@
import { html, TemplateResult } from "lit";
import { until } from "lit/directives/until";
import checkValidDate from "../common/datetime/check_valid_date";
import { formatDate } from "../common/datetime/format_date";
import { formatDateTimeWithSeconds } from "../common/datetime/format_date_time";
import { formatNumber } from "../common/number/format_number";
import { capitalizeFirstLetter } from "../common/string/capitalize-first-letter";
import { isDate } from "../common/string/is_date";
import { isTimestamp } from "../common/string/is_timestamp";
import { HomeAssistant } from "../types";
let jsYamlPromise: Promise<typeof import("../resources/js-yaml-dump")>;
export const STATE_ATTRIBUTES = [
"assumed_state",
"attribution",
"custom_ui_more_info",
"custom_ui_state_card",
"device_class",
"editable",
"emulated_hue_name",
"emulated_hue",
"entity_picture",
"friendly_name",
"haaska_hidden",
"haaska_name",
"icon",
"initial_state",
"last_reset",
"restored",
"state_class",
"supported_features",
"unit_of_measurement",
];
// Convert from internal snake_case format to user-friendly format
export function formatAttributeName(value: string): string {
value = value
.replace(/_/g, " ")
.replace(/\bid\b/g, "ID")
.replace(/\bip\b/g, "IP")
.replace(/\bmac\b/g, "MAC")
.replace(/\bgps\b/g, "GPS");
return capitalizeFirstLetter(value);
}
export function formatAttributeValue(
hass: HomeAssistant,
value: any
): string | TemplateResult {
if (value === null) {
return "—";
}
// YAML handling
if (
(Array.isArray(value) && value.some((val) => val instanceof Object)) ||
(!Array.isArray(value) && value instanceof Object)
) {
if (!jsYamlPromise) {
jsYamlPromise = import("../resources/js-yaml-dump");
}
const yaml = jsYamlPromise.then((jsYaml) => jsYaml.dump(value));
return html`<pre>${until(yaml, "")}</pre>`;
}
if (typeof value === "number") {
return formatNumber(value, hass.locale);
}
if (typeof value === "string") {
// URL handling
if (value.startsWith("http")) {
try {
// If invalid URL, exception will be raised
const url = new URL(value);
if (url.protocol === "http:" || url.protocol === "https:")
return html`<a target="_blank" rel="noreferrer" href=${value}
>${value}</a
>`;
} catch (_) {
// Nothing to do here
}
}
// Date handling
if (isDate(value, true)) {
// Timestamp handling
if (isTimestamp(value)) {
const date = new Date(value);
if (checkValidDate(date)) {
return formatDateTimeWithSeconds(date, hass.locale);
}
}
// Value was not a timestamp, so only do date formatting
const date = new Date(value);
if (checkValidDate(date)) {
return formatDate(date, hass.locale);
}
}
}
return Array.isArray(value) ? value.join(", ") : value;
}

View File

@@ -1,6 +1,7 @@
import { Connection, createCollection } from "home-assistant-js-websocket";
import { Store } from "home-assistant-js-websocket/dist/store";
import { computeStateName } from "../common/entity/compute_state_name";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import { debounce } from "../common/util/debounce";
import { HomeAssistant } from "../types";
@@ -133,3 +134,8 @@ export const subscribeEntityRegistry = (
conn,
onChange
);
export const sortEntityRegistryByName = (entries: EntityRegistryEntry[]) =>
entries.sort((entry1, entry2) =>
caseInsensitiveStringCompare(entry1.name || "", entry2.name || "")
);

View File

@@ -8,3 +8,6 @@ export interface GoogleEntity {
export const fetchCloudGoogleEntities = (hass: HomeAssistant) =>
hass.callWS<GoogleEntity[]>({ type: "cloud/google_assistant/entities" });
export const syncCloudGoogleEntities = (hass: HomeAssistant) =>
hass.callApi("POST", "cloud/google_actions/sync");

View File

@@ -88,7 +88,7 @@ export const BROWSER_PLAYER = "browser";
export type MediaClassBrowserSetting = {
icon: string;
thumbnail_ratio?: string;
layout?: string;
layout?: "grid";
show_list_images?: boolean;
};
@@ -185,15 +185,6 @@ export const browseMediaPlayer = (
media_content_type: mediaContentType,
});
export const browseLocalMediaPlayer = (
hass: HomeAssistant,
mediaContentId?: string
): Promise<MediaPlayerItem> =>
hass.callWS<MediaPlayerItem>({
type: "media_source/browse_media",
media_content_id: mediaContentId,
});
export const getCurrentProgress = (stateObj: MediaPlayerEntity): number => {
let progress = stateObj.attributes.media_position!;
@@ -321,8 +312,8 @@ export const computeMediaControls = (
return buttons.length > 0 ? buttons : undefined;
};
export const formatMediaTime = (seconds: number): string => {
if (!seconds) {
export const formatMediaTime = (seconds: number | undefined): string => {
if (seconds === undefined) {
return "";
}

View File

@@ -1,4 +1,5 @@
import { HomeAssistant } from "../types";
import { MediaPlayerItem } from "./media-player";
export interface ResolvedMediaSource {
url: string;
@@ -13,3 +14,12 @@ export const resolveMediaSource = (
type: "media_source/resolve_media",
media_content_id,
});
export const browseLocalMediaPlayer = (
hass: HomeAssistant,
mediaContentId?: string
): Promise<MediaPlayerItem> =>
hass.callWS<MediaPlayerItem>({
type: "media_source/browse_media",
media_content_id: mediaContentId,
});

View File

@@ -82,7 +82,7 @@ async function processEvent(
event: SupervisorEvent,
key: string
) {
if (event.event !== "supervisor-update" || event.update_key !== key) {
if (event.event !== "supervisor_update" || event.update_key !== key) {
return;
}

View File

@@ -116,15 +116,14 @@ class DataEntryFlowDialog extends LitElement {
params.continueFlowId
);
} catch (err: any) {
this._step = undefined;
this._params = undefined;
this.closeDialog();
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_flow.error"
),
text: this.hass.localize(
text: `${this.hass.localize(
"ui.panel.config.integrations.config_flow.could_not_load"
),
)}: ${err.message || err.body}`,
});
return;
}
@@ -177,6 +176,7 @@ class DataEntryFlowDialog extends LitElement {
});
}
this._loading = undefined;
this._step = undefined;
this._params = undefined;
this._devices = undefined;
@@ -372,15 +372,14 @@ class DataEntryFlowDialog extends LitElement {
try {
step = await this._params!.flowConfig.createFlow(this.hass, handler);
} catch (err: any) {
this._step = undefined;
this._params = undefined;
this.closeDialog();
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_flow.error"
),
text: this.hass.localize(
text: `${this.hass.localize(
"ui.panel.config.integrations.config_flow.could_not_load"
),
)}: ${err.message || err.body}`,
});
return;
} finally {
@@ -405,6 +404,15 @@ class DataEntryFlowDialog extends LitElement {
this._loading = "loading_step";
try {
this._step = await step;
} catch (err: any) {
this.closeDialog();
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_flow.error"
),
text: err.message || err.body,
});
return;
} finally {
this._loading = undefined;
}

View File

@@ -26,6 +26,7 @@ interface ShowDialogParams<T> {
dialogTag: keyof HTMLElementTagNameMap;
dialogImport: () => Promise<unknown>;
dialogParams: T;
addHistory?: boolean;
}
export interface DialogClosedParams {
@@ -124,8 +125,15 @@ export const makeDialogManager = (
element.addEventListener(
"show-dialog",
(e: HASSDomEvent<ShowDialogParams<unknown>>) => {
const { dialogTag, dialogImport, dialogParams } = e.detail;
showDialog(element, root, dialogTag, dialogParams, dialogImport);
const { dialogTag, dialogImport, dialogParams, addHistory } = e.detail;
showDialog(
element,
root,
dialogTag,
dialogParams,
dialogImport,
addHistory
);
}
);
};

View File

@@ -103,8 +103,9 @@ class MoreInfoClimate extends LitElement {
stateObj.attributes.temperature !== null
? html`
<ha-climate-control
.hass=${this.hass}
.value=${stateObj.attributes.temperature}
.units=${hass.config.unit_system.temperature}
.unit=${hass.config.unit_system.temperature}
.step=${temperatureStepSize}
.min=${stateObj.attributes.min_temp}
.max=${stateObj.attributes.max_temp}
@@ -118,8 +119,9 @@ class MoreInfoClimate extends LitElement {
stateObj.attributes.target_temp_high !== null)
? html`
<ha-climate-control
.hass=${this.hass}
.value=${stateObj.attributes.target_temp_low}
.units=${hass.config.unit_system.temperature}
.unit=${hass.config.unit_system.temperature}
.step=${temperatureStepSize}
.min=${stateObj.attributes.min_temp}
.max=${stateObj.attributes.target_temp_high}
@@ -127,8 +129,9 @@ class MoreInfoClimate extends LitElement {
@change=${this._targetTemperatureLowChanged}
></ha-climate-control>
<ha-climate-control
.hass=${this.hass}
.value=${stateObj.attributes.target_temp_high}
.units=${hass.config.unit_system.temperature}
.unit=${hass.config.unit_system.temperature}
.step=${temperatureStepSize}
.min=${stateObj.attributes.target_temp_low}
.max=${stateObj.attributes.max_temp}

View File

@@ -105,7 +105,7 @@ export class MoreInfoDialog extends LitElement {
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${true}
.heading=${name}
hideActions
data-domain=${domain}
>

View File

@@ -108,7 +108,9 @@ export class QuickBar extends LitElement {
public async showDialog(params: QuickBarParams) {
this._commandMode = params.commandMode || this._toggleIfAlreadyOpened();
this._hint = params.hint;
this._narrow = matchMedia("(max-width: 600px)").matches;
this._narrow = matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
this._initializeItemsIfNeeded();
this._opened = true;
}
@@ -137,7 +139,7 @@ export class QuickBar extends LitElement {
return html`
<ha-dialog
.heading=${true}
.heading=${this.hass.localize("ui.dialogs.quick-bar.title")}
open
@opened=${this._handleOpened}
@closed=${this.closeDialog}
@@ -154,7 +156,7 @@ export class QuickBar extends LitElement {
)}
.value=${this._commandMode ? `>${this._search}` : this._search}
.icon=${true}
.iconTrailing=${this._search !== undefined}
.iconTrailing=${this._search !== undefined || this._narrow}
@input=${this._handleSearchChange}
@keydown=${this._handleInputKeyDown}
@focus=${this._setFocusFirstListItem}
@@ -174,24 +176,27 @@ export class QuickBar extends LitElement {
.path=${mdiMagnify}
></ha-svg-icon>
`}
${this._search &&
html`
<ha-icon-button
slot="trailingIcon"
@click=${this._clearSearch}
.label=${this.hass!.localize("ui.common.clear")}
.path=${mdiClose}
></ha-icon-button>
`}
${this._search || this._narrow
? html`
<div slot="trailingIcon">
${this._search &&
html`<ha-icon-button
@click=${this._clearSearch}
.label=${this.hass!.localize("ui.common.clear")}
.path=${mdiClose}
></ha-icon-button>`}
${this._narrow
? html`
<mwc-button
.label=${this.hass!.localize("ui.common.close")}
@click=${this.closeDialog}
></mwc-button>
`
: ""}
</div>
`
: ""}
</ha-textfield>
${this._narrow
? html`
<mwc-button
.label=${this.hass!.localize("ui.common.close")}
@click=${this.closeDialog}
></mwc-button>
`
: ""}
</div>
${!items
? html`<ha-circular-progress
@@ -210,10 +215,12 @@ export class QuickBar extends LitElement {
@keydown=${this._handleListItemKeyDown}
@selected=${this._handleSelected}
style=${styleMap({
height: `${Math.min(
items.length * (this._commandMode ? 56 : 72) + 26,
this._done ? 500 : 0
)}px`,
height: this._narrow
? "calc(100vh - 56px)"
: `${Math.min(
items.length * (this._commandMode ? 56 : 72) + 26,
this._done ? 500 : 0
)}px`,
})}
>
${scroll({
@@ -224,9 +231,7 @@ export class QuickBar extends LitElement {
})}
</mwc-list>
`}
${!this._narrow && this._hint
? html`<div class="hint">${this._hint}</div>`
: ""}
${this._hint ? html`<div class="hint">${this._hint}</div>` : ""}
</ha-dialog>
`;
}
@@ -705,6 +710,18 @@ export class QuickBar extends LitElement {
}
}
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-textfield {
--mdc-shape-small: 0;
}
}
@media all and (max-width: 450px), all and (max-height: 690px) {
.hint {
display: none;
}
}
ha-icon.entity,
ha-svg-icon.entity {
margin-left: 20px;
@@ -758,6 +775,11 @@ export class QuickBar extends LitElement {
padding: 16px 0px;
text-align: center;
}
div[slot="trailingIcon"] {
display: flex;
align-items: center;
}
`,
];
}

View File

@@ -16,5 +16,6 @@ export const showQuickBar = (
dialogTag: "ha-quick-bar",
dialogImport: loadQuickBar,
dialogParams,
addHistory: false,
});
};

View File

@@ -5,5 +5,3 @@ import "../resources/roboto";
import "../util/legacy-support";
setPassiveTouchGestures(true);
(window as any).frontendVersion = __VERSION__;

View File

@@ -29,6 +29,7 @@ import { HomeAssistant } from "../types";
import { MAIN_WINDOW_NAME } from "../data/main_window";
window.name = MAIN_WINDOW_NAME;
(window as any).frontendVersion = __VERSION__;
declare global {
interface Window {

View File

@@ -1,4 +1,4 @@
<meta name='viewport' content='width=device-width, user-scalable=no, viewport-fit=cover'>
<meta name='viewport' content='width=device-width, user-scalable=no, viewport-fit=cover, initial-scale=1'>
<style>
body {
font-family: Roboto, sans-serif;

View File

@@ -1,13 +1,17 @@
import "@material/mwc-button";
import { mdiImagePlus, mdiPencil } from "@mdi/js";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import { mdiImagePlus, mdiPencil } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket/dist/types";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import { groupBy } from "../../../common/util/group-by";
import { afterNextRender } from "../../../common/util/render-status";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
@@ -17,14 +21,19 @@ import {
deleteAreaRegistryEntry,
updateAreaRegistryEntry,
} from "../../../data/area_registry";
import { AutomationEntity } from "../../../data/automation";
import {
computeDeviceName,
DeviceRegistryEntry,
sortDeviceRegistryByName,
} from "../../../data/device_registry";
import {
computeEntityRegistryName,
EntityRegistryEntry,
sortEntityRegistryByName,
} from "../../../data/entity_registry";
import { SceneEntity } from "../../../data/scene";
import { ScriptEntity } from "../../../data/script";
import { findRelated, RelatedResult } from "../../../data/search";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../resources/styles";
@@ -35,11 +44,11 @@ import {
loadAreaRegistryDetailDialog,
showAreaRegistryDetailDialog,
} from "./show-dialog-area-registry-detail";
import { computeDomain } from "../../../common/entity/compute_domain";
import { SceneEntity } from "../../../data/scene";
import { ScriptEntity } from "../../../data/script";
import { AutomationEntity } from "../../../data/automation";
import { groupBy } from "../../../common/util/group-by";
declare type NameAndEntity<EntityType extends HassEntity> = {
name: string;
entity: EntityType;
};
@customElement("ha-config-area-page")
class HaConfigAreaPage extends LitElement {
@@ -136,10 +145,59 @@ class HaConfigAreaPage extends LitElement {
this.entities
);
const grouped = groupBy(entities, (entity) =>
// Pre-compute the entity and device names, so we can sort by them
if (devices) {
devices.forEach((entry) => {
entry.name = computeDeviceName(entry, this.hass);
});
sortDeviceRegistryByName(devices);
}
if (entities) {
entities.forEach((entry) => {
entry.name = computeEntityRegistryName(this.hass, entry);
});
sortEntityRegistryByName(entities);
}
// Group entities by domain
const groupedEntities = groupBy(entities, (entity) =>
computeDomain(entity.entity_id)
);
// Pre-compute the name also for the grouped and related entities so we can sort by them
let groupedAutomations: NameAndEntity<AutomationEntity>[] = [];
let groupedScenes: NameAndEntity<SceneEntity>[] = [];
let groupedScripts: NameAndEntity<ScriptEntity>[] = [];
let relatedAutomations: NameAndEntity<AutomationEntity>[] = [];
let relatedScenes: NameAndEntity<SceneEntity>[] = [];
let relatedScripts: NameAndEntity<ScriptEntity>[] = [];
if (isComponentLoaded(this.hass, "automation")) {
({
groupedEntities: groupedAutomations,
relatedEntities: relatedAutomations,
} = this._prepareEntities<AutomationEntity>(
groupedEntities.automation,
this._related?.automation
));
}
if (isComponentLoaded(this.hass, "scene")) {
({ groupedEntities: groupedScenes, relatedEntities: relatedScenes } =
this._prepareEntities<SceneEntity>(
groupedEntities.scene,
this._related?.scene
));
}
if (isComponentLoaded(this.hass, "script")) {
({ groupedEntities: groupedScripts, relatedEntities: relatedScripts } =
this._prepareEntities<ScriptEntity>(
groupedEntities.script,
this._related?.script
));
}
return html`
<hass-tabs-subpage
.hass=${this.hass}
@@ -208,9 +266,7 @@ class HaConfigAreaPage extends LitElement {
html`
<a href="/config/devices/device/${device.id}">
<paper-item>
<paper-item-body>
${computeDeviceName(device, this.hass)}
</paper-item-body>
<paper-item-body> ${device.name} </paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
</a>
@@ -240,9 +296,7 @@ class HaConfigAreaPage extends LitElement {
@click=${this._openEntity}
.entity=${entity}
>
<paper-item-body>
${computeEntityRegistryName(this.hass, entity)}
</paper-item-body>
<paper-item-body> ${entity.name} </paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
`
@@ -261,46 +315,36 @@ class HaConfigAreaPage extends LitElement {
? html`
<ha-card
.header=${this.hass.localize(
"ui.panel.config.devices.automation.automations"
"ui.panel.config.devices.automation.automations_heading"
)}
>
${grouped.automation?.length
${groupedAutomations?.length
? html`<h3>
${this.hass.localize(
"ui.panel.config.areas.assigned_to_area"
)}:
</h3>
${grouped.automation.map((entity) => {
const entityState = this.hass.states[
entity.entity_id
] as AutomationEntity | undefined;
return entityState
? this._renderAutomation(entityState)
: "";
})}`
${groupedAutomations.map((automation) =>
this._renderAutomation(
automation.name,
automation.entity
)
)}`
: ""}
${this._related?.automation?.filter(
(entityId) =>
!grouped.automation?.find(
(entity) => entity.entity_id === entityId
)
).length
${relatedAutomations?.length
? html`<h3>
${this.hass.localize(
"ui.panel.config.areas.targeting_area"
)}:
</h3>
${this._related.automation.map((scene) => {
const entityState = this.hass.states[scene] as
| AutomationEntity
| undefined;
return entityState
? this._renderAutomation(entityState)
: "";
})}`
${relatedAutomations.map((automation) =>
this._renderAutomation(
automation.name,
automation.entity
)
)}`
: ""}
${!grouped.automation?.length &&
!this._related?.automation?.length
${!groupedAutomations?.length && !relatedAutomations?.length
? html`
<paper-item class="no-link"
>${this.hass.localize(
@@ -318,42 +362,30 @@ class HaConfigAreaPage extends LitElement {
? html`
<ha-card
.header=${this.hass.localize(
"ui.panel.config.devices.scene.scenes"
"ui.panel.config.devices.scene.scenes_heading"
)}
>
${grouped.scene?.length
${groupedScenes?.length
? html`<h3>
${this.hass.localize(
"ui.panel.config.areas.assigned_to_area"
)}:
</h3>
${grouped.scene.map((entity) => {
const entityState =
this.hass.states[entity.entity_id];
return entityState
? this._renderScene(entityState)
: "";
})}`
${groupedScenes.map((scene) =>
this._renderScene(scene.name, scene.entity)
)}`
: ""}
${this._related?.scene?.filter(
(entityId) =>
!grouped.scene?.find(
(entity) => entity.entity_id === entityId
)
).length
${relatedScenes?.length
? html`<h3>
${this.hass.localize(
"ui.panel.config.areas.targeting_area"
)}:
</h3>
${this._related.scene.map((scene) => {
const entityState = this.hass.states[scene];
return entityState
? this._renderScene(entityState)
: "";
})}`
${relatedScenes.map((scene) =>
this._renderScene(scene.name, scene.entity)
)}`
: ""}
${!grouped.scene?.length && !this._related?.scene?.length
${!groupedScenes?.length && !relatedScenes?.length
? html`
<paper-item class="no-link"
>${this.hass.localize(
@@ -369,45 +401,30 @@ class HaConfigAreaPage extends LitElement {
? html`
<ha-card
.header=${this.hass.localize(
"ui.panel.config.devices.script.scripts"
"ui.panel.config.devices.script.scripts_heading"
)}
>
${grouped.script?.length
${groupedScripts?.length
? html`<h3>
${this.hass.localize(
"ui.panel.config.areas.assigned_to_area"
)}:
</h3>
${grouped.script.map((entity) => {
const entityState = this.hass.states[
entity.entity_id
] as ScriptEntity | undefined;
return entityState
? this._renderScript(entityState)
: "";
})}`
${groupedScripts.map((script) =>
this._renderScript(script.name, script.entity)
)}`
: ""}
${this._related?.script?.filter(
(entityId) =>
!grouped.script?.find(
(entity) => entity.entity_id === entityId
)
).length
${relatedScripts?.length
? html`<h3>
${this.hass.localize(
"ui.panel.config.areas.targeting_area"
)}:
</h3>
${this._related.script.map((scene) => {
const entityState = this.hass.states[scene] as
| ScriptEntity
| undefined;
return entityState
? this._renderScript(entityState)
: "";
})}`
${relatedScripts.map((script) =>
this._renderScript(script.name, script.entity)
)}`
: ""}
${!grouped.script?.length && !this._related?.script?.length
${!groupedScripts?.length && !relatedScripts?.length
? html`
<paper-item class="no-link"
>${this.hass.localize(
@@ -425,7 +442,51 @@ class HaConfigAreaPage extends LitElement {
`;
}
private _renderScene(entityState: SceneEntity) {
private _prepareEntities<EntityType extends HassEntity>(
entries?: EntityRegistryEntry[],
relatedEntityIds?: string[]
): {
groupedEntities: NameAndEntity<EntityType>[];
relatedEntities: NameAndEntity<EntityType>[];
} {
const groupedEntities: NameAndEntity<EntityType>[] = [];
const relatedEntities: NameAndEntity<EntityType>[] = [];
if (entries?.length) {
entries.forEach((entity) => {
const entityState = this.hass.states[
entity.entity_id
] as unknown as EntityType;
if (entityState) {
groupedEntities.push({
name: computeStateName(entityState),
entity: entityState,
});
}
});
groupedEntities.sort((entry1, entry2) =>
caseInsensitiveStringCompare(entry1.name!, entry2.name!)
);
}
if (relatedEntityIds?.length) {
relatedEntityIds.forEach((entity) => {
const entityState = this.hass.states[entity] as EntityType;
if (entityState) {
relatedEntities.push({
name: entityState ? computeStateName(entityState) : "",
entity: entityState,
});
}
});
relatedEntities.sort((entry1, entry2) =>
caseInsensitiveStringCompare(entry1.name!, entry2.name!)
);
}
return { groupedEntities, relatedEntities };
}
private _renderScene(name: string, entityState: SceneEntity) {
return html`<div>
<a
href=${ifDefined(
@@ -435,7 +496,7 @@ class HaConfigAreaPage extends LitElement {
)}
>
<paper-item .disabled=${!entityState.attributes.id}>
<paper-item-body> ${computeStateName(entityState)} </paper-item-body>
<paper-item-body> ${name} </paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
</a>
@@ -449,7 +510,7 @@ class HaConfigAreaPage extends LitElement {
</div>`;
}
private _renderAutomation(entityState: AutomationEntity) {
private _renderAutomation(name: string, entityState: AutomationEntity) {
return html`<div>
<a
href=${ifDefined(
@@ -459,7 +520,7 @@ class HaConfigAreaPage extends LitElement {
)}
>
<paper-item .disabled=${!entityState.attributes.id}>
<paper-item-body> ${computeStateName(entityState)} </paper-item-body>
<paper-item-body> ${name} </paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
</a>
@@ -473,10 +534,10 @@ class HaConfigAreaPage extends LitElement {
</div>`;
}
private _renderScript(entityState: ScriptEntity) {
private _renderScript(name: string, entityState: ScriptEntity) {
return html`<a href=${`/config/script/edit/${entityState.entity_id}`}>
<paper-item>
<paper-item-body> ${computeStateName(entityState)} </paper-item-body>
<paper-item-body> ${name} </paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
</a>`;

View File

@@ -1,5 +1,8 @@
import "@material/mwc-button";
import "@material/mwc-list/mwc-list-item";
import type { ActionDetail } from "@material/mwc-list";
import "@polymer/paper-item/paper-item-body";
import { mdiDotsVertical } from "@mdi/js";
import { LitElement, css, html, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { formatDateTime } from "../../../../common/datetime/format_date_time";
@@ -7,6 +10,8 @@ import { fireEvent } from "../../../../common/dom/fire_event";
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
import "../../../../components/buttons/ha-call-api-button";
import "../../../../components/ha-card";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-icon-button";
import {
cloudLogout,
CloudStatusLoggedIn,
@@ -21,9 +26,10 @@ import "./cloud-google-pref";
import "./cloud-remote-pref";
import "./cloud-tts-pref";
import "./cloud-webhooks";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
@customElement("cloud-account")
export class CloudAccount extends LitElement {
export class CloudAccount extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public isWide = false;
@@ -43,6 +49,23 @@ export class CloudAccount extends LitElement {
.narrow=${this.narrow}
header="Home Assistant Cloud"
>
<ha-button-menu
slot="toolbar-icon"
corner="BOTTOM_START"
@action=${this._handleMenuAction}
activatable
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<mwc-list-item>
${this.hass.localize("ui.panel.config.cloud.account.sign_out")}
</mwc-list-item>
</ha-button-menu>
<div class="content">
<ha-config-section .isWide=${this.isWide}>
<span slot="header">Home Assistant Cloud</span>
@@ -115,11 +138,6 @@ export class CloudAccount extends LitElement {
)}
</mwc-button>
</a>
<mwc-button @click=${this._handleLogout}
>${this.hass.localize(
"ui.panel.config.cloud.account.sign_out"
)}</mwc-button
>
</div>
</ha-card>
</ha-config-section>
@@ -200,6 +218,33 @@ export class CloudAccount extends LitElement {
}
}
protected override hassSubscribe() {
const googleCheck = () => {
if (!this.cloudStatus?.google_registered) {
fireEvent(this, "ha-refresh-cloud-status");
}
};
return [
this.hass.connection.subscribeEvents(() => {
if (!this.cloudStatus?.alexa_registered) {
fireEvent(this, "ha-refresh-cloud-status");
}
}, "alexa_smart_home"),
this.hass.connection.subscribeEvents(
googleCheck,
"google_assistant_command"
),
this.hass.connection.subscribeEvents(
googleCheck,
"google_assistant_query"
),
this.hass.connection.subscribeEvents(
googleCheck,
"google_assistant_sync"
),
];
}
private async _fetchSubscriptionInfo() {
this._subscription = await fetchCloudSubscriptionInfo(this.hass);
if (
@@ -211,9 +256,12 @@ export class CloudAccount extends LitElement {
}
}
private async _handleLogout() {
await cloudLogout(this.hass);
fireEvent(this, "ha-refresh-cloud-status");
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
await cloudLogout(this.hass);
fireEvent(this, "ha-refresh-cloud-status");
}
}
_computeRTLDirection(hass) {
@@ -237,7 +285,7 @@ export class CloudAccount extends LitElement {
}
.card-actions {
display: flex;
justify-content: space-between;
flex-direction: row-reverse;
}
.card-actions a {
text-decoration: none;

View File

@@ -10,7 +10,7 @@ import { CloudStatusLoggedIn, updateCloudPref } from "../../../../data/cloud";
import type { HomeAssistant } from "../../../../types";
export class CloudAlexaPref extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public cloudStatus?: CloudStatusLoggedIn;
@@ -21,6 +21,7 @@ export class CloudAlexaPref extends LitElement {
return html``;
}
const alexa_registered = this.cloudStatus.alexa_registered;
const { alexa_enabled, alexa_report_state } = this.cloudStatus!.prefs;
return html`
@@ -36,33 +37,49 @@ export class CloudAlexaPref extends LitElement {
></ha-switch>
</div>
<div class="card-content">
${this.hass!.localize("ui.panel.config.cloud.account.alexa.info")}
<ul>
<li>
<a
href="https://skills-store.amazon.com/deeplink/dp/B0772J1QKB?deviceType=app"
target="_blank"
rel="noreferrer"
>
${this.hass!.localize(
"ui.panel.config.cloud.account.alexa.enable_ha_skill"
)}
</a>
</li>
<li>
<a
href="https://www.nabucasa.com/config/amazon_alexa/"
target="_blank"
rel="noreferrer"
>
${this.hass!.localize(
"ui.panel.config.cloud.account.alexa.config_documentation"
)}
</a>
</li>
</ul>
${alexa_enabled
<p>
${this.hass!.localize("ui.panel.config.cloud.account.alexa.info")}
</p>
${!alexa_enabled
? ""
: !alexa_registered
? html`
<ha-alert
.title=${this.hass.localize(
"ui.panel.config.cloud.account.alexa.not_configured_title"
)}
>
${this.hass.localize(
"ui.panel.config.cloud.account.alexa.not_configured_text"
)}
<ul>
<li>
<a
href="https://skills-store.amazon.com/deeplink/dp/B0772J1QKB?deviceType=app"
target="_blank"
rel="noreferrer"
>
${this.hass!.localize(
"ui.panel.config.cloud.account.alexa.enable_ha_skill"
)}
</a>
</li>
<li>
<a
href="https://www.nabucasa.com/config/amazon_alexa/"
target="_blank"
rel="noreferrer"
>
${this.hass!.localize(
"ui.panel.config.cloud.account.alexa.config_documentation"
)}
</a>
</li>
</ul>
</ha-alert>
`
: html`
<div class="state-reporting">
<h3>
${this.hass!.localize(
@@ -81,18 +98,21 @@ export class CloudAlexaPref extends LitElement {
"ui.panel.config.cloud.account.alexa.info_state_reporting"
)}
</p>
`
: ""}
`}
</div>
<div class="card-actions">
<mwc-button
@click=${this._handleSync}
.disabled=${!alexa_enabled || this._syncing}
>
${this.hass!.localize(
"ui.panel.config.cloud.account.alexa.sync_entities"
)}
</mwc-button>
${alexa_registered
? html`
<mwc-button
@click=${this._handleSync}
.disabled=${!alexa_enabled || this._syncing}
>
${this.hass!.localize(
"ui.panel.config.cloud.account.alexa.sync_entities"
)}
</mwc-button>
`
: ""}
<div class="spacer"></div>
<a href="/config/cloud/alexa">
<mwc-button

View File

@@ -1,14 +1,14 @@
import "@material/mwc-button";
import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import "@material/mwc-textfield/mwc-textfield";
import type { TextField } from "@material/mwc-textfield/mwc-textfield";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property } from "lit/decorators";
import { property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/buttons/ha-call-api-button";
import "../../../../components/ha-card";
import "../../../../components/ha-alert";
import "../../../../components/ha-card";
import type { HaSwitch } from "../../../../components/ha-switch";
import { CloudStatusLoggedIn, updateCloudPref } from "../../../../data/cloud";
import { syncCloudGoogleEntities } from "../../../../data/google_assistant";
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../../types";
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
@@ -16,13 +16,16 @@ import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
export class CloudGooglePref extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public cloudStatus?: CloudStatusLoggedIn;
@property({ attribute: false }) public cloudStatus?: CloudStatusLoggedIn;
@state() private _syncing = false;
protected render(): TemplateResult {
if (!this.cloudStatus) {
return html``;
}
const google_registered = this.cloudStatus.google_registered;
const { google_enabled, google_report_state, google_secure_devices_pin } =
this.cloudStatus.prefs;
@@ -43,7 +46,9 @@ export class CloudGooglePref extends LitElement {
<p>
${this.hass.localize("ui.panel.config.cloud.account.google.info")}
</p>
${google_enabled && !this.cloudStatus.google_registered
${!google_enabled
? ""
: !google_registered
? html`
<ha-alert
.title=${this.hass.localize(
@@ -80,9 +85,30 @@ export class CloudGooglePref extends LitElement {
</ul>
</ha-alert>
`
: ""}
${google_enabled
? html`
: html`
${this.cloudStatus.http_use_ssl
? html`
<ha-alert
alert-type="warning"
.title=${this.hass.localize(
"ui.panel.config.cloud.account.google.http_use_ssl_warning_title"
)}
>
${this.hass.localize(
"ui.panel.config.cloud.account.google.http_use_ssl_warning_text"
)}
<a
href="https://www.nabucasa.com/config/google_assistant/#local-communication"
target="_blank"
rel="noreferrer"
>${this.hass.localize(
"ui.panel.config.common.learn_more"
)}</a
>
</ha-alert>
`
: ""}
<div class="state-reporting">
<h3>
${this.hass.localize(
@@ -110,32 +136,34 @@ export class CloudGooglePref extends LitElement {
${this.hass.localize(
"ui.panel.config.cloud.account.google.enter_pin_info"
)}
<paper-input
label=${this.hass.localize(
<mwc-textfield
id="google_secure_devices_pin"
.label=${this.hass.localize(
"ui.panel.config.cloud.account.google.devices_pin"
)}
id="google_secure_devices_pin"
placeholder=${this.hass.localize(
.placeholder=${this.hass.localize(
"ui.panel.config.cloud.account.google.enter_pin_hint"
)}
.value=${google_secure_devices_pin || ""}
@change=${this._pinChanged}
></paper-input>
></mwc-textfield>
</div>
`
: ""}
`}
</div>
<div class="card-actions">
<ha-call-api-button
.hass=${this.hass}
.disabled=${!google_enabled}
@hass-api-called=${this._syncEntitiesCalled}
path="cloud/google_actions/sync"
>
${this.hass.localize(
"ui.panel.config.cloud.account.google.sync_entities"
)}
</ha-call-api-button>
${google_registered
? html`
<mwc-button
@click=${this._handleSync}
.disabled=${!google_enabled || this._syncing}
>
${this.hass.localize(
"ui.panel.config.cloud.account.google.sync_entities"
)}
</mwc-button>
`
: ""}
<div class="spacer"></div>
<a href="/config/cloud/google-assistant">
<mwc-button>
${this.hass.localize(
@@ -148,24 +176,31 @@ export class CloudGooglePref extends LitElement {
`;
}
private async _syncEntitiesCalled(ev: CustomEvent) {
if (!ev.detail.success && ev.detail.response.status_code === 404) {
this._syncFailed();
private async _handleSync() {
this._syncing = true;
try {
await syncCloudGoogleEntities(this.hass!);
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize(
`ui.panel.config.cloud.account.google.${
err.status_code === 404
? "not_configured_title"
: "sync_failed_title"
}`
),
text: this.hass.localize(
`ui.panel.config.cloud.account.google.${
err.status_code === 404 ? "not_configured_text" : "sync_failed_text"
}`
),
});
fireEvent(this, "ha-refresh-cloud-status");
} finally {
this._syncing = false;
}
}
private async _syncFailed() {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.cloud.account.google.not_configured_title"
),
text: this.hass.localize(
"ui.panel.config.cloud.account.google.not_configured_text"
),
});
fireEvent(this, "ha-refresh-cloud-status");
}
private async _enableToggleChanged(ev) {
const toggle = ev.target as HaSwitch;
try {
@@ -194,7 +229,7 @@ export class CloudGooglePref extends LitElement {
}
private async _pinChanged(ev) {
const input = ev.target as PaperInputElement;
const input = ev.target as TextField;
try {
await updateCloudPref(this.hass, {
[input.id]: input.value || null,
@@ -207,7 +242,7 @@ export class CloudGooglePref extends LitElement {
"ui.panel.config.cloud.account.google.enter_pin_error"
)} ${err.message}`
);
input.value = this.cloudStatus!.prefs.google_secure_devices_pin;
input.value = this.cloudStatus!.prefs.google_secure_devices_pin || "";
}
}
@@ -225,16 +260,13 @@ export class CloudGooglePref extends LitElement {
right: auto;
left: 24px;
}
ha-call-api-button {
color: var(--primary-color);
font-weight: 500;
}
paper-input {
mwc-textfield {
width: 250px;
display: block;
margin-top: 8px;
}
.card-actions {
display: flex;
justify-content: space-between;
}
.card-actions a {
text-decoration: none;
@@ -245,6 +277,10 @@ export class CloudGooglePref extends LitElement {
.secure_devices {
padding-top: 8px;
}
.spacer {
flex-grow: 1;
}
.state-reporting {
display: flex;
margin-top: 1.5em;

View File

@@ -6,6 +6,7 @@ import {
mdiCloseBox,
mdiCloseBoxMultiple,
} from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@@ -33,9 +34,14 @@ import {
updateCloudAlexaEntityConfig,
updateCloudPref,
} from "../../../../data/cloud";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../../../data/entity_registry";
import { showDomainTogglerDialog } from "../../../../dialogs/domain-toggler/show-dialog-domain-toggler";
import "../../../../layouts/hass-loading-screen";
import "../../../../layouts/hass-subpage";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
@@ -43,7 +49,7 @@ const DEFAULT_CONFIG_EXPOSE = true;
const IGNORE_INTERFACES = ["Alexa.EndpointHealth"];
@customElement("cloud-alexa")
class CloudAlexa extends LitElement {
class CloudAlexa extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property()
@@ -53,9 +59,15 @@ class CloudAlexa extends LitElement {
@state() private _entities?: AlexaEntity[];
@property()
@state()
private _entityConfigs: CloudPreferences["alexa_entity_configs"] = {};
@state()
private _entityCategories?: Record<
string,
EntityRegistryEntry["entity_category"]
>;
private _popstateSyncAttached = false;
private _popstateReloadStatusAttached = false;
@@ -72,7 +84,7 @@ class CloudAlexa extends LitElement {
);
protected render(): TemplateResult {
if (this._entities === undefined) {
if (this._entities === undefined || this._entityCategories === undefined) {
return html` <hass-loading-screen></hass-loading-screen> `;
}
const emptyFilter = isEmptyFilter(this.cloudStatus.alexa_entities);
@@ -99,10 +111,17 @@ class CloudAlexa extends LitElement {
should_expose: null,
};
const isExposed = emptyFilter
? this._configIsExposed(entity.entity_id, config)
? this._configIsExposed(
entity.entity_id,
config,
this._entityCategories![entity.entity_id]
)
: filterFunc(entity.entity_id);
const isDomainExposed = emptyFilter
? this._configIsDomainExposed(entity.entity_id)
? this._configIsDomainExposed(
entity.entity_id,
this._entityCategories![entity.entity_id]
)
: filterFunc(entity.entity_id);
if (isExposed) {
selected++;
@@ -287,6 +306,23 @@ class CloudAlexa extends LitElement {
}
}
protected override hassSubscribe(): (
| UnsubscribeFunc
| Promise<UnsubscribeFunc>
)[] {
return [
subscribeEntityRegistry(this.hass.connection, (entries) => {
const categories = {};
for (const entry of entries) {
categories[entry.entity_id] = entry.entity_category;
}
this._entityCategories = categories;
}),
];
}
private async _fetchData() {
const entities = await fetchCloudAlexaEntities(this.hass);
entities.sort((a, b) => {
@@ -305,15 +341,26 @@ class CloudAlexa extends LitElement {
fireEvent(this, "hass-more-info", { entityId });
}
private _configIsDomainExposed(entityId: string) {
private _configIsDomainExposed(
entityId: string,
entityCategory: EntityRegistryEntry["entity_category"] | undefined
) {
const domain = computeDomain(entityId);
return this.cloudStatus.prefs.alexa_default_expose
? this.cloudStatus.prefs.alexa_default_expose.includes(domain)
? !entityCategory &&
this.cloudStatus.prefs.alexa_default_expose.includes(domain)
: DEFAULT_CONFIG_EXPOSE;
}
private _configIsExposed(entityId: string, config: AlexaEntityConfig) {
return config.should_expose ?? this._configIsDomainExposed(entityId);
private _configIsExposed(
entityId: string,
config: AlexaEntityConfig,
entityCategory: EntityRegistryEntry["entity_category"] | undefined
) {
return (
config.should_expose ??
this._configIsDomainExposed(entityId, entityCategory)
);
}
private async _exposeChanged(ev: CustomEvent<ActionDetail>) {

View File

@@ -6,6 +6,7 @@ import {
mdiCloseBox,
mdiCloseBoxMultiple,
} from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@@ -35,6 +36,10 @@ import {
updateCloudGoogleEntityConfig,
updateCloudPref,
} from "../../../../data/cloud";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../../../data/entity_registry";
import {
fetchCloudGoogleEntities,
GoogleEntity,
@@ -42,6 +47,7 @@ import {
import { showDomainTogglerDialog } from "../../../../dialogs/domain-toggler/show-dialog-domain-toggler";
import "../../../../layouts/hass-loading-screen";
import "../../../../layouts/hass-subpage";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { showToast } from "../../../../util/toast";
@@ -49,7 +55,7 @@ import { showToast } from "../../../../util/toast";
const DEFAULT_CONFIG_EXPOSE = true;
@customElement("cloud-google-assistant")
class CloudGoogleAssistant extends LitElement {
class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public cloudStatus!: CloudStatusLoggedIn;
@@ -58,9 +64,15 @@ class CloudGoogleAssistant extends LitElement {
@state() private _entities?: GoogleEntity[];
@property()
@state()
private _entityConfigs: CloudPreferences["google_entity_configs"] = {};
@state()
private _entityCategories?: Record<
string,
EntityRegistryEntry["entity_category"]
>;
private _popstateSyncAttached = false;
private _popstateReloadStatusAttached = false;
@@ -77,7 +89,7 @@ class CloudGoogleAssistant extends LitElement {
);
protected render(): TemplateResult {
if (this._entities === undefined) {
if (this._entities === undefined || this._entityCategories === undefined) {
return html` <hass-loading-screen></hass-loading-screen> `;
}
const emptyFilter = isEmptyFilter(this.cloudStatus.google_entities);
@@ -105,10 +117,17 @@ class CloudGoogleAssistant extends LitElement {
should_expose: null,
};
const isExposed = emptyFilter
? this._configIsExposed(entity.entity_id, config)
? this._configIsExposed(
entity.entity_id,
config,
this._entityCategories![entity.entity_id]
)
: filterFunc(entity.entity_id);
const isDomainExposed = emptyFilter
? this._configIsDomainExposed(entity.entity_id)
? this._configIsDomainExposed(
entity.entity_id,
this._entityCategories![entity.entity_id]
)
: filterFunc(entity.entity_id);
if (isExposed) {
selected++;
@@ -311,15 +330,43 @@ class CloudGoogleAssistant extends LitElement {
}
}
private _configIsDomainExposed(entityId: string) {
protected override hassSubscribe(): (
| UnsubscribeFunc
| Promise<UnsubscribeFunc>
)[] {
return [
subscribeEntityRegistry(this.hass.connection, (entries) => {
const categories = {};
for (const entry of entries) {
categories[entry.entity_id] = entry.entity_category;
}
this._entityCategories = categories;
}),
];
}
private _configIsDomainExposed(
entityId: string,
entityCategory: EntityRegistryEntry["entity_category"] | undefined
) {
const domain = computeDomain(entityId);
return this.cloudStatus.prefs.google_default_expose
? this.cloudStatus.prefs.google_default_expose.includes(domain)
? !entityCategory &&
this.cloudStatus.prefs.google_default_expose.includes(domain)
: DEFAULT_CONFIG_EXPOSE;
}
private _configIsExposed(entityId: string, config: GoogleEntityConfig) {
return config.should_expose ?? this._configIsDomainExposed(entityId);
private _configIsExposed(
entityId: string,
config: GoogleEntityConfig,
entityCategory: EntityRegistryEntry["entity_category"] | undefined
) {
return (
config.should_expose ??
this._configIsDomainExposed(entityId, entityCategory)
);
}
private async _fetchData() {

View File

@@ -3,7 +3,14 @@ import "@material/mwc-list/mwc-list-item";
import type { ActionDetail } from "@material/mwc-list";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/ha-card";
@@ -26,6 +33,7 @@ import "./ha-config-navigation";
import "./ha-config-updates";
import { fireEvent } from "../../../common/dom/fire_event";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import { showToast } from "../../../util/toast";
@customElement("ha-config-dashboard")
class HaConfigDashboard extends LitElement {
@@ -43,6 +51,8 @@ class HaConfigDashboard extends LitElement {
@property() public showAdvanced!: boolean;
private _notifyUpdates = false;
protected render(): TemplateResult {
return html`
<ha-app-layout>
@@ -129,6 +139,26 @@ class HaConfigDashboard extends LitElement {
`;
}
protected override updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!changedProps.has("supervisorUpdates") || !this._notifyUpdates) {
return;
}
this._notifyUpdates = false;
if (this.supervisorUpdates?.length) {
showToast(this, {
message: this.hass.localize(
"ui.panel.config.updates.updates_refreshed"
),
});
} else {
showToast(this, {
message: this.hass.localize("ui.panel.config.updates.no_new_updates"),
});
}
}
private _showQuickBar(): void {
showQuickBar(this, {
commandMode: true,
@@ -140,6 +170,7 @@ class HaConfigDashboard extends LitElement {
switch (ev.detail.index) {
case 0:
if (isComponentLoaded(this.hass, "hassio")) {
this._notifyUpdates = true;
await refreshSupervisorAvailableUpdates(this.hass);
fireEvent(this, "ha-refresh-supervisor");
return;
@@ -161,10 +192,6 @@ class HaConfigDashboard extends LitElement {
return [
haStyle,
css`
app-header {
border-bottom: var(--app-header-border-bottom);
--header-height: 55px;
}
:host(:not([narrow])) ha-card:last-child {
margin-bottom: 24px;
}

View File

@@ -8,7 +8,6 @@ import "../../../components/ha-alert";
import "../../../components/ha-logo-svg";
import "../../../components/ha-svg-icon";
import { SupervisorAvailableUpdates } from "../../../data/supervisor/root";
import { buttonLinkStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import "../../../components/ha-icon-next";
@@ -78,7 +77,7 @@ class HaConfigUpdates extends LitElement {
)}
${!this._showAll && this.supervisorUpdates.length >= 4
? html`
<button class="link show-all" @click=${this._showAllClicked}>
<button class="show-more" @click=${this._showAllClicked}>
${this.hass.localize("ui.panel.config.updates.more_updates", {
count: this.supervisorUpdates!.length - updates.length,
})}
@@ -94,7 +93,6 @@ class HaConfigUpdates extends LitElement {
static get styles(): CSSResultGroup[] {
return [
buttonLinkStyle,
css`
.title {
font-size: 16px;
@@ -125,6 +123,22 @@ class HaConfigUpdates extends LitElement {
height: 24px;
width: 24px;
}
button.show-more {
color: var(--primary-color);
text-align: left;
cursor: pointer;
background: none;
border-width: initial;
border-style: none;
border-color: initial;
border-image: initial;
padding: 16px;
font: inherit;
}
button.show-more:focus {
outline: none;
text-decoration: underline;
}
`,
];
}

View File

@@ -23,7 +23,15 @@ export class HaDeviceCard extends LitElement {
protected render(): TemplateResult {
return html`
<ha-card
.header=${this.hass.localize("ui.panel.config.devices.device_info")}
.header=${this.hass.localize(
"ui.panel.config.devices.device_info",
"type",
this.hass.localize(
`ui.panel.config.devices.type.${
this.device.entry_type || "device"
}_heading`
)
)}
>
<div class="card-content">
${this.device.model
@@ -59,7 +67,12 @@ export class HaDeviceCard extends LitElement {
? html`
<div class="extra-info">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.firmware",
`ui.panel.config.integrations.config_entry.${
this.device.entry_type === "service" &&
!this.device.hw_version
? "version"
: "firmware"
}`,
"version",
this.device.sw_version
)}

View File

@@ -82,12 +82,26 @@ class DialogDeviceRegistryDetail extends LitElement {
</ha-switch>
<div>
<div>
${this.hass.localize("ui.panel.config.devices.enabled_label")}
${this.hass.localize(
"ui.panel.config.devices.enabled_label",
"type",
this.hass.localize(
`ui.panel.config.devices.type.${
device.entry_type || "device"
}`
)
)}
</div>
<div class="secondary">
${this._disabledBy && this._disabledBy !== "user"
? this.hass.localize(
"ui.panel.config.devices.enabled_cause",
"type",
this.hass.localize(
`ui.panel.config.devices.type.${
device.entry_type || "device"
}`
),
"cause",
this.hass.localize(
`config_entry.disabled_by.${this._disabledBy}`

View File

@@ -39,6 +39,7 @@ import {
findBatteryEntity,
updateEntityRegistryEntry,
} from "../../../data/entity_registry";
import { domainToName } from "../../../data/integration";
import { SceneEntities, showSceneEditor } from "../../../data/scene";
import { findRelated, RelatedResult } from "../../../data/search";
import {
@@ -201,6 +202,10 @@ export class HaConfigDevicePage extends LitElement {
}
private async _renderDiagnosticButtons(requestId: number): Promise<void> {
if (!isComponentLoaded(this.hass, "diagnostics")) {
return;
}
const device = this._device(this.deviceId, this.devices);
if (!device) {
@@ -208,34 +213,53 @@ export class HaConfigDevicePage extends LitElement {
}
let links = await Promise.all(
this._integrations(device, this.entries)
.filter((entry) => entry.state === "loaded")
.map(async (entry) => {
const info = await fetchDiagnosticHandler(this.hass, entry.domain);
this._integrations(device, this.entries).map(async (entry) => {
if (entry.state !== "loaded") {
return false;
}
const info = await fetchDiagnosticHandler(this.hass, entry.domain);
if (!info.handlers.device && !info.handlers.config_entry) {
return "";
}
const link = info.handlers.device
if (!info.handlers.device && !info.handlers.config_entry) {
return false;
}
return {
link: info.handlers.device
? getDeviceDiagnosticsDownloadUrl(entry.entry_id, this.deviceId)
: getConfigEntryDiagnosticsDownloadUrl(entry.entry_id);
return html`
<a href=${link} @click=${this._signUrl}>
<mwc-button>
${this.hass.localize(
`ui.panel.config.devices.download_diagnostics`
)}
</mwc-button>
</a>
`;
})
: getConfigEntryDiagnosticsDownloadUrl(entry.entry_id),
domain: entry.domain,
};
})
);
links = links.filter(Boolean);
if (this._diagnosticDownloadLinks !== requestId) {
return;
}
links = links.filter(Boolean);
if (links.length > 0) {
this._diagnosticDownloadLinks = links;
this._diagnosticDownloadLinks = (
links as { link: string; domain: string }[]
).map(
(link) => html`
<a href=${link.link} @click=${this._signUrl}>
<mwc-button>
${links.length > 1
? this.hass.localize(
`ui.panel.config.devices.download_diagnostics_integration`,
{
integration: domainToName(
this.hass.localize,
link.domain
),
}
)
: this.hass.localize(
`ui.panel.config.devices.download_diagnostics`
)}
</mwc-button>
</a>
`
);
}
}
@@ -296,6 +320,10 @@ export class HaConfigDevicePage extends LitElement {
<ha-alert alert-type="warning">
${this.hass.localize(
"ui.panel.config.devices.enabled_cause",
"type",
this.hass.localize(
`ui.panel.config.devices.type.${device.entry_type || "device"}`
),
"cause",
this.hass.localize(
`ui.panel.config.devices.disabled_by.${device.disabled_by}`
@@ -485,17 +513,29 @@ export class HaConfigDevicePage extends LitElement {
<ha-card>
<h1 class="card-header">
${this.hass.localize(
"ui.panel.config.devices.automation.automations"
"ui.panel.config.devices.automation.automations_heading"
)}
<ha-icon-button
@click=${this._showAutomationDialog}
.disabled=${device.disabled_by}
.label=${device.disabled_by
? this.hass.localize(
"ui.panel.config.devices.automation.create_disabled"
"ui.panel.config.devices.automation.create_disabled",
"type",
this.hass.localize(
`ui.panel.config.devices.type.${
device.entry_type || "device"
}`
)
)
: this.hass.localize(
"ui.panel.config.devices.automation.create"
"ui.panel.config.devices.automation.create",
"type",
this.hass.localize(
`ui.panel.config.devices.type.${
device.entry_type || "device"
}`
)
)}
.path=${mdiPlusCircle}
></ha-icon-button>
@@ -547,6 +587,12 @@ export class HaConfigDevicePage extends LitElement {
"name",
this.hass.localize(
"ui.panel.config.devices.automation.automations"
),
"type",
this.hass.localize(
`ui.panel.config.devices.type.${
device.entry_type || "device"
}`
)
)}
</div>
@@ -561,7 +607,7 @@ export class HaConfigDevicePage extends LitElement {
<ha-card>
<h1 class="card-header">
${this.hass.localize(
"ui.panel.config.devices.scene.scenes"
"ui.panel.config.devices.scene.scenes_heading"
)}
<ha-icon-button
@@ -569,10 +615,22 @@ export class HaConfigDevicePage extends LitElement {
.disabled=${device.disabled_by}
.label=${device.disabled_by
? this.hass.localize(
"ui.panel.config.devices.scene.create_disabled"
"ui.panel.config.devices.scene.create_disabled",
"type",
this.hass.localize(
`ui.panel.config.devices.type.${
device.entry_type || "device"
}`
)
)
: this.hass.localize(
"ui.panel.config.devices.scene.create"
"ui.panel.config.devices.scene.create",
"type",
this.hass.localize(
`ui.panel.config.devices.type.${
device.entry_type || "device"
}`
)
)}
.path=${mdiPlusCircle}
></ha-icon-button>
@@ -627,6 +685,12 @@ export class HaConfigDevicePage extends LitElement {
"name",
this.hass.localize(
"ui.panel.config.devices.scene.scenes"
),
"type",
this.hass.localize(
`ui.panel.config.devices.type.${
device.entry_type || "device"
}`
)
)}
</div>
@@ -641,17 +705,29 @@ export class HaConfigDevicePage extends LitElement {
<ha-card>
<h1 class="card-header">
${this.hass.localize(
"ui.panel.config.devices.script.scripts"
"ui.panel.config.devices.script.scripts_heading"
)}
<ha-icon-button
@click=${this._showScriptDialog}
.disabled=${device.disabled_by}
.label=${device.disabled_by
? this.hass.localize(
"ui.panel.config.devices.script.create_disabled"
"ui.panel.config.devices.script.create_disabled",
"type",
this.hass.localize(
`ui.panel.config.devices.type.${
device.entry_type || "device"
}`
)
)
: this.hass.localize(
"ui.panel.config.devices.script.create"
"ui.panel.config.devices.script.create",
"type",
this.hass.localize(
`ui.panel.config.devices.type.${
device.entry_type || "device"
}`
)
)}
.path=${mdiPlusCircle}
></ha-icon-button>
@@ -685,6 +761,12 @@ export class HaConfigDevicePage extends LitElement {
"name",
this.hass.localize(
"ui.panel.config.devices.script.scripts"
),
"type",
this.hass.localize(
`ui.panel.config.devices.type.${
device.entry_type || "device"
}`
)
)}
</div>
@@ -1065,6 +1147,7 @@ export class HaConfigDevicePage extends LitElement {
align-self: center;
align-items: center;
display: flex;
white-space: nowrap;
}
.column > *:not(:first-child) {

View File

@@ -338,7 +338,7 @@ export class HaConfigDeviceDashboard extends LitElement {
${this.hass.localize("ui.panel.config.devices.disabled")}
</paper-tooltip>
</div>`
: "",
: "",
};
}
return columns;

View File

@@ -74,7 +74,9 @@ export class DialogEntityEditor extends LitElement {
return html`
<ha-dialog
open
.heading=${true}
.heading=${stateObj
? computeStateName(stateObj)
: entry?.name || entityId}
hideActions
@closed=${this.closeDialog}
@close-dialog=${this.closeDialog}

View File

@@ -42,7 +42,7 @@ import type { HomeAssistant } from "../../../types";
import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail";
const OVERRIDE_DEVICE_CLASSES = {
cover: ["window", "door", "garage"],
cover: ["window", "door", "garage", "gate"],
binary_sensor: ["window", "door", "garage_door", "opening"],
};

View File

@@ -76,7 +76,7 @@ export interface StateEntity extends EntityRegistryEntry {
}
export interface EntityRow extends StateEntity {
entity: HassEntity;
entity?: HassEntity;
unavailable: boolean;
restored: boolean;
status: string;
@@ -165,13 +165,13 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
);
private _columns = memoize(
(narrow, _language, showDisabled): DataTableColumnContainer => ({
(narrow, _language, showDisabled): DataTableColumnContainer<EntityRow> => ({
icon: {
title: "",
type: "icon",
template: (_, entry: any) => html`
template: (_, entry: EntityRow) => html`
<ha-state-icon
.title=${entry.entity.state}
.title=${entry.entity?.state}
slot="item-icon"
.state=${entry.entity}
></ha-state-icon>
@@ -186,7 +186,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
direction: "asc",
grows: true,
template: narrow
? (name, entity: any) =>
? (name, entity: EntityRow) =>
html`
${name}<br />
<div class="secondary">
@@ -237,7 +237,9 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
template: (disabled_by) =>
this.hass.localize(
`ui.panel.config.devices.disabled_by.${disabled_by}`
) || disabled_by,
) ||
disabled_by ||
"—",
},
status: {
title: this.hass.localize(
@@ -247,7 +249,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
sortable: true,
filterable: true,
width: "68px",
template: (_status, entity: any) =>
template: (_status, entity: EntityRow) =>
entity.unavailable || entity.disabled_by || entity.readonly
? html`
<div

View File

@@ -12,6 +12,7 @@ import {
} from "../../../data/integration";
import { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
import { documentationUrl } from "../../../util/documentation-url";
@customElement("integrations-card")
class IntegrationsCard extends LitElement {
@@ -66,7 +67,12 @@ class IntegrationsCard extends LitElement {
const manifest = this._manifests && this._manifests[domain];
const docLink = manifest
? html`<a
href=${manifest.documentation}
href=${manifest.is_built_in
? documentationUrl(
this.hass,
`/integrations/${manifest.domain}`
)
: manifest.documentation}
target="_blank"
rel="noreferrer"
>${this.hass.localize(

View File

@@ -15,6 +15,7 @@ import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box
import type { HomeAssistant } from "../../../types";
import type { DataEntryFlowProgressExtended } from "./ha-config-integrations";
import "./ha-integration-action-card";
import { documentationUrl } from "../../../util/documentation-url";
@customElement("ha-config-flow-card")
export class HaConfigFlowCard extends LitElement {
@@ -82,7 +83,12 @@ export class HaConfigFlowCard extends LitElement {
: ""}
${this.manifest
? html`<a
href=${this.manifest.documentation}
href=${this.manifest.is_built_in
? documentationUrl(
this.hass,
`/integrations/${this.manifest.domain}`
)
: this.manifest.documentation}
rel="noreferrer"
target="_blank"
>

View File

@@ -46,6 +46,7 @@ import {
} from "../../../dialogs/generic/show-dialog-box";
import { haStyle, haStyleScrollbar } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { fileDownload } from "../../../util/file_download";
import type { ConfigEntryExtended } from "./ha-config-integrations";
import "./ha-integration-header";
@@ -331,7 +332,12 @@ export class HaIntegrationCard extends LitElement {
</mwc-list-item>
${this.manifest
? html` <a
href=${this.manifest.documentation}
href=${this.manifest.is_built_in
? documentationUrl(
this.hass,
`/integrations/${this.manifest.domain}`
)
: this.manifest.documentation}
rel="noreferrer"
target="_blank"
>

View File

@@ -21,6 +21,7 @@ import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import "../../../ha-config-section";
import "../../../../../components/ha-alert";
export const ozwTabs: PageNavigation[] = [];
@@ -64,6 +65,30 @@ class OZWConfigDashboard extends LitElement {
.tabs=${ozwTabs}
back-path="/config/integrations"
>
<ha-alert
alert-type="warning"
title="This integration will stop working soon"
>
The OpenZWave integration is deprecated and will no longer receive any
updates. The technical dependencies will render this integration
unusable in the near future. We strongly advise you to migrate to the
new
<a
href="https://www.home-assistant.io/integrations/zwave_js"
target="_blank"
rel="noreferrer"
>Z-Wave JS integration</a
>.
<a
slot="action"
href="https://alerts.home-assistant.io/#ozw.markdown"
target="_blank"
rel="noreferrer"
>
<mwc-button>learn more</mwc-button>
</a>
</ha-alert>
<ha-config-section .narrow=${this.narrow} .isWide=${this.isWide}>
<div slot="header">
${this.hass.localize("ui.panel.config.ozw.select_instance.header")}
@@ -162,6 +187,13 @@ class OZWConfigDashboard extends LitElement {
:host([narrow]) ha-config-section {
margin-top: -20px;
}
ha-alert {
display: block;
margin: 16px;
}
ha-alert a {
text-decoration: none;
}
ha-card {
overflow: hidden;
}

View File

@@ -55,6 +55,7 @@ class DialogZHAReconfigureDevice extends LitElement {
this._status = undefined;
this._stages = undefined;
this._clusterConfigurationStatuses = undefined;
this._showDetails = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -225,10 +226,10 @@ class DialogZHAReconfigureDevice extends LitElement {
)}
</h2>
${this._clusterConfigurationStatuses!.size > 0
${this._clusterConfigurationStatuses?.size
? html`
${Array.from(
this._clusterConfigurationStatuses!.values()
this._clusterConfigurationStatuses.values()
).map(
(clusterStatus) => html`
<div class="grid-item">

View File

@@ -12,6 +12,7 @@ import { computeStateName } from "../../../../../common/entity/compute_state_nam
import { sortStatesByName } from "../../../../../common/entity/states_sort_by_name";
import "../../../../../components/buttons/ha-call-service-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-icon";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-icon-button-arrow-prev";
@@ -43,6 +44,14 @@ class HaConfigZwave extends LocalizeMixin(EventsMixin(PolymerElement)) {
border-bottom: 1px solid var(--divider-color);
}
ha-alert {
display: block;
margin: 16px;
}
ha-alert a {
text-decoration: none;
}
.content {
margin-top: 24px;
}
@@ -101,6 +110,30 @@ class HaConfigZwave extends LocalizeMixin(EventsMixin(PolymerElement)) {
</app-toolbar>
</app-header>
<ha-alert
alert-type="warning"
title="This integration will stop working soon"
>
This Z-Wave integration is deprecated and will no longer receive any
updates. The technical dependencies will render this integration
unusable in the near future. We strongly advise you to migrate to the
new
<a
href="https://www.home-assistant.io/integrations/zwave_js"
target="_blank"
rel="noreferrer"
>Z-Wave JS integration</a
>.
<a
slot="action"
href="https://alerts.home-assistant.io/#zwave.markdown"
target="_blank"
rel="noreferrer"
>
<mwc-button>learn more</mwc-button>
</a>
</ha-alert>
<ha-config-section is-wide="[[isWide]]">
<ha-card
class="content"

View File

@@ -21,6 +21,7 @@ import {
} from "../../../data/system_log";
import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast";
import type { SystemLogDetailDialogParams } from "./show-dialog-system-log-detail";
import { formatSystemLogTime } from "./util";
@@ -68,8 +69,18 @@ class DialogSystemLogDetail extends LitElement {
// Custom components with our official docs should not link to our docs
!this._manifest.documentation.includes("://www.home-assistant.io"));
const title = this.hass.localize(
"ui.panel.config.logs.details",
"level",
html`<span class=${item.level.toLowerCase()}
>${this.hass.localize(
"ui.panel.config.logs.level." + item.level.toLowerCase()
)}</span
>`
);
return html`
<ha-dialog open @closed=${this.closeDialog} hideActions .heading=${true}>
<ha-dialog open @closed=${this.closeDialog} hideActions .heading=${title}>
<ha-header-bar slot="heading">
<ha-icon-button
slot="navigationIcon"
@@ -77,17 +88,7 @@ class DialogSystemLogDetail extends LitElement {
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
<span slot="title">
${this.hass.localize(
"ui.panel.config.logs.details",
"level",
html`<span class=${item.level.toLowerCase()}
>${this.hass.localize(
"ui.panel.config.logs.level." + item.level.toLowerCase()
)}</span
>`
)}
</span>
<span slot="title"> ${title} </span>
<ha-icon-button
id="copy"
@click=${this._copyLog}
@@ -117,7 +118,12 @@ class DialogSystemLogDetail extends LitElement {
? ""
: html`
(<a
href=${this._manifest.documentation}
href=${this._manifest.is_built_in
? documentationUrl(
this.hass,
`/integrations/${this._manifest.domain}`
)
: this._manifest.documentation}
target="_blank"
rel="noreferrer"
>documentation</a

View File

@@ -9,6 +9,10 @@ import { HomeAssistant } from "../../../types";
class ErrorLogCard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public filter = "";
@state() private _isLogLoaded = false;
@state() private _errorHTML!: TemplateResult[] | string;
protected render(): TemplateResult {
@@ -43,6 +47,14 @@ class ErrorLogCard extends LitElement {
}
}
protected updated(changedProps) {
super.updated(changedProps);
if (changedProps.has("filter") && this._isLogLoaded) {
this._refreshErrorLog();
}
}
static get styles(): CSSResultGroup {
return css`
.error-log-intro {
@@ -55,12 +67,16 @@ class ErrorLogCard extends LitElement {
}
.error-log {
@apply --paper-font-code)
clear: both;
font-family: var(--code-font-family, monospace);
clear: both;
text-align: left;
padding-top: 12px;
}
.error-log > div:hover {
background-color: var(--secondary-background-color);
}
.error {
color: var(--error-color);
}
@@ -74,24 +90,33 @@ class ErrorLogCard extends LitElement {
private async _refreshErrorLog(): Promise<void> {
this._errorHTML = this.hass.localize("ui.panel.config.logs.loading_log");
const log = await fetchErrorLog(this.hass!);
this._isLogLoaded = true;
this._errorHTML = log
? log.split("\n").map((entry) => {
if (entry.includes("INFO"))
return html`<div class="info">${entry}</div>`;
? log
.split("\n")
.filter((entry) => {
if (this.filter) {
return entry.toLowerCase().includes(this.filter.toLowerCase());
}
return entry;
})
.map((entry) => {
if (entry.includes("INFO"))
return html`<div class="info">${entry}</div>`;
if (entry.includes("WARNING"))
return html`<div class="warning">${entry}</div>`;
if (entry.includes("WARNING"))
return html`<div class="warning">${entry}</div>`;
if (
entry.includes("ERROR") ||
entry.includes("FATAL") ||
entry.includes("CRITICAL")
)
return html`<div class="error">${entry}</div>`;
if (
entry.includes("ERROR") ||
entry.includes("FATAL") ||
entry.includes("CRITICAL")
)
return html`<div class="error">${entry}</div>`;
return html`<div>${entry}</div>`;
})
return html`<div>${entry}</div>`;
})
: this.hass.localize("ui.panel.config.logs.no_errors");
}
}

View File

@@ -1,9 +1,11 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import "../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config";
import "../../../common/search/search-input";
import { extractSearchParam } from "../../../common/url/search-params";
import "./error-log-card";
import "./system-log-card";
import type { SystemLogCard } from "./system-log-card";
@@ -20,6 +22,8 @@ export class HaConfigLogs extends LitElement {
@property() public route!: Route;
@state() private _filter = extractSearchParam("filter") ?? "";
@query("system-log-card", true) private systemLog?: SystemLogCard;
public connectedCallback() {
@@ -29,7 +33,39 @@ export class HaConfigLogs extends LitElement {
}
}
private async _filterChanged(ev) {
this._filter = ev.detail.value;
}
protected render(): TemplateResult {
const search = this.narrow
? html`
<div slot="header">
<search-input
class="header"
no-label-float
no-underline
@value-changed=${this._filterChanged}
.hass=${this.hass}
.filter=${this._filter}
.label=${this.hass.localize("ui.panel.config.logs.search")}
></search-input>
</div>
`
: html`
<div class="search">
<search-input
autofocus
no-label-float
no-underline
@value-changed=${this._filterChanged}
.hass=${this.hass}
.filter=${this._filter}
.label=${this.hass.localize("ui.panel.config.logs.search")}
></search-input>
</div>
`;
return html`
<hass-tabs-subpage
.hass=${this.hass}
@@ -38,9 +74,16 @@ export class HaConfigLogs extends LitElement {
.route=${this.route}
.tabs=${configSections.general}
>
${search}
<div class="content">
<system-log-card .hass=${this.hass}></system-log-card>
<error-log-card .hass=${this.hass}></error-log-card>
<system-log-card
.hass=${this.hass}
.filter=${this._filter}
></system-log-card>
<error-log-card
.hass=${this.hass}
.filter=${this._filter}
></error-log-card>
</div>
</hass-tabs-subpage>
`;
@@ -56,6 +99,17 @@ export class HaConfigLogs extends LitElement {
-moz-user-select: initial;
}
.search {
padding: 0 16px;
background: var(--sidebar-background-color);
border-bottom: 1px solid var(--divider-color);
}
.search search-input {
position: relative;
top: 2px;
}
.content {
direction: ltr;
}

View File

@@ -2,6 +2,7 @@ import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/buttons/ha-progress-button";
import "../../../components/ha-card";
@@ -22,6 +23,8 @@ import { formatSystemLogTime } from "./util";
export class SystemLogCard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public filter = "";
public loaded = false;
@state() private _items?: LoggedError[];
@@ -31,9 +34,44 @@ export class SystemLogCard extends LitElement {
this._items = await fetchSystemLog(this.hass!);
}
private _timestamp(item: LoggedError): string {
return formatSystemLogTime(item.timestamp, this.hass!.locale);
}
private _multipleMessages(item: LoggedError): string {
return this.hass.localize(
"ui.panel.config.logs.multiple_messages",
"time",
formatSystemLogTime(item.first_occurred, this.hass!.locale),
"counter",
item.count
);
}
private _getFilteredItems = memoizeOne(
(items: LoggedError[], filter: string) =>
items.filter((item: LoggedError) => {
if (filter) {
return (
item.message.some((message: string) =>
message.toLowerCase().includes(filter)
) ||
item.source[0].toLowerCase().includes(filter) ||
item.name.toLowerCase().includes(filter) ||
this._timestamp(item).toLowerCase().includes(filter) ||
this._multipleMessages(item).toLowerCase().includes(filter)
);
}
return item;
})
);
protected render(): TemplateResult {
const integrations = this._items
? this._items.map((item) => getLoggedErrorIntegration(item))
const filteredItems = this._items
? this._getFilteredItems(this._items, this.filter.toLowerCase())
: [];
const integrations = filteredItems.length
? filteredItems.map((item) => getLoggedErrorIntegration(item))
: [];
return html`
<div class="system-log-intro">
@@ -51,17 +89,21 @@ export class SystemLogCard extends LitElement {
${this.hass.localize("ui.panel.config.logs.no_issues")}
</div>
`
: this._items.map(
: filteredItems.length === 0 && this.filter
? html`<div class="card-content">
${this.hass.localize(
"ui.panel.config.logs.no_issues_search",
"term",
this.filter
)}
</div>`
: filteredItems.map(
(item, idx) => html`
<paper-item @click=${this._openLog} .logItem=${item}>
<paper-item-body two-line>
<div class="row">${item.message[0]}</div>
<div secondary>
${formatSystemLogTime(
item.timestamp,
this.hass!.locale
)}
${this._timestamp(item)}
${html`(<span class=${item.level.toLowerCase()}
>${this.hass.localize(
"ui.panel.config.logs.level." +
@@ -81,19 +123,7 @@ export class SystemLogCard extends LitElement {
}`
: item.source[0]}
${item.count > 1
? html`
-
${this.hass.localize(
"ui.panel.config.logs.multiple_messages",
"time",
formatSystemLogTime(
item.first_occurred,
this.hass!.locale
),
"counter",
item.count
)}
`
? html` - ${this._multipleMessages(item)} `
: html``}
</div>
</paper-item-body>

View File

@@ -7,7 +7,9 @@ import {
import "@polymer/paper-tooltip/paper-tooltip";
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import memoize from "memoize-one";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import { navigate } from "../../../../common/navigate";
import { stringCompare } from "../../../../common/string/compare";
import {
@@ -52,9 +54,19 @@ export class HaConfigLovelaceDashboards extends LitElement {
icon: {
title: "",
type: "icon",
template: (icon) =>
template: (icon, dashboard) =>
icon
? html` <ha-icon slot="item-icon" .icon=${icon}></ha-icon> `
? html`
<ha-icon
slot="item-icon"
.icon=${icon}
style=${ifDefined(
dashboard.iconColor
? `color: ${dashboard.iconColor}`
: undefined
)}
></ha-icon>
`
: html``,
},
title: {
@@ -63,7 +75,6 @@ export class HaConfigLovelaceDashboards extends LitElement {
),
sortable: true,
filterable: true,
direction: "asc",
grows: true,
template: (title, dashboard: any) => {
const titleTemplate = html`
@@ -183,29 +194,41 @@ export class HaConfigLovelaceDashboards extends LitElement {
).mode;
const defaultUrlPath = this.hass.defaultPanel;
const isDefault = defaultUrlPath === "lovelace";
return [
const result: Record<string, any>[] = [
{
icon: "hass:view-dashboard",
title: this.hass.localize("panel.states"),
default: isDefault,
sidebar: isDefault,
show_in_sidebar: isDefault,
require_admin: false,
url_path: "lovelace",
mode: defaultMode,
filename: defaultMode === "yaml" ? "ui-lovelace.yaml" : "",
iconColor: "var(--primary-color)",
},
{
];
if (isComponentLoaded(this.hass, "energy")) {
result.push({
icon: "hass:lightning-bolt",
title: this.hass.localize(`ui.panel.config.dashboard.energy.title`),
show_in_sidebar: true,
mode: "storage",
url_path: "energy",
filename: "",
},
...dashboards.map((dashboard) => ({
filename: "",
...dashboard,
default: defaultUrlPath === dashboard.url_path,
})),
];
iconColor: "var(--label-badge-yellow)",
});
}
result.push(
...dashboards
.sort((a, b) => stringCompare(a.title, b.title))
.map((dashboard) => ({
filename: "",
...dashboard,
default: defaultUrlPath === dashboard.url_path,
}))
);
return result;
});
protected render(): TemplateResult {

View File

@@ -42,6 +42,8 @@ class DialogTagDetail
this._id = "";
this._name = "";
}
this._generateQR();
}
public closeDialog(): void {
@@ -121,16 +123,9 @@ class DialogTagDetail
)}
</p>
</div>
<div id="qr">
${this._qrCode
? this._qrCode
: html`
<mwc-button @click=${this._generateQR}
>Generate QR code
</mwc-button>
`}
</div>
${this._qrCode
? html` <div id="qr">${this._qrCode}</div> `
: ""}
`
: ``}
</div>
@@ -225,6 +220,9 @@ class DialogTagDetail
{
width: 180,
errorCorrectionLevel: "Q",
color: {
light: "#fff",
},
}
);
const context = canvas.getContext("2d");

View File

@@ -51,7 +51,12 @@ export class EnergySetupWizard extends LitElement implements LovelaceCard {
protected render(): TemplateResult {
return html`
<p>Step ${this._step + 1} of 5</p>
<p>
${this.hass.localize("ui.panel.energy.setup.step", {
step: this._step + 1,
steps: 5,
})}
</p>
${this._step === 0
? html`<ha-energy-grid-settings
.hass=${this.hass}

View File

@@ -12,7 +12,11 @@ import {
} from "date-fns";
import { css, html, LitElement, PropertyValues } from "lit";
import { property, state } from "lit/decorators";
import { extractSearchParam } from "../../common/url/search-params";
import { navigate } from "../../common/navigate";
import {
createSearchParam,
extractSearchParam,
} from "../../common/url/search-params";
import { computeRTL } from "../../common/util/compute_rtl";
import "../../components/chart/state-history-charts";
import "../../components/entity/ha-entity-picker";
@@ -144,6 +148,10 @@ class HaPanelHistory extends LitElement {
if (startDate) {
this._startDate = new Date(startDate);
}
const endDate = extractSearchParam("end_date");
if (endDate) {
this._endDate = new Date(endDate);
}
}
protected updated(changedProps: PropertyValues) {
@@ -191,10 +199,32 @@ class HaPanelHistory extends LitElement {
endDate.setMilliseconds(endDate.getMilliseconds() - 1);
}
this._endDate = endDate;
this._updatePath();
}
private _entityPicked(ev) {
this._entityId = ev.target.value;
this._updatePath();
}
private _updatePath() {
const params: Record<string, string> = {};
if (this._entityId) {
params.entity_id = this._entityId;
}
if (this._startDate) {
params.start_date = this._startDate.toISOString();
}
if (this._endDate) {
params.end_date = this._endDate.toISOString();
}
navigate(`/history?${createSearchParam(params)}`, { replace: true });
}
static get styles() {

View File

@@ -1,8 +1,6 @@
import { mdiRefresh } from "@mdi/js";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import { css, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
addDays,
endOfToday,
@@ -12,8 +10,15 @@ import {
startOfWeek,
startOfYesterday,
} from "date-fns";
import { css, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { navigate } from "../../common/navigate";
import {
createSearchParam,
extractSearchParam,
} from "../../common/url/search-params";
import { computeRTL } from "../../common/util/compute_rtl";
import "../../components/entity/ha-entity-picker";
import "../../components/ha-circular-progress";
@@ -33,7 +38,6 @@ import "../../layouts/ha-app-layout";
import { haStyle } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import "./ha-logbook";
import { extractSearchParam } from "../../common/url/search-params";
@customElement("ha-panel-logbook")
export class HaPanelLogbook extends LitElement {
@@ -166,6 +170,10 @@ export class HaPanelLogbook extends LitElement {
if (startDate) {
this._startDate = new Date(startDate);
}
const endDate = extractSearchParam("end_date");
if (endDate) {
this._endDate = new Date(endDate);
}
}
protected updated(changedProps: PropertyValues<this>) {
@@ -223,10 +231,32 @@ export class HaPanelLogbook extends LitElement {
endDate.setMilliseconds(endDate.getMilliseconds() - 1);
}
this._endDate = endDate;
this._updatePath();
}
private _entityPicked(ev) {
this._entityId = ev.target.value;
this._updatePath();
}
private _updatePath() {
const params: Record<string, string> = {};
if (this._entityId) {
params.entity_id = this._entityId;
}
if (this._startDate) {
params.start_date = this._startDate.toISOString();
}
if (this._endDate) {
params.end_date = this._endDate.toISOString();
}
navigate(`/logbook?${createSearchParam(params)}`, { replace: true });
}
private _refreshLogbook() {

View File

@@ -170,6 +170,21 @@ export class HuiEnergyDevicesGraphCard
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
);
const startMinHour = addHours(energyData.start, -1);
Object.values(this._data).forEach((stat) => {
// if the start of the first value is after the requested period, we have the first data point, and should add a zero point
if (stat.length && new Date(stat[0].start) > startMinHour) {
stat.unshift({
...stat[0],
start: startMinHour.toISOString(),
end: startMinHour.toISOString(),
sum: 0,
state: 0,
});
}
});
const data: Array<ChartDataset<"bar", ParsedDataType<"bar">>["data"]> = [];
const borderColor: string[] = [];
const backgroundColor: string[] = [];

View File

@@ -23,8 +23,8 @@ import { iconColorCSS } from "../../../common/style/icon_color_css";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import { UNAVAILABLE_STATES } from "../../../data/entity";
import { formatAttributeValue } from "../../../data/entity_attributes";
import { HomeAssistant } from "../../../types";
import { formatAttributeValue } from "../../../util/hass-attributes-util";
import { computeCardSize } from "../common/compute-card-size";
import { findEntities } from "../common/find-entities";
import { hasConfigOrEntityChanged } from "../common/has-changed";

View File

@@ -106,6 +106,9 @@ export class HuiImage extends LitElement {
} else if (!this.hass!.connected) {
this._stopUpdateCameraInterval();
this._stopIntersectionObserver();
this._loadState = LoadState.Loading;
this._cameraImageSrc = undefined;
this._loadedImageSrc = undefined;
}
}
if (changedProps.has("_imageVisible")) {

View File

@@ -65,28 +65,26 @@ export class HuiCreateDialogCard
return html``;
}
const title = this._viewConfig.title
? this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.pick_card_view_title",
"name",
`"${this._viewConfig.title}"`
)
: this.hass!.localize("ui.panel.lovelace.editor.edit_card.pick_card");
return html`
<ha-dialog
open
scrimClickAction
@keydown=${this._ignoreKeydown}
@closed=${this._cancel}
.heading=${true}
.heading=${title}
class=${classMap({ table: this._currTabIndex === 1 })}
>
<div slot="heading">
<ha-header-bar>
<span slot="title">
${this._viewConfig.title
? this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.pick_card_view_title",
"name",
`"${this._viewConfig.title}"`
)
: this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.pick_card"
)}
</span>
<span slot="title"> ${title} </span>
</ha-header-bar>
<mwc-tab-bar
.activeIndex=${this._currTabIndex}

View File

@@ -176,7 +176,7 @@ export class HuiDialogEditCard
@keydown=${this._ignoreKeydown}
@closed=${this._cancel}
@opened=${this._opened}
.heading=${true}
.heading=${heading}
>
<div slot="heading">
<ha-header-bar>

View File

@@ -173,7 +173,7 @@ export class HuiDialogEditView extends LitElement {
scrimClickAction
escapeKeyAction
@closed=${this.closeDialog}
.heading=${true}
.heading=${this._viewConfigTitle}
>
<div slot="heading">
<h2>${this._viewConfigTitle}</h2>

View File

@@ -130,6 +130,7 @@ class HUIRoot extends LitElement {
></ha-icon-button>
</div>
<mwc-button
outlined
class="exit-edit-mode"
.label=${this.hass!.localize(
"ui.panel.lovelace.menu.exit_edit_mode"
@@ -938,7 +939,8 @@ class HUIRoot extends LitElement {
);
}
.exit-edit-mode {
--mdc-theme-primary: var(--primary-text-color);
--mdc-theme-primary: var(--app-header-edit-text-color, #fff);
--mdc-button-outline-color: var(--app-header-edit-text-color, #fff);
--mdc-typography-button-font-size: 14px;
}
`,

View File

@@ -9,8 +9,8 @@ import {
import { customElement, property, state } from "lit/decorators";
import checkValidDate from "../../../common/datetime/check_valid_date";
import { formatNumber } from "../../../common/number/format_number";
import { formatAttributeValue } from "../../../data/entity_attributes";
import { HomeAssistant } from "../../../types";
import { formatAttributeValue } from "../../../util/hass-attributes-util";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import "../components/hui-generic-entity-row";
import "../components/hui-timestamp-display";

View File

@@ -3,7 +3,7 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { subscribeOne } from "../../../common/util/subscribe-one";
import { subscribeAreaRegistry } from "../../../data/area_registry";
import { subscribeDeviceRegistry } from "../../../data/device_registry";
import { EnergyPreferences, getEnergyPreferences } from "../../../data/energy";
import { getEnergyPreferences } from "../../../data/energy";
import { subscribeEntityRegistry } from "../../../data/entity_registry";
import { generateDefaultViewConfig } from "../common/generate-lovelace-config";
import {
@@ -39,30 +39,18 @@ export class OriginalStatesStrategy {
subscribeEntityRegistry(hass.connection, () => undefined);
}
let energyPromise: Promise<EnergyPreferences> | undefined;
if (isComponentLoaded(hass, "energy")) {
energyPromise = getEnergyPreferences(hass);
}
const [areaEntries, deviceEntries, entityEntries, localize] =
const [areaEntries, deviceEntries, entityEntries, localize, energyPrefs] =
await Promise.all([
subscribeOne(hass.connection, subscribeAreaRegistry),
subscribeOne(hass.connection, subscribeDeviceRegistry),
subscribeOne(hass.connection, subscribeEntityRegistry),
hass.loadBackendTranslation("title"),
isComponentLoaded(hass, "energy")
? // It raises if not configured, just swallow that.
getEnergyPreferences(hass).catch(() => undefined)
: undefined,
]);
let energyPrefs: EnergyPreferences | undefined;
if (energyPromise) {
try {
energyPrefs = await energyPromise;
} catch (_) {
// Nothing to do here
}
}
// User can override default view. If they didn't, we will add one
// that contains all entities.
const view = generateDefaultViewConfig(

View File

@@ -19,6 +19,7 @@ import {
TemplateResult,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
@@ -114,7 +115,9 @@ class BarMediaPlayer extends LitElement {
protected render(): TemplateResult {
const isBrowser = this.entityId === BROWSER_PLAYER;
const stateObj = this._stateObj;
const controls = !this.narrow
const controls = !stateObj
? undefined
: !this.narrow
? computeMediaControls(stateObj)
: (stateObj.state === "playing" &&
(supportsFeature(stateObj, SUPPORT_PAUSE) ||
@@ -143,16 +146,22 @@ class BarMediaPlayer extends LitElement {
},
]
: [{}];
const mediaDescription = computeMediaDescription(stateObj);
const mediaDuration = formatMediaTime(stateObj!.attributes.media_duration!);
const mediaTitleClean = cleanupMediaTitle(stateObj.attributes.media_title);
const mediaDescription = stateObj ? computeMediaDescription(stateObj) : "";
const mediaDuration = formatMediaTime(stateObj?.attributes.media_duration);
const mediaTitleClean = cleanupMediaTitle(
stateObj?.attributes.media_title || ""
);
const mediaArt =
stateObj.attributes.entity_picture_local ||
stateObj.attributes.entity_picture;
const mediaArt = stateObj
? stateObj.attributes.entity_picture_local ||
stateObj.attributes.entity_picture
: undefined;
return html`
<div class="info">
<div
class="info ${!isBrowser ? "pointer" : ""}"
@click=${this._openMoreInfo}
>
${mediaArt ? html`<img src=${this.hass.hassUrl(mediaArt)} />` : ""}
<div class="media-info">
<hui-marquee
@@ -212,7 +221,7 @@ class BarMediaPlayer extends LitElement {
slot="trigger"
.label=${this.narrow
? ""
: `${computeStateName(stateObj)}
: `${stateObj ? computeStateName(stateObj) : this.entityId}
`}
>
<ha-svg-icon
@@ -284,7 +293,7 @@ class BarMediaPlayer extends LitElement {
if (
!this._progressInterval &&
this._showProgressBar &&
stateObj.state === "playing"
stateObj?.state === "playing"
) {
this._progressInterval = window.setInterval(
() => this._updateProgressBar(),
@@ -292,21 +301,27 @@ class BarMediaPlayer extends LitElement {
);
} else if (
this._progressInterval &&
(!this._showProgressBar || stateObj.state !== "playing")
(!this._showProgressBar || stateObj?.state !== "playing")
) {
clearInterval(this._progressInterval);
this._progressInterval = undefined;
}
}
private get _stateObj(): MediaPlayerEntity {
if (this._browserPlayer) {
return this._browserPlayer.toStateObj();
private get _stateObj(): MediaPlayerEntity | undefined {
if (this.entityId === BROWSER_PLAYER) {
return this._browserPlayer
? this._browserPlayer.toStateObj()
: BrowserMediaPlayer.idleStateObj();
}
return (
(this.hass!.states[this.entityId] as MediaPlayerEntity | undefined) ||
BrowserMediaPlayer.idleStateObj()
);
return this.hass!.states[this.entityId] as MediaPlayerEntity | undefined;
}
private _openMoreInfo() {
if (this._browserPlayer) {
return;
}
fireEvent(this, "hass-more-info", { entityId: this.entityId });
}
private get _showProgressBar() {
@@ -317,6 +332,7 @@ class BarMediaPlayer extends LitElement {
const stateObj = this._stateObj;
return (
stateObj &&
(stateObj.state === "playing" || stateObj.state === "paused") &&
"media_duration" in stateObj.attributes &&
"media_position" in stateObj.attributes
@@ -332,19 +348,21 @@ class BarMediaPlayer extends LitElement {
}
private _updateProgressBar(): void {
if (!this._progressBar || !this._currentProgress) {
const stateObj = this._stateObj;
if (!this._progressBar || !this._currentProgress || !stateObj) {
return;
}
if (!this._stateObj.attributes.media_duration) {
if (!stateObj.attributes.media_duration) {
this._progressBar.progress = 0;
this._currentProgress.innerHTML = "";
return;
}
const currentProgress = getCurrentProgress(this._stateObj);
const currentProgress = getCurrentProgress(stateObj);
this._progressBar.progress =
currentProgress / this._stateObj.attributes.media_duration;
currentProgress / stateObj.attributes.media_duration;
if (this._currentProgress) {
this._currentProgress.innerHTML = formatMediaTime(currentProgress);
@@ -418,6 +436,10 @@ class BarMediaPlayer extends LitElement {
overflow: hidden;
}
.pointer {
cursor: pointer;
}
.secondary,
.progress {
color: var(--secondary-text-color);
@@ -433,6 +455,7 @@ class BarMediaPlayer extends LitElement {
.controls {
height: 48px;
padding-bottom: 4px;
}
.controls-progress {
@@ -486,6 +509,7 @@ class BarMediaPlayer extends LitElement {
:host([narrow]) .controls {
display: flex;
padding-bottom: 0;
}
:host([narrow]) .choose-player {

View File

@@ -1,3 +1,4 @@
import { mdiArrowLeft } from "@mdi/js";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import {
@@ -13,15 +14,21 @@ import { LocalStorage } from "../../common/decorators/local-storage";
import { HASSDomEvent } from "../../common/dom/fire_event";
import { navigate } from "../../common/navigate";
import "../../components/ha-menu-button";
import "../../components/ha-icon-button";
import "../../components/media-player/ha-media-player-browse";
import type { MediaPlayerItemId } from "../../components/media-player/ha-media-player-browse";
import { BROWSER_PLAYER, MediaPickedEvent } from "../../data/media-player";
import {
BROWSER_PLAYER,
MediaPickedEvent,
MediaPlayerItem,
} from "../../data/media-player";
import { resolveMediaSource } from "../../data/media_source";
import "../../layouts/ha-app-layout";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant, Route } from "../../types";
import "./ha-bar-media-player";
import { showWebBrowserPlayMediaDialog } from "./show-media-player-dialog";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
@customElement("ha-panel-media-browser")
class PanelMediaBrowser extends LitElement {
@@ -32,6 +39,8 @@ class PanelMediaBrowser extends LitElement {
@property() public route!: Route;
@property() _currentItem?: MediaPlayerItem;
private _navigateIds: MediaPlayerItemId[] = [
{
media_content_id: undefined,
@@ -47,15 +56,26 @@ class PanelMediaBrowser extends LitElement {
<ha-app-layout>
<app-header fixed slot="header">
<app-toolbar>
<ha-menu-button
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
${this._navigateIds.length > 1
? html`
<ha-icon-button
.path=${mdiArrowLeft}
@click=${this._goBack}
></ha-icon-button>
`
: html`
<ha-menu-button
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`}
<div main-title class="heading">
<div>
${this.hass.localize(
"ui.components.media-browser.media-player-browser"
)}
${!this._currentItem
? this.hass.localize(
"ui.components.media-browser.media-player-browser"
)
: this._currentItem.title}
</div>
</div>
</app-toolbar>
@@ -93,6 +113,23 @@ class PanelMediaBrowser extends LitElement {
.split("/");
if (routePlayer !== this._entityId) {
// Detect if picked player doesn't exist (anymore)
// Can happen if URL bookmarked or stored in local storage
if (
routePlayer !== BROWSER_PLAYER &&
this.hass.states[routePlayer] === undefined
) {
navigate(`/media-browser/${BROWSER_PLAYER}`, { replace: true });
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.media-browser.error.player_not_exist",
{
name: routePlayer,
}
),
});
return;
}
this._entityId = routePlayer;
}
@@ -110,13 +147,19 @@ class PanelMediaBrowser extends LitElement {
};
}),
];
this._currentItem = undefined;
}
private _mediaBrowsed(ev) {
if (ev.detail.back) {
history.back();
private _goBack() {
history.back();
}
private _mediaBrowsed(ev: { detail: HASSDomEvents["media-browsed"] }) {
if (ev.detail.ids === this._navigateIds) {
this._currentItem = ev.detail.current;
return;
}
let path = "";
for (const item of ev.detail.ids.slice(1)) {
path +=
@@ -152,6 +195,7 @@ class PanelMediaBrowser extends LitElement {
sourceUrl: resolvedUrl.url,
sourceType: resolvedUrl.mime_type,
title: item.title,
can_play: item.can_play,
});
}
}

View File

@@ -4,6 +4,7 @@ export interface WebBrowserPlayMediaDialogParams {
sourceUrl: string;
sourceType: string;
title?: string;
can_play?: boolean;
}
export const showWebBrowserPlayMediaDialog = (

View File

@@ -65,7 +65,7 @@ export class HaLongLivedAccessTokenDialog extends LitElement {
private async _generateQR() {
const qrcode = await import("qrcode");
const canvas = await qrcode.toCanvas(this._params?.token, {
const canvas = await qrcode.toCanvas(this._params!.token, {
width: 180,
errorCorrectionLevel: "Q",
});

View File

@@ -1,3 +1,4 @@
// Caution before editing - For latest builds, this module is replaced with emptiness and thus not imported (see build-scripts/bundle.js)
import "core-js";
import "regenerator-runtime/runtime";
import "lit/polyfill-support";

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