Compare commits

..

172 Commits

Author SHA1 Message Date
Zack Arnett
42f720496b this kind of works :) 2020-09-14 15:06:34 -05:00
Zack Arnett
c64d88d8b5 fix action default 2020-09-14 14:43:16 -05:00
Bram Kragten
3030b8d476 Fix input number helper (#6988) 2020-09-14 17:00:56 +02:00
Joakim Sørensen
92ed14c0e4 Merge pull request #6983 from home-assistant/fix-mnuted
Fix muted on video
2020-09-14 11:29:14 +02:00
Bram Kragten
5b94a4de9a Fix muted on video 2020-09-14 11:14:32 +02:00
Kendell R
709112c498 Do safety check before detecting hex value and handle YAML numbers better (#6956)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-09-14 09:40:35 +02:00
Kendell R
e465ec8835 Make code editor font family follow theme (#6958) 2020-09-14 09:39:47 +02:00
Kendell R
f6eb31bf9d Use --error-color instead of a fixed color (#6961)
Co-authored-by: Zack Barett <zackbarett@hey.com>
2020-09-14 09:37:59 +02:00
Thomas Lovén
426f939982 Add Execute button to script editor (#6957) 2020-09-14 09:34:59 +02:00
Zack Barett
fab6cebf0d fix hard to read text (#6980) 2020-09-14 09:33:23 +02:00
HomeAssistant Azure
ff081dd0f1 [ci skip] Translation update 2020-09-14 00:32:37 +00:00
Zack Barett
868399ed6f HA Logs: Copy log (#6945) 2020-09-13 15:27:16 -05:00
Kendell R
1bc9b95289 Remove useless "My Title" (#6970) 2020-09-13 21:43:54 +02:00
Kendell R
9af805ab5e Make moon icon more readable (#6969) 2020-09-13 21:43:17 +02:00
HomeAssistant Azure
6b88081360 [ci skip] Translation update 2020-09-13 00:32:53 +00:00
Joakim Sørensen
50d37ce4f6 Remove icon slot (#6964) 2020-09-12 21:25:40 +02:00
Joakim Sørensen
af0246cd27 convert ha-refresh-tokens-card (#6962) 2020-09-12 21:05:01 +02:00
Ludeeus
857e4e49d8 Bumped version to 20200912.0 2020-09-12 18:58:49 +00:00
Joakim Sørensen
c1afed7f98 Sort media sources (#6960)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-09-12 20:34:34 +02:00
Bram Kragten
5480e54185 Mute stream outside of more info (#6959) 2020-09-12 20:07:22 +02:00
Bram Kragten
99d0a0a6fd Lazy load more info content, split logbook and history (#6936) 2020-09-12 19:39:54 +02:00
Joakim Sørensen
8a998369d6 Add padding to rendered template result (#6954) 2020-09-12 19:15:00 +02:00
Zack Barett
8b490c5047 Media Browser: Use Media Class (#6904)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-09-12 11:59:19 -05:00
Bram Kragten
7e70ba6ab2 FIx entities picker (#6951)
Fixes #6947 ?
2020-09-12 18:18:57 +02:00
Bram Kragten
90e09fc384 Add default hold actions (#6952)
Fixes #6942
2020-09-12 18:07:14 +02:00
Bram Kragten
266f2e763d Sort listening entity and domain in template dev tools (#6953) 2020-09-12 17:48:40 +02:00
Bram Kragten
c979cfb912 Fix sidebar for not existing hidden panel (#6944)
Fixes #6940
2020-09-12 12:52:37 +02:00
HomeAssistant Azure
8ee29b1e43 [ci skip] Translation update 2020-09-12 00:32:19 +00:00
Bram Kragten
26fbc07cac Add edit sidebar button to profile (#6943) 2020-09-12 00:04:25 +02:00
Bram Kragten
f01fe65be4 Show title and name for default panels (#6941)
Fixes #6927
2020-09-11 22:42:11 +02:00
Bram Kragten
3fdd6a80f9 Update codeql-analysis.yml 2020-09-11 22:15:08 +02:00
Bram Kragten
da1de8db1d Create codeql-analysis.yml 2020-09-11 22:13:57 +02:00
Ian Richardson
dd1bf7b49d show first visible view on default (#6567)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-09-11 22:10:36 +02:00
J. Nick Koston
f18913b5a0 Show which state changed events a template listeners for in dev tools (#6939)
* Show which state changed events a template listeners for in dev tools

* Update src/data/ws-templates.ts

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

* Update src/data/ws-templates.ts

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

* Update src/panels/developer-tools/template/developer-tools-template.ts

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

* Update src/panels/developer-tools/template/developer-tools-template.ts

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

* Update src/panels/developer-tools/template/developer-tools-template.ts

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

* Update src/panels/developer-tools/template/developer-tools-template.ts

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

* merge

* Update src/panels/lovelace/cards/hui-markdown-card.ts

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

* fix string reversal

* Update src/panels/lovelace/cards/hui-markdown-card.ts

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

* Update src/panels/developer-tools/template/developer-tools-template.ts

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

* error to string

* cleanup

* cleanup

* no listeners is probably worth warning about as well

* handle unknown error

* fix error alignment in pre

* fix error alignment in pre

* fix error alignment in pre

* fix error alignment in pre

* reformat

* reformat

* reformat

* fix accidential revert

* Update src/panels/developer-tools/template/developer-tools-template.ts

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

* Update src/panels/developer-tools/template/developer-tools-template.ts

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

* clear error on success

* tweak to not error if listeners are not returned

* Update src/panels/developer-tools/template/developer-tools-template.ts

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

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-09-11 21:36:39 +02:00
Bram Kragten
a2cd227f1a Remove backpath in ozw (#6937)
Fixes #6934
2020-09-11 17:31:04 +02:00
Bram Kragten
78e64e1f60 Show brigtness slider when light is off (#6935)
Fixes #6928
2020-09-11 17:07:46 +02:00
Joakim Sørensen
23a9b79320 Expand groups in entitry row to check toggle (#6930) 2020-09-11 15:46:41 +02:00
Joakim Sørensen
76394ce341 Use secondary text color for no entries (#6931) 2020-09-11 14:52:12 +02:00
Arielpod
1935df1faa Fixed height of circular progress in history (#6929) 2020-09-11 14:45:47 +02:00
Bram Kragten
5af4ce28ab Restrict long press to header of sidebar (#6933) 2020-09-11 14:42:39 +02:00
Bram Kragten
ce8ee569c4 Check if history and logbook are loaded (#6908) 2020-09-11 10:17:05 +02:00
HomeAssistant Azure
b0508f430e [ci skip] Translation update 2020-09-11 00:32:26 +00:00
Philip Allgaier
2139a80a7a Use proper constants for "unavailable" checks (#6922)
* Use proper constants for "unavailable"

* Additional usage of constants
2020-09-10 22:59:45 +02:00
Bram Kragten
aa4bc2ce03 Make logbook a bit smaller in more info (#6921) 2020-09-10 15:40:54 -05:00
Joakim Sørensen
fa65f84e09 Ignore disconnect codes for shutdown and reboot (#6901)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-09-10 22:04:16 +02:00
Bram Kragten
c06357a351 Only show what triggered a change if it wasn't a user (#6919)
* Only show what triggered a change if it wasn't a user

* Update ha-logbook.ts
2020-09-10 21:47:18 +02:00
Joakim Sørensen
092a02a624 Convert ha-long-lived-access-tokens-card (#6917)
Co-authored-by: Zack Barett <arnett.zackary@gmail.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-09-10 20:51:23 +02:00
J. Nick Koston
b9699f745f Avoid watching all states in the default template (#6918) 2020-09-10 20:50:24 +02:00
Bram Kragten
7fa9f10c30 Don't add space on the bottom when not showing tabs (#6913) 2020-09-10 17:24:01 +02:00
Bram Kragten
7bf0655dae Diable tts inputs when entities is unavailable (#6909)
Fixes #6890
2020-09-10 16:59:12 +02:00
Bram Kragten
96c5fdcbeb Fix some lovelace editors (#6911)
* Fix some lovelace editors

* let -> const
2020-09-10 15:17:32 +02:00
dependabot[bot]
c2e6d40382 Bump http-proxy from 1.17.0 to 1.18.1 (#6914)
Bumps [http-proxy](https://github.com/http-party/node-http-proxy) from 1.17.0 to 1.18.1.
- [Release notes](https://github.com/http-party/node-http-proxy/releases)
- [Changelog](https://github.com/http-party/node-http-proxy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/http-party/node-http-proxy/compare/1.17.0...1.18.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-09-10 15:16:03 +02:00
Bram Kragten
810d2a1ceb Fix onboarding dark mode (#6910)
Fixes #6882
2020-09-10 13:11:27 +02:00
Bram Kragten
af74f21af9 Dont virtualize logbook in more info (#6907) 2020-09-10 11:12:05 +02:00
HomeAssistant Azure
cdf7558a8e [ci skip] Translation update 2020-09-10 00:32:03 +00:00
Zack Barett
41b86e6c10 Fix media browse item width (#6870) 2020-09-09 17:03:11 -05:00
Bram Kragten
3039c678a5 Fix check 2020-09-09 23:08:16 +02:00
Joakim Sørensen
498882d014 Remove mobile_app from generated Lovelace (#6873)
* Hide mobile_app from generated Lovelace

* simplify

* Move to computeDefaultViewStates

* removed -> hidden

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

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

* Adjust for Set

* Review comments

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-09-09 23:01:50 +02:00
Bram Kragten
6c2b8c2abb Bumped version to 20200909.0 2020-09-09 23:00:48 +02:00
Bram Kragten
e955cc4378 Check for hass when setting themes (#6897) 2020-09-09 22:57:44 +02:00
Bram Kragten
eb96dd4803 Handle not defined entities (#6898) 2020-09-09 22:55:43 +02:00
Bram Kragten
e0bdef98a6 Only show history tabs for certain domains (#6895)
Co-authored-by: Zack Barett <arnett.zackary@gmail.com>
2020-09-09 22:10:23 +02:00
Bram Kragten
1130007d14 Fix mjpeg player (#6896) 2020-09-09 22:09:56 +02:00
Bram Kragten
d99d092784 Enlarge touch target delete button (#6893) 2020-09-09 21:02:14 +02:00
Philip Allgaier
e3b18a33ca Disable "Execute" if automation is unavailable (#6866) 2020-09-09 20:49:56 +02:00
Philip Allgaier
1890aab1e6 Color all deletion options consistenly red (#6891)
* Color all deletion options consistenly red

* CSS cleanup

* Color the "Remove Selected" entity config button

* Make eslint happy

* Getting rid of a wayward bracket
2020-09-09 20:48:51 +02:00
Joakim Sørensen
42bf350034 Add ha-user-badge to view visibility editor (#6885) 2020-09-09 17:26:22 +02:00
epenet
5ff52ea113 Update constant name to make it clearer (#6881) 2020-09-09 17:24:13 +02:00
Bram Kragten
432e3ba636 Fix entity drag (#6884) 2020-09-09 17:23:03 +02:00
Zack Barett
f7ab52fe9a Remove sort from frontend for now (#6886) 2020-09-09 17:22:34 +02:00
Bram Kragten
ad8430049d Merge pull request #6878 from home-assistant/external-header-fallback 2020-09-09 17:02:24 +02:00
epenet
2dffe7ba9e Add binary sensor icon for DEVICE_CLASS_BATTERY_CHARGING (#6876)
* Add binary sensor icon for DEVICE_CLASS_BATTERY_CHARGING

* Update icons for DEVICE_CLASS_BATTERY_CHARGING
2020-09-09 13:27:54 +02:00
Ludeeus
5b8f97e0f6 fix missing step 2020-09-09 10:17:57 +00:00
Ludeeus
b3a763a48d Add fallback for renderExternalStepHeader 2020-09-09 10:16:54 +00:00
HomeAssistant Azure
07569f10b5 [ci skip] Translation update 2020-09-09 00:32:22 +00:00
Philip Allgaier
7c5a78a1cf Media player visual improvements (#6817)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-09-08 16:36:44 -05:00
Bram Kragten
0e021e7d7d Bumped version to 20200908.0 2020-09-08 20:58:47 +02:00
Zack Barett
b30ee884a7 Fix for Camera streams that don't support stream (#6863) 2020-09-08 20:57:17 +02:00
J. Nick Koston
869b7c85ca Ensure we pickup all the reloadable domains (#6861) 2020-09-08 20:56:45 +02:00
Zack Barett
4d0d1ed2a1 Undo my commit into dev (#6864) 2020-09-08 20:52:21 +02:00
Zack Arnett
291983e4c3 Merge branch 'dev' of https://github.com/home-assistant/frontend into dev 2020-09-08 10:25:50 -05:00
Bram Kragten
909cff2158 Fix timer entity display (#6849) 2020-09-08 17:01:04 +02:00
Bram Kragten
4e676b1dba Fix light more info (#6855) 2020-09-08 09:17:01 -05:00
Paulus Schoutsen
9149bb9333 Remove deprecated HTML support (#6858) 2020-09-08 15:41:17 +02:00
Bram Kragten
4631994f20 Fix sidebar issues (#6853)
* Fix sidebar issues

* fix navigate in demo
2020-09-08 14:10:34 +02:00
Joakim Sørensen
82e9178320 Add warning class to delete (#6852)
* Add error class to delete

* Apply suggestions from code review

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

* Add missing haStyle

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-09-08 13:56:51 +02:00
Bram Kragten
67b4688168 Make time wider in logbook (#6854)
Fixes #6842
2020-09-08 13:28:29 +02:00
Joakim Sørensen
6e0e169b6e Add reload for platforms with reload service (#6851) 2020-09-08 13:27:59 +02:00
Zack Barett
100ba8edfa Add allowed options to entities struct so UI editor can still be used (#6823) 2020-09-08 11:37:49 +02:00
Zack Barett
d7448ecb95 Fix Calendar Card in Add Card dialog (#6833) 2020-09-08 09:17:49 +02:00
Zack Barett
8b1801f378 Fix header on media browser in safari (#6838) 2020-09-08 09:14:34 +02:00
Bram Kragten
01a4d57566 Merge pull request #6835 from home-assistant/fix-more-info
Fix More info content from having space on right
2020-09-08 09:13:52 +02:00
Zack Arnett
7edc9064d9 Fix light extra attributes start fix for history 2020-09-07 20:49:53 -05:00
Zack Arnett
30c47a65f4 fix more info content 2020-09-07 19:47:25 -05:00
HomeAssistant Azure
0889f42a00 [ci skip] Translation update 2020-09-08 00:32:39 +00:00
Bram Kragten
f15fbe53cf Bumped version to 20200907.0 2020-09-07 20:40:03 +02:00
Bram Kragten
046f7b5153 Handle media browser errors (#6813)
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
2020-09-07 20:39:26 +02:00
Zack Barett
5339fe6e06 Updates to correct zindexs on new dialogs (#6816) 2020-09-07 20:11:49 +02:00
Bram Kragten
de7ffb10cb Automation editor tweaks (#6713)
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
2020-09-07 19:53:10 +02:00
Bram Kragten
80224e6974 Fix onboarding styling (#6819) 2020-09-07 19:48:19 +02:00
Joakim Sørensen
0c7c536f73 Fixes issues with channel toggle (#6812) 2020-09-07 19:30:16 +02:00
Bram Kragten
e5c386c39a Fix white flash in dark mode (#6815) 2020-09-07 18:19:28 +02:00
Zack Barett
bb2462483e Use Sortable to move entities in entities editor (#6810) 2020-09-07 13:47:24 +02:00
Bram Kragten
d5bc498373 Fix action handler bugs (#6811) 2020-09-07 13:41:59 +02:00
Bram Kragten
979b7ae651 Add attention required for config flows in progress (#6808) 2020-09-07 09:09:42 +02:00
HomeAssistant Azure
c73330a466 [ci skip] Translation update 2020-09-07 00:32:51 +00:00
Zack Barett
efe8eca4e3 Media browser updates (#6801) 2020-09-06 18:28:15 -05:00
Sean Mooney
a37aad18b7 Minor typo fix (#6809)
Unkown = Unknown
2020-09-06 23:53:00 +02:00
Philip Allgaier
cfa0c45213 Fix media browser panel title + selection header color (#6807) 2020-09-06 22:17:34 +02:00
Joakim Sørensen
509481ef06 Ignore more proxy disconnect codes (#6805)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-09-06 20:29:02 +02:00
Bram Kragten
9aa8175e23 Update ha-logbook.ts 2020-09-06 19:59:29 +02:00
Joakim Sørensen
76f59d99a2 Remove extra > from button (#6804) 2020-09-06 18:29:39 +02:00
Joakim Sørensen
bd66bd6cf0 Add color to hass-error-screen (#6803) 2020-09-06 18:29:22 +02:00
HomeAssistant Azure
d69333dea4 [ci skip] Translation update 2020-09-06 00:32:31 +00:00
Joakim Sørensen
3fd7899b93 Display services as services and not devices (#6798)
* Display services as services and not devices

* remove seperator

* Add comma

* Update src/panels/config/integrations/ha-integration-card.ts

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

* Fix spacing

* Remove check

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-09-06 02:16:50 +02:00
Pawel
8f8a2cea56 Don't show source select dialog when media player Unavailable. (#6799) 2020-09-05 17:35:34 -05:00
Donnie
879011c8e9 Fix incorrect link to mode documentation in automation editor (#6793) 2020-09-05 15:51:52 +02:00
HomeAssistant Azure
d5794c3e2e [ci skip] Translation update 2020-09-05 00:32:46 +00:00
Charles Garwood
fcc22ba560 Add node details shortcut to OZW device pages (#6791)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-09-04 23:14:15 +02:00
Bram Kragten
2adeb88fe6 Bumped version to 20200904.0 2020-09-04 23:02:24 +02:00
Zack Arnett
e63a78bcdb Media Browser Panel (#6772)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-09-04 23:01:20 +02:00
Bram Kragten
b065f002a4 Allow local storage decorator to register as property (#6776) 2020-09-04 22:22:55 +02:00
Zack Arnett
349a5f52b1 More Info History: Scrollbar Style (#6790) 2020-09-04 20:56:48 +02:00
Charles Garwood
aa5e20df05 Add basic nodes list & node metadata to OZW config panel (#6719)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-09-04 20:55:40 +02:00
Bram Kragten
793b9f238c Tweak card create dialog a bit (#6787) 2020-09-04 18:30:04 +02:00
Philip Allgaier
9c4fdaa4f3 Minor EN text improvements / fixes (#6788) 2020-09-04 16:21:15 +02:00
Bram Kragten
d1a9cb488a Add person badge (#6785)
* Add person badge

* Update src/components/user/ha-person-badge.ts

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

* Revert screwup by @ludeeus

Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
Co-authored-by: Ludeeus <ludeeus@ludeeus.dev>
2020-09-04 15:54:42 +02:00
Bram Kragten
faee2c3e1b Fix gauge editor (#6783) 2020-09-04 15:18:01 +02:00
Joakim Sørensen
b7845c318e Error extraction and target cleanup (#6782) 2020-09-04 15:08:30 +02:00
Bram Kragten
426a0727c3 Add person picture to user badge (#6784)
* Use person picture ha-user-badge

* Fix missing import

* lint

* User person picture in user-badge

Co-authored-by: Ludeeus <ludeeus@ludeeus.dev>
2020-09-04 14:52:21 +02:00
HomeAssistant Azure
584e509a9c [ci skip] Translation update 2020-09-04 00:32:27 +00:00
Zack Arnett
f3639c2663 Card Picker: Entity Picker (#6693) 2020-09-03 18:28:53 -05:00
Zack Arnett
1431e75f8b More Info: Add History Tab (#6758)
Co-authored-by: J. Nick Koston <nick@koston.org>
2020-09-03 18:28:10 -05:00
Bram Kragten
be8812e0af Add input field to ha form integer when it has a min and max (#6781) 2020-09-03 23:23:10 +02:00
J. Nick Koston
fd6436d490 Update reloadables to include telegram/smtp/mqtt (#6759) 2020-09-03 17:47:08 +02:00
Tomasz
fd1342f9d1 add rpi_gpio translation (#6778) 2020-09-03 16:55:31 +02:00
Joakim Sørensen
5fa0012195 hassio-addon-info feedback (#6734)
* hassio-addon-info feedback

* lint

* init config validation

* better error

* Finish

* sort imports

* Use startup type for watchdog

* Only show error if issue with config

* Adjust
2020-09-03 16:38:59 +02:00
Bram Kragten
9dbb67ef01 Fix shouldHandleRequestSelectedEvent (#6777) 2020-09-03 15:36:15 +02:00
Bram Kragten
d16e2f37d4 Generalize reloadableDomains (#6773)
* Generalize reloadableDomains

* Add back translations
2020-09-03 15:24:43 +02:00
Florian Gareis
d9e8b53ffe Add static color for home and not_home states (#6700)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-09-03 14:39:11 +02:00
Bram Kragten
1997e63b7c Fix action handler for touch (#6775)
* Fix action handler for touch

* Console
2020-09-03 14:09:25 +02:00
Joakim Sørensen
6f673359ff hassio-supervisor-log feedback (#6736)
* hassio-supervisor-log feedback

* Update hassio/src/system/hassio-supervisor-log.ts

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

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-09-03 11:49:10 +02:00
Joakim Sørensen
45dfbff10a hassio-addon-config feedback (#6732)
* hassio-addon-config feedback

* Update hassio/src/addon-view/config/hassio-addon-config.ts

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

* Update hassio/src/addon-view/config/hassio-addon-config.ts

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

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-09-03 11:03:08 +02:00
Joakim Sørensen
348ee96274 hassio-addon-audio feedback (#6731) 2020-09-03 10:45:18 +02:00
Joakim Sørensen
8edee32e77 hassio-supervisor-info feedback (#6737) 2020-09-03 10:44:53 +02:00
Joakim Sørensen
6d8d263ca6 hassio-addon-network feedback (#6733) 2020-09-03 10:33:45 +02:00
Joakim Sørensen
35923709e2 hassio-snapshots feedback (#6735) 2020-09-03 10:32:21 +02:00
Joakim Sørensen
fdd4d53448 hassio-host-info feedback (#6738)
* hassio-host-info feedback

* lint
2020-09-03 10:27:34 +02:00
HomeAssistant Azure
06419f662e [ci skip] Translation update 2020-09-03 00:32:39 +00:00
Bram Kragten
57763ef032 Fix layout of domain toggler dialog (#6771) 2020-09-02 20:46:38 +02:00
Joakim Sørensen
8e506f7749 Handle connection drops when upgrading (#6767) 2020-09-02 16:21:59 +02:00
Joakim Sørensen
c7f8fe1468 Don't show NM before 115 (#6768) 2020-09-02 15:58:14 +02:00
HomeAssistant Azure
4156a4e36d [ci skip] Translation update 2020-09-02 00:32:17 +00:00
Bram Kragten
0c212d39eb Bumped version to 20200901.0 2020-09-01 23:31:59 +02:00
Bram Kragten
3bd2e8dbf5 Allow to move and hide sidebar items (#6755) 2020-09-01 23:28:03 +02:00
Bram Kragten
5292119e6e Allow exposing domains in cloud (#6696)
* Allow exposing domains in cloud

https://github.com/home-assistant/core/pull/39216

* Update styles

* Lint

* Apply suggestions from code review

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

* Comments

* Add translations

* Apply suggestions from code review

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

Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
2020-09-01 10:23:59 +02:00
HomeAssistant Azure
994a397231 [ci skip] Translation update 2020-09-01 00:32:43 +00:00
Aidan Timson
353b71f803 Stop image from rendering for camera when disconnected and update when reconnected (#6677)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-08-31 23:54:57 +02:00
Bram Kragten
eb12afe8cc Media browser tweaks (#6720)
Co-authored-by: Zack Arnett <arnett.zackary@gmail.com>
2020-08-31 23:30:41 +02:00
Joakim Sørensen
4a176f1b43 Call service button feedback (#6752) 2020-08-31 14:38:24 -05:00
Kendell R
8e228baa82 Change spot clean icon (#6750)
* Change spot clean icon

* Switch to target-variant

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

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-08-31 09:38:32 -05:00
Pascal Roeleven
154b53b0d8 Fix automation modes documentation link (#6754) 2020-08-31 16:04:02 +02:00
Tomasz
a3f680d80c Fix render modifiers - public to protected (#6753) 2020-08-31 14:59:12 +02:00
HomeAssistant Azure
0d75fe6b81 [ci skip] Translation update 2020-08-31 00:32:41 +00:00
Paulus Schoutsen
4070380ded Remove credentials for load module (#6746)
Fixes #6745
2020-08-30 16:42:02 +02:00
Bram Kragten
41195dcef0 Remove animation delay from paper tooltip (#6716) 2020-08-30 10:03:04 +02:00
Joakim Sørensen
78a1e45be2 Dismiss dialog if the user clicks outside it or hit the escape button (#6741)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-08-30 09:42:58 +02:00
Joakim Sørensen
d8e88bc58d Ignore 504 errors while updating (#6743) 2020-08-30 09:39:34 +02:00
HomeAssistant Azure
448e9b71b8 [ci skip] Translation update 2020-08-30 00:32:28 +00:00
HomeAssistant Azure
2e178164cc [ci skip] Translation update 2020-08-29 00:32:29 +00:00
Zack Arnett
9f2e3f05a1 Entities & Glance Card: Add state color to editors (#6723) 2020-08-29 00:09:42 +02:00
Joakim Sørensen
405bd29ebd Convert ha-progress-button to lit (#6728) 2020-08-29 00:08:56 +02:00
260 changed files with 11667 additions and 5141 deletions

60
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,60 @@
name: "CodeQL"
on:
push:
branches: [dev, master]
pull_request:
# The branches below must be a subset of the branches above
branches: [dev]
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
language: ['javascript']
# Learn more...
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -2,8 +2,8 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */ /* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import "../../../src/dialogs/more-info/more-info-content";
import "../../../src/state-summary/state-card-content"; import "../../../src/state-summary/state-card-content";
import "./more-info-content";
class DemoMoreInfo extends PolymerElement { class DemoMoreInfo extends PolymerElement {
static get template() { static get template() {

View File

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

View File

@@ -3,10 +3,10 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import { SUPPORT_BRIGHTNESS } from "../../../src/data/light"; import { SUPPORT_BRIGHTNESS } from "../../../src/data/light";
import "../../../src/dialogs/more-info/more-info-content";
import { getEntity } from "../../../src/fake_data/entity"; import { getEntity } from "../../../src/fake_data/entity";
import { provideHass } from "../../../src/fake_data/provide_hass"; import { provideHass } from "../../../src/fake_data/provide_hass";
import "../components/demo-more-infos"; import "../components/demo-more-infos";
import "../components/more-info-content";
const ENTITIES = [ const ENTITIES = [
getEntity("light", "bed_light", "on", { getEntity("light", "bed_light", "on", {

View File

@@ -1,12 +1,13 @@
import "@material/mwc-icon-button/mwc-icon-button"; import "@material/mwc-icon-button/mwc-icon-button";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js"; import { mdiDotsVertical } from "@mdi/js";
import { import {
css, css,
CSSResult, CSSResult,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
PropertyValues, PropertyValues,
} from "lit-element"; } from "lit-element";
import { html, TemplateResult } from "lit-html"; import { html, TemplateResult } from "lit-html";
@@ -19,13 +20,13 @@ import {
HassioAddonRepository, HassioAddonRepository,
reloadHassioAddons, reloadHassioAddons,
} from "../../../src/data/hassio/addon"; } from "../../../src/data/hassio/addon";
import "../../../src/layouts/hass-tabs-subpage"; import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import "../../../src/layouts/hass-loading-screen"; import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-tabs-subpage";
import { HomeAssistant, Route } from "../../../src/types"; import { HomeAssistant, Route } from "../../../src/types";
import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories"; import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories";
import { supervisorTabs } from "../hassio-tabs"; import { supervisorTabs } from "../hassio-tabs";
import "./hassio-addon-repository"; import "./hassio-addon-repository";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => { const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => {
if (a.slug === "local") { if (a.slug === "local") {
@@ -179,7 +180,7 @@ class HassioAddonStore extends LitElement {
this._repos.sort(sortRepos); this._repos.sort(sortRepos);
this._addons = addonsInfo.addons; this._addons = addonsInfo.addons;
} catch (err) { } catch (err) {
alert("Failed to fetch add-on info"); alert(extractApiErrorMessage(err));
} }
} }

View File

@@ -28,6 +28,7 @@ import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types"; import { HomeAssistant } from "../../../../src/types";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart"; import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
import { hassioStyle } from "../../resources/hassio-style"; import { hassioStyle } from "../../resources/hassio-style";
import "../../../../src/components/buttons/ha-progress-button";
@customElement("hassio-addon-audio") @customElement("hassio-addon-audio")
class HassioAddonAudio extends LitElement { class HassioAddonAudio extends LitElement {
@@ -91,7 +92,9 @@ class HassioAddonAudio extends LitElement {
</paper-dropdown-menu> </paper-dropdown-menu>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<mwc-button @click=${this._saveSettings}>Save</mwc-button> <ha-progress-button @click=${this._saveSettings}>
Save
</ha-progress-button>
</div> </div>
</ha-card> </ha-card>
`; `;
@@ -172,7 +175,10 @@ class HassioAddonAudio extends LitElement {
} }
} }
private async _saveSettings(): Promise<void> { private async _saveSettings(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
this._error = undefined; this._error = undefined;
const data: HassioAddonSetOptionParams = { const data: HassioAddonSetOptionParams = {
audio_input: audio_input:
@@ -182,12 +188,14 @@ class HassioAddonAudio extends LitElement {
}; };
try { try {
await setHassioAddonOption(this.hass, this.addon.slug, data); await setHassioAddonOption(this.hass, this.addon.slug, data);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
}
} catch { } catch {
this._error = "Failed to set addon audio device"; this._error = "Failed to set addon audio device";
} }
if (!this._error && this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon); button.progress = false;
}
} }
} }

View File

@@ -5,14 +5,15 @@ import {
CSSResult, CSSResult,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
PropertyValues, PropertyValues,
query, query,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-card"; import "../../../../src/components/ha-card";
import "../../../../src/components/ha-yaml-editor"; import "../../../../src/components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../src/components/ha-yaml-editor"; import type { HaYamlEditor } from "../../../../src/components/ha-yaml-editor";
@@ -21,6 +22,7 @@ import {
HassioAddonSetOptionParams, HassioAddonSetOptionParams,
setHassioAddonOption, setHassioAddonOption,
} from "../../../../src/data/hassio/addon"; } from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box"; import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../src/resources/styles"; import { haStyle } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types"; import type { HomeAssistant } from "../../../../src/types";
@@ -55,20 +57,103 @@ class HassioAddonConfig extends LitElement {
${valid ? "" : html` <div class="errors">Invalid YAML</div> `} ${valid ? "" : html` <div class="errors">Invalid YAML</div> `}
</div> </div>
<div class="card-actions"> <div class="card-actions">
<mwc-button class="warning" @click=${this._resetTapped}> <ha-progress-button class="warning" @click=${this._resetTapped}>
Reset to defaults Reset to defaults
</mwc-button> </ha-progress-button>
<mwc-button <ha-progress-button
@click=${this._saveTapped} @click=${this._saveTapped}
.disabled=${!this._configHasChanged || !valid} .disabled=${!this._configHasChanged || !valid}
> >
Save Save
</mwc-button> </ha-progress-button>
</div> </div>
</ha-card> </ha-card>
`; `;
} }
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (changedProperties.has("addon")) {
this._editor.setValue(this.addon.options);
}
}
private _configChanged(): void {
this._configHasChanged = true;
this.requestUpdate();
}
private async _resetTapped(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.addon.name,
text: "Are you sure you want to reset all your options?",
confirmText: "reset options",
dismissText: "no",
});
if (!confirmed) {
button.progress = false;
return;
}
this._error = undefined;
const data: HassioAddonSetOptionParams = {
options: null,
};
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
this._configHasChanged = false;
const eventdata = {
success: true,
response: undefined,
path: "options",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to reset addon configuration, ${extractApiErrorMessage(
err
)}`;
}
button.progress = false;
}
private async _saveTapped(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
let data: HassioAddonSetOptionParams;
this._error = undefined;
try {
data = {
options: this._editor.value,
};
} catch (err) {
this._error = err;
return;
}
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
this._configHasChanged = false;
const eventdata = {
success: true,
response: undefined,
path: "options",
};
fireEvent(this, "hass-api-called", eventdata);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
}
} catch (err) {
this._error = `Failed to save addon configuration, ${extractApiErrorMessage(
err
)}`;
}
button.progress = false;
}
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
haStyle, haStyle,
@@ -98,80 +183,6 @@ class HassioAddonConfig extends LitElement {
`, `,
]; ];
} }
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (changedProperties.has("addon")) {
this._editor.setValue(this.addon.options);
}
}
private _configChanged(): void {
this._configHasChanged = true;
this.requestUpdate();
}
private async _resetTapped(): Promise<void> {
const confirmed = await showConfirmationDialog(this, {
title: this.addon.name,
text: "Are you sure you want to reset all your options?",
confirmText: "reset options",
dismissText: "no",
});
if (!confirmed) {
return;
}
this._error = undefined;
const data: HassioAddonSetOptionParams = {
options: null,
};
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
this._configHasChanged = false;
const eventdata = {
success: true,
response: undefined,
path: "options",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to reset addon configuration, ${
err.body?.message || err
}`;
}
}
private async _saveTapped(): Promise<void> {
let data: HassioAddonSetOptionParams;
this._error = undefined;
try {
data = {
options: this._editor.value,
};
} catch (err) {
this._error = err;
return;
}
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
this._configHasChanged = false;
const eventdata = {
success: true,
response: undefined,
path: "options",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to save addon configuration, ${
err.body?.message || err
}`;
}
if (!this._error && this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
}
}
} }
declare global { declare global {

View File

@@ -4,19 +4,21 @@ import {
CSSResult, CSSResult,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-card"; import "../../../../src/components/ha-card";
import { import {
HassioAddonDetails, HassioAddonDetails,
HassioAddonSetOptionParams, HassioAddonSetOptionParams,
setHassioAddonOption, setHassioAddonOption,
} from "../../../../src/data/hassio/addon"; } from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { haStyle } from "../../../../src/resources/styles"; import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types"; import { HomeAssistant } from "../../../../src/types";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart"; import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
@@ -85,38 +87,17 @@ class HassioAddonNetwork extends LitElement {
</table> </table>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<mwc-button class="warning" @click=${this._resetTapped}> <ha-progress-button class="warning" @click=${this._resetTapped}>
Reset to defaults Reset to defaults
</mwc-button> </ha-progress-button>
<mwc-button @click=${this._saveTapped}>Save</mwc-button> <ha-progress-button @click=${this._saveTapped}>
Save
</ha-progress-button>
</div> </div>
</ha-card> </ha-card>
`; `;
} }
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
:host {
display: block;
}
ha-card {
display: block;
}
.errors {
color: var(--error-color);
margin-bottom: 16px;
}
.card-actions {
display: flex;
justify-content: space-between;
}
`,
];
}
protected update(changedProperties: PropertyValues): void { protected update(changedProperties: PropertyValues): void {
super.update(changedProperties); super.update(changedProperties);
if (changedProperties.has("addon")) { if (changedProperties.has("addon")) {
@@ -149,7 +130,10 @@ class HassioAddonNetwork extends LitElement {
}); });
} }
private async _resetTapped(): Promise<void> { private async _resetTapped(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const data: HassioAddonSetOptionParams = { const data: HassioAddonSetOptionParams = {
network: null, network: null,
}; };
@@ -162,17 +146,22 @@ class HassioAddonNetwork extends LitElement {
path: "option", path: "option",
}; };
fireEvent(this, "hass-api-called", eventdata); fireEvent(this, "hass-api-called", eventdata);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
}
} catch (err) { } catch (err) {
this._error = `Failed to set addon network configuration, ${ this._error = `Failed to set addon network configuration, ${extractApiErrorMessage(
err.body?.message || err err
}`; )}`;
}
if (!this._error && this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
} }
button.progress = false;
} }
private async _saveTapped(): Promise<void> { private async _saveTapped(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
this._error = undefined; this._error = undefined;
const networkconfiguration = {}; const networkconfiguration = {};
this._config!.forEach((item) => { this._config!.forEach((item) => {
@@ -191,14 +180,38 @@ class HassioAddonNetwork extends LitElement {
path: "option", path: "option",
}; };
fireEvent(this, "hass-api-called", eventdata); fireEvent(this, "hass-api-called", eventdata);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
}
} catch (err) { } catch (err) {
this._error = `Failed to set addon network configuration, ${ this._error = `Failed to set addon network configuration, ${extractApiErrorMessage(
err.body?.message || err err
}`; )}`;
}
if (!this._error && this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
} }
button.progress = false;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
:host {
display: block;
}
ha-card {
display: block;
}
.errors {
color: var(--error-color);
margin-bottom: 16px;
}
.card-actions {
display: flex;
justify-content: space-between;
}
`,
];
} }
} }

View File

@@ -3,18 +3,19 @@ import {
CSSResult, CSSResult,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-markdown"; import "../../../../src/components/ha-markdown";
import { import {
fetchHassioAddonDocumentation, fetchHassioAddonDocumentation,
HassioAddonDetails, HassioAddonDetails,
} from "../../../../src/data/hassio/addon"; } from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import "../../../../src/layouts/hass-loading-screen"; import "../../../../src/layouts/hass-loading-screen";
import "../../../../src/components/ha-circular-progress";
import { haStyle } from "../../../../src/resources/styles"; import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types"; import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style"; import { hassioStyle } from "../../resources/hassio-style";
@@ -80,9 +81,9 @@ class HassioAddonDocumentationDashboard extends LitElement {
this.addon!.slug this.addon!.slug
); );
} catch (err) { } catch (err) {
this._error = `Failed to get addon documentation, ${ this._error = `Failed to get addon documentation, ${extractApiErrorMessage(
err.body?.message || err err
}`; )}`;
} }
} }
} }

View File

@@ -14,15 +14,14 @@ import {
mdiPound, mdiPound,
mdiShield, mdiShield,
} from "@mdi/js"; } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip";
import { import {
css, css,
CSSResult, CSSResult,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { classMap } from "lit-html/directives/class-map"; import { classMap } from "lit-html/directives/class-map";
@@ -34,25 +33,32 @@ import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-card"; import "../../../../src/components/ha-card";
import "../../../../src/components/ha-label-badge"; import "../../../../src/components/ha-label-badge";
import "../../../../src/components/ha-markdown"; import "../../../../src/components/ha-markdown";
import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-switch"; import "../../../../src/components/ha-switch";
import { import {
fetchHassioAddonChangelog, fetchHassioAddonChangelog,
fetchHassioAddonInfo,
HassioAddonDetails, HassioAddonDetails,
HassioAddonSetOptionParams, HassioAddonSetOptionParams,
HassioAddonSetSecurityParams, HassioAddonSetSecurityParams,
installHassioAddon, installHassioAddon,
setHassioAddonOption, setHassioAddonOption,
setHassioAddonSecurity, setHassioAddonSecurity,
startHassioAddon,
uninstallHassioAddon, uninstallHassioAddon,
validateHassioAddonOption,
} from "../../../../src/data/hassio/addon"; } from "../../../../src/data/hassio/addon";
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box"; import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../src/resources/styles"; import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types"; import { HomeAssistant } from "../../../../src/types";
import "../../components/hassio-card-content"; import "../../components/hassio-card-content";
import { showHassioMarkdownDialog } from "../../dialogs/markdown/show-dialog-hassio-markdown"; import { showHassioMarkdownDialog } from "../../dialogs/markdown/show-dialog-hassio-markdown";
import { hassioStyle } from "../../resources/hassio-style"; import { hassioStyle } from "../../resources/hassio-style";
import "../../../../src/components/ha-settings-row";
const STAGE_ICON = { const STAGE_ICON = {
stable: mdiCheckCircle, stable: mdiCheckCircle,
@@ -127,8 +133,6 @@ class HassioAddonInfo extends LitElement {
@internalProperty() private _error?: string; @internalProperty() private _error?: string;
@property({ type: Boolean }) private _installing = false;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
${this._computeUpdateAvailable ${this._computeUpdateAvailable
@@ -401,7 +405,7 @@ class HassioAddonInfo extends LitElement {
></ha-switch> ></ha-switch>
</ha-settings-row> </ha-settings-row>
${this.hass.userData?.showAdvanced ${this.addon.startup !== "once"
? html` ? html`
<ha-settings-row ?three-line=${this.narrow}> <ha-settings-row ?three-line=${this.narrow}>
<span slot="heading"> <span slot="heading">
@@ -499,12 +503,9 @@ class HassioAddonInfo extends LitElement {
</ha-call-api-button> </ha-call-api-button>
` `
: html` : html`
<ha-call-api-button <ha-progress-button @click=${this._startClicked}>
.hass=${this.hass}
.path="hassio/addons/${this.addon.slug}/start"
>
Start Start
</ha-call-api-button> </ha-progress-button>
`} `}
${this._computeShowWebUI ${this._computeShowWebUI
? html` ? html`
@@ -528,12 +529,12 @@ class HassioAddonInfo extends LitElement {
</mwc-button> </mwc-button>
` `
: ""} : ""}
<mwc-button <ha-progress-button
class=" right warning" class=" right warning"
@click=${this._uninstallClicked} @click=${this._uninstallClicked}
> >
Uninstall Uninstall
</mwc-button> </ha-progress-button>
${this.addon.build ${this.addon.build
? html` ? html`
<ha-call-api-button <ha-call-api-button
@@ -555,8 +556,7 @@ class HassioAddonInfo extends LitElement {
` `
: ""} : ""}
<ha-progress-button <ha-progress-button
.disabled=${!this.addon.available || this._installing} .disabled=${!this.addon.available}
.progress=${this._installing}
@click=${this._installClicked} @click=${this._installClicked}
> >
Install Install
@@ -663,7 +663,9 @@ class HassioAddonInfo extends LitElement {
}; };
fireEvent(this, "hass-api-called", eventdata); fireEvent(this, "hass-api-called", eventdata);
} catch (err) { } catch (err) {
this._error = `Failed to set addon option, ${err.body?.message || err}`; this._error = `Failed to set addon option, ${extractApiErrorMessage(
err
)}`;
} }
} }
@@ -681,7 +683,9 @@ class HassioAddonInfo extends LitElement {
}; };
fireEvent(this, "hass-api-called", eventdata); fireEvent(this, "hass-api-called", eventdata);
} catch (err) { } catch (err) {
this._error = `Failed to set addon option, ${err.body?.message || err}`; this._error = `Failed to set addon option, ${extractApiErrorMessage(
err
)}`;
} }
} }
@@ -699,7 +703,9 @@ class HassioAddonInfo extends LitElement {
}; };
fireEvent(this, "hass-api-called", eventdata); fireEvent(this, "hass-api-called", eventdata);
} catch (err) { } catch (err) {
this._error = `Failed to set addon option, ${err.body?.message || err}`; this._error = `Failed to set addon option, ${extractApiErrorMessage(
err
)}`;
} }
} }
@@ -717,9 +723,9 @@ class HassioAddonInfo extends LitElement {
}; };
fireEvent(this, "hass-api-called", eventdata); fireEvent(this, "hass-api-called", eventdata);
} catch (err) { } catch (err) {
this._error = `Failed to set addon security option, ${ this._error = `Failed to set addon security option, ${extractApiErrorMessage(
err.body?.message || err err
}`; )}`;
} }
} }
@@ -737,12 +743,13 @@ class HassioAddonInfo extends LitElement {
}; };
fireEvent(this, "hass-api-called", eventdata); fireEvent(this, "hass-api-called", eventdata);
} catch (err) { } catch (err) {
this._error = `Failed to set addon option, ${err.body?.message || err}`; this._error = `Failed to set addon option, ${extractApiErrorMessage(
err
)}`;
} }
} }
private async _openChangelog(): Promise<void> { private async _openChangelog(): Promise<void> {
this._error = undefined;
try { try {
const content = await fetchHassioAddonChangelog( const content = await fetchHassioAddonChangelog(
this.hass, this.hass,
@@ -753,15 +760,17 @@ class HassioAddonInfo extends LitElement {
content, content,
}); });
} catch (err) { } catch (err) {
this._error = `Failed to get addon changelog, ${ showAlertDialog(this, {
err.body?.message || err title: "Failed to get addon changelog",
}`; text: extractApiErrorMessage(err),
});
} }
} }
private async _installClicked(): Promise<void> { private async _installClicked(ev: CustomEvent): Promise<void> {
this._error = undefined; const button = ev.currentTarget as any;
this._installing = true; button.progress = true;
try { try {
await installHassioAddon(this.hass, this.addon.slug); await installHassioAddon(this.hass, this.addon.slug);
const eventdata = { const eventdata = {
@@ -771,12 +780,62 @@ class HassioAddonInfo extends LitElement {
}; };
fireEvent(this, "hass-api-called", eventdata); fireEvent(this, "hass-api-called", eventdata);
} catch (err) { } catch (err) {
this._error = `Failed to install addon, ${err.body?.message || err}`; showAlertDialog(this, {
title: "Failed to install addon",
text: extractApiErrorMessage(err),
});
} }
this._installing = false; button.progress = false;
} }
private async _uninstallClicked(): Promise<void> { private async _startClicked(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
try {
const validate = await validateHassioAddonOption(
this.hass,
this.addon.slug
);
if (!validate.data.valid) {
await showConfirmationDialog(this, {
title: "Failed to start addon - configruation validation faled!",
text: validate.data.message.split(" Got ")[0],
confirm: () => this._openConfiguration(),
confirmText: "Go to configruation",
dismissText: "Cancel",
});
button.progress = false;
return;
}
} catch (err) {
showAlertDialog(this, {
title: "Failed to validate addon configuration",
text: extractApiErrorMessage(err),
});
button.progress = false;
return;
}
try {
await startHassioAddon(this.hass, this.addon.slug);
this.addon = await fetchHassioAddonInfo(this.hass, this.addon.slug);
} catch (err) {
showAlertDialog(this, {
title: "Failed to start addon",
text: extractApiErrorMessage(err),
});
}
button.progress = false;
}
private _openConfiguration(): void {
navigate(this, `/hassio/addon/${this.addon.slug}/config`);
}
private async _uninstallClicked(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, { const confirmed = await showConfirmationDialog(this, {
title: this.addon.name, title: this.addon.name,
text: "Are you sure you want to uninstall this add-on?", text: "Are you sure you want to uninstall this add-on?",
@@ -785,6 +844,7 @@ class HassioAddonInfo extends LitElement {
}); });
if (!confirmed) { if (!confirmed) {
button.progress = false;
return; return;
} }
@@ -798,8 +858,12 @@ class HassioAddonInfo extends LitElement {
}; };
fireEvent(this, "hass-api-called", eventdata); fireEvent(this, "hass-api-called", eventdata);
} catch (err) { } catch (err) {
this._error = `Failed to uninstall addon, ${err.body?.message || err}`; showAlertDialog(this, {
title: "Failed to uninstall addon",
text: extractApiErrorMessage(err),
});
} }
button.progress = false;
} }
static get styles(): CSSResult[] { static get styles(): CSSResult[] {

View File

@@ -4,9 +4,9 @@ import {
CSSResult, CSSResult,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import "../../../../src/components/ha-card"; import "../../../../src/components/ha-card";
@@ -14,6 +14,7 @@ import {
fetchHassioAddonLogs, fetchHassioAddonLogs,
HassioAddonDetails, HassioAddonDetails,
} from "../../../../src/data/hassio/addon"; } from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { haStyle } from "../../../../src/resources/styles"; import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types"; import { HomeAssistant } from "../../../../src/types";
import "../../components/hassio-ansi-to-html"; import "../../components/hassio-ansi-to-html";
@@ -75,7 +76,7 @@ class HassioAddonLogs extends LitElement {
try { try {
this._content = await fetchHassioAddonLogs(this.hass, this.addon.slug); this._content = await fetchHassioAddonLogs(this.hass, this.addon.slug);
} catch (err) { } catch (err) {
this._error = `Failed to get addon logs, ${err.body?.message || err}`; this._error = `Failed to get addon logs, ${extractApiErrorMessage(err)}`;
} }
} }

View File

@@ -21,7 +21,7 @@ interface State {
class HassioAnsiToHtml extends LitElement { class HassioAnsiToHtml extends LitElement {
@property() public content!: string; @property() public content!: string;
public render(): TemplateResult | void { protected render(): TemplateResult | void {
return html`${this._parseTextToColoredPre(this.content)}`; return html`${this._parseTextToColoredPre(this.content)}`;
} }

View File

@@ -5,27 +5,31 @@ import {
CSSResult, CSSResult,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import "../../../src/components/ha-svg-icon"; import "../../../src/components/ha-svg-icon";
import {
extractApiErrorMessage,
HassioResponse,
ignoredStatusCodes,
} from "../../../src/data/hassio/common";
import { HassioHassOSInfo } from "../../../src/data/hassio/host"; import { HassioHassOSInfo } from "../../../src/data/hassio/host";
import { import {
HassioHomeAssistantInfo, HassioHomeAssistantInfo,
HassioSupervisorInfo, HassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor"; } from "../../../src/data/hassio/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles"; import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types"; import { HomeAssistant } from "../../../src/types";
import { hassioStyle } from "../resources/hassio-style"; import { hassioStyle } from "../resources/hassio-style";
import {
showConfirmationDialog,
showAlertDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import { HassioResponse } from "../../../src/data/hassio/common";
@customElement("hassio-update") @customElement("hassio-update")
export class HassioUpdate extends LitElement { export class HassioUpdate extends LitElement {
@@ -145,7 +149,7 @@ export class HassioUpdate extends LitElement {
} }
private async _confirmUpdate(ev): Promise<void> { private async _confirmUpdate(ev): Promise<void> {
const item = ev.target; const item = ev.currentTarget;
item.progress = true; item.progress = true;
const confirmed = await showConfirmationDialog(this, { const confirmed = await showConfirmationDialog(this, {
title: `Update ${item.name}`, title: `Update ${item.name}`,
@@ -161,11 +165,14 @@ export class HassioUpdate extends LitElement {
try { try {
await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath); await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath);
} catch (err) { } catch (err) {
showAlertDialog(this, { // Only show an error if the status code was not expected (user behind proxy)
title: "Update failed", // or no status at all(connection terminated)
text: if (err.status_code && !ignoredStatusCodes.has(err.status_code)) {
typeof err === "object" ? err.body?.message || "Unkown error" : err, showAlertDialog(this, {
}); title: "Update failed",
text: extractApiErrorMessage(err),
});
}
} }
item.progress = false; item.progress = false;
} }

View File

@@ -1,43 +1,42 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@material/mwc-icon-button"; import "@material/mwc-icon-button";
import "@material/mwc-tab-bar";
import "@material/mwc-tab"; import "@material/mwc-tab";
import { PaperInputElement } from "@polymer/paper-input/paper-input"; import "@material/mwc-tab-bar";
import { mdiClose } from "@mdi/js"; import { mdiClose } from "@mdi/js";
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { import {
css, css,
CSSResult, CSSResult,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { cache } from "lit-html/directives/cache"; import { cache } from "lit-html/directives/cache";
import {
updateNetworkInterface,
NetworkInterface,
} from "../../../../src/data/hassio/network";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import { HassioNetworkDialogParams } from "./show-dialog-network";
import { haStyleDialog } from "../../../../src/resources/styles";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../../src/dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../../src/types";
import type { HaRadio } from "../../../../src/components/ha-radio";
import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
import "../../../../src/components/ha-circular-progress"; import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog"; import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-formfield"; import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-header-bar"; import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-radio"; import "../../../../src/components/ha-radio";
import type { HaRadio } from "../../../../src/components/ha-radio";
import "../../../../src/components/ha-related-items"; import "../../../../src/components/ha-related-items";
import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-svg-icon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import {
NetworkInterface,
updateNetworkInterface,
} from "../../../../src/data/hassio/network";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../../src/dialogs/generic/show-dialog-box";
import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { HassioNetworkDialogParams } from "./show-dialog-network";
@customElement("dialog-hassio-network") @customElement("dialog-hassio-network")
export class DialogHassioNetwork extends LitElement implements HassDialog { export class DialogHassioNetwork extends LitElement implements HassDialog {
@@ -90,7 +89,14 @@ export class DialogHassioNetwork extends LitElement implements HassDialog {
} }
return html` return html`
<ha-dialog open .heading=${true} hideActions @closed=${this.closeDialog}> <ha-dialog
open
scrimClickAction
escapeKeyAction
.heading=${true}
hideActions
@closed=${this.closeDialog}
>
<div slot="heading"> <div slot="heading">
<ha-header-bar> <ha-header-bar>
<span slot="title"> <span slot="title">
@@ -194,8 +200,7 @@ export class DialogHassioNetwork extends LitElement implements HassDialog {
} catch (err) { } catch (err) {
showAlertDialog(this, { showAlertDialog(this, {
title: "Failed to change network settings", title: "Failed to change network settings",
text: text: extractApiErrorMessage(err),
typeof err === "object" ? err.body.message || "Unkown error" : err,
}); });
this._prosessing = false; this._prosessing = false;
return; return;

View File

@@ -5,25 +5,26 @@ import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input"; import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-item/paper-item-body";
import "../../../../src/components/ha-circular-progress";
import { import {
css, css,
CSSResult, CSSResult,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
query, query,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog"; import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-svg-icon";
import { import {
fetchHassioAddonsInfo, fetchHassioAddonsInfo,
HassioAddonRepository, HassioAddonRepository,
} from "../../../../src/data/hassio/addon"; } from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { setSupervisorOption } from "../../../../src/data/hassio/supervisor"; import { setSupervisorOption } from "../../../../src/data/hassio/supervisor";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types"; import type { HomeAssistant } from "../../../../src/types";
@@ -190,7 +191,7 @@ class HassioRepositoriesDialog extends LitElement {
input.value = ""; input.value = "";
} catch (err) { } catch (err) {
this._error = err.message; this._error = extractApiErrorMessage(err);
} }
this._prosessing = false; this._prosessing = false;
} }
@@ -222,7 +223,7 @@ class HassioRepositoriesDialog extends LitElement {
await this._dialogParams!.loadData(); await this._dialogParams!.loadData();
} catch (err) { } catch (err) {
this._error = err.message; this._error = extractApiErrorMessage(err);
} }
} }
} }

View File

@@ -15,6 +15,7 @@ import {
import { createCloseHeading } from "../../../../src/components/ha-dialog"; import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-svg-icon";
import { getSignedPath } from "../../../../src/data/auth"; import { getSignedPath } from "../../../../src/data/auth";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { import {
fetchHassioSnapshotInfo, fetchHassioSnapshotInfo,
HassioSnapshotDetail, HassioSnapshotDetail,
@@ -379,7 +380,7 @@ class HassioSnapshotDialog extends LitElement {
`/api/hassio/snapshots/${this._snapshot!.slug}/download` `/api/hassio/snapshots/${this._snapshot!.slug}/download`
); );
} catch (err) { } catch (err) {
alert(`Error: ${err.message}`); alert(`Error: ${extractApiErrorMessage(err)}`);
return; return;
} }

View File

@@ -3,6 +3,7 @@ import {
HassioAddonDetails, HassioAddonDetails,
restartHassioAddon, restartHassioAddon,
} from "../../../src/data/hassio/addon"; } from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { import {
showAlertDialog, showAlertDialog,
showConfirmationDialog, showConfirmationDialog,
@@ -26,7 +27,7 @@ export const suggestAddonRestart = async (
} catch (err) { } catch (err) {
showAlertDialog(element, { showAlertDialog(element, {
title: "Failed to restart", title: "Failed to restart",
text: err.body.message, text: extractApiErrorMessage(err),
}); });
} }
} }

View File

@@ -13,15 +13,17 @@ import {
CSSResultArray, CSSResultArray,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { fireEvent } from "../../../src/common/dom/fire_event"; import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import "../../../src/components/ha-svg-icon"; import "../../../src/components/ha-svg-icon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { import {
createHassioFullSnapshot, createHassioFullSnapshot,
createHassioPartialSnapshot, createHassioPartialSnapshot,
@@ -80,8 +82,6 @@ class HassioSnapshots extends LitElement {
{ slug: "addons/local", name: "Local add-ons", checked: true }, { slug: "addons/local", name: "Local add-ons", checked: true },
]; ];
@internalProperty() private _creatingSnapshot = false;
@internalProperty() private _error = ""; @internalProperty() private _error = "";
public async refreshData() { public async refreshData() {
@@ -192,12 +192,9 @@ class HassioSnapshots extends LitElement {
: undefined} : undefined}
</div> </div>
<div class="card-actions"> <div class="card-actions">
<mwc-button <ha-progress-button @click=${this._createSnapshot}>
.disabled=${this._creatingSnapshot}
@click=${this._createSnapshot}
>
Create Create
</mwc-button> </ha-progress-button>
</div> </div>
</ha-card> </ha-card>
</div> </div>
@@ -230,7 +227,7 @@ class HassioSnapshots extends LitElement {
.icon=${snapshot.type === "full" .icon=${snapshot.type === "full"
? mdiPackageVariantClosed ? mdiPackageVariantClosed
: mdiPackageVariant} : mdiPackageVariant}
.icon-class="snapshot" icon-class="snapshot"
></hassio-card-content> ></hassio-card-content>
</div> </div>
</ha-card> </ha-card>
@@ -293,17 +290,20 @@ class HassioSnapshots extends LitElement {
this._snapshots = await fetchHassioSnapshots(this.hass); this._snapshots = await fetchHassioSnapshots(this.hass);
this._snapshots.sort((a, b) => (a.date < b.date ? 1 : -1)); this._snapshots.sort((a, b) => (a.date < b.date ? 1 : -1));
} catch (err) { } catch (err) {
this._error = err.message; this._error = extractApiErrorMessage(err);
} }
} }
private async _createSnapshot() { private async _createSnapshot(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
this._error = ""; this._error = "";
if (this._snapshotHasPassword && !this._snapshotPassword.length) { if (this._snapshotHasPassword && !this._snapshotPassword.length) {
this._error = "Please enter a password."; this._error = "Please enter a password.";
button.progress = false;
return; return;
} }
this._creatingSnapshot = true;
await this.updateComplete; await this.updateComplete;
const name = const name =
@@ -343,10 +343,9 @@ class HassioSnapshots extends LitElement {
this._updateSnapshots(); this._updateSnapshots();
fireEvent(this, "hass-api-called", { success: true, response: null }); fireEvent(this, "hass-api-called", { success: true, response: null });
} catch (err) { } catch (err) {
this._error = err.message; this._error = extractApiErrorMessage(err);
} finally {
this._creatingSnapshot = false;
} }
button.progress = false;
} }
private _computeDetails(snapshot: HassioSnapshot) { private _computeDetails(snapshot: HassioSnapshot) {

View File

@@ -1,9 +1,8 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "@material/mwc-list/mwc-list-item";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js"; import { mdiDotsVertical } from "@mdi/js";
import { safeDump } from "js-yaml"; import { safeDump } from "js-yaml";
import memoizeOne from "memoize-one";
import { import {
css, css,
CSSResult, CSSResult,
@@ -14,7 +13,17 @@ import {
property, property,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row";
import {
extractApiErrorMessage,
ignoredStatusCodes,
} from "../../../src/data/hassio/common";
import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware";
import { import {
changeHostOptions, changeHostOptions,
configSyncOS, configSyncOS,
@@ -25,26 +34,21 @@ import {
shutdownHost, shutdownHost,
updateOS, updateOS,
} from "../../../src/data/hassio/host"; } from "../../../src/data/hassio/host";
import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware";
import { import {
fetchNetworkInfo, fetchNetworkInfo,
NetworkInfo, NetworkInfo,
} from "../../../src/data/hassio/network"; } from "../../../src/data/hassio/network";
import { HassioInfo } from "../../../src/data/hassio/supervisor"; import { HassioInfo } from "../../../src/data/hassio/supervisor";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { import {
showAlertDialog, showAlertDialog,
showConfirmationDialog, showConfirmationDialog,
showPromptDialog, showPromptDialog,
} from "../../../src/dialogs/generic/show-dialog-box"; } from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown"; import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
import { showNetworkDialog } from "../dialogs/network/show-dialog-network"; import { showNetworkDialog } from "../dialogs/network/show-dialog-network";
import { hassioStyle } from "../resources/hassio-style";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row";
@customElement("hassio-host-info") @customElement("hassio-host-info")
class HassioHostInfo extends LitElement { class HassioHostInfo extends LitElement {
@@ -58,7 +62,7 @@ class HassioHostInfo extends LitElement {
@internalProperty() public _networkInfo?: NetworkInfo; @internalProperty() public _networkInfo?: NetworkInfo;
public render(): TemplateResult | void { protected render(): TemplateResult | void {
const primaryIpAddress = this.hostInfo.features.includes("network") const primaryIpAddress = this.hostInfo.features.includes("network")
? this._primaryIpAddress(this._networkInfo!) ? this._primaryIpAddress(this._networkInfo!)
: ""; : "";
@@ -81,7 +85,8 @@ class HassioHostInfo extends LitElement {
</mwc-button> </mwc-button>
</ha-settings-row>` </ha-settings-row>`
: ""} : ""}
${this.hostInfo.features.includes("network") ${this.hostInfo.features.includes("network") &&
atLeastVersion(this.hass.config.version, 0, 115)
? html` <ha-settings-row> ? html` <ha-settings-row>
<span slot="heading"> <span slot="heading">
IP address IP address
@@ -108,12 +113,12 @@ class HassioHostInfo extends LitElement {
${this.hostInfo.version !== this.hostInfo.version_latest && ${this.hostInfo.version !== this.hostInfo.version_latest &&
this.hostInfo.features.includes("hassos") this.hostInfo.features.includes("hassos")
? html` ? html`
<mwc-button <ha-progress-button
title="Update the host OS" title="Update the host OS"
label="Update"
@click=${this._osUpdate} @click=${this._osUpdate}
> >
</mwc-button> Update
</ha-progress-button>
` `
: ""} : ""}
</ha-settings-row> </ha-settings-row>
@@ -141,24 +146,24 @@ class HassioHostInfo extends LitElement {
<div class="card-actions"> <div class="card-actions">
${this.hostInfo.features.includes("reboot") ${this.hostInfo.features.includes("reboot")
? html` ? html`
<mwc-button <ha-progress-button
title="Reboot the host OS" title="Reboot the host OS"
label="Reboot"
class="warning" class="warning"
@click=${this._hostReboot} @click=${this._hostReboot}
> >
</mwc-button> Reboot
</ha-progress-button>
` `
: ""} : ""}
${this.hostInfo.features.includes("shutdown") ${this.hostInfo.features.includes("shutdown")
? html` ? html`
<mwc-button <ha-progress-button
title="Shutdown the host OS" title="Shutdown the host OS"
label="Shutdown"
class="warning" class="warning"
@click=${this._hostShutdown} @click=${this._hostShutdown}
> >
</mwc-button> Shutdown
</ha-progress-button>
` `
: ""} : ""}
@@ -185,6 +190,177 @@ class HassioHostInfo extends LitElement {
`; `;
} }
protected firstUpdated(): void {
this._loadData();
}
private _primaryIpAddress = memoizeOne((network_info: NetworkInfo) => {
if (!network_info) {
return "";
}
return Object.keys(network_info?.interfaces)
.map((device) => network_info.interfaces[device])
.find((device) => device.primary)?.ip_address;
});
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
await this._showHardware();
break;
case 1:
await this._importFromUSB();
break;
}
}
private async _showHardware(): Promise<void> {
try {
const content = await fetchHassioHardwareInfo(this.hass);
showHassioMarkdownDialog(this, {
title: "Hardware",
content: `<pre>${safeDump(content, { indent: 2 })}</pre>`,
});
} catch (err) {
showAlertDialog(this, {
title: "Failed to get Hardware list",
text: extractApiErrorMessage(err),
});
}
}
private async _hostReboot(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: "Reboot",
text: "Are you sure you want to reboot the host?",
confirmText: "reboot host",
dismissText: "no",
});
if (!confirmed) {
button.progress = false;
return;
}
try {
await rebootHost(this.hass);
} catch (err) {
// Ignore connection errors, these are all expected
if (err.status_code && !ignoredStatusCodes.has(err.status_code)) {
showAlertDialog(this, {
title: "Failed to reboot",
text: extractApiErrorMessage(err),
});
}
}
button.progress = false;
}
private async _hostShutdown(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: "Shutdown",
text: "Are you sure you want to shutdown the host?",
confirmText: "shutdown host",
dismissText: "no",
});
if (!confirmed) {
button.progress = false;
return;
}
try {
await shutdownHost(this.hass);
} catch (err) {
// Ignore connection errors, these are all expected
if (err.status_code && !ignoredStatusCodes.has(err.status_code)) {
showAlertDialog(this, {
title: "Failed to shutdown",
text: extractApiErrorMessage(err),
});
}
}
button.progress = false;
}
private async _osUpdate(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: "Update",
text: "Are you sure you want to update the OS?",
confirmText: "update os",
dismissText: "no",
});
if (!confirmed) {
button.progress = false;
return;
}
try {
await updateOS(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to update",
text: extractApiErrorMessage(err),
});
}
button.progress = false;
}
private async _changeNetworkClicked(): Promise<void> {
showNetworkDialog(this, {
network: this._networkInfo!,
loadData: () => this._loadData(),
});
}
private async _changeHostnameClicked(): Promise<void> {
const curHostname: string = this.hostInfo.hostname;
const hostname = await showPromptDialog(this, {
title: "Change hostname",
inputLabel: "Please enter a new hostname:",
inputType: "string",
defaultValue: curHostname,
});
if (hostname && hostname !== curHostname) {
try {
await changeHostOptions(this.hass, { hostname });
this.hostInfo = await fetchHassioHostInfo(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Setting hostname failed",
text: extractApiErrorMessage(err),
});
}
}
}
private async _importFromUSB(): Promise<void> {
try {
await configSyncOS(this.hass);
this.hostInfo = await fetchHassioHostInfo(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to import from USB",
text: extractApiErrorMessage(err),
});
}
}
private async _loadData(): Promise<void> {
this._networkInfo = await fetchNetworkInfo(this.hass);
}
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
haStyle, haStyle,
@@ -240,162 +416,6 @@ class HassioHostInfo extends LitElement {
`, `,
]; ];
} }
protected firstUpdated(): void {
this._loadData();
}
private _primaryIpAddress = memoizeOne((network_info: NetworkInfo) => {
if (!network_info) {
return "";
}
return Object.keys(network_info?.interfaces)
.map((device) => network_info.interfaces[device])
.find((device) => device.primary)?.ip_address;
});
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
await this._showHardware();
break;
case 1:
await this._importFromUSB();
break;
}
}
private async _showHardware(): Promise<void> {
try {
const content = await fetchHassioHardwareInfo(this.hass);
showHassioMarkdownDialog(this, {
title: "Hardware",
content: `<pre>${safeDump(content, { indent: 2 })}</pre>`,
});
} catch (err) {
showAlertDialog(this, {
title: "Failed to get Hardware list",
text:
typeof err === "object" ? err.body?.message || "Unkown error" : err,
});
}
}
private async _hostReboot(): Promise<void> {
const confirmed = await showConfirmationDialog(this, {
title: "Reboot",
text: "Are you sure you want to reboot the host?",
confirmText: "reboot host",
dismissText: "no",
});
if (!confirmed) {
return;
}
try {
await rebootHost(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to reboot",
text:
typeof err === "object" ? err.body?.message || "Unkown error" : err,
});
}
}
private async _hostShutdown(): Promise<void> {
const confirmed = await showConfirmationDialog(this, {
title: "Shutdown",
text: "Are you sure you want to shutdown the host?",
confirmText: "shutdown host",
dismissText: "no",
});
if (!confirmed) {
return;
}
try {
await shutdownHost(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to shutdown",
text:
typeof err === "object" ? err.body?.message || "Unkown error" : err,
});
}
}
private async _osUpdate(): Promise<void> {
const confirmed = await showConfirmationDialog(this, {
title: "Update",
text: "Are you sure you want to update the OS?",
confirmText: "update os",
dismissText: "no",
});
if (!confirmed) {
return;
}
try {
await updateOS(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to update",
text:
typeof err === "object" ? err.body?.message || "Unkown error" : err,
});
}
}
private async _changeNetworkClicked(): Promise<void> {
showNetworkDialog(this, {
network: this._networkInfo!,
loadData: () => this._loadData(),
});
}
private async _changeHostnameClicked(): Promise<void> {
const curHostname: string = this.hostInfo.hostname;
const hostname = await showPromptDialog(this, {
title: "Change hostname",
inputLabel: "Please enter a new hostname:",
inputType: "string",
defaultValue: curHostname,
});
if (hostname && hostname !== curHostname) {
try {
await changeHostOptions(this.hass, { hostname });
this.hostInfo = await fetchHassioHostInfo(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Setting hostname failed",
text:
typeof err === "object" ? err.body?.message || "Unkown error" : err,
});
}
}
}
private async _importFromUSB(): Promise<void> {
try {
await configSyncOS(this.hass);
this.hostInfo = await fetchHassioHostInfo(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to import from USB",
text:
typeof err === "object" ? err.body?.message || "Unkown error" : err,
});
}
}
private async _loadData(): Promise<void> {
this._networkInfo = await fetchNetworkInfo(this.hass);
}
} }
declare global { declare global {

View File

@@ -1,4 +1,3 @@
import "@material/mwc-button";
import { import {
css, css,
CSSResult, CSSResult,
@@ -8,26 +7,27 @@ import {
property, property,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-switch";
import { HassioHostInfo as HassioHostInfoType } from "../../../src/data/hassio/host"; import { HassioHostInfo as HassioHostInfoType } from "../../../src/data/hassio/host";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { import {
HassioSupervisorInfo as HassioSupervisorInfoType, HassioSupervisorInfo as HassioSupervisorInfoType,
reloadSupervisor, reloadSupervisor,
setSupervisorOption, setSupervisorOption,
SupervisorOptions, SupervisorOptions,
updateSupervisor, updateSupervisor,
fetchHassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor"; } from "../../../src/data/hassio/supervisor";
import { import {
showAlertDialog, showAlertDialog,
showConfirmationDialog, showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box"; } from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles";
import "../../../src/components/ha-card"; import { HomeAssistant } from "../../../src/types";
import "../../../src/components/ha-settings-row"; import { hassioStyle } from "../resources/hassio-style";
import "../../../src/components/ha-switch"; import { extractApiErrorMessage } from "../../../src/data/hassio/common";
@customElement("hassio-supervisor-info") @customElement("hassio-supervisor-info")
class HassioSupervisorInfo extends LitElement { class HassioSupervisorInfo extends LitElement {
@@ -37,7 +37,7 @@ class HassioSupervisorInfo extends LitElement {
@property() public hostInfo!: HassioHostInfoType; @property() public hostInfo!: HassioHostInfoType;
public render(): TemplateResult | void { protected render(): TemplateResult | void {
return html` return html`
<ha-card header="Supervisor"> <ha-card header="Supervisor">
<div class="card-content"> <div class="card-content">
@@ -58,12 +58,12 @@ class HassioSupervisorInfo extends LitElement {
</span> </span>
${this.supervisorInfo.version !== this.supervisorInfo.version_latest ${this.supervisorInfo.version !== this.supervisorInfo.version_latest
? html` ? html`
<mwc-button <ha-progress-button
title="Update the supervisor" title="Update the supervisor"
label="Update"
@click=${this._supervisorUpdate} @click=${this._supervisorUpdate}
> >
</mwc-button> Update
</ha-progress-button>
` `
: ""} : ""}
</ha-settings-row> </ha-settings-row>
@@ -76,21 +76,21 @@ class HassioSupervisorInfo extends LitElement {
</span> </span>
${this.supervisorInfo.channel === "beta" ${this.supervisorInfo.channel === "beta"
? html` ? html`
<mwc-button <ha-progress-button
@click=${this._toggleBeta} @click=${this._toggleBeta}
label="Leave beta channel"
title="Get stable updates for Home Assistant, supervisor and host" title="Get stable updates for Home Assistant, supervisor and host"
> >
</mwc-button> Leave beta channel
</ha-progress-button>
` `
: this.supervisorInfo.channel === "stable" : this.supervisorInfo.channel === "stable"
? html` ? html`
<mwc-button <ha-progress-button
@click=${this._toggleBeta} @click=${this._toggleBeta}
label="Join beta channel"
title="Get beta updates for Home Assistant (RCs), supervisor and host" title="Get beta updates for Home Assistant (RCs), supervisor and host"
> >
</mwc-button> Join beta channel
</ha-progress-button>
` `
: ""} : ""}
</ha-settings-row> </ha-settings-row>
@@ -133,17 +133,136 @@ class HassioSupervisorInfo extends LitElement {
</div>`} </div>`}
</div> </div>
<div class="card-actions"> <div class="card-actions">
<mwc-button <ha-progress-button
@click=${this._supervisorReload} @click=${this._supervisorReload}
title="Reload parts of the supervisor." title="Reload parts of the supervisor."
label="Reload"
> >
</mwc-button> Reload
</ha-progress-button>
</div> </div>
</ha-card> </ha-card>
`; `;
} }
private async _toggleBeta(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
if (this.supervisorInfo.channel === "stable") {
const confirmed = await showConfirmationDialog(this, {
title: "WARNING",
text: html` Beta releases are for testers and early adopters and can
contain unstable code changes.
<br />
<b>
Make sure you have backups of your data before you activate this
feature.
</b>
<br /><br />
This includes beta releases for:
<li>Home Assistant Core</li>
<li>Home Assistant Supervisor</li>
<li>Home Assistant Operating System</li>
<br />
Do you want to join the beta channel?`,
confirmText: "join beta",
dismissText: "no",
});
if (!confirmed) {
button.progress = false;
return;
}
}
try {
const data: Partial<SupervisorOptions> = {
channel: this.supervisorInfo.channel === "stable" ? "beta" : "stable",
};
await setSupervisorOption(this.hass, data);
await reloadSupervisor(this.hass);
this.supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to set supervisor option",
text: extractApiErrorMessage(err),
});
}
button.progress = false;
}
private async _supervisorReload(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
try {
await reloadSupervisor(this.hass);
this.supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to reload the supervisor",
text: extractApiErrorMessage(err),
});
}
button.progress = false;
}
private async _supervisorUpdate(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: "Update supervisor",
text: `Are you sure you want to upgrade supervisor to version ${this.supervisorInfo.version_latest}?`,
confirmText: "update",
dismissText: "cancel",
});
if (!confirmed) {
button.progress = false;
return;
}
try {
await updateSupervisor(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to update the supervisor",
text: extractApiErrorMessage(err),
});
}
button.progress = false;
}
private async _diagnosticsInformationDialog(): Promise<void> {
await showAlertDialog(this, {
title: "Help Improve Home Assistant",
text: html`Would you want to automatically share crash reports and
diagnostic information when the supervisor encounters unexpected errors?
<br /><br />
This will allow us to fix the problems, the information is only
accessible to the Home Assistant Core team and will not be shared with
others.
<br /><br />
The data does not include any private/sensitive information and you can
disable this in settings at any time you want.`,
});
}
private async _toggleDiagnostics(): Promise<void> {
try {
const data: SupervisorOptions = {
diagnostics: !this.supervisorInfo?.diagnostics,
};
await setSupervisorOption(this.hass, data);
} catch (err) {
showAlertDialog(this, {
title: "Failed to set supervisor option",
text: extractApiErrorMessage(err),
});
}
}
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
haStyle, haStyle,
@@ -173,109 +292,13 @@ class HassioSupervisorInfo extends LitElement {
ha-settings-row[three-line] { ha-settings-row[three-line] {
height: 74px; height: 74px;
} }
ha-settings-row > span[slot="description"] { ha-settings-row > div[slot="description"] {
white-space: normal; white-space: normal;
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
`, `,
]; ];
} }
private async _toggleBeta(): Promise<void> {
if (this.supervisorInfo.channel === "stable") {
const confirmed = await showConfirmationDialog(this, {
title: "WARNING",
text: html` Beta releases are for testers and early adopters and can
contain unstable code changes.
<br />
<b>
Make sure you have backups of your data before you activate this
feature.
</b>
<br /><br />
This includes beta releases for:
<li>Home Assistant Core</li>
<li>Home Assistant Supervisor</li>
<li>Home Assistant Operating System</li>
<br />
Do you want to join the beta channel?`,
confirmText: "join beta",
dismissText: "no",
});
if (!confirmed) {
return;
}
}
try {
const data: Partial<SupervisorOptions> = {
channel: this.supervisorInfo.channel !== "stable" ? "beta" : "stable",
};
await setSupervisorOption(this.hass, data);
await reloadSupervisor(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to set supervisor option",
text:
typeof err === "object" ? err.body?.message || "Unkown error" : err,
});
}
}
private async _supervisorReload(): Promise<void> {
try {
await reloadSupervisor(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to reload the supervisor",
text:
typeof err === "object" ? err.body?.message || "Unkown error" : err,
});
}
}
private async _supervisorUpdate(): Promise<void> {
try {
await updateSupervisor(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to update the supervisor",
text:
typeof err === "object" ? err.body.message || "Unkown error" : err,
});
}
}
private async _diagnosticsInformationDialog(): Promise<void> {
await showAlertDialog(this, {
title: "Help Improve Home Assistant",
text: html`Would you want to automatically share crash reports and
diagnostic information when the supervisor encounters unexpected errors?
<br /><br />
This will allow us to fix the problems, the information is only
accessible to the Home Assistant Core team and will not be shared with
others.
<br /><br />
The data does not include any private/sensitive information and you can
disable this in settings at any time you want.`,
});
}
private async _toggleDiagnostics(): Promise<void> {
try {
const data: SupervisorOptions = {
diagnostics: !this.supervisorInfo?.diagnostics,
};
await setSupervisorOption(this.hass, data);
} catch (err) {
showAlertDialog(this, {
title: "Failed to set supervisor option",
text:
typeof err === "object" ? err.body.message || "Unkown error" : err,
});
}
}
} }
declare global { declare global {

View File

@@ -12,15 +12,15 @@ import {
property, property,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { fetchHassioLogs } from "../../../src/data/hassio/supervisor"; import { fetchHassioLogs } from "../../../src/data/hassio/supervisor";
import { hassioStyle } from "../resources/hassio-style"; import "../../../src/layouts/hass-loading-screen";
import { haStyle } from "../../../src/resources/styles"; import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types"; import { HomeAssistant } from "../../../src/types";
import "../../../src/components/ha-card";
import "../../../src/layouts/hass-loading-screen";
import "../components/hassio-ansi-to-html"; import "../components/hassio-ansi-to-html";
import { hassioStyle } from "../resources/hassio-style";
interface LogProvider { interface LogProvider {
key: string; key: string;
@@ -69,7 +69,7 @@ class HassioSupervisorLog extends LitElement {
await this._loadData(); await this._loadData();
} }
public render(): TemplateResult | void { protected render(): TemplateResult | void {
return html` return html`
<ha-card> <ha-card>
${this._error ? html` <div class="errors">${this._error}</div> ` : ""} ${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
@@ -104,12 +104,42 @@ class HassioSupervisorLog extends LitElement {
: html`<hass-loading-screen no-toolbar></hass-loading-screen>`} : html`<hass-loading-screen no-toolbar></hass-loading-screen>`}
</div> </div>
<div class="card-actions"> <div class="card-actions">
<mwc-button @click=${this._loadData}>Refresh</mwc-button> <ha-progress-button @click=${this._refresh}>
Refresh
</ha-progress-button>
</div> </div>
</ha-card> </ha-card>
`; `;
} }
private async _setLogProvider(ev): Promise<void> {
const provider = ev.detail.item.getAttribute("provider");
this._selectedLogProvider = provider;
this._loadData();
}
private async _refresh(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
await this._loadData();
button.progress = false;
}
private async _loadData(): Promise<void> {
this._error = undefined;
try {
this._content = await fetchHassioLogs(
this.hass,
this._selectedLogProvider
);
} catch (err) {
this._error = `Failed to get supervisor logs, ${extractApiErrorMessage(
err
)}`;
}
}
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
haStyle, haStyle,
@@ -133,27 +163,6 @@ class HassioSupervisorLog extends LitElement {
`, `,
]; ];
} }
private async _setLogProvider(ev): Promise<void> {
const provider = ev.detail.item.getAttribute("provider");
this._selectedLogProvider = provider;
await this._loadData();
}
private async _loadData(): Promise<void> {
this._error = undefined;
try {
this._content = await fetchHassioLogs(
this.hass,
this._selectedLogProvider
);
} catch (err) {
this._error = `Failed to get supervisor logs, ${
typeof err === "object" ? err.body?.message || "Unkown error" : err
}`;
}
}
} }
declare global { declare global {

View File

@@ -12,8 +12,8 @@ import {
HassioHostInfo, HassioHostInfo,
} from "../../../src/data/hassio/host"; } from "../../../src/data/hassio/host";
import { import {
HassioSupervisorInfo,
HassioInfo, HassioInfo,
HassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor"; } from "../../../src/data/hassio/supervisor";
import "../../../src/layouts/hass-tabs-subpage"; import "../../../src/layouts/hass-tabs-subpage";
import { haStyle } from "../../../src/resources/styles"; import { haStyle } from "../../../src/resources/styles";
@@ -40,7 +40,7 @@ class HassioSystem extends LitElement {
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo; @property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
public render(): TemplateResult | void { protected render(): TemplateResult | void {
return html` return html`
<hass-tabs-subpage <hass-tabs-subpage
.hass=${this.hass} .hass=${this.hass}

View File

@@ -79,6 +79,7 @@
"@polymer/polymer": "3.1.0", "@polymer/polymer": "3.1.0",
"@thomasloven/round-slider": "0.5.0", "@thomasloven/round-slider": "0.5.0",
"@types/chromecast-caf-sender": "^1.0.3", "@types/chromecast-caf-sender": "^1.0.3",
"@types/sortablejs": "^1.10.6",
"@vaadin/vaadin-combo-box": "^5.0.10", "@vaadin/vaadin-combo-box": "^5.0.10",
"@vaadin/vaadin-date-picker": "^4.0.7", "@vaadin/vaadin-date-picker": "^4.0.7",
"@vue/web-component-wrapper": "^1.2.0", "@vue/web-component-wrapper": "^1.2.0",
@@ -114,6 +115,7 @@
"regenerator-runtime": "^0.13.2", "regenerator-runtime": "^0.13.2",
"resize-observer-polyfill": "^1.5.1", "resize-observer-polyfill": "^1.5.1",
"roboto-fontface": "^0.10.0", "roboto-fontface": "^0.10.0",
"sortablejs": "^1.10.2",
"superstruct": "^0.10.12", "superstruct": "^0.10.12",
"unfetch": "^4.1.0", "unfetch": "^4.1.0",
"vue": "^2.6.11", "vue": "^2.6.11",

View File

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

View File

@@ -0,0 +1,9 @@
import { HomeAssistant } from "../../types";
/** Return an array of domains with the service. */
export const componentsWithService = (
hass: HomeAssistant,
service: string
): Array<string> =>
hass &&
Object.keys(hass.services).filter((key) => service in hass.services[key]);

View File

@@ -44,7 +44,6 @@ export const DOMAINS_WITH_MORE_INFO = [
"script", "script",
"sun", "sun",
"timer", "timer",
"updater",
"vacuum", "vacuum",
"water_heater", "water_heater",
"weather", "weather",

View File

@@ -0,0 +1,155 @@
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { PropertyDeclaration, UpdatingElement } from "lit-element";
import type { ClassElement } from "../../types";
type Callback = (oldValue: any, newValue: any) => void;
class Storage {
constructor() {
window.addEventListener("storage", (ev: StorageEvent) => {
if (ev.key && this.hasKey(ev.key)) {
this._storage[ev.key] = ev.newValue
? JSON.parse(ev.newValue)
: ev.newValue;
if (this._listeners[ev.key]) {
this._listeners[ev.key].forEach((listener) =>
listener(
ev.oldValue ? JSON.parse(ev.oldValue) : ev.oldValue,
this._storage[ev.key!]
)
);
}
}
});
}
private _storage: { [storageKey: string]: any } = {};
private _listeners: {
[storageKey: string]: Callback[];
} = {};
public addFromStorage(storageKey: any): void {
if (!this._storage[storageKey]) {
const data = window.localStorage.getItem(storageKey);
if (data) {
this._storage[storageKey] = JSON.parse(data);
}
}
}
public subscribeChanges(
storageKey: string,
callback: Callback
): UnsubscribeFunc {
if (this._listeners[storageKey]) {
this._listeners[storageKey].push(callback);
} else {
this._listeners[storageKey] = [callback];
}
return () => {
this.unsubscribeChanges(storageKey, callback);
};
}
public unsubscribeChanges(storageKey: string, callback: Callback) {
if (!(storageKey in this._listeners)) {
return;
}
const index = this._listeners[storageKey].indexOf(callback);
if (index !== -1) {
this._listeners[storageKey].splice(index, 1);
}
}
public hasKey(storageKey: string): any {
return storageKey in this._storage;
}
public getValue(storageKey: string): any {
return this._storage[storageKey];
}
public setValue(storageKey: string, value: any): any {
this._storage[storageKey] = value;
try {
window.localStorage.setItem(storageKey, JSON.stringify(value));
} catch (err) {
// Safari in private mode doesn't allow localstorage
}
}
}
const storage = new Storage();
export const LocalStorage = (
storageKey?: string,
property?: boolean,
propertyOptions?: PropertyDeclaration
): any => {
return (clsElement: ClassElement) => {
const key = String(clsElement.key);
storageKey = storageKey || String(clsElement.key);
const initVal = clsElement.initializer
? clsElement.initializer()
: undefined;
storage.addFromStorage(storageKey);
const subscribe = (el: UpdatingElement): UnsubscribeFunc =>
storage.subscribeChanges(storageKey!, (oldValue) => {
el.requestUpdate(clsElement.key, oldValue);
});
const getValue = (): any => {
return storage.hasKey(storageKey!)
? storage.getValue(storageKey!)
: initVal;
};
const setValue = (el: UpdatingElement, value: any) => {
let oldValue: unknown | undefined;
if (property) {
oldValue = getValue();
}
storage.setValue(storageKey!, value);
if (property) {
el.requestUpdate(clsElement.key, oldValue);
}
};
return {
kind: "method",
placement: "prototype",
key: clsElement.key,
descriptor: {
set(this: UpdatingElement, value: unknown) {
setValue(this, value);
},
get() {
return getValue();
},
enumerable: true,
configurable: true,
},
finisher(cls: typeof UpdatingElement) {
if (property) {
const connectedCallback = cls.prototype.connectedCallback;
const disconnectedCallback = cls.prototype.disconnectedCallback;
cls.prototype.connectedCallback = function () {
connectedCallback.call(this);
this[`__unbsubLocalStorage${key}`] = subscribe(this);
};
cls.prototype.disconnectedCallback = function () {
disconnectedCallback.call(this);
this[`__unbsubLocalStorage${key}`]();
};
cls.createProperty(clsElement.key, {
noAccessor: true,
...propertyOptions,
});
}
},
};
};
};

View File

@@ -105,12 +105,12 @@ const processTheme = (
const keys = {}; const keys = {};
for (const key of Object.keys(combinedTheme)) { for (const key of Object.keys(combinedTheme)) {
const prefixedKey = `--${key}`; const prefixedKey = `--${key}`;
const value = combinedTheme[key]!; const value = String(combinedTheme[key]!);
styles[prefixedKey] = value; styles[prefixedKey] = value;
keys[prefixedKey] = ""; keys[prefixedKey] = "";
// Try to create a rgb value for this key if it is not a var // Try to create a rgb value for this key if it is not a var
if (!value.startsWith("#")) { if (value.startsWith("#")) {
// Can't convert non hex value // Can't convert non hex value
continue; continue;
} }

View File

@@ -22,9 +22,6 @@ const _load = (
(element as HTMLScriptElement).async = true; (element as HTMLScriptElement).async = true;
if (type) { if (type) {
(element as HTMLScriptElement).type = type; (element as HTMLScriptElement).type = type;
// https://github.com/home-assistant/frontend/pull/6328
(element as HTMLScriptElement).crossOrigin =
url.substr(0, 1) === "/" ? "use-credentials" : "anonymous";
} }
break; break;
case "link": case "link":

View File

@@ -3,49 +3,51 @@ import { HassEntity } from "home-assistant-js-websocket";
/** Return an icon representing a binary sensor state. */ /** Return an icon representing a binary sensor state. */
export const binarySensorIcon = (state: HassEntity) => { export const binarySensorIcon = (state: HassEntity) => {
const activated = state.state && state.state === "off"; const is_off = state.state && state.state === "off";
switch (state.attributes.device_class) { switch (state.attributes.device_class) {
case "battery": case "battery":
return activated ? "hass:battery" : "hass:battery-outline"; return is_off ? "hass:battery" : "hass:battery-outline";
case "battery_charging":
return is_off ? "hass:battery" : "hass:battery-charging";
case "cold": case "cold":
return activated ? "hass:thermometer" : "hass:snowflake"; return is_off ? "hass:thermometer" : "hass:snowflake";
case "connectivity": case "connectivity":
return activated ? "hass:server-network-off" : "hass:server-network"; return is_off ? "hass:server-network-off" : "hass:server-network";
case "door": case "door":
return activated ? "hass:door-closed" : "hass:door-open"; return is_off ? "hass:door-closed" : "hass:door-open";
case "garage_door": case "garage_door":
return activated ? "hass:garage" : "hass:garage-open"; return is_off ? "hass:garage" : "hass:garage-open";
case "gas": case "gas":
case "power": case "power":
case "problem": case "problem":
case "safety": case "safety":
case "smoke": case "smoke":
return activated ? "hass:shield-check" : "hass:alert"; return is_off ? "hass:shield-check" : "hass:alert";
case "heat": case "heat":
return activated ? "hass:thermometer" : "hass:fire"; return is_off ? "hass:thermometer" : "hass:fire";
case "light": case "light":
return activated ? "hass:brightness-5" : "hass:brightness-7"; return is_off ? "hass:brightness-5" : "hass:brightness-7";
case "lock": case "lock":
return activated ? "hass:lock" : "hass:lock-open"; return is_off ? "hass:lock" : "hass:lock-open";
case "moisture": case "moisture":
return activated ? "hass:water-off" : "hass:water"; return is_off ? "hass:water-off" : "hass:water";
case "motion": case "motion":
return activated ? "hass:walk" : "hass:run"; return is_off ? "hass:walk" : "hass:run";
case "occupancy": case "occupancy":
return activated ? "hass:home-outline" : "hass:home"; return is_off ? "hass:home-outline" : "hass:home";
case "opening": case "opening":
return activated ? "hass:square" : "hass:square-outline"; return is_off ? "hass:square" : "hass:square-outline";
case "plug": case "plug":
return activated ? "hass:power-plug-off" : "hass:power-plug"; return is_off ? "hass:power-plug-off" : "hass:power-plug";
case "presence": case "presence":
return activated ? "hass:home-outline" : "hass:home"; return is_off ? "hass:home-outline" : "hass:home";
case "sound": case "sound":
return activated ? "hass:music-note-off" : "hass:music-note"; return is_off ? "hass:music-note-off" : "hass:music-note";
case "vibration": case "vibration":
return activated ? "hass:crop-portrait" : "hass:vibrate"; return is_off ? "hass:crop-portrait" : "hass:vibrate";
case "window": case "window":
return activated ? "hass:window-closed" : "hass:window-open"; return is_off ? "hass:window-closed" : "hass:window-open";
default: default:
return activated ? "hass:radiobox-blank" : "hass:checkbox-marked-circle"; return is_off ? "hass:radiobox-blank" : "hass:checkbox-marked-circle";
} }
}; };

View File

@@ -3,9 +3,10 @@ import { HomeAssistant } from "../../types";
import { DOMAINS_WITH_CARD } from "../const"; import { DOMAINS_WITH_CARD } from "../const";
import { canToggleState } from "./can_toggle_state"; import { canToggleState } from "./can_toggle_state";
import { computeStateDomain } from "./compute_state_domain"; import { computeStateDomain } from "./compute_state_domain";
import { UNAVAILABLE } from "../../data/entity";
export const stateCardType = (hass: HomeAssistant, stateObj: HassEntity) => { export const stateCardType = (hass: HomeAssistant, stateObj: HassEntity) => {
if (stateObj.state === "unavailable") { if (stateObj.state === UNAVAILABLE) {
return "display"; return "display";
} }

View File

@@ -1,7 +1,12 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import durationToSeconds from "../datetime/duration_to_seconds"; import durationToSeconds from "../datetime/duration_to_seconds";
export const timerTimeRemaining = (stateObj: HassEntity) => { export const timerTimeRemaining = (
stateObj: HassEntity
): undefined | number => {
if (!stateObj.attributes.remaining) {
return undefined;
}
let timeRemaining = durationToSeconds(stateObj.attributes.remaining); let timeRemaining = durationToSeconds(stateObj.attributes.remaining);
if (stateObj.state === "active") { if (stateObj.state === "active") {

View File

@@ -1,14 +1,14 @@
import { import {
RequestSelectedDetail,
ListItem, ListItem,
RequestSelectedDetail,
} from "@material/mwc-list/mwc-list-item"; } from "@material/mwc-list/mwc-list-item";
export const shouldHandleRequestSelectedEvent = ( export const shouldHandleRequestSelectedEvent = (
ev: CustomEvent<RequestSelectedDetail> ev: CustomEvent<RequestSelectedDetail>
): boolean => { ): boolean => {
if (!ev.detail.selected && ev.detail.source !== "property") { if (!ev.detail.selected || ev.detail.source !== "property") {
return false; return false;
} }
(ev.target as ListItem).selected = false; (ev.currentTarget as ListItem).selected = false;
return true; return true;
}; };

View File

@@ -0,0 +1,50 @@
// From: underscore.js https://github.com/jashkenas/underscore/blob/master/underscore.js
// Returns a function, that, when invoked, will only be triggered at most once
// during a given window of time. Normally, the throttled function will run
// as much as it can, without ever going more than once per `wait` duration;
// but if you'd like to disable the execution on the leading edge, pass
// `false for leading`. To disable execution on the trailing edge, ditto.
export const throttle = <T extends Function>(
func: T,
wait: number,
leading = true,
trailing = true
): T => {
let timeout: number | undefined;
let previous = 0;
let context: any;
let args: any;
const later = () => {
previous = leading === false ? 0 : Date.now();
timeout = undefined;
func.apply(context, args);
if (!timeout) {
context = null;
args = null;
}
};
// @ts-ignore
return function (...argmnts) {
// @ts-ignore
// @typescript-eslint/no-this-alias
context = this;
args = argmnts;
const now = Date.now();
if (!previous && leading === false) {
previous = now;
}
const remaining = wait - (now - previous);
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = undefined;
}
previous = now;
func.apply(context, args);
} else if (!timeout && trailing !== false) {
timeout = window.setTimeout(later, remaining);
}
};
};

View File

@@ -1,110 +0,0 @@
import "@material/mwc-button";
import "../ha-circular-progress";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
class HaProgressButton extends PolymerElement {
static get template() {
return html`
<style>
:host {
outline: none;
}
.container {
position: relative;
display: inline-block;
}
mwc-button {
transition: all 1s;
}
.success mwc-button {
--mdc-theme-primary: white;
background-color: var(--success-color);
transition: none;
}
.error mwc-button {
--mdc-theme-primary: white;
background-color: var(--error-color);
transition: none;
}
.progress {
@apply --layout;
@apply --layout-center-center;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
</style>
<div class="container" id="container">
<mwc-button
id="button"
disabled="[[computeDisabled(disabled, progress)]]"
on-click="buttonTapped"
>
<slot></slot>
</mwc-button>
<template is="dom-if" if="[[progress]]">
<div class="progress">
<ha-circular-progress active size="small"></ha-circular-progress>
</div>
</template>
</div>
`;
}
static get properties() {
return {
hass: {
type: Object,
},
progress: {
type: Boolean,
value: false,
},
disabled: {
type: Boolean,
value: false,
},
};
}
tempClass(className) {
const classList = this.$.container.classList;
classList.add(className);
setTimeout(() => {
classList.remove(className);
}, 1000);
}
ready() {
super.ready();
this.addEventListener("click", (ev) => this.buttonTapped(ev));
}
buttonTapped(ev) {
if (this.progress) ev.stopPropagation();
}
actionSuccess() {
this.tempClass("success");
}
actionError() {
this.tempClass("error");
}
computeDisabled(disabled, progress) {
return disabled || progress;
}
}
customElements.define("ha-progress-button", HaProgressButton);

View File

@@ -0,0 +1,114 @@
import "@material/mwc-button";
import type { Button } from "@material/mwc-button";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
query,
} from "lit-element";
import "../ha-circular-progress";
@customElement("ha-progress-button")
class HaProgressButton extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public progress = false;
@property({ type: Boolean }) public raised = false;
@query("mwc-button") private _button?: Button;
public render(): TemplateResult {
return html`
<mwc-button
?raised=${this.raised}
.disabled=${this.disabled || this.progress}
@click=${this._buttonTapped}
>
<slot></slot>
</mwc-button>
${this.progress
? html`<div class="progress">
<ha-circular-progress size="small" active></ha-circular-progress>
</div>`
: ""}
`;
}
public actionSuccess(): void {
this._tempClass("success");
}
public actionError(): void {
this._tempClass("error");
}
private _tempClass(className: string): void {
this._button!.classList.add(className);
setTimeout(() => {
this._button!.classList.remove(className);
}, 1000);
}
private _buttonTapped(ev: Event): void {
if (this.progress) {
ev.stopPropagation();
}
}
static get styles(): CSSResult {
return css`
:host {
outline: none;
display: inline-block;
position: relative;
}
mwc-button {
transition: all 1s;
}
mwc-button.success {
--mdc-theme-primary: white;
background-color: var(--success-color);
transition: none;
}
mwc-button[raised].success {
--mdc-theme-primary: var(--success-color);
--mdc-theme-on-primary: white;
}
mwc-button.error {
--mdc-theme-primary: white;
background-color: var(--error-color);
transition: none;
}
mwc-button[raised].error {
--mdc-theme-primary: var(--error-color);
--mdc-theme-on-primary: white;
}
.progress {
bottom: 0;
margin-top: 4px;
position: absolute;
text-align: center;
top: 0;
width: 100%;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-progress-button": HaProgressButton;
}
}

View File

@@ -0,0 +1,178 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
import { HassEntity } from "home-assistant-js-websocket";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
PropertyValues,
query,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../common/dom/fire_event";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import "../ha-icon-button";
import "./state-badge";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
const rowRenderer = (root: HTMLElement, _owner, model: { item: string }) => {
if (!root.firstElementChild) {
root.innerHTML = `
<style>
paper-item {
margin: -10px;
padding: 0;
}
</style>
<paper-item></paper-item>
`;
}
root.querySelector("paper-item")!.textContent = model.item;
};
@customElement("ha-entity-attribute-picker")
class HaEntityAttributePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public entityId?: string;
@property({ type: Boolean }) public autofocus = false;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean, attribute: "allow-custom-value" })
public allowCustomValue;
@property() public label?: string;
@property() public value?: string;
@property({ type: Boolean }) private _opened = false;
@query("vaadin-combo-box-light") private _comboBox!: HTMLElement;
protected shouldUpdate(changedProps: PropertyValues) {
return !(!changedProps.has("_opened") && this._opened);
}
protected updated(changedProps: PropertyValues) {
if (changedProps.has("_opened") && this._opened) {
const state = this.entityId ? this.hass.states[this.entityId] : undefined;
(this._comboBox as any).items = state
? Object.keys(state.attributes)
: [];
}
}
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
<vaadin-combo-box-light
.value=${this._value}
.allowCustomValue=${this.allowCustomValue}
.renderer=${rowRenderer}
@opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged}
>
<paper-input
.autofocus=${this.autofocus}
.label=${this.label ??
this.hass.localize(
"ui.components.entity.entity-attribute-picker.attribute"
)}
.value=${this._value}
.disabled=${this.disabled || !this.entityId}
class="input"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
>
${this.value
? html`
<ha-icon-button
aria-label=${this.hass.localize(
"ui.components.entity.entity-picker.clear"
)}
slot="suffix"
class="clear-button"
icon="hass:close"
@click=${this._clearValue}
no-ripple
>
Clear
</ha-icon-button>
`
: ""}
<ha-icon-button
aria-label=${this.hass.localize(
"ui.components.entity.entity-attribute-picker.show_attributes"
)}
slot="suffix"
class="toggle-button"
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
>
Toggle
</ha-icon-button>
</paper-input>
</vaadin-combo-box-light>
`;
}
private _clearValue(ev: Event) {
ev.stopPropagation();
this._setValue("");
}
private get _value() {
return this.value || "";
}
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private _valueChanged(ev: PolymerChangedEvent<string>) {
const newValue = ev.detail.value;
if (newValue !== this._value) {
this._setValue(newValue);
}
}
private _setValue(value: string) {
this.value = value;
setTimeout(() => {
fireEvent(this, "value-changed", { value });
fireEvent(this, "change");
}, 0);
}
static get styles(): CSSResult {
return css`
paper-input > ha-icon-button {
--mdc-icon-button-size: 24px;
padding: 0px 2px;
color: var(--secondary-text-color);
}
[hidden] {
display: none;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-entity-attribute-picker": HaEntityAttributePicker;
}
}

View File

@@ -1,4 +1,3 @@
import "../ha-icon-button";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-item/paper-item-body";
@@ -7,6 +6,7 @@ import { HassEntity } from "home-assistant-js-websocket";
import { import {
css, css,
CSSResult, CSSResult,
customElement,
html, html,
LitElement, LitElement,
property, property,
@@ -20,6 +20,7 @@ import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name"; import { computeStateName } from "../../common/entity/compute_state_name";
import { PolymerChangedEvent } from "../../polymer-types"; import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-icon-button";
import "./state-badge"; import "./state-badge";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
@@ -51,7 +52,8 @@ const rowRenderer = (
root.querySelector("[secondary]")!.textContent = model.item.entity_id; root.querySelector("[secondary]")!.textContent = model.item.entity_id;
}; };
class HaEntityPicker extends LitElement { @customElement("ha-entity-picker")
export class HaEntityPicker extends LitElement {
@property({ type: Boolean }) public autofocus = false; @property({ type: Boolean }) public autofocus = false;
@property({ type: Boolean }) public disabled?: boolean; @property({ type: Boolean }) public disabled?: boolean;
@@ -95,6 +97,8 @@ class HaEntityPicker extends LitElement {
@query("vaadin-combo-box-light") private _comboBox!: HTMLElement; @query("vaadin-combo-box-light") private _comboBox!: HTMLElement;
private _initedStates = false;
private _getStates = memoizeOne( private _getStates = memoizeOne(
( (
_opened: boolean, _opened: boolean,
@@ -148,11 +152,18 @@ class HaEntityPicker extends LitElement {
); );
protected shouldUpdate(changedProps: PropertyValues) { protected shouldUpdate(changedProps: PropertyValues) {
if (
changedProps.has("value") ||
changedProps.has("label") ||
changedProps.has("disabled")
) {
return true;
}
return !(!changedProps.has("_opened") && this._opened); return !(!changedProps.has("_opened") && this._opened);
} }
protected updated(changedProps: PropertyValues) { protected updated(changedProps: PropertyValues) {
if (changedProps.has("_opened") && this._opened) { if (!this._initedStates || (changedProps.has("_opened") && this._opened)) {
const states = this._getStates( const states = this._getStates(
this._opened, this._opened,
this.hass, this.hass,
@@ -162,6 +173,7 @@ class HaEntityPicker extends LitElement {
this.includeDeviceClasses this.includeDeviceClasses
); );
(this._comboBox as any).items = states; (this._comboBox as any).items = states;
this._initedStates = true;
} }
} }
@@ -169,7 +181,6 @@ class HaEntityPicker extends LitElement {
if (!this.hass) { if (!this.hass) {
return html``; return html``;
} }
return html` return html`
<vaadin-combo-box-light <vaadin-combo-box-light
item-value-path="entity_id" item-value-path="entity_id"
@@ -267,8 +278,6 @@ class HaEntityPicker extends LitElement {
} }
} }
customElements.define("ha-entity-picker", HaEntityPicker);
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-entity-picker": HaEntityPicker; "ha-entity-picker": HaEntityPicker;

View File

@@ -20,6 +20,7 @@ import { stateIcon } from "../../common/entity/state_icon";
import { timerTimeRemaining } from "../../common/entity/timer_time_remaining"; import { timerTimeRemaining } from "../../common/entity/timer_time_remaining";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-label-badge"; import "../ha-label-badge";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
@customElement("ha-state-label-badge") @customElement("ha-state-label-badge")
export class HaStateLabelBadge extends LitElement { export class HaStateLabelBadge extends LitElement {
@@ -81,7 +82,8 @@ export class HaStateLabelBadge extends LitElement {
? "" ? ""
: this.image : this.image
? this.image ? this.image
: state.attributes.entity_picture_local || state.attributes.entity_picture}" : state.attributes.entity_picture_local ||
state.attributes.entity_picture}"
.label="${this._computeLabel(domain, state, this._timerTimeRemaining)}" .label="${this._computeLabel(domain, state, this._timerTimeRemaining)}"
.description="${this.name ? this.name : computeStateName(state)}" .description="${this.name ? this.name : computeStateName(state)}"
></ha-label-badge> ></ha-label-badge>
@@ -108,7 +110,7 @@ export class HaStateLabelBadge extends LitElement {
return null; return null;
case "sensor": case "sensor":
default: default:
return state.state === "unknown" return state.state === UNKNOWN
? "-" ? "-"
: state.attributes.unit_of_measurement : state.attributes.unit_of_measurement
? state.state ? state.state
@@ -121,7 +123,7 @@ export class HaStateLabelBadge extends LitElement {
} }
private _computeIcon(domain: string, state: HassEntity) { private _computeIcon(domain: string, state: HassEntity) {
if (state.state === "unavailable") { if (state.state === UNAVAILABLE) {
return null; return null;
} }
switch (domain) { switch (domain) {
@@ -166,7 +168,7 @@ export class HaStateLabelBadge extends LitElement {
private _computeLabel(domain, state, _timerTimeRemaining) { private _computeLabel(domain, state, _timerTimeRemaining) {
if ( if (
state.state === "unavailable" || state.state === UNAVAILABLE ||
["device_tracker", "alarm_control_panel", "person"].includes(domain) ["device_tracker", "alarm_control_panel", "person"].includes(domain)
) { ) {
// Localize the state with a special state_badge namespace, which has variations of // Localize the state with a special state_badge namespace, which has variations of

View File

@@ -3,56 +3,43 @@ import {
CSSResult, CSSResult,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { computeStateName } from "../common/entity/compute_state_name"; import { computeStateName } from "../common/entity/compute_state_name";
import { supportsFeature } from "../common/entity/supports-feature"; import { supportsFeature } from "../common/entity/supports-feature";
import { nextRender } from "../common/util/render-status";
import { getExternalConfig } from "../external_app/external_config";
import { import {
CAMERA_SUPPORT_STREAM, CAMERA_SUPPORT_STREAM,
computeMJPEGStreamUrl, computeMJPEGStreamUrl,
fetchStreamUrl, fetchStreamUrl,
} from "../data/camera"; } from "../data/camera";
import { CameraEntity, HomeAssistant } from "../types"; import { CameraEntity, HomeAssistant } from "../types";
import "./ha-hls-player";
type HLSModule = typeof import("hls.js");
@customElement("ha-camera-stream") @customElement("ha-camera-stream")
class HaCameraStream extends LitElement { class HaCameraStream extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property() public stateObj?: CameraEntity; @property({ attribute: false }) public stateObj?: CameraEntity;
@property({ type: Boolean }) public showControls = false; @property({ type: Boolean, attribute: "controls" })
public controls = false;
@internalProperty() private _attached = false; @property({ type: Boolean, attribute: "muted" })
public muted = false;
// We keep track if we should force MJPEG with a string // We keep track if we should force MJPEG with a string
// that way it automatically resets if we change entity. // that way it automatically resets if we change entity.
@internalProperty() private _forceMJPEG: string | undefined = undefined; @internalProperty() private _forceMJPEG?: string;
private _hlsPolyfillInstance?: Hls; @internalProperty() private _url?: string;
private _useExoPlayer = false;
public connectedCallback() {
super.connectedCallback();
this._attached = true;
}
public disconnectedCallback() {
super.disconnectedCallback();
this._attached = false;
}
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.stateObj || !this._attached) { if (!this.stateObj) {
return html``; return html``;
} }
@@ -69,51 +56,25 @@ class HaCameraStream extends LitElement {
)} camera.`} )} camera.`}
/> />
` `
: html` : this._url
<video ? html`
<ha-hls-player
autoplay autoplay
muted
playsinline playsinline
?controls=${this.showControls} .muted=${this.muted}
@loadeddata=${this._elementResized} .controls=${this.controls}
></video> .hass=${this.hass}
`} .url=${this._url}
></ha-hls-player>
`
: ""}
`; `;
} }
protected updated(changedProps: PropertyValues) { protected updated(changedProps: PropertyValues): void {
super.updated(changedProps); if (changedProps.has("stateObj") && !this._shouldRenderMJPEG) {
this._forceMJPEG = undefined;
const stateObjChanged = changedProps.has("stateObj"); this._getStreamUrl();
const attachedChanged = changedProps.has("_attached");
const oldState = changedProps.get("stateObj") as this["stateObj"];
const oldEntityId = oldState ? oldState.entity_id : undefined;
const curEntityId = this.stateObj ? this.stateObj.entity_id : undefined;
if (
(!stateObjChanged && !attachedChanged) ||
(stateObjChanged && oldEntityId === curEntityId)
) {
return;
}
// If we are no longer attached, destroy polyfill.
if (attachedChanged && !this._attached) {
this._destroyPolyfill();
return;
}
// Nothing to do if we are render MJPEG.
if (this._shouldRenderMJPEG) {
return;
}
// Tear down existing polyfill, if available
this._destroyPolyfill();
if (curEntityId) {
this._startHls();
} }
} }
@@ -125,136 +86,35 @@ class HaCameraStream extends LitElement {
); );
} }
private get _videoEl(): HTMLVideoElement { private async _getStreamUrl(): Promise<void> {
return this.shadowRoot!.querySelector("video")!;
}
private async _getUseExoPlayer(): Promise<boolean> {
if (!this.hass!.auth.external) {
return false;
}
const externalConfig = await getExternalConfig(this.hass!.auth.external);
return externalConfig && externalConfig.hasExoPlayer;
}
private async _startHls(): Promise<void> {
// eslint-disable-next-line
let hls;
const videoEl = this._videoEl;
this._useExoPlayer = await this._getUseExoPlayer();
if (!this._useExoPlayer) {
hls = ((await import(/* webpackChunkName: "hls.js" */ "hls.js")) as any)
.default as HLSModule;
let hlsSupported = hls.isSupported();
if (!hlsSupported) {
hlsSupported =
videoEl.canPlayType("application/vnd.apple.mpegurl") !== "";
}
if (!hlsSupported) {
this._forceMJPEG = this.stateObj!.entity_id;
return;
}
}
try { try {
const { url } = await fetchStreamUrl( const { url } = await fetchStreamUrl(
this.hass!, this.hass!,
this.stateObj!.entity_id this.stateObj!.entity_id
); );
if (this._useExoPlayer) { this._url = url;
this._renderHLSExoPlayer(url);
} else if (hls.isSupported()) {
this._renderHLSPolyfill(videoEl, hls, url);
} else {
this._renderHLSNative(videoEl, url);
}
return;
} catch (err) { } catch (err) {
// Fails if we were unable to get a stream // Fails if we were unable to get a stream
// eslint-disable-next-line // eslint-disable-next-line
console.error(err); console.error(err);
this._forceMJPEG = this.stateObj!.entity_id; this._forceMJPEG = this.stateObj!.entity_id;
} }
} }
private async _renderHLSExoPlayer(url: string) {
window.addEventListener("resize", this._resizeExoPlayer);
this.updateComplete.then(() => nextRender()).then(this._resizeExoPlayer);
this._videoEl.style.visibility = "hidden";
await this.hass!.auth.external!.sendMessage({
type: "exoplayer/play_hls",
payload: new URL(url, window.location.href).toString(),
});
}
private _resizeExoPlayer = () => {
const rect = this._videoEl.getBoundingClientRect();
this.hass!.auth.external!.fireMessage({
type: "exoplayer/resize",
payload: {
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
},
});
};
private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) {
videoEl.src = url;
await new Promise((resolve) =>
videoEl.addEventListener("loadedmetadata", resolve)
);
videoEl.play();
}
private async _renderHLSPolyfill(
videoEl: HTMLVideoElement,
// eslint-disable-next-line
Hls: HLSModule,
url: string
) {
const hls = new Hls({
liveBackBufferLength: 60,
fragLoadingTimeOut: 30000,
manifestLoadingTimeOut: 30000,
levelLoadingTimeOut: 30000,
});
this._hlsPolyfillInstance = hls;
hls.attachMedia(videoEl);
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
hls.loadSource(url);
});
}
private _elementResized() { private _elementResized() {
fireEvent(this, "iron-resize"); fireEvent(this, "iron-resize");
} }
private _destroyPolyfill() {
if (this._hlsPolyfillInstance) {
this._hlsPolyfillInstance.destroy();
this._hlsPolyfillInstance = undefined;
}
if (this._useExoPlayer) {
window.removeEventListener("resize", this._resizeExoPlayer);
this.hass!.auth.external!.fireMessage({ type: "exoplayer/stop" });
}
}
static get styles(): CSSResult { static get styles(): CSSResult {
return css` return css`
:host, :host,
img, img {
video {
display: block; display: block;
} }
img, img {
video {
width: 100%; width: 100%;
} }
`; `;

View File

@@ -1,16 +1,16 @@
import {
LitElement,
TemplateResult,
property,
svg,
html,
customElement,
unsafeCSS,
SVGTemplateResult,
css,
} from "lit-element";
// @ts-ignore // @ts-ignore
import progressStyles from "@material/circular-progress/dist/mdc.circular-progress.min.css"; import progressStyles from "@material/circular-progress/dist/mdc.circular-progress.min.css";
import {
css,
customElement,
html,
LitElement,
property,
svg,
SVGTemplateResult,
TemplateResult,
unsafeCSS,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map"; import { classMap } from "lit-html/directives/class-map";
@customElement("ha-circular-progress") @customElement("ha-circular-progress")
@@ -24,7 +24,7 @@ export class HaCircularProgress extends LitElement {
@property() @property()
public size: "small" | "medium" | "large" = "medium"; public size: "small" | "medium" | "large" = "medium";
protected render(): TemplateResult | void { protected render(): TemplateResult {
let indeterminatePart: SVGTemplateResult; let indeterminatePart: SVGTemplateResult;
if (this.size === "small") { if (this.size === "small") {

View File

@@ -97,6 +97,7 @@ export class HaCodeEditor extends UpdatingElement {
.CodeMirror { .CodeMirror {
height: var(--code-mirror-height, auto); height: var(--code-mirror-height, auto);
direction: var(--code-mirror-direction, ltr); direction: var(--code-mirror-direction, ltr);
font-family: var(--code-font-family, monospace);
} }
.CodeMirror-scroll { .CodeMirror-scroll {
max-height: var(--code-mirror-max-height, --code-mirror-height); max-height: var(--code-mirror-max-height, --code-mirror-height);

View File

@@ -176,6 +176,11 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
this.drawColorWheel(); this.drawColorWheel();
this.drawMarker(); this.drawMarker();
if (this.desiredHsColor) {
this.setMarkerOnColor(this.desiredHsColor);
this.applyColorToCanvas(this.desiredHsColor);
}
this.interactionLayer.addEventListener("mousedown", (ev) => this.interactionLayer.addEventListener("mousedown", (ev) =>
this.onMouseDown(ev) this.onMouseDown(ev)
); );
@@ -319,6 +324,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
// set marker position to the given color // set marker position to the given color
setMarkerOnColor(hs) { setMarkerOnColor(hs) {
if (!this.marker || !this.tooltip) {
return;
}
const dist = hs.s * this.radius; const dist = hs.s * this.radius;
const theta = ((hs.h - 180) / 180) * Math.PI; const theta = ((hs.h - 180) / 180) * Math.PI;
const markerdX = -dist * Math.cos(theta); const markerdX = -dist * Math.cos(theta);
@@ -330,6 +338,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
// apply given color to interface elements // apply given color to interface elements
applyColorToCanvas(hs) { applyColorToCanvas(hs) {
if (!this.interactionLayer) {
return;
}
// we're not really converting hs to hsl here, but we keep it cheap // we're not really converting hs to hsl here, but we keep it cheap
// setting the color on the interactionLayer, the svg elements can inherit // setting the color on the interactionLayer, the svg elements can inherit
this.interactionLayer.style.color = `hsl(${hs.h}, 100%, ${ this.interactionLayer.style.color = `hsl(${hs.h}, 100%, ${

View File

@@ -1,16 +1,16 @@
import "@material/mwc-dialog"; import "@material/mwc-dialog";
import type { Dialog } from "@material/mwc-dialog"; import type { Dialog } from "@material/mwc-dialog";
import { style } from "@material/mwc-dialog/mwc-dialog-css"; import { style } from "@material/mwc-dialog/mwc-dialog-css";
import "./ha-icon-button";
import { css, CSSResult, customElement, html } from "lit-element";
import type { Constructor, HomeAssistant } from "../types";
import { mdiClose } from "@mdi/js"; import { mdiClose } from "@mdi/js";
import { css, CSSResult, customElement, html } from "lit-element";
import { computeRTLDirection } from "../common/util/compute_rtl"; import { computeRTLDirection } from "../common/util/compute_rtl";
import type { Constructor, HomeAssistant } from "../types";
import "./ha-icon-button";
const MwcDialog = customElements.get("mwc-dialog") as Constructor<Dialog>; const MwcDialog = customElements.get("mwc-dialog") as Constructor<Dialog>;
export const createCloseHeading = (hass: HomeAssistant, title: string) => html` export const createCloseHeading = (hass: HomeAssistant, title: string) => html`
${title} <span class="header_title">${title}</span>
<mwc-icon-button <mwc-icon-button
aria-label=${hass.localize("ui.dialogs.generic.close")} aria-label=${hass.localize("ui.dialogs.generic.close")}
dialogAction="close" dialogAction="close"
@@ -23,6 +23,10 @@ export const createCloseHeading = (hass: HomeAssistant, title: string) => html`
@customElement("ha-dialog") @customElement("ha-dialog")
export class HaDialog extends MwcDialog { export class HaDialog extends MwcDialog {
public scrollToPos(x: number, y: number) {
this.contentElement.scrollTo(x, y);
}
protected renderHeading() { protected renderHeading() {
return html`<slot name="heading"> return html`<slot name="heading">
${super.renderHeading()} ${super.renderHeading()}
@@ -60,8 +64,13 @@ export class HaDialog extends MwcDialog {
} }
.mdc-dialog .mdc-dialog__surface { .mdc-dialog .mdc-dialog__surface {
position: var(--dialog-surface-position, relative); position: var(--dialog-surface-position, relative);
top: var(--dialog-surface-top);
min-height: var(--mdc-dialog-min-height, auto); min-height: var(--mdc-dialog-min-height, auto);
} }
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
display: flex;
flex-direction: column;
}
.header_button { .header_button {
position: absolute; position: absolute;
right: 16px; right: 16px;
@@ -69,10 +78,17 @@ export class HaDialog extends MwcDialog {
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
} }
.header_title {
margin-right: 40px;
}
[dir="rtl"].header_button { [dir="rtl"].header_button {
right: auto; right: auto;
left: 16px; left: 16px;
} }
[dir="rtl"].header_title {
margin-left: 40px;
margin-right: 0px;
}
`, `,
]; ];
} }

View File

@@ -54,7 +54,8 @@ export class HaFormInteger extends LitElement implements HaFormElement {
` `
: ""} : ""}
<ha-paper-slider <ha-paper-slider
pin="" pin
editable
.value=${this._value} .value=${this._value}
.min=${this.schema.valueMin} .min=${this.schema.valueMin}
.max=${this.schema.valueMax} .max=${this.schema.valueMax}
@@ -111,6 +112,10 @@ export class HaFormInteger extends LitElement implements HaFormElement {
.flex { .flex {
display: flex; display: flex;
} }
ha-paper-slider {
width: 100%;
margin-right: 16px;
}
`; `;
} }
} }

View File

@@ -1,4 +1,3 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox"; import "@polymer/paper-listbox/paper-listbox";
import { import {
@@ -12,6 +11,7 @@ import {
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import "../ha-paper-dropdown-menu";
import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./ha-form"; import { HaFormElement, HaFormSelectData, HaFormSelectSchema } from "./ha-form";
@customElement("ha-form-select") @customElement("ha-form-select")
@@ -24,7 +24,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
@property() public suffix!: string; @property() public suffix!: string;
@query("paper-dropdown-menu") private _input?: HTMLElement; @query("ha-paper-dropdown-menu") private _input?: HTMLElement;
public focus() { public focus() {
if (this._input) { if (this._input) {
@@ -34,7 +34,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<paper-dropdown-menu .label=${this.label}> <ha-paper-dropdown-menu .label=${this.label}>
<paper-listbox <paper-listbox
slot="dropdown-content" slot="dropdown-content"
attr-for-selected="item-value" attr-for-selected="item-value"
@@ -51,7 +51,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
` `
)} )}
</paper-listbox> </paper-listbox>
</paper-dropdown-menu> </ha-paper-dropdown-menu>
`; `;
} }
@@ -74,7 +74,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
static get styles(): CSSResult { static get styles(): CSSResult {
return css` return css`
paper-dropdown-menu { ha-paper-dropdown-menu {
display: block; display: block;
} }
`; `;

View File

@@ -1,6 +1,6 @@
import { customElement, LitElement, html, unsafeCSS, css } from "lit-element";
// @ts-ignore // @ts-ignore
import topAppBarStyles from "@material/top-app-bar/dist/mdc.top-app-bar.min.css"; import topAppBarStyles from "@material/top-app-bar/dist/mdc.top-app-bar.min.css";
import { css, customElement, html, LitElement, unsafeCSS } from "lit-element";
@customElement("ha-header-bar") @customElement("ha-header-bar")
export class HaHeaderBar extends LitElement { export class HaHeaderBar extends LitElement {

View File

@@ -0,0 +1,216 @@
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
PropertyValues,
query,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../common/dom/fire_event";
import { nextRender } from "../common/util/render-status";
import { getExternalConfig } from "../external_app/external_config";
import type { HomeAssistant } from "../types";
type HLSModule = typeof import("hls.js");
@customElement("ha-hls-player")
class HaHLSPlayer extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public url!: string;
@property({ type: Boolean, attribute: "controls" })
public controls = false;
@property({ type: Boolean, attribute: "muted" })
public muted = false;
@property({ type: Boolean, attribute: "autoplay" })
public autoPlay = false;
@property({ type: Boolean, attribute: "playsinline" })
public playsInline = false;
@query("video") private _videoEl!: HTMLVideoElement;
@internalProperty() private _attached = false;
private _hlsPolyfillInstance?: Hls;
private _useExoPlayer = false;
public connectedCallback() {
super.connectedCallback();
this._attached = true;
}
public disconnectedCallback() {
super.disconnectedCallback();
this._attached = false;
}
protected render(): TemplateResult {
if (!this._attached) {
return html``;
}
return html`
<video
?autoplay=${this.autoPlay}
.muted=${this.muted}
?playsinline=${this.playsInline}
?controls=${this.controls}
@loadeddata=${this._elementResized}
></video>
`;
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
const attachedChanged = changedProps.has("_attached");
const urlChanged = changedProps.has("url");
if (!urlChanged && !attachedChanged) {
return;
}
// If we are no longer attached, destroy polyfill
if (attachedChanged && !this._attached) {
// Tear down existing polyfill, if available
this._destroyPolyfill();
return;
}
this._destroyPolyfill();
this._startHls();
}
private async _getUseExoPlayer(): Promise<boolean> {
if (!this.hass!.auth.external) {
return false;
}
const externalConfig = await getExternalConfig(this.hass!.auth.external);
return externalConfig && externalConfig.hasExoPlayer;
}
private async _startHls(): Promise<void> {
let hls: any;
const videoEl = this._videoEl;
this._useExoPlayer = await this._getUseExoPlayer();
if (!this._useExoPlayer) {
hls = ((await import(/* webpackChunkName: "hls.js" */ "hls.js")) as any)
.default as HLSModule;
let hlsSupported = hls.isSupported();
if (!hlsSupported) {
hlsSupported =
videoEl.canPlayType("application/vnd.apple.mpegurl") !== "";
}
if (!hlsSupported) {
this._videoEl.innerHTML = this.hass.localize(
"ui.components.media-browser.video_not_supported"
);
return;
}
}
const url = this.url;
if (this._useExoPlayer) {
this._renderHLSExoPlayer(url);
} else if (hls.isSupported()) {
this._renderHLSPolyfill(videoEl, hls, url);
} else {
this._renderHLSNative(videoEl, url);
}
}
private async _renderHLSExoPlayer(url: string) {
window.addEventListener("resize", this._resizeExoPlayer);
this.updateComplete.then(() => nextRender()).then(this._resizeExoPlayer);
this._videoEl.style.visibility = "hidden";
await this.hass!.auth.external!.sendMessage({
type: "exoplayer/play_hls",
payload: new URL(url, window.location.href).toString(),
});
}
private _resizeExoPlayer = () => {
const rect = this._videoEl.getBoundingClientRect();
this.hass!.auth.external!.fireMessage({
type: "exoplayer/resize",
payload: {
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
},
});
};
private async _renderHLSPolyfill(
videoEl: HTMLVideoElement,
Hls: HLSModule,
url: string
) {
const hls = new Hls({
liveBackBufferLength: 60,
fragLoadingTimeOut: 30000,
manifestLoadingTimeOut: 30000,
levelLoadingTimeOut: 30000,
});
this._hlsPolyfillInstance = hls;
hls.attachMedia(videoEl);
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
hls.loadSource(url);
});
}
private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) {
videoEl.src = url;
await new Promise((resolve) =>
videoEl.addEventListener("loadedmetadata", resolve)
);
videoEl.play();
}
private _elementResized() {
fireEvent(this, "iron-resize");
}
private _destroyPolyfill() {
if (this._hlsPolyfillInstance) {
this._hlsPolyfillInstance.destroy();
this._hlsPolyfillInstance = undefined;
}
if (this._useExoPlayer) {
window.removeEventListener("resize", this._resizeExoPlayer);
this.hass!.auth.external!.fireMessage({ type: "exoplayer/stop" });
}
}
static get styles(): CSSResult {
return css`
:host,
video {
display: block;
}
video {
width: 100%;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-hls-player": HaHLSPlayer;
}
}

View File

@@ -68,6 +68,10 @@ class HaPaperSlider extends PaperSliderClass {
-webkit-transform: scale(1) translate(0, -17px) scaleX(-1) !important; -webkit-transform: scale(1) translate(0, -17px) scaleX(-1) !important;
transform: scale(1) translate(0, -17px) scaleX(-1) !important; transform: scale(1) translate(0, -17px) scaleX(-1) !important;
} }
.slider-input {
width: 54px;
}
`; `;
tpl.content.appendChild(styleEl); tpl.content.appendChild(styleEl);
return tpl; return tpl;

View File

@@ -1,3 +1,4 @@
import "@polymer/paper-item/paper-item-body";
import { import {
css, css,
CSSResult, CSSResult,
@@ -7,7 +8,6 @@ import {
property, property,
SVGTemplateResult, SVGTemplateResult,
} from "lit-element"; } from "lit-element";
import "@polymer/paper-item/paper-item-body";
@customElement("ha-settings-row") @customElement("ha-settings-row")
export class HaSettingsRow extends LitElement { export class HaSettingsRow extends LitElement {
@@ -49,6 +49,9 @@ export class HaSettingsRow extends LitElement {
border-top: 1px solid var(--divider-color); border-top: 1px solid var(--divider-color);
padding-bottom: 8px; padding-bottom: 8px;
} }
::slotted(ha-switch) {
padding: 16px 0;
}
`; `;
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -279,6 +279,7 @@ class LocationEditor extends LitElement {
} }
#map { #map {
height: 100%; height: 100%;
background: inherit;
} }
.leaflet-edit-move { .leaflet-edit-move {
border-radius: 50%; border-radius: 50%;

View File

@@ -8,14 +8,14 @@ import {
property, property,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { HASSDomEvent } from "../../common/dom/fire_event"; import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event";
import type { import type {
MediaPickedEvent, MediaPickedEvent,
MediaPlayerBrowseAction, MediaPlayerBrowseAction,
} from "../../data/media-player"; } from "../../data/media-player";
import { haStyleDialog } from "../../resources/styles"; import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { createCloseHeading } from "../ha-dialog"; import "../ha-dialog";
import "./ha-media-player-browse"; import "./ha-media-player-browse";
import { MediaPlayerBrowseDialogParams } from "./show-media-browser-dialog"; import { MediaPlayerBrowseDialogParams } from "./show-media-browser-dialog";
@@ -33,16 +33,17 @@ class DialogMediaPlayerBrowse extends LitElement {
@internalProperty() private _params?: MediaPlayerBrowseDialogParams; @internalProperty() private _params?: MediaPlayerBrowseDialogParams;
public async showDialog( public showDialog(params: MediaPlayerBrowseDialogParams): void {
params: MediaPlayerBrowseDialogParams
): Promise<void> {
this._params = params; this._params = params;
this._entityId = this._params.entityId; this._entityId = this._params.entityId;
this._mediaContentId = this._params.mediaContentId; this._mediaContentId = this._params.mediaContentId;
this._mediaContentType = this._params.mediaContentType; this._mediaContentType = this._params.mediaContentType;
this._action = this._params.action || "play"; this._action = this._params.action || "play";
}
await this.updateComplete; public closeDialog() {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
protected render(): TemplateResult { protected render(): TemplateResult {
@@ -56,32 +57,27 @@ class DialogMediaPlayerBrowse extends LitElement {
scrimClickAction scrimClickAction
escapeKeyAction escapeKeyAction
hideActions hideActions
.heading=${createCloseHeading( flexContent
this.hass, @closed=${this.closeDialog}
this.hass.localize("ui.components.media-browser.media-player-browser")
)}
@closed=${this._closeDialog}
> >
<ha-media-player-browse <ha-media-player-browse
dialog
.hass=${this.hass} .hass=${this.hass}
.entityId=${this._entityId} .entityId=${this._entityId}
.action=${this._action!} .action=${this._action!}
.mediaContentId=${this._mediaContentId} .mediaContentId=${this._mediaContentId}
.mediaContentType=${this._mediaContentType} .mediaContentType=${this._mediaContentType}
@close-dialog=${this.closeDialog}
@media-picked=${this._mediaPicked} @media-picked=${this._mediaPicked}
></ha-media-player-browse> ></ha-media-player-browse>
</ha-dialog> </ha-dialog>
`; `;
} }
private _closeDialog() {
this._params = undefined;
}
private _mediaPicked(ev: HASSDomEvent<MediaPickedEvent>): void { private _mediaPicked(ev: HASSDomEvent<MediaPickedEvent>): void {
this._params!.mediaPickedCallback(ev.detail); this._params!.mediaPickedCallback(ev.detail);
if (this._action !== "play") { if (this._action !== "play") {
this._closeDialog(); this.closeDialog();
} }
} }
@@ -97,10 +93,12 @@ class DialogMediaPlayerBrowse extends LitElement {
@media (min-width: 800px) { @media (min-width: 800px) {
ha-dialog { ha-dialog {
--mdc-dialog-max-width: 800px; --mdc-dialog-max-width: 800px;
--dialog-surface-position: fixed;
--dialog-surface-top: 40px;
--mdc-dialog-max-height: calc(100% - 72px);
} }
ha-media-player-browse { ha-media-player-browse {
width: 700px; width: 700px;
padding: 20px 24px;
} }
} }
`, `,

View File

@@ -2,7 +2,7 @@ import "@material/mwc-button/mwc-button";
import "@material/mwc-fab/mwc-fab"; import "@material/mwc-fab/mwc-fab";
import "@material/mwc-list/mwc-list"; import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiArrowLeft, mdiFolder, mdiPlay, mdiPlus } from "@mdi/js"; import { mdiArrowLeft, mdiClose, mdiPlay, mdiPlus } from "@mdi/js";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox"; import "@polymer/paper-listbox/paper-listbox";
import { import {
@@ -16,11 +16,22 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import memoizeOne from "memoize-one"; import { classMap } from "lit-html/directives/class-map";
import { ifDefined } from "lit-html/directives/if-defined";
import { styleMap } from "lit-html/directives/style-map";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { computeRTLDirection } from "../../common/util/compute_rtl";
import { debounce } from "../../common/util/debounce"; import { debounce } from "../../common/util/debounce";
import { browseMediaPlayer, MediaPickedEvent } from "../../data/media-player"; import {
browseLocalMediaPlayer,
browseMediaPlayer,
BROWSER_SOURCE,
MediaClassBrowserSettings,
MediaPickedEvent,
MediaPlayerBrowseAction,
} from "../../data/media-player";
import type { MediaPlayerItem } from "../../data/media-player"; import type { MediaPlayerItem } from "../../data/media-player";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer"; import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
import { haStyle } from "../../resources/styles"; import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
@@ -47,13 +58,17 @@ export class HaMediaPlayerBrowse extends LitElement {
@property() public mediaContentType?: string; @property() public mediaContentType?: string;
@property() public action: "pick" | "play" = "play"; @property() public action: MediaPlayerBrowseAction = "play";
@property({ type: Boolean }) public dialog = false;
@property({ type: Boolean, attribute: "narrow", reflect: true }) @property({ type: Boolean, attribute: "narrow", reflect: true })
private _narrow = false; private _narrow = false;
@internalProperty() private _loading = false; @internalProperty() private _loading = false;
@internalProperty() private _error?: { message: string; code: string };
@internalProperty() private _mediaPlayerItems: MediaPlayerItem[] = []; @internalProperty() private _mediaPlayerItems: MediaPlayerItem[] = [];
private _resizeObserver?: ResizeObserver; private _resizeObserver?: ResizeObserver;
@@ -69,94 +84,128 @@ export class HaMediaPlayerBrowse extends LitElement {
} }
} }
protected render(): TemplateResult { public navigateBack() {
if (!this._mediaPlayerItems.length) { this._mediaPlayerItems!.pop();
return html``; const item = this._mediaPlayerItems!.pop();
if (!item) {
return;
} }
this._navigate(item);
}
protected render(): TemplateResult {
if (this._loading) { if (this._loading) {
return html`<ha-circular-progress active></ha-circular-progress>`; return html`<ha-circular-progress active></ha-circular-progress>`;
} }
const mostRecentItem = this._mediaPlayerItems[ if (this._error && !this._mediaPlayerItems.length) {
if (this.dialog) {
this._closeDialogAction();
showAlertDialog(this, {
title: this.hass.localize(
"ui.components.media-browser.media_browsing_error"
),
text: this._renderError(this._error),
});
} else {
return html`<div class="container">
${this._renderError(this._error)}
</div>`;
}
}
if (!this._mediaPlayerItems.length) {
return html``;
}
const currentItem = this._mediaPlayerItems[
this._mediaPlayerItems.length - 1 this._mediaPlayerItems.length - 1
]; ];
const previousItem =
const previousItem: MediaPlayerItem | undefined =
this._mediaPlayerItems.length > 1 this._mediaPlayerItems.length > 1
? this._mediaPlayerItems[this._mediaPlayerItems.length - 2] ? this._mediaPlayerItems[this._mediaPlayerItems.length - 2]
: undefined; : undefined;
const hasExpandableChildren: const subtitle = this.hass.localize(
| MediaPlayerItem `ui.components.media-browser.class.${currentItem.media_class}`
| undefined = this._hasExpandableChildren(mostRecentItem.children); );
const mediaClass = MediaClassBrowserSettings[currentItem.media_class];
const childrenMediaClass =
MediaClassBrowserSettings[currentItem.children_media_class];
return html` return html`
<div class="header"> <div
<div class="header-content"> class="header ${classMap({
${mostRecentItem.thumbnail "no-img": !currentItem.thumbnail,
? html` "no-dialog": !this.dialog,
<div })}"
class="img" >
style="background-image: url(${mostRecentItem.thumbnail})" <div class="header-wrapper">
> <div class="header-content">
${this._narrow && mostRecentItem?.can_play ${currentItem.thumbnail
? html` ? html`
<mwc-fab <div
mini class="img"
.item=${mostRecentItem} style=${styleMap({
@click=${this._actionClicked} backgroundImage: currentItem.thumbnail
> ? `url(${currentItem.thumbnail})`
<ha-svg-icon : "none",
slot="icon" })}
.label=${this.hass.localize( >
`ui.components.media-browser.${this.action}-media` ${this._narrow && currentItem?.can_play
? html`
<mwc-fab
mini
.item=${currentItem}
@click=${this._actionClicked}
>
<ha-svg-icon
slot="icon"
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
.path=${this.action === "play"
? mdiPlay
: mdiPlus}
></ha-svg-icon>
${this.hass.localize(
`ui.components.media-browser.${this.action}`
)} )}
.path=${this.action === "play" ? mdiPlay : mdiPlus} </mwc-fab>
></ha-svg-icon> `
${this.hass.localize( : ""}
`ui.components.media-browser.${this.action}` </div>
)} `
</mwc-fab> : html``}
` <div class="header-info">
: ""}
</div>
`
: html``}
<div class="header-info">
<div class="breadcrumb-overflow">
<div class="breadcrumb"> <div class="breadcrumb">
${previousItem ${previousItem
? html` ? html`
<div <div class="previous-title" @click=${this.navigateBack}>
class="previous-title"
.previous=${true}
.item=${previousItem}
@click=${this._navigate}
>
<ha-svg-icon .path=${mdiArrowLeft}></ha-svg-icon> <ha-svg-icon .path=${mdiArrowLeft}></ha-svg-icon>
${previousItem.title} ${previousItem.title}
</div> </div>
` `
: ""} : ""}
<h1 class="title">${mostRecentItem.title}</h1> <h1 class="title">${currentItem.title}</h1>
<h2 class="subtitle"> ${subtitle
${this.hass.localize( ? html`
`ui.components.media-browser.content-type.${mostRecentItem.media_content_type}` <h2 class="subtitle">
)} ${subtitle}
</h2> </h2>
`
: ""}
</div> </div>
</div> ${currentItem.can_play &&
${mostRecentItem?.can_play && (!currentItem.thumbnail || !this._narrow)
(!this._narrow || (this._narrow && !mostRecentItem.thumbnail)) ? html`
? html`
<div class="actions">
<mwc-button <mwc-button
raised raised
.item=${mostRecentItem} .item=${currentItem}
@click=${this._actionClicked} @click=${this._actionClicked}
> >
<ha-svg-icon <ha-svg-icon
slot="icon"
.label=${this.hass.localize( .label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media` `ui.components.media-browser.${this.action}-media`
)} )}
@@ -166,100 +215,159 @@ export class HaMediaPlayerBrowse extends LitElement {
`ui.components.media-browser.${this.action}` `ui.components.media-browser.${this.action}`
)} )}
</mwc-button> </mwc-button>
</div> `
` : ""}
: ""} </div>
</div> </div>
${this.dialog
? html`
<mwc-icon-button
aria-label=${this.hass.localize("ui.dialogs.generic.close")}
@click=${this._closeDialogAction}
class="header_button"
dir=${computeRTLDirection(this.hass)}
>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
`
: ""}
</div> </div>
</div> </div>
<div class="divider"></div> ${this._error
${mostRecentItem.children?.length ? html`
? hasExpandableChildren <div class="container error">
${this._renderError(this._error)}
</div>
`
: currentItem.children?.length
? childrenMediaClass.layout === "grid"
? html` ? html`
<div class="children"> <div
${mostRecentItem.children?.length class="children ${classMap({
? html` portrait: childrenMediaClass.thumbnail_ratio === "portrait",
${mostRecentItem.children.map( })}"
(child) => html` >
<div ${currentItem.children.map(
class="child" (child) => html`
.item=${child} <div
@click=${this._navigate} class="child"
> .item=${child}
<div class="ha-card-parent"> @click=${this._childClicked}
<ha-card >
outlined <div class="ha-card-parent">
style="background-image: url(${child.thumbnail})" <ha-card
outlined
style=${styleMap({
backgroundImage: child.thumbnail
? `url(${child.thumbnail})`
: "none",
})}
>
${!child.thumbnail
? html`
<ha-svg-icon
class="folder"
.path=${MediaClassBrowserSettings[
child.media_class === "directory"
? child.children_media_class ||
child.media_class
: child.media_class
].icon}
></ha-svg-icon>
`
: ""}
</ha-card>
${child.can_play
? html`
<mwc-icon-button
class="play ${classMap({
can_expand: child.can_expand,
})}"
.item=${child}
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
@click=${this._actionClicked}
> >
${child.can_expand && !child.thumbnail <ha-svg-icon
? html` .path=${this.action === "play"
<ha-svg-icon ? mdiPlay
class="folder" : mdiPlus}
.path=${mdiFolder} ></ha-svg-icon>
></ha-svg-icon> </mwc-icon-button>
` `
: ""} : ""}
</ha-card> </div>
${child.can_play <div class="title">${child.title}</div>
? html` <div class="type">
<mwc-icon-button ${this.hass.localize(
class="play" `ui.components.media-browser.content-type.${child.media_content_type}`
.item=${child} )}
.label=${this.hass.localize( </div>
`ui.components.media-browser.${this.action}-media` </div>
)} `
@click=${this._actionClicked} )}
>
<ha-svg-icon
.path=${this.action === "play"
? mdiPlay
: mdiPlus}
></ha-svg-icon>
</mwc-icon-button>
`
: ""}
</div>
<div class="title">${child.title}</div>
<div class="type">
${this.hass.localize(
`ui.components.media-browser.content-type.${child.media_content_type}`
)}
</div>
</div>
`
)}
`
: ""}
</div> </div>
` `
: html` : html`
<mwc-list> <mwc-list>
${mostRecentItem.children.map( ${currentItem.children.map(
(child) => html`<mwc-list-item (child) => html`
@click=${this._actionClicked} <mwc-list-item
@click=${this._childClicked}
.item=${child} .item=${child}
graphic="icon" graphic="avatar"
hasMeta
dir=${computeRTLDirection(this.hass)}
> >
<span>${child.title}</span> <div
<ha-svg-icon class="graphic"
slot="graphic" style=${ifDefined(
.label=${this.hass.localize( mediaClass.show_list_images && child.thumbnail
`ui.components.media-browser.${this.action}-media` ? `background-image: url(${child.thumbnail})`
: undefined
)} )}
.path=${this.action === "play" ? mdiPlay : mdiPlus} slot="graphic"
></ha-svg-icon >
></mwc-list-item> <mwc-icon-button
<li divider role="separator"></li>` class="play ${classMap({
show:
!mediaClass.show_list_images || !child.thumbnail,
})}"
.item=${child}
.label=${this.hass.localize(
`ui.components.media-browser.${this.action}-media`
)}
@click=${this._actionClicked}
>
<ha-svg-icon
.path=${this.action === "play" ? mdiPlay : mdiPlus}
></ha-svg-icon>
</mwc-icon-button>
</div>
<span class="title">${child.title}</span>
</mwc-list-item>
<li divider role="separator"></li>
`
)} )}
</mwc-list> </mwc-list>
` `
: this.hass.localize("ui.components.media-browser.no_items")} : html`
<div class="container">
${this.hass.localize("ui.components.media-browser.no_items")}
</div>
`}
`; `;
} }
protected firstUpdated(): void { protected firstUpdated(): void {
this._measureCard(); this._measureCard();
this._attachObserver(); this._attachObserver();
this.addEventListener("scroll", this._scroll, { passive: true });
this.addEventListener("touchmove", this._scroll, {
passive: true,
});
} }
protected updated(changedProps: PropertyValues): void { protected updated(changedProps: PropertyValues): void {
@@ -274,11 +382,22 @@ export class HaMediaPlayerBrowse extends LitElement {
return; return;
} }
this._fetchData(this.mediaContentId, this.mediaContentType).then( if (changedProps.has("entityId")) {
(itemData) => { this._error = undefined;
this._mediaPlayerItems = [];
}
this._fetchData(this.mediaContentId, this.mediaContentType)
.then((itemData) => {
if (!itemData) {
return;
}
this._mediaPlayerItems = [itemData]; this._mediaPlayerItems = [itemData];
} })
); .catch((err) => {
this._error = err;
});
} }
private _actionClicked(ev: MouseEvent): void { private _actionClicked(ev: MouseEvent): void {
@@ -289,31 +408,46 @@ export class HaMediaPlayerBrowse extends LitElement {
} }
private _runAction(item: MediaPlayerItem): void { private _runAction(item: MediaPlayerItem): void {
fireEvent(this, "media-picked", { fireEvent(this, "media-picked", { item });
media_content_id: item.media_content_id,
media_content_type: item.media_content_type,
});
} }
private async _navigate(ev: MouseEvent): Promise<void> { private async _childClicked(ev: MouseEvent): Promise<void> {
const target = ev.currentTarget as any; const target = ev.currentTarget as any;
let item: MediaPlayerItem | undefined; const item: MediaPlayerItem = target.item;
if (target.previous) {
this._mediaPlayerItems!.pop();
item = this._mediaPlayerItems!.pop();
}
item = target.item;
if (!item) { if (!item) {
return; return;
} }
const itemData = await this._fetchData( if (!item.can_expand) {
item.media_content_id, this._runAction(item);
item.media_content_type return;
); }
this._navigate(item);
}
private async _navigate(item: MediaPlayerItem) {
this._error = undefined;
let itemData: MediaPlayerItem;
try {
itemData = await this._fetchData(
item.media_content_id,
item.media_content_type
);
} catch (err) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.components.media-browser.media_browsing_error"
),
text: this._renderError(err),
});
return;
}
this.scrollTo(0, 0);
this._mediaPlayerItems = [...this._mediaPlayerItems, itemData]; this._mediaPlayerItems = [...this._mediaPlayerItems, itemData];
} }
@@ -321,18 +455,29 @@ export class HaMediaPlayerBrowse extends LitElement {
mediaContentId?: string, mediaContentId?: string,
mediaContentType?: string mediaContentType?: string
): Promise<MediaPlayerItem> { ): Promise<MediaPlayerItem> {
const itemData = await browseMediaPlayer( const itemData =
this.hass, this.entityId !== BROWSER_SOURCE
this.entityId, ? await browseMediaPlayer(
!mediaContentId ? undefined : mediaContentId, this.hass,
mediaContentType this.entityId,
); mediaContentId,
mediaContentType
)
: await browseLocalMediaPlayer(this.hass, mediaContentId);
return itemData; return itemData;
} }
private _measureCard(): void { private _measureCard(): void {
this._narrow = this.offsetWidth < 500; this._narrow = (this.dialog ? window.innerWidth : this.offsetWidth) < 450;
}
private _scroll(): void {
if (this.scrollTop > (this._narrow ? 224 : 125)) {
this.setAttribute("scroll", "");
} else if (this.scrollTop === 0) {
this.removeAttribute("scroll");
}
} }
private async _attachObserver(): Promise<void> { private async _attachObserver(): Promise<void> {
@@ -346,9 +491,37 @@ export class HaMediaPlayerBrowse extends LitElement {
this._resizeObserver.observe(this); this._resizeObserver.observe(this);
} }
private _hasExpandableChildren = memoizeOne((children) => private _closeDialogAction(): void {
children.find((item: MediaPlayerItem) => item.can_expand) fireEvent(this, "close-dialog");
); }
private _renderError(err: { message: string; code: string }) {
if (err.message === "Media directory does not exist.") {
return html`
<h2>No local media found.</h2>
<p>
It looks like you have not yet created a media directory.
<br />Create a directory with the name <b>"media"</b> in the
configuration directory of Home Assistant
(${this.hass.config.config_dir}). <br />Place your video, audio and
image files in this directory to be able to browse and play them in
the browser or on supported media players.
</p>
<p>
Check the
<a
href="https://www.home-assistant.io/integrations/media_source/#local-media"
target="_blank"
rel="noreferrer"
>documentation</a
>
for more info
</p>
`;
}
return html`<span class="error">err.message</span>`;
}
static get styles(): CSSResultArray { static get styles(): CSSResultArray {
return [ return [
@@ -356,16 +529,30 @@ export class HaMediaPlayerBrowse extends LitElement {
css` css`
:host { :host {
display: block; display: block;
overflow-y: auto;
display: flex;
padding: 0px 0px 20px;
flex-direction: column;
}
.container {
padding: 16px;
} }
.header { .header {
display: flex; display: block;
justify-content: space-between; justify-content: space-between;
border-bottom: 1px solid var(--divider-color);
background-color: var(--card-background-color);
position: sticky;
position: -webkit-sticky;
top: 0;
z-index: 5;
padding: 20px 24px 10px;
} }
.breadcrumb-overflow { .header-wrapper {
display: flex; display: flex;
justify-content: space-between;
} }
.header-content { .header-content {
@@ -380,6 +567,8 @@ export class HaMediaPlayerBrowse extends LitElement {
width: 200px; width: 200px;
margin-right: 16px; margin-right: 16px;
background-size: cover; background-size: cover;
border-radius: 4px;
transition: width 0.4s, height 0.4s;
} }
.header-info { .header-info {
@@ -391,8 +580,8 @@ export class HaMediaPlayerBrowse extends LitElement {
flex: 1; flex: 1;
} }
.header-info .actions { .header-info mwc-button {
padding-top: 24px; display: block;
--mdc-theme-primary: var(--primary-color); --mdc-theme-primary: var(--primary-color);
} }
@@ -404,7 +593,7 @@ export class HaMediaPlayerBrowse extends LitElement {
} }
.breadcrumb .title { .breadcrumb .title {
font-size: 48px; font-size: 32px;
line-height: 1.2; line-height: 1.2;
font-weight: bold; font-weight: bold;
margin: 0; margin: 0;
@@ -412,6 +601,7 @@ export class HaMediaPlayerBrowse extends LitElement {
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
padding-right: 8px;
} }
.breadcrumb .previous-title { .breadcrumb .previous-title {
@@ -428,26 +618,17 @@ export class HaMediaPlayerBrowse extends LitElement {
font-size: 16px; font-size: 16px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} margin-bottom: 0;
transition: height 0.5s, margin 0.5s;
.divider {
padding: 10px 0;
}
.divider::before {
height: 1px;
display: block;
background-color: var(--divider-color);
content: " ";
} }
/* ============= CHILDREN ============= */ /* ============= CHILDREN ============= */
mwc-list { mwc-list {
--mdc-list-vertical-padding: 0; --mdc-list-vertical-padding: 0;
--mdc-list-item-graphic-margin: 0;
--mdc-theme-text-icon-on-background: var(--secondary-text-color); --mdc-theme-text-icon-on-background: var(--secondary-text-color);
border: 1px solid var(--divider-color); margin-top: 10px;
border-radius: 4px;
} }
mwc-list li:last-child { mwc-list li:last-child {
@@ -462,10 +643,18 @@ export class HaMediaPlayerBrowse extends LitElement {
display: grid; display: grid;
grid-template-columns: repeat( grid-template-columns: repeat(
auto-fit, auto-fit,
minmax(var(--media-browse-item-size, 175px), 0.33fr) minmax(var(--media-browse-item-size, 175px), 0.1fr)
); );
grid-gap: 16px; grid-gap: 16px;
margin: 8px 0px; margin: 8px 0px;
padding: 0px 24px;
}
:host([dialog]) .children {
grid-template-columns: repeat(
auto-fit,
minmax(var(--media-browse-item-size, 175px), 0.33fr)
);
} }
.child { .child {
@@ -479,7 +668,7 @@ export class HaMediaPlayerBrowse extends LitElement {
width: 100%; width: 100%;
} }
ha-card { .children ha-card {
width: 100%; width: 100%;
padding-bottom: 100%; padding-bottom: 100%;
position: relative; position: relative;
@@ -487,6 +676,11 @@ export class HaMediaPlayerBrowse extends LitElement {
background-size: cover; background-size: cover;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center; background-position: center;
transition: padding-bottom 0.1s ease-out;
}
.portrait.children ha-card {
padding-bottom: 150%;
} }
.child .folder, .child .folder,
@@ -502,24 +696,43 @@ export class HaMediaPlayerBrowse extends LitElement {
} }
.child .play { .child .play {
transition: color 0.5s;
border-radius: 50%;
bottom: calc(50% - 35px);
right: calc(50% - 35px);
opacity: 0;
transition: opacity 0.1s ease-out;
}
.child .play:not(.can_expand) {
--mdc-icon-button-size: 70px;
--mdc-icon-size: 48px;
}
.ha-card-parent:hover .play:not(.can_expand) {
opacity: 1;
color: var(--primary-color);
}
.child .play.can_expand {
opacity: 1;
background-color: rgba(var(--rgb-card-background-color), 0.5);
bottom: 4px; bottom: 4px;
right: 4px; right: 4px;
transition: all 0.5s;
background-color: rgba(255, 255, 255, 0.5);
border-radius: 50%;
} }
.child .play:hover { .child .play:hover {
color: var(--primary-color); color: var(--primary-color);
} }
ha-card:hover { .ha-card-parent:hover ha-card {
opacity: 0.5; opacity: 0.5;
} }
.child .title { .child .title {
font-size: 16px; font-size: 16px;
padding-top: 8px; padding-top: 8px;
padding-left: 2px;
overflow: hidden; overflow: hidden;
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
@@ -529,6 +742,37 @@ export class HaMediaPlayerBrowse extends LitElement {
.child .type { .child .type {
font-size: 12px; font-size: 12px;
color: var(--secondary-text-color); color: var(--secondary-text-color);
padding-left: 2px;
}
mwc-list-item .graphic {
background-size: cover;
}
mwc-list-item .graphic .play {
opacity: 0;
transition: all 0.5s;
background-color: rgba(var(--rgb-card-background-color), 0.5);
border-radius: 50%;
--mdc-icon-button-size: 40px;
}
mwc-list-item:hover .graphic .play {
opacity: 1;
color: var(--primary-color);
}
mwc-list-item .graphic .play.show {
opacity: 1;
background-color: transparent;
}
mwc-list-item .title {
margin-left: 16px;
}
mwc-list-item[dir="rtl"] .title {
margin-right: 16px;
margin-left: 0;
} }
/* ============= Narrow ============= */ /* ============= Narrow ============= */
@@ -537,16 +781,22 @@ export class HaMediaPlayerBrowse extends LitElement {
padding: 0; padding: 0;
} }
:host([narrow]) mwc-list {
border: 0;
}
:host([narrow]) .breadcrumb .title { :host([narrow]) .breadcrumb .title {
font-size: 38px; font-size: 24px;
} }
:host([narrow]) .breadcrumb-overflow { :host([narrow]) .header {
align-items: flex-end; padding: 0;
}
:host([narrow]) .header.no-dialog {
display: block;
}
:host([narrow]) .header_button {
position: absolute;
top: 14px;
right: 8px;
} }
:host([narrow]) .header-content { :host([narrow]) .header-content {
@@ -558,26 +808,103 @@ export class HaMediaPlayerBrowse extends LitElement {
height: auto; height: auto;
width: 100%; width: 100%;
margin-right: 0; margin-right: 0;
padding-bottom: 100%; padding-bottom: 50%;
margin-bottom: 8px; margin-bottom: 8px;
position: relative; position: relative;
background-position: center;
border-radius: 0;
transition: width 0.4s, height 0.4s, padding-bottom 0.4s;
} }
:host([narrow]) .header-content .img mwc-fab { mwc-fab {
position: absolute; position: absolute;
--mdc-theme-secondary: var(--primary-color); --mdc-theme-secondary: var(--primary-color);
bottom: -20px; bottom: -20px;
right: 20px; right: 20px;
} }
:host([narrow]) .header-info, :host([narrow]) .header-info mwc-button {
:host([narrow]) .media-source, margin-top: 16px;
:host([narrow]) .children { margin-bottom: 8px;
}
:host([narrow]) .header-info {
padding: 20px 24px 10px;
}
:host([narrow]) .media-source {
padding: 0 24px; padding: 0 24px;
} }
:host([narrow]) .children { :host([narrow]) .children {
grid-template-columns: 1fr 1fr !important; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) !important;
}
/* ============= Scroll ============= */
:host([scroll]) .breadcrumb .subtitle {
height: 0;
margin: 0;
}
:host([scroll]) .breadcrumb .title {
-webkit-line-clamp: 1;
}
:host(:not([narrow])[scroll]) .header:not(.no-img) mwc-icon-button {
align-self: center;
}
:host([scroll]) .header-info mwc-button,
.no-img .header-info mwc-button {
padding-right: 4px;
}
:host([scroll][narrow]) .no-img .header-info mwc-button {
padding-right: 16px;
}
:host([scroll]) .header-info {
flex-direction: row;
}
:host([scroll]) .header-info mwc-button {
align-self: center;
margin-top: 0;
margin-bottom: 0;
}
:host([scroll][narrow]) .no-img .header-info {
flex-direction: row-reverse;
}
:host([scroll][narrow]) .header-info {
padding: 20px 24px 10px 24px;
align-items: center;
}
:host([scroll]) .header-content {
align-items: flex-end;
flex-direction: row;
}
:host([scroll]) .header-content .img {
height: 75px;
width: 75px;
}
:host([scroll][narrow]) .header-content .img {
height: 100px;
width: 100px;
padding-bottom: initial;
margin-bottom: 0;
}
:host([scroll]) mwc-fab {
bottom: 4px;
right: 4px;
--mdc-fab-box-shadow: none;
--mdc-theme-secondary: rgba(var(--rgb-primary-color), 0.5);
} }
`, `,
]; ];

View File

@@ -76,6 +76,8 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
const staticColors = { const staticColors = {
on: 1, on: 1,
off: 0, off: 0,
home: 1,
not_home: 0,
unavailable: "#a0a0a0", unavailable: "#a0a0a0",
unknown: "#606060", unknown: "#606060",
idle: 2, idle: 2,

View File

@@ -0,0 +1,74 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { styleMap } from "lit-html/directives/style-map";
import { Person } from "../../data/person";
import { computeInitials } from "./ha-user-badge";
@customElement("ha-person-badge")
class PersonBadge extends LitElement {
@property({ attribute: false }) public person?: Person;
protected render(): TemplateResult {
if (!this.person) {
return html``;
}
const picture = this.person.picture;
if (picture) {
return html`<div
style=${styleMap({ backgroundImage: `url(${picture})` })}
class="picture"
></div>`;
}
const initials = computeInitials(this.person.name);
return html`<div
class="initials ${classMap({ long: initials!.length > 2 })}"
>
${initials}
</div>`;
}
static get styles(): CSSResult {
return css`
:host {
display: contents;
}
.picture {
width: 40px;
height: 40px;
background-size: cover;
border-radius: 50%;
}
.initials {
display: inline-block;
box-sizing: border-box;
width: 40px;
line-height: 40px;
border-radius: 50%;
text-align: center;
background-color: var(--light-primary-color);
text-decoration: none;
color: var(--text-light-primary-color, var(--primary-text-color));
overflow: hidden;
}
.initials.long {
font-size: 80%;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-person-badge": PersonBadge;
}
}

View File

@@ -3,17 +3,20 @@ import {
CSSResult, CSSResult,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { toggleAttribute } from "../../common/dom/toggle_attribute"; import { classMap } from "lit-html/directives/class-map";
import { styleMap } from "lit-html/directives/style-map";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { User } from "../../data/user"; import { User } from "../../data/user";
import { CurrentUser } from "../../types"; import { CurrentUser, HomeAssistant } from "../../types";
const computeInitials = (name: string) => { export const computeInitials = (name: string) => {
if (!name) { if (!name) {
return "user"; return "?";
} }
return ( return (
name name
@@ -28,27 +31,89 @@ const computeInitials = (name: string) => {
}; };
@customElement("ha-user-badge") @customElement("ha-user-badge")
class StateBadge extends LitElement { class UserBadge extends LitElement {
@property() public user?: User | CurrentUser; @property({ attribute: false }) public hass!: HomeAssistant;
protected render(): TemplateResult { @property({ attribute: false }) public user?: User | CurrentUser;
const user = this.user;
const initials = user ? computeInitials(user.name) : "?"; @internalProperty() private _personPicture?: string;
return html` ${initials} `;
} private _personEntityId?: string;
protected updated(changedProps) { protected updated(changedProps) {
super.updated(changedProps); super.updated(changedProps);
toggleAttribute( if (changedProps.has("user")) {
this, this._getPersonPicture();
"long", return;
(this.user ? computeInitials(this.user.name) : "?").length > 2 }
); const oldHass = changedProps.get("hass");
if (
this._personEntityId &&
oldHass &&
this.hass.states[this._personEntityId] !==
oldHass.states[this._personEntityId]
) {
const state = this.hass.states[this._personEntityId];
if (state) {
this._personPicture = state.attributes.entity_picture;
} else {
this._getPersonPicture();
}
} else if (!this._personEntityId && oldHass) {
this._getPersonPicture();
}
}
protected render(): TemplateResult {
if (!this.hass || !this.user) {
return html``;
}
const picture = this._personPicture;
if (picture) {
return html`<div
style=${styleMap({ backgroundImage: `url(${picture})` })}
class="picture"
></div>`;
}
const initials = computeInitials(this.user.name);
return html`<div
class="initials ${classMap({ long: initials!.length > 2 })}"
>
${initials}
</div>`;
}
private _getPersonPicture() {
this._personEntityId = undefined;
this._personPicture = undefined;
if (!this.hass || !this.user) {
return;
}
for (const entity of Object.values(this.hass.states)) {
if (
entity.attributes.user_id === this.user.id &&
computeStateDomain(entity) === "person"
) {
this._personEntityId = entity.entity_id;
this._personPicture = entity.attributes.entity_picture;
break;
}
}
} }
static get styles(): CSSResult { static get styles(): CSSResult {
return css` return css`
:host { :host {
display: contents;
}
.picture {
width: 40px;
height: 40px;
background-size: cover;
border-radius: 50%;
}
.initials {
display: inline-block; display: inline-block;
box-sizing: border-box; box-sizing: border-box;
width: 40px; width: 40px;
@@ -60,8 +125,7 @@ class StateBadge extends LitElement {
color: var(--text-light-primary-color, var(--primary-text-color)); color: var(--text-light-primary-color, var(--primary-text-color));
overflow: hidden; overflow: hidden;
} }
.initials.long {
:host([long]) {
font-size: 80%; font-size: 80%;
} }
`; `;
@@ -70,6 +134,6 @@ class StateBadge extends LitElement {
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-user-badge": StateBadge; "ha-user-badge": UserBadge;
} }
} }

View File

@@ -53,7 +53,11 @@ class HaUserPicker extends LitElement {
${this._sortedUsers(this.users).map( ${this._sortedUsers(this.users).map(
(user) => html` (user) => html`
<paper-icon-item data-user-id=${user.id}> <paper-icon-item data-user-id=${user.id}>
<ha-user-badge .user=${user} slot="item-icon"></ha-user-badge> <ha-user-badge
.hass=${this.hass}
.user=${user}
slot="item-icon"
></ha-user-badge>
${user.name} ${user.name}
</paper-icon-item> </paper-icon-item>
` `

View File

@@ -3,7 +3,7 @@ import {
HassEntityBase, HassEntityBase,
} from "home-assistant-js-websocket"; } from "home-assistant-js-websocket";
import { navigate } from "../common/navigate"; import { navigate } from "../common/navigate";
import { HomeAssistant, Context } from "../types"; import { Context, HomeAssistant } from "../types";
import { DeviceCondition, DeviceTrigger } from "./device_automation"; import { DeviceCondition, DeviceTrigger } from "./device_automation";
import { Action } from "./script"; import { Action } from "./script";
@@ -15,6 +15,7 @@ export interface AutomationEntity extends HassEntityBase {
} }
export interface AutomationConfig { export interface AutomationConfig {
id?: string;
alias: string; alias: string;
description: string; description: string;
trigger: Trigger[]; trigger: Trigger[];
@@ -32,7 +33,8 @@ export interface ForDict {
export interface StateTrigger { export interface StateTrigger {
platform: "state"; platform: "state";
entity_id?: string; entity_id: string;
attribute?: string;
from?: string | number; from?: string | number;
to?: string | number; to?: string | number;
for?: string | number | ForDict; for?: string | number | ForDict;
@@ -59,6 +61,7 @@ export interface HassTrigger {
export interface NumericStateTrigger { export interface NumericStateTrigger {
platform: "numeric_state"; platform: "numeric_state";
entity_id: string; entity_id: string;
attribute?: string;
above?: number; above?: number;
below?: number; below?: number;
value_template?: string; value_template?: string;
@@ -136,12 +139,14 @@ export interface LogicalCondition {
export interface StateCondition { export interface StateCondition {
condition: "state"; condition: "state";
entity_id: string; entity_id: string;
attribute?: string;
state: string | number; state: string | number;
} }
export interface NumericStateCondition { export interface NumericStateCondition {
condition: "numeric_state"; condition: "numeric_state";
entity_id: string; entity_id: string;
attribute?: string;
above?: number; above?: number;
below?: number; below?: number;
value_template?: string; value_template?: string;

View File

@@ -9,14 +9,14 @@ interface CloudStatusBase {
} }
export interface GoogleEntityConfig { export interface GoogleEntityConfig {
should_expose?: boolean; should_expose?: boolean | null;
override_name?: string; override_name?: string;
aliases?: string[]; aliases?: string[];
disable_2fa?: boolean; disable_2fa?: boolean;
} }
export interface AlexaEntityConfig { export interface AlexaEntityConfig {
should_expose?: boolean; should_expose?: boolean | null;
} }
export interface CertificateInformation { export interface CertificateInformation {
@@ -31,9 +31,11 @@ export interface CloudPreferences {
remote_enabled: boolean; remote_enabled: boolean;
google_secure_devices_pin: string | undefined; google_secure_devices_pin: string | undefined;
cloudhooks: { [webhookId: string]: CloudWebhook }; cloudhooks: { [webhookId: string]: CloudWebhook };
google_default_expose: string[] | null;
google_entity_configs: { google_entity_configs: {
[entityId: string]: GoogleEntityConfig; [entityId: string]: GoogleEntityConfig;
}; };
alexa_default_expose: string[] | null;
alexa_entity_configs: { alexa_entity_configs: {
[entityId: string]: AlexaEntityConfig; [entityId: string]: AlexaEntityConfig;
}; };
@@ -106,8 +108,10 @@ export const updateCloudPref = (
prefs: { prefs: {
google_enabled?: CloudPreferences["google_enabled"]; google_enabled?: CloudPreferences["google_enabled"];
alexa_enabled?: CloudPreferences["alexa_enabled"]; alexa_enabled?: CloudPreferences["alexa_enabled"];
alexa_default_expose?: CloudPreferences["alexa_default_expose"];
alexa_report_state?: CloudPreferences["alexa_report_state"]; alexa_report_state?: CloudPreferences["alexa_report_state"];
google_report_state?: CloudPreferences["google_report_state"]; google_report_state?: CloudPreferences["google_report_state"];
google_default_expose?: CloudPreferences["google_default_expose"];
google_secure_devices_pin?: CloudPreferences["google_secure_devices_pin"]; google_secure_devices_pin?: CloudPreferences["google_secure_devices_pin"];
} }
) => ) =>

View File

@@ -13,6 +13,8 @@ export const DISCOVERY_SOURCES = [
"discovery", "discovery",
]; ];
export const ATTENTION_SOURCES = ["reauth"];
export const createConfigFlow = (hass: HomeAssistant, handler: string) => export const createConfigFlow = (hass: HomeAssistant, handler: string) =>
hass.callApi<DataEntryFlowStep>("POST", "config/config_entries/flow", { hass.callApi<DataEntryFlowStep>("POST", "config/config_entries/flow", {
handler, handler,

View File

@@ -51,6 +51,7 @@ export interface HassioAddonDetails extends HassioAddonInfo {
changelog: boolean; changelog: boolean;
hassio_api: boolean; hassio_api: boolean;
hassio_role: "default" | "homeassistant" | "manager" | "admin"; hassio_role: "default" | "homeassistant" | "manager" | "admin";
startup: "initialize" | "system" | "services" | "application" | "once";
homeassistant_api: boolean; homeassistant_api: boolean;
auth_api: boolean; auth_api: boolean;
full_access: boolean; full_access: boolean;
@@ -158,6 +159,19 @@ export const setHassioAddonOption = async (
); );
}; };
export const validateHassioAddonOption = async (
hass: HomeAssistant,
slug: string
) => {
return await hass.callApi<
HassioResponse<{ message: string; valid: boolean }>
>("POST", `hassio/addons/${slug}/options/validate`);
};
export const startHassioAddon = async (hass: HomeAssistant, slug: string) => {
return hass.callApi<string>("POST", `hassio/addons/${slug}/start`);
};
export const setHassioAddonSecurity = async ( export const setHassioAddonSecurity = async (
hass: HomeAssistant, hass: HomeAssistant,
slug: string, slug: string,

View File

@@ -5,3 +5,13 @@ export interface HassioResponse<T> {
export const hassioApiResultExtractor = <T>(response: HassioResponse<T>) => export const hassioApiResultExtractor = <T>(response: HassioResponse<T>) =>
response.data; response.data;
export const extractApiErrorMessage = (error: any): string => {
return typeof error === "object"
? typeof error.body === "object"
? error.body.message || "Unknown error, see logs"
: error.body || "Unknown error, see logs"
: error;
};
export const ignoredStatusCodes = new Set([502, 503, 504]);

View File

@@ -23,7 +23,8 @@ export const getLogbookData = (
hass: HomeAssistant, hass: HomeAssistant,
startDate: string, startDate: string,
endDate: string, endDate: string,
entityId?: string entityId?: string,
entity_matches_only?: boolean
) => { ) => {
const ALL_ENTITIES = "*"; const ALL_ENTITIES = "*";
@@ -51,7 +52,8 @@ export const getLogbookData = (
hass, hass,
startDate, startDate,
endDate, endDate,
entityId !== ALL_ENTITIES ? entityId : undefined entityId !== ALL_ENTITIES ? entityId : undefined,
entity_matches_only
).then((entries) => entries.reverse()); ).then((entries) => entries.reverse());
return DATA_CACHE[cacheKey][entityId]; return DATA_CACHE[cacheKey][entityId];
}; };
@@ -60,11 +62,13 @@ const getLogbookDataFromServer = async (
hass: HomeAssistant, hass: HomeAssistant,
startDate: string, startDate: string,
endDate: string, endDate: string,
entityId?: string entityId?: string,
entity_matches_only?: boolean
) => { ) => {
const url = `logbook/${startDate}?end_time=${endDate}${ const url = `logbook/${startDate}?end_time=${endDate}${
entityId ? `&entity=${entityId}` : "" entityId ? `&entity=${entityId}` : ""
}`; }${entity_matches_only ? `&entity_matches_only` : ""}`;
return hass.callApi<LogbookEntry[]>("GET", url); return hass.callApi<LogbookEntry[]>("GET", url);
}; };

View File

@@ -318,10 +318,11 @@ export interface WindowWithLovelaceProm extends Window {
export interface ActionHandlerOptions { export interface ActionHandlerOptions {
hasHold?: boolean; hasHold?: boolean;
hasDoubleClick?: boolean; hasDoubleClick?: boolean;
disabled?: boolean;
} }
export interface ActionHandlerDetail { export interface ActionHandlerDetail {
action: string; action: "hold" | "tap" | "double_tap";
} }
export type ActionHandlerEvent = HASSDomEvent<ActionHandlerDetail>; export type ActionHandlerEvent = HASSDomEvent<ActionHandlerDetail>;

View File

@@ -1,5 +1,23 @@
import type { HassEntity } from "home-assistant-js-websocket"; import type { HassEntity } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import {
mdiFolder,
mdiPlaylistMusic,
mdiFileMusic,
mdiAlbum,
mdiMusic,
mdiTelevisionClassic,
mdiMovie,
mdiVideo,
mdiImage,
mdiWeb,
mdiGamepadVariant,
mdiAccountMusic,
mdiPodcast,
mdiApplication,
mdiAccountMusicOutline,
mdiDramaMasks,
} from "@mdi/js";
export const SUPPORT_PAUSE = 1; export const SUPPORT_PAUSE = 1;
export const SUPPORT_SEEK = 2; export const SUPPORT_SEEK = 2;
@@ -20,9 +38,70 @@ export const CONTRAST_RATIO = 4.5;
export type MediaPlayerBrowseAction = "pick" | "play"; export type MediaPlayerBrowseAction = "pick" | "play";
export const BROWSER_SOURCE = "browser";
export type MediaClassBrowserSetting = {
icon: string;
thumbnail_ratio?: string;
layout?: string;
show_list_images?: boolean;
};
export const MediaClassBrowserSettings: {
[type: string]: MediaClassBrowserSetting;
} = {
album: { icon: mdiAlbum, layout: "grid" },
app: { icon: mdiApplication, layout: "grid" },
artist: { icon: mdiAccountMusic, layout: "grid", show_list_images: true },
channel: {
icon: mdiTelevisionClassic,
thumbnail_ratio: "portrait",
layout: "grid",
},
composer: {
icon: mdiAccountMusicOutline,
layout: "grid",
show_list_images: true,
},
contributing_artist: {
icon: mdiAccountMusic,
layout: "grid",
show_list_images: true,
},
directory: { icon: mdiFolder, layout: "grid", show_list_images: true },
episode: {
icon: mdiTelevisionClassic,
layout: "grid",
thumbnail_ratio: "portrait",
},
game: {
icon: mdiGamepadVariant,
layout: "grid",
thumbnail_ratio: "portrait",
},
genre: { icon: mdiDramaMasks, layout: "grid", show_list_images: true },
image: { icon: mdiImage, layout: "grid" },
movie: { icon: mdiMovie, thumbnail_ratio: "portrait", layout: "grid" },
music: { icon: mdiMusic },
playlist: { icon: mdiPlaylistMusic, layout: "grid", show_list_images: true },
podcast: { icon: mdiPodcast, layout: "grid" },
season: {
icon: mdiTelevisionClassic,
layout: "grid",
thumbnail_ratio: "portrait",
},
track: { icon: mdiFileMusic },
tv_show: {
icon: mdiTelevisionClassic,
layout: "grid",
thumbnail_ratio: "portrait",
},
url: { icon: mdiWeb },
video: { icon: mdiVideo, layout: "grid" },
};
export interface MediaPickedEvent { export interface MediaPickedEvent {
media_content_id: string; item: MediaPlayerItem;
media_content_type: string;
} }
export interface MediaPlayerThumbnail { export interface MediaPlayerThumbnail {
@@ -39,6 +118,8 @@ export interface MediaPlayerItem {
title: string; title: string;
media_content_type: string; media_content_type: string;
media_content_id: string; media_content_id: string;
media_class: string;
children_media_class: string;
can_play: boolean; can_play: boolean;
can_expand: boolean; can_expand: boolean;
thumbnail?: string; thumbnail?: string;
@@ -58,6 +139,15 @@ export const browseMediaPlayer = (
media_content_type: mediaContentType, 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: HassEntity): number => { export const getCurrentProgress = (stateObj: HassEntity): number => {
let progress = stateObj.attributes.media_position; let progress = stateObj.attributes.media_position;

View File

@@ -14,6 +14,8 @@ export interface OZWDevice {
is_zwave_plus: boolean; is_zwave_plus: boolean;
ozw_instance: number; ozw_instance: number;
event: string; event: string;
node_manufacturer_name: string;
node_product_name: string;
} }
export interface OZWDeviceMetaDataResponse { export interface OZWDeviceMetaDataResponse {
@@ -147,6 +149,15 @@ export const fetchOZWNetworkStatistics = (
ozw_instance: ozw_instance, ozw_instance: ozw_instance,
}); });
export const fetchOZWNodes = (
hass: HomeAssistant,
ozw_instance: number
): Promise<OZWDevice[]> =>
hass.callWS({
type: "ozw/get_nodes",
ozw_instance: ozw_instance,
});
export const fetchOZWNodeStatus = ( export const fetchOZWNodeStatus = (
hass: HomeAssistant, hass: HomeAssistant,
ozw_instance: number, ozw_instance: number,

17
src/data/refresh_token.ts Normal file
View File

@@ -0,0 +1,17 @@
declare global {
interface HASSDomEvents {
"hass-refresh-tokens": undefined;
}
}
export interface RefreshToken {
client_icon?: string;
client_id: string;
client_name?: string;
created_at: string;
id: string;
is_current: boolean;
last_used_at?: string;
last_used_ip?: string;
type: "normal" | "long_lived_access_token";
}

View File

@@ -5,7 +5,7 @@ import {
import { computeObjectId } from "../common/entity/compute_object_id"; import { computeObjectId } from "../common/entity/compute_object_id";
import { navigate } from "../common/navigate"; import { navigate } from "../common/navigate";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { Condition } from "./automation"; import { Condition, Trigger } from "./automation";
export const MODES = ["single", "restart", "queued", "parallel"]; export const MODES = ["single", "restart", "queued", "parallel"];
export const MODES_MAX = ["queued", "parallel"]; export const MODES_MAX = ["queued", "parallel"];
@@ -56,6 +56,13 @@ export interface SceneAction {
export interface WaitAction { export interface WaitAction {
wait_template: string; wait_template: string;
timeout?: number; timeout?: number;
continue_on_timeout?: boolean;
}
export interface WaitForTriggerAction {
wait_for_trigger: Trigger[];
timeout?: number;
continue_on_timeout?: boolean;
} }
export interface RepeatAction { export interface RepeatAction {
@@ -91,6 +98,7 @@ export type Action =
| DelayAction | DelayAction
| SceneAction | SceneAction
| WaitAction | WaitAction
| WaitForTriggerAction
| RepeatAction | RepeatAction
| ChooseAction; | ChooseAction;

View File

@@ -200,7 +200,7 @@ export const weatherSVGStyles = css`
fill: var(--weather-icon-sun-color, #fdd93c); fill: var(--weather-icon-sun-color, #fdd93c);
} }
.moon { .moon {
fill: var(--weather-icon-moon-color, #fdf9cc); fill: var(--weather-icon-moon-color, #fcf497);
} }
.cloud-back { .cloud-back {
fill: var(--weather-icon-cloud-back-color, #d4d4d4); fill: var(--weather-icon-cloud-back-color, #d4d4d4);

View File

@@ -1,20 +1,27 @@
import { Connection, UnsubscribeFunc } from "home-assistant-js-websocket"; import { Connection, UnsubscribeFunc } from "home-assistant-js-websocket";
interface RenderTemplateResult { export interface RenderTemplateResult {
result: string; result: string;
listeners: TemplateListeners;
}
interface TemplateListeners {
all: boolean;
domains: string[];
entities: string[];
} }
export const subscribeRenderTemplate = ( export const subscribeRenderTemplate = (
conn: Connection, conn: Connection,
onChange: (result: string) => void, onChange: (result: RenderTemplateResult) => void,
params: { params: {
template: string; template: string;
entity_ids?: string | string[]; entity_ids?: string | string[];
variables?: object; variables?: object;
} }
): Promise<UnsubscribeFunc> => { ): Promise<UnsubscribeFunc> => {
return conn.subscribeMessage( return conn.subscribeMessage((msg: RenderTemplateResult) => onChange(msg), {
(msg: RenderTemplateResult) => onChange(msg.result), type: "render_template",
{ type: "render_template", ...params } ...params,
); });
}; };

View File

@@ -1,21 +1,22 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "../../components/ha-icon-button";
import "../../components/ha-circular-progress";
import "@polymer/paper-tooltip/paper-tooltip";
import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { import {
css, css,
CSSResultArray, CSSResultArray,
customElement, customElement,
html, html,
LitElement,
internalProperty, internalProperty,
LitElement,
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { fireEvent } from "../../common/dom/fire_event";
import { computeRTL } from "../../common/util/compute_rtl";
import "../../components/ha-circular-progress";
import "../../components/ha-dialog"; import "../../components/ha-dialog";
import "../../components/ha-form/ha-form"; import "../../components/ha-form/ha-form";
import "../../components/ha-icon-button";
import "../../components/ha-markdown"; import "../../components/ha-markdown";
import { import {
AreaRegistryEntry, AreaRegistryEntry,
@@ -35,8 +36,6 @@ import "./step-flow-external";
import "./step-flow-form"; import "./step-flow-form";
import "./step-flow-loading"; import "./step-flow-loading";
import "./step-flow-pick-handler"; import "./step-flow-pick-handler";
import { fireEvent } from "../../common/dom/fire_event";
import { computeRTL } from "../../common/util/compute_rtl";
let instance = 0; let instance = 0;

View File

@@ -97,8 +97,13 @@ export const showConfigFlowDialog = (
}, },
renderExternalStepHeader(hass, step) { renderExternalStepHeader(hass, step) {
return hass.localize( return (
`component.${step.handler}.config.step.${step.step_id}.title` hass.localize(
`component.${step.handler}.config.step.${step.step_id}.title`
) ||
hass.localize(
"ui.panel.config.integrations.config_flow.external_step.open_site"
)
); );
}, },

View File

@@ -1,5 +1,4 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "../../components/ha-circular-progress";
import "@polymer/paper-tooltip/paper-tooltip"; import "@polymer/paper-tooltip/paper-tooltip";
import { import {
css, css,
@@ -12,6 +11,7 @@ import {
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-circular-progress";
import "../../components/ha-form/ha-form"; import "../../components/ha-form/ha-form";
import type { HaFormSchema } from "../../components/ha-form/ha-form"; import type { HaFormSchema } from "../../components/ha-form/ha-form";
import "../../components/ha-markdown"; import "../../components/ha-markdown";
@@ -91,7 +91,7 @@ class StepFlowForm extends LitElement {
${!allRequiredInfoFilledIn ${!allRequiredInfoFilledIn
? html` ? html`
<paper-tooltip position="left" <paper-tooltip animation-delay="0" position="left"
>${this.hass.localize( >${this.hass.localize(
"ui.panel.config.integrations.config_flow.not_all_required_fields" "ui.panel.config.integrations.config_flow.not_all_required_fields"
)} )}

View File

@@ -4,27 +4,35 @@ import {
CSSResultArray, CSSResultArray,
customElement, customElement,
html, html,
LitElement,
internalProperty, internalProperty,
LitElement,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import "../../components/dialog/ha-paper-dialog"; import { fireEvent } from "../../common/dom/fire_event";
import { createCloseHeading } from "../../components/ha-dialog";
import "../../components/ha-formfield";
import "../../components/ha-switch";
import { domainToName } from "../../data/integration"; import { domainToName } from "../../data/integration";
import { PolymerChangedEvent } from "../../polymer-types";
import { haStyleDialog } from "../../resources/styles"; import { haStyleDialog } from "../../resources/styles";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { HassDialog } from "../make-dialog-manager";
import { HaDomainTogglerDialogParams } from "./show-dialog-domain-toggler"; import { HaDomainTogglerDialogParams } from "./show-dialog-domain-toggler";
@customElement("dialog-domain-toggler") @customElement("dialog-domain-toggler")
class DomainTogglerDialog extends LitElement { class DomainTogglerDialog extends LitElement implements HassDialog {
public hass!: HomeAssistant; public hass!: HomeAssistant;
@internalProperty() private _params?: HaDomainTogglerDialogParams; @internalProperty() private _params?: HaDomainTogglerDialogParams;
public async showDialog(params: HaDomainTogglerDialogParams): Promise<void> { public showDialog(params: HaDomainTogglerDialogParams): void {
this._params = params; this._params = params;
} }
public closeDialog() {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._params) { if (!this._params) {
return html``; return html``;
@@ -35,46 +43,49 @@ class DomainTogglerDialog extends LitElement {
.sort(); .sort();
return html` return html`
<ha-paper-dialog <ha-dialog
with-backdrop open
opened @closed=${this.closeDialog}
@opened-changed=${this._openedChanged} scrimClickAction
escapeKeyAction
hideActions
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.dialogs.domain_toggler.title")
)}
> >
<h2>
${this.hass.localize("ui.dialogs.domain_toggler.title")}
</h2>
<div> <div>
${domains.map( ${domains.map(
(domain) => (domain) =>
html` html`
<div>${domain[0]}</div> <ha-formfield .label=${domain[0]}>
<mwc-button .domain=${domain[1]} @click=${this._handleOff}> <ha-switch
${this.hass.localize("state.default.off")} .domain=${domain[1]}
</mwc-button> .checked=${!this._params!.exposedDomains ||
<mwc-button .domain=${domain[1]} @click=${this._handleOn}> this._params!.exposedDomains.includes(domain[1])}
${this.hass.localize("state.default.on")} @change=${this._handleSwitch}
>
</ha-switch>
</ha-formfield>
<mwc-button .domain=${domain[1]} @click=${this._handleReset}>
${this.hass.localize(
"ui.dialogs.domain_toggler.reset_entities"
)}
</mwc-button> </mwc-button>
` `
)} )}
</div> </div>
</ha-paper-dialog> </ha-dialog>
`; `;
} }
private _openedChanged(ev: PolymerChangedEvent<boolean>): void { private _handleSwitch(ev) {
// Closed dialog by clicking on the overlay this._params!.toggleDomain(ev.currentTarget.domain, ev.target.checked);
if (!ev.detail.value) {
this._params = undefined;
}
}
private _handleOff(ev) {
this._params!.toggleDomain(ev.currentTarget.domain, false);
ev.currentTarget.blur(); ev.currentTarget.blur();
} }
private _handleOn(ev) { private _handleReset(ev) {
this._params!.toggleDomain(ev.currentTarget.domain, true); this._params!.resetDomain(ev.currentTarget.domain);
ev.currentTarget.blur(); ev.currentTarget.blur();
} }
@@ -82,12 +93,13 @@ class DomainTogglerDialog extends LitElement {
return [ return [
haStyleDialog, haStyleDialog,
css` css`
ha-paper-dialog { ha-dialog {
max-width: 500px; --mdc-dialog-max-width: 500px;
} }
div { div {
display: grid; display: grid;
grid-template-columns: auto auto auto; grid-template-columns: auto auto;
grid-row-gap: 8px;
align-items: center; align-items: center;
} }
`, `,

View File

@@ -2,7 +2,9 @@ import { fireEvent } from "../../common/dom/fire_event";
export interface HaDomainTogglerDialogParams { export interface HaDomainTogglerDialogParams {
domains: string[]; domains: string[];
exposedDomains: string[] | null;
toggleDomain: (domain: string, turnOn: boolean) => void; toggleDomain: (domain: string, turnOn: boolean) => void;
resetDomain: (domain: string) => void;
} }
export const loadDomainTogglerDialog = () => export const loadDomainTogglerDialog = () =>

View File

@@ -5,19 +5,19 @@ import {
CSSResult, CSSResult,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { classMap } from "lit-html/directives/class-map"; import { classMap } from "lit-html/directives/class-map";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-dialog"; import "../../components/ha-dialog";
import "../../components/ha-switch"; import "../../components/ha-switch";
import { PolymerChangedEvent } from "../../polymer-types"; import { PolymerChangedEvent } from "../../polymer-types";
import { haStyleDialog } from "../../resources/styles"; import { haStyleDialog } from "../../resources/styles";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { DialogParams } from "./show-dialog-box"; import { DialogParams } from "./show-dialog-box";
import { fireEvent } from "../../common/dom/fire_event";
@customElement("dialog-box") @customElement("dialog-box")
class DialogBox extends LitElement { class DialogBox extends LitElement {
@@ -55,9 +55,10 @@ class DialogBox extends LitElement {
return html` return html`
<ha-dialog <ha-dialog
open open
scrimClickAction ?scrimClickAction=${this._params.prompt}
escapeKeyAction ?escapeKeyAction=${this._params.prompt}
@close=${this._close} @closed=${this._dialogClosed}
defaultAction="ignore"
.heading=${this._params.title .heading=${this._params.title
? this._params.title ? this._params.title
: this._params.confirmation && : this._params.confirmation &&
@@ -78,10 +79,10 @@ class DialogBox extends LitElement {
${this._params.prompt ${this._params.prompt
? html` ? html`
<paper-input <paper-input
autofocus dialogInitialFocus
.value=${this._value} .value=${this._value}
@value-changed=${this._valueChanged}
@keyup=${this._handleKeyUp} @keyup=${this._handleKeyUp}
@value-changed=${this._valueChanged}
.label=${this._params.inputLabel .label=${this._params.inputLabel
? this._params.inputLabel ? this._params.inputLabel
: ""} : ""}
@@ -100,7 +101,11 @@ class DialogBox extends LitElement {
: this.hass.localize("ui.dialogs.generic.cancel")} : this.hass.localize("ui.dialogs.generic.cancel")}
</mwc-button> </mwc-button>
`} `}
<mwc-button @click=${this._confirm} slot="primaryAction"> <mwc-button
@click=${this._confirm}
?dialogInitialFocus=${!this._params.prompt}
slot="primaryAction"
>
${this._params.confirmText ${this._params.confirmText
? this._params.confirmText ? this._params.confirmText
: this.hass.localize("ui.dialogs.generic.ok")} : this.hass.localize("ui.dialogs.generic.ok")}
@@ -114,8 +119,8 @@ class DialogBox extends LitElement {
} }
private _dismiss(): void { private _dismiss(): void {
if (this._params!.cancel) { if (this._params?.cancel) {
this._params!.cancel(); this._params.cancel();
} }
this._close(); this._close();
} }
@@ -133,7 +138,17 @@ class DialogBox extends LitElement {
this._close(); this._close();
} }
private _dialogClosed(ev) {
if (ev.detail.action === "ignore") {
return;
}
this.closeDialog();
}
private _close(): void { private _close(): void {
if (!this._params) {
return;
}
this._params = undefined; this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }

View File

@@ -12,12 +12,13 @@ import {
import "../../../components/ha-relative-time"; import "../../../components/ha-relative-time";
import { triggerAutomation } from "../../../data/automation"; import { triggerAutomation } from "../../../data/automation";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { UNAVAILABLE_STATES } from "../../../data/entity";
@customElement("more-info-automation") @customElement("more-info-automation")
class MoreInfoAutomation extends LitElement { class MoreInfoAutomation extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public stateObj?: HassEntity; @property({ attribute: false }) public stateObj?: HassEntity;
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.hass || !this.stateObj) { if (!this.hass || !this.stateObj) {
@@ -34,7 +35,10 @@ class MoreInfoAutomation extends LitElement {
</div> </div>
<div class="actions"> <div class="actions">
<mwc-button @click=${this.handleAction}> <mwc-button
@click=${this.handleAction}
.disabled=${UNAVAILABLE_STATES.includes(this.stateObj!.state)}
>
${this.hass.localize("ui.card.automation.trigger")} ${this.hass.localize("ui.card.automation.trigger")}
</mwc-button> </mwc-button>
</div> </div>
@@ -52,7 +56,7 @@ class MoreInfoAutomation extends LitElement {
justify-content: space-between; justify-content: space-between;
} }
.actions { .actions {
margin: 36px 0 8px 0; margin: 8px 0;
text-align: right; text-align: right;
} }
`; `;

View File

@@ -4,9 +4,9 @@ import {
css, css,
CSSResult, CSSResult,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
@@ -47,8 +47,8 @@ class MoreInfoCamera extends LitElement {
return html` return html`
<ha-camera-stream <ha-camera-stream
.hass=${this.hass} .hass=${this.hass}
.stateObj="${this.stateObj}" .stateObj=${this.stateObj}
showcontrols controls
></ha-camera-stream> ></ha-camera-stream>
${this._cameraPrefs ${this._cameraPrefs
? html` ? html`

View File

@@ -61,20 +61,20 @@ class MoreInfoLight extends LitElement {
"is-on": this.stateObj.state === "on", "is-on": this.stateObj.state === "on",
})}" })}"
> >
${supportsFeature(this.stateObj!, SUPPORT_BRIGHTNESS)
? html`
<ha-labeled-slider
caption=${this.hass.localize("ui.card.light.brightness")}
icon="hass:brightness-5"
min="1"
max="255"
value=${this._brightnessSliderValue}
@change=${this._brightnessSliderChanged}
></ha-labeled-slider>
`
: ""}
${this.stateObj.state === "on" ${this.stateObj.state === "on"
? html` ? html`
${supportsFeature(this.stateObj!, SUPPORT_BRIGHTNESS)
? html`
<ha-labeled-slider
caption=${this.hass.localize("ui.card.light.brightness")}
icon="hass:brightness-5"
min="1"
max="255"
value=${this._brightnessSliderValue}
@change=${this._brightnessSliderChanged}
></ha-labeled-slider>
`
: ""}
${supportsFeature(this.stateObj, SUPPORT_COLOR_TEMP) ${supportsFeature(this.stateObj, SUPPORT_COLOR_TEMP)
? html` ? html`
<ha-labeled-slider <ha-labeled-slider
@@ -134,7 +134,7 @@ class MoreInfoLight extends LitElement {
attr-for-selected="item-name" attr-for-selected="item-name"
>${this.stateObj.attributes.effect_list.map( >${this.stateObj.attributes.effect_list.map(
(effect: string) => html` (effect: string) => html`
<paper-item itemName=${effect} <paper-item .itemName=${effect}
>${effect}</paper-item >${effect}</paper-item
> >
` `
@@ -170,7 +170,7 @@ class MoreInfoLight extends LitElement {
} }
private _effectChanged(ev: CustomEvent) { private _effectChanged(ev: CustomEvent) {
const newVal = ev.detail.value; const newVal = ev.detail.item.itemName;
if (!newVal || this.stateObj!.attributes.effect === newVal) { if (!newVal || this.stateObj!.attributes.effect === newVal) {
return; return;

View File

@@ -130,7 +130,7 @@ class MoreInfoMediaPlayer extends LitElement {
</div> </div>
` `
: ""} : ""}
${stateObj.state !== "off" && ${![UNAVAILABLE, UNKNOWN, "off"].includes(stateObj.state) &&
supportsFeature(stateObj, SUPPORT_SELECT_SOURCE) && supportsFeature(stateObj, SUPPORT_SELECT_SOURCE) &&
stateObj.attributes.source_list?.length stateObj.attributes.source_list?.length
? html` ? html`
@@ -188,14 +188,17 @@ class MoreInfoMediaPlayer extends LitElement {
<div class="tts"> <div class="tts">
<paper-input <paper-input
id="ttsInput" id="ttsInput"
.disabled=${UNAVAILABLE_STATES.includes(stateObj.state)}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.card.media_player.text_to_speak" "ui.card.media_player.text_to_speak"
)} )}
@keydown=${this._ttsCheckForEnter} @keydown=${this._ttsCheckForEnter}
></paper-input> ></paper-input>
<ha-icon-button icon="hass:send" @click=${ <ha-icon-button
this._sendTTS icon="hass:send"
}></ha-icon-button> .disabled=${UNAVAILABLE_STATES.includes(stateObj.state)}
@click=${this._sendTTS}
></ha-icon-button>
</div> </div>
</div> </div>
` `
@@ -409,8 +412,8 @@ class MoreInfoMediaPlayer extends LitElement {
entityId: this.stateObj!.entity_id, entityId: this.stateObj!.entity_id,
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => mediaPickedCallback: (pickedMedia: MediaPickedEvent) =>
this._playMedia( this._playMedia(
pickedMedia.media_content_id, pickedMedia.item.media_content_id,
pickedMedia.media_content_type pickedMedia.item.media_content_type
), ),
}); });
} }

View File

@@ -26,15 +26,12 @@ class MoreInfoTimer extends LitElement {
return html` return html`
<ha-attributes <ha-attributes
.stateObj=${this.stateObj} .stateObj=${this.stateObj}
.extraFilters=${"remaining"} extra-filters="remaining"
></ha-attributes> ></ha-attributes>
<div class="actions"> <div class="actions">
${this.stateObj.state === "idle" || this.stateObj.state === "paused" ${this.stateObj.state === "idle" || this.stateObj.state === "paused"
? html` ? html`
<mwc-button <mwc-button .action=${"start"} @click=${this._handleActionClick}>
.action="${"start"}"
@click="${this._handleActionClick}"
>
${this.hass!.localize("ui.card.timer.actions.start")} ${this.hass!.localize("ui.card.timer.actions.start")}
</mwc-button> </mwc-button>
` `
@@ -42,7 +39,7 @@ class MoreInfoTimer extends LitElement {
${this.stateObj.state === "active" ${this.stateObj.state === "active"
? html` ? html`
<mwc-button <mwc-button
.action="${"pause"}" .action=${"pause"}
@click="${this._handleActionClick}" @click="${this._handleActionClick}"
> >
${this.hass!.localize("ui.card.timer.actions.pause")} ${this.hass!.localize("ui.card.timer.actions.pause")}
@@ -52,13 +49,13 @@ class MoreInfoTimer extends LitElement {
${this.stateObj.state === "active" || this.stateObj.state === "paused" ${this.stateObj.state === "active" || this.stateObj.state === "paused"
? html` ? html`
<mwc-button <mwc-button
.action="${"cancel"}" .action=${"cancel"}
@click="${this._handleActionClick}" @click="${this._handleActionClick}"
> >
${this.hass!.localize("ui.card.timer.actions.cancel")} ${this.hass!.localize("ui.card.timer.actions.cancel")}
</mwc-button> </mwc-button>
<mwc-button <mwc-button
.action="${"finish"}" .action=${"finish"}
@click="${this._handleActionClick}" @click="${this._handleActionClick}"
> >
${this.hass!.localize("ui.card.timer.actions.finish")} ${this.hass!.localize("ui.card.timer.actions.finish")}

View File

@@ -68,7 +68,7 @@ const VACUUM_COMMANDS: VacuumCommand[] = [
}, },
{ {
translationKey: "clean_spot", translationKey: "clean_spot",
icon: "hass:broom", icon: "hass:target-variant",
serviceName: "clean_spot", serviceName: "clean_spot",
isVisible: (stateObj) => isVisible: (stateObj) =>
supportsFeature(stateObj, VACUUM_SUPPORT_CLEAN_SPOT), supportsFeature(stateObj, VACUUM_SUPPORT_CLEAN_SPOT),

View File

@@ -1,38 +1,72 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "@material/mwc-icon-button"; import "@material/mwc-icon-button";
import "../../components/ha-header-bar"; import "@material/mwc-tab";
import "../../components/ha-dialog"; import "@material/mwc-tab-bar";
import "../../components/ha-svg-icon"; import { mdiClose, mdiCog, mdiPencil } from "@mdi/js";
import {
css,
customElement,
html,
internalProperty,
LitElement,
property,
} from "lit-element";
import { cache } from "lit-html/directives/cache";
import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { DOMAINS_MORE_INFO_NO_HISTORY } from "../../common/const"; import {
import { computeStateName } from "../../common/entity/compute_state_name"; DOMAINS_MORE_INFO_NO_HISTORY,
import { navigate } from "../../common/navigate"; DOMAINS_WITH_MORE_INFO,
} from "../../common/const";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { stateMoreInfoType } from "../../common/entity/state_more_info_type";
import { navigate } from "../../common/navigate";
import "../../components/ha-dialog";
import "../../components/ha-header-bar";
import "../../components/ha-svg-icon";
import "../../components/state-history-charts"; import "../../components/state-history-charts";
import { removeEntityRegistryEntry } from "../../data/entity_registry"; import { removeEntityRegistryEntry } from "../../data/entity_registry";
import { showEntityEditorDialog } from "../../panels/config/entities/show-dialog-entity-editor"; import { showEntityEditorDialog } from "../../panels/config/entities/show-dialog-entity-editor";
import "../../state-summary/state-card-content"; import "../../panels/logbook/ha-logbook";
import { showConfirmationDialog } from "../generic/show-dialog-box";
import "./more-info-content";
import {
customElement,
LitElement,
property,
internalProperty,
css,
html,
} from "lit-element";
import { haStyleDialog } from "../../resources/styles"; import { haStyleDialog } from "../../resources/styles";
import "../../state-summary/state-card-content";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { getRecentWithCache } from "../../data/cached-history"; import { showConfirmationDialog } from "../generic/show-dialog-box";
import { computeDomain } from "../../common/entity/compute_domain"; import "./ha-more-info-history";
import { mdiClose, mdiCog, mdiPencil } from "@mdi/js"; import "./ha-more-info-logbook";
import { HistoryResult } from "../../data/history";
const DOMAINS_NO_INFO = ["camera", "configurator"]; const DOMAINS_NO_INFO = ["camera", "configurator"];
const EDITABLE_DOMAINS_WITH_ID = ["scene", "automation"]; const EDITABLE_DOMAINS_WITH_ID = ["scene", "automation"];
const EDITABLE_DOMAINS = ["script"]; const EDITABLE_DOMAINS = ["script"];
const MORE_INFO_CONTROL_IMPORT = {
alarm_control_panel: () => import("./controls/more-info-alarm_control_panel"),
automation: () => import("./controls/more-info-automation"),
camera: () => import("./controls/more-info-camera"),
climate: () => import("./controls/more-info-climate"),
configurator: () => import("./controls/more-info-configurator"),
counter: () => import("./controls/more-info-counter"),
cover: () => import("./controls/more-info-cover"),
fan: () => import("./controls/more-info-fan"),
group: () => import("./controls/more-info-group"),
humidifier: () => import("./controls/more-info-humidifier"),
input_datetime: () => import("./controls/more-info-input_datetime"),
light: () => import("./controls/more-info-light"),
lock: () => import("./controls/more-info-lock"),
media_player: () => import("./controls/more-info-media_player"),
person: () => import("./controls/more-info-person"),
script: () => import("./controls/more-info-script"),
sun: () => import("./controls/more-info-sun"),
timer: () => import("./controls/more-info-timer"),
vacuum: () => import("./controls/more-info-vacuum"),
water_heater: () => import("./controls/more-info-water_heater"),
weather: () => import("./controls/more-info-weather"),
hidden: () => {},
default: () => import("./controls/more-info-default"),
};
export interface MoreInfoDialogParams { export interface MoreInfoDialogParams {
entityId: string | null; entityId: string | null;
} }
@@ -43,11 +77,11 @@ export class MoreInfoDialog extends LitElement {
@property({ type: Boolean, reflect: true }) public large = false; @property({ type: Boolean, reflect: true }) public large = false;
@internalProperty() private _stateHistory?: HistoryResult;
@internalProperty() private _entityId?: string | null; @internalProperty() private _entityId?: string | null;
private _historyRefreshInterval?: number; @internalProperty() private _moreInfoType?: string;
@internalProperty() private _currTabIndex = 0;
public showDialog(params: MoreInfoDialogParams) { public showDialog(params: MoreInfoDialogParams) {
this._entityId = params.entityId; this._entityId = params.entityId;
@@ -55,24 +89,31 @@ export class MoreInfoDialog extends LitElement {
this.closeDialog(); this.closeDialog();
} }
this.large = false; this.large = false;
this._stateHistory = undefined;
if (this._computeShowHistoryComponent(this._entityId)) {
this._getStateHistory();
clearInterval(this._historyRefreshInterval);
this._historyRefreshInterval = window.setInterval(() => {
this._getStateHistory();
}, 60 * 1000);
}
} }
public closeDialog() { public closeDialog() {
this._entityId = undefined; this._entityId = undefined;
this._stateHistory = undefined; this._currTabIndex = 0;
clearInterval(this._historyRefreshInterval);
this._historyRefreshInterval = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
protected updated(changedProperties) {
if (!this.hass || !this._entityId || !changedProperties.has("_entityId")) {
return;
}
const stateObj = this.hass.states[this._entityId];
if (!stateObj) {
return;
}
if (stateObj.attributes && "custom_ui_more_info" in stateObj.attributes) {
this._moreInfoType = stateObj.attributes.custom_ui_more_info;
} else {
const type = stateMoreInfoType(stateObj);
this._moreInfoType = `more-info-${type}`;
MORE_INFO_CONTROL_IMPORT[type]();
}
}
protected render() { protected render() {
if (!this._entityId) { if (!this._entityId) {
return html``; return html``;
@@ -93,85 +134,135 @@ export class MoreInfoDialog extends LitElement {
hideActions hideActions
data-domain=${domain} data-domain=${domain}
> >
<ha-header-bar slot="heading"> <div slot="heading" class="heading">
<mwc-icon-button <ha-header-bar>
slot="navigationIcon" <mwc-icon-button
.label=${this.hass.localize("ui.dialogs.more_info_control.dismiss")} slot="navigationIcon"
dialogAction="cancel" dialogAction="cancel"
> .label=${this.hass.localize(
<ha-svg-icon .path=${mdiClose}></ha-svg-icon> "ui.dialogs.more_info_control.dismiss"
</mwc-icon-button> )}
<div slot="title" class="main-title" @click=${this._enlarge}> >
${computeStateName(stateObj)} <ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</div> </mwc-icon-button>
${this.hass.user!.is_admin <div slot="title" class="main-title" @click=${this._enlarge}>
? html`<mwc-icon-button ${computeStateName(stateObj)}
slot="actionItems" </div>
.label=${this.hass.localize( ${this.hass.user!.is_admin
"ui.dialogs.more_info_control.settings" ? html`
)} <mwc-icon-button
@click=${this._gotoSettings} slot="actionItems"
> .label=${this.hass.localize(
<ha-svg-icon .path=${mdiCog}></ha-svg-icon> "ui.dialogs.more_info_control.settings"
</mwc-icon-button>` )}
: ""} @click=${this._gotoSettings}
${this.hass.user!.is_admin && >
((EDITABLE_DOMAINS_WITH_ID.includes(domain) && <ha-svg-icon .path=${mdiCog}></ha-svg-icon>
stateObj.attributes.id) || </mwc-icon-button>
EDITABLE_DOMAINS.includes(domain)) `
? html` <mwc-icon-button : ""}
slot="actionItems" ${this.hass.user!.is_admin &&
.label=${this.hass.localize( ((EDITABLE_DOMAINS_WITH_ID.includes(domain) &&
"ui.dialogs.more_info_control.edit" stateObj.attributes.id) ||
)} EDITABLE_DOMAINS.includes(domain))
@click=${this._gotoEdit} ? html`
> <mwc-icon-button
<ha-svg-icon .path=${mdiPencil}></ha-svg-icon> slot="actionItems"
</mwc-icon-button>` .label=${this.hass.localize(
: ""} "ui.dialogs.more_info_control.edit"
</ha-header-bar> )}
<div class="content"> @click=${this._gotoEdit}
${DOMAINS_NO_INFO.includes(domain) >
? "" <ha-svg-icon .path=${mdiPencil}></ha-svg-icon>
: html` </mwc-icon-button>
<state-card-content `
.stateObj=${stateObj} : ""}
.hass=${this.hass} </ha-header-bar>
in-dialog ${DOMAINS_WITH_MORE_INFO.includes(domain) &&
></state-card-content> this._computeShowHistoryComponent(entityId)
`}
${this._computeShowHistoryComponent(entityId)
? html` ? html`
<state-history-charts <mwc-tab-bar
.hass=${this.hass} .activeIndex=${this._currTabIndex}
.historyData=${this._stateHistory} @MDCTabBar:activated=${this._handleTabChanged}
up-to-now >
.isLoadingData=${!this._stateHistory} <mwc-tab
></state-history-charts> .label=${this.hass.localize(
"ui.dialogs.more_info_control.details"
)}
></mwc-tab>
<mwc-tab
.label=${this.hass.localize(
"ui.dialogs.more_info_control.history"
)}
></mwc-tab>
</mwc-tab-bar>
` `
: ""} : ""}
<more-info-content </div>
.stateObj=${stateObj} <div class="content">
.hass=${this.hass} ${cache(
></more-info-content> this._currTabIndex === 0
? html`
${stateObj.attributes.restored ${DOMAINS_NO_INFO.includes(domain)
? html`<p> ? ""
${this.hass.localize( : html`
"ui.dialogs.more_info_control.restored.not_provided" <state-card-content
)} in-dialog
</p> .stateObj=${stateObj}
<p> .hass=${this.hass}
${this.hass.localize( ></state-card-content>
"ui.dialogs.more_info_control.restored.remove_intro" `}
)} ${DOMAINS_WITH_MORE_INFO.includes(domain) ||
</p> !this._computeShowHistoryComponent(entityId)
<mwc-button class="warning" @click=${this._removeEntity}> ? ""
${this.hass.localize( : html`<ha-more-info-history
"ui.dialogs.more_info_control.restored.remove_action" .hass=${this.hass}
)} .entityId=${this._entityId}
</mwc-button>` ></ha-more-info-history>
: ""} <ha-more-info-logbook
.hass=${this.hass}
.entityId=${this._entityId}
></ha-more-info-logbook>`}
${this._moreInfoType
? dynamicElement(this._moreInfoType, {
hass: this.hass,
stateObj,
})
: ""}
${stateObj.attributes.restored
? html`
<p>
${this.hass.localize(
"ui.dialogs.more_info_control.restored.not_provided"
)}
</p>
<p>
${this.hass.localize(
"ui.dialogs.more_info_control.restored.remove_intro"
)}
</p>
<mwc-button
class="warning"
@click=${this._removeEntity}
>
${this.hass.localize(
"ui.dialogs.more_info_control.restored.remove_action"
)}
</mwc-button>
`
: ""}
`
: html`
<ha-more-info-history
.hass=${this.hass}
.entityId=${this._entityId}
></ha-more-info-history>
<ha-more-info-logbook
.hass=${this.hass}
.entityId=${this._entityId}
></ha-more-info-logbook>
`
)}
</div> </div>
</ha-dialog> </ha-dialog>
`; `;
@@ -181,26 +272,10 @@ export class MoreInfoDialog extends LitElement {
this.large = !this.large; this.large = !this.large;
} }
private async _getStateHistory(): Promise<void> {
if (!this._entityId) {
return;
}
this._stateHistory = await getRecentWithCache(
this.hass!,
this._entityId,
{
refresh: 60,
cacheKey: `more_info.${this._entityId}`,
hoursToShow: 24,
},
this.hass!.localize,
this.hass!.language
);
}
private _computeShowHistoryComponent(entityId) { private _computeShowHistoryComponent(entityId) {
return ( return (
isComponentLoaded(this.hass, "history") && (isComponentLoaded(this.hass, "history") ||
isComponentLoaded(this.hass, "logbook")) &&
!DOMAINS_MORE_INFO_NO_HISTORY.includes(computeDomain(entityId)) !DOMAINS_MORE_INFO_NO_HISTORY.includes(computeDomain(entityId))
); );
} }
@@ -243,6 +318,15 @@ export class MoreInfoDialog extends LitElement {
this.closeDialog(); this.closeDialog();
} }
private _handleTabChanged(ev: CustomEvent): void {
const newTab = ev.detail.index;
if (newTab === this._currTabIndex) {
return;
}
this._currTabIndex = ev.detail.index;
}
static get styles() { static get styles() {
return [ return [
haStyleDialog, haStyleDialog,
@@ -256,8 +340,7 @@ export class MoreInfoDialog extends LitElement {
--mdc-theme-on-primary: var(--primary-text-color); --mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--mdc-theme-surface); --mdc-theme-primary: var(--mdc-theme-surface);
flex-shrink: 0; flex-shrink: 0;
border-bottom: 1px solid display: block;
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
} }
@media all and (max-width: 450px), all and (max-height: 500px) { @media all and (max-width: 450px), all and (max-height: 500px) {
@@ -268,6 +351,11 @@ export class MoreInfoDialog extends LitElement {
} }
} }
.heading {
border-bottom: 1px solid
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
}
@media all and (min-width: 451px) and (min-height: 501px) { @media all and (min-width: 451px) and (min-height: 501px) {
ha-dialog { ha-dialog {
--mdc-dialog-max-width: 90vw; --mdc-dialog-max-width: 90vw;
@@ -306,8 +394,7 @@ export class MoreInfoDialog extends LitElement {
--dialog-content-padding: 0; --dialog-content-padding: 0;
} }
state-card-content, state-card-content {
state-history-charts {
display: block; display: block;
margin-bottom: 16px; margin-bottom: 16px;
} }
@@ -315,3 +402,9 @@ export class MoreInfoDialog extends LitElement {
]; ];
} }
} }
declare global {
interface HTMLElementTagNameMap {
"ha-more-info-dialog": MoreInfoDialog;
}
}

View File

@@ -0,0 +1,109 @@
import {
css,
customElement,
html,
internalProperty,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { throttle } from "../../common/util/throttle";
import "../../components/state-history-charts";
import { getRecentWithCache } from "../../data/cached-history";
import { HistoryResult } from "../../data/history";
import { haStyle } from "../../resources/styles";
import { HomeAssistant } from "../../types";
@customElement("ha-more-info-history")
export class MoreInfoHistory extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public entityId!: string;
@internalProperty() private _stateHistory?: HistoryResult;
private _throttleGetStateHistory = throttle(() => {
this._getStateHistory();
}, 10000);
protected render(): TemplateResult {
if (!this.entityId) {
return html``;
}
return html`${isComponentLoaded(this.hass, "history")
? html`<state-history-charts
up-to-now
.hass=${this.hass}
.historyData=${this._stateHistory}
.isLoadingData=${!this._stateHistory}
></state-history-charts>`
: ""} `;
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (changedProps.has("entityId")) {
this._stateHistory = undefined;
if (!this.entityId) {
return;
}
this._throttleGetStateHistory();
return;
}
if (!this.entityId || !changedProps.has("hass")) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (
oldHass &&
this.hass.states[this.entityId] !== oldHass?.states[this.entityId]
) {
// wait for commit of data (we only account for the default setting of 1 sec)
setTimeout(this._throttleGetStateHistory, 1000);
}
}
private async _getStateHistory(): Promise<void> {
if (!isComponentLoaded(this.hass, "history")) {
return;
}
this._stateHistory = await getRecentWithCache(
this.hass!,
this.entityId,
{
refresh: 60,
cacheKey: `more_info.${this.entityId}`,
hoursToShow: 24,
},
this.hass!.localize,
this.hass!.language
);
}
static get styles() {
return [
haStyle,
css`
state-history-charts {
display: block;
margin-bottom: 16px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-more-info-history": MoreInfoHistory;
}
}

View File

@@ -0,0 +1,171 @@
import {
css,
customElement,
html,
internalProperty,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { throttle } from "../../common/util/throttle";
import "../../components/ha-circular-progress";
import "../../components/state-history-charts";
import { getLogbookData, LogbookEntry } from "../../data/logbook";
import "../../panels/logbook/ha-logbook";
import { haStyle, haStyleScrollbar } from "../../resources/styles";
import { HomeAssistant } from "../../types";
@customElement("ha-more-info-logbook")
export class MoreInfoLogbook extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public entityId!: string;
@internalProperty() private _logbookEntries?: LogbookEntry[];
@internalProperty() private _persons = {};
private _lastLogbookDate?: Date;
private _throttleGetLogbookEntries = throttle(() => {
this._getLogBookData();
}, 10000);
protected render(): TemplateResult {
if (!this.entityId) {
return html``;
}
const stateObj = this.hass.states[this.entityId];
if (!stateObj) {
return html``;
}
return html`
${isComponentLoaded(this.hass, "logbook")
? !this._logbookEntries
? html`
<ha-circular-progress
active
alt=${this.hass.localize("ui.common.loading")}
></ha-circular-progress>
`
: this._logbookEntries.length
? html`
<ha-logbook
class="ha-scrollbar"
narrow
no-icon
no-name
.hass=${this.hass}
.entries=${this._logbookEntries}
.userIdToName=${this._persons}
></ha-logbook>
`
: html`<div class="no-entries">
${this.hass.localize("ui.components.logbook.entries_not_found")}
</div>`
: ""}
`;
}
protected firstUpdated(): void {
this._fetchPersonNames();
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (changedProps.has("entityId")) {
this._lastLogbookDate = undefined;
this._logbookEntries = undefined;
if (!this.entityId) {
return;
}
this._throttleGetLogbookEntries();
return;
}
if (!this.entityId || !changedProps.has("hass")) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (
oldHass &&
this.hass.states[this.entityId] !== oldHass?.states[this.entityId]
) {
// wait for commit of data (we only account for the default setting of 1 sec)
setTimeout(this._throttleGetLogbookEntries, 1000);
}
}
private async _getLogBookData() {
if (!isComponentLoaded(this.hass, "logbook")) {
return;
}
const lastDate =
this._lastLogbookDate ||
new Date(new Date().getTime() - 24 * 60 * 60 * 1000);
const now = new Date();
const newEntries = await getLogbookData(
this.hass,
lastDate.toISOString(),
now.toISOString(),
this.entityId,
true
);
this._logbookEntries = this._logbookEntries
? [...newEntries, ...this._logbookEntries]
: newEntries;
this._lastLogbookDate = now;
}
private _fetchPersonNames() {
Object.values(this.hass.states).forEach((entity) => {
if (
entity.attributes.user_id &&
computeStateDomain(entity) === "person"
) {
this._persons[entity.attributes.user_id] =
entity.attributes.friendly_name;
}
});
}
static get styles() {
return [
haStyle,
haStyleScrollbar,
css`
.no-entries {
text-align: center;
padding: 16px;
color: var(--secondary-text-color);
}
ha-logbook {
max-height: 250px;
overflow: auto;
display: block;
margin-top: 16px;
}
ha-circular-progress {
display: flex;
justify-content: center;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-more-info-logbook": MoreInfoLogbook;
}
}

View File

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

View File

@@ -43,12 +43,9 @@ export class HuiPersistentNotificationItem extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.datetime="${this.notification.created_at}" .datetime="${this.notification.created_at}"
></ha-relative-time> ></ha-relative-time>
<paper-tooltip <paper-tooltip animation-delay="0">
>${this._computeTooltip( ${this._computeTooltip(this.hass, this.notification)}
this.hass, </paper-tooltip>
this.notification
)}</paper-tooltip
>
</span> </span>
</div> </div>

View File

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

View File

@@ -48,7 +48,7 @@
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
html { html {
background-color: var(--primary-background-color, #111111); background-color: #111111;
} }
#ha-init-skeleton::before { #ha-init-skeleton::before {
background-color: #1c1c1c; background-color: #1c1c1c;
@@ -100,9 +100,5 @@
{% endfor -%} {% endfor -%}
} }
</script> </script>
{% for extra_url in extra_urls -%}
<link rel="import" href="{{ extra_url }}" async />
{% endfor -%}
</body> </body>
</html> </html>

View File

@@ -5,6 +5,20 @@
<link rel="preload" href="<%= latestPageJS %>" as="script" crossorigin="use-credentials" /> <link rel="preload" href="<%= latestPageJS %>" as="script" crossorigin="use-credentials" />
<%= renderTemplate('_header') %> <%= renderTemplate('_header') %>
<style> <style>
html {
color: var(--primary-text-color, #212121);
}
@media (prefers-color-scheme: dark) {
html {
background-color: #111111;
color: #e1e1e1;
}
ha-onboarding {
--primary-text-color: #e1e1e1;
--secondary-text-color: #9b9b9b;
--disabled-text-color: #6f6f6f;
}
}
.content { .content {
padding: 20px 16px; padding: 20px 16px;
max-width: 400px; max-width: 400px;
@@ -23,14 +37,6 @@
.header img { .header img {
margin-right: 16px; margin-right: 16px;
} }
@media (prefers-color-scheme: dark) {
body {
background-color: #111111;
color: #e1e1e1;
--primary-text-color: #e1e1e1;
--secondary-text-color: #9b9b9b;
}
}
</style> </style>
</head> </head>
<body> <body>

View File

@@ -63,6 +63,7 @@ class HassErrorScreen extends LitElement {
pointer-events: auto; pointer-events: auto;
} }
.content { .content {
color: var(--primary-text-color);
height: calc(100% - 64px); height: calc(100% - 64px);
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -11,6 +11,7 @@ import {
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { navigate } from "../common/navigate"; import { navigate } from "../common/navigate";
import { computeRTLDirection } from "../common/util/compute_rtl";
import "../components/data-table/ha-data-table"; import "../components/data-table/ha-data-table";
import type { import type {
DataTableColumnContainer, DataTableColumnContainer,
@@ -20,7 +21,6 @@ import type {
import type { HomeAssistant, Route } from "../types"; import type { HomeAssistant, Route } from "../types";
import "./hass-tabs-subpage"; import "./hass-tabs-subpage";
import type { PageNavigation } from "./hass-tabs-subpage"; import type { PageNavigation } from "./hass-tabs-subpage";
import { computeRTLDirection } from "../common/util/compute_rtl";
@customElement("hass-tabs-subpage-data-table") @customElement("hass-tabs-subpage-data-table")
export class HaTabsSubpageDataTable extends LitElement { export class HaTabsSubpageDataTable extends LitElement {
@@ -136,7 +136,7 @@ export class HaTabsSubpageDataTable extends LitElement {
? html`<div class="active-filters"> ? html`<div class="active-filters">
<div> <div>
<ha-icon icon="hass:filter-variant"></ha-icon> <ha-icon icon="hass:filter-variant"></ha-icon>
<paper-tooltip position="left"> <paper-tooltip animation-delay="0" position="left">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.filtering.filtering_by" "ui.panel.config.filtering.filtering_by"
)} )}

View File

@@ -3,26 +3,26 @@ import {
css, css,
CSSResult, CSSResult,
customElement, customElement,
eventOptions,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
eventOptions,
} from "lit-element"; } from "lit-element";
import { classMap } from "lit-html/directives/class-map"; import { classMap } from "lit-html/directives/class-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../common/config/is_component_loaded"; import { isComponentLoaded } from "../common/config/is_component_loaded";
import { navigate } from "../common/navigate";
import "../components/ha-menu-button";
import "../components/ha-icon-button-arrow-prev";
import { HomeAssistant, Route } from "../types";
import "../components/ha-svg-icon";
import "../components/ha-icon";
import "../components/ha-tab";
import { restoreScroll } from "../common/decorators/restore-scroll"; import { restoreScroll } from "../common/decorators/restore-scroll";
import { navigate } from "../common/navigate";
import { computeRTL } from "../common/util/compute_rtl"; import { computeRTL } from "../common/util/compute_rtl";
import "../components/ha-icon";
import "../components/ha-icon-button-arrow-prev";
import "../components/ha-menu-button";
import "../components/ha-svg-icon";
import "../components/ha-tab";
import { HomeAssistant, Route } from "../types";
export interface PageNavigation { export interface PageNavigation {
path: string; path: string;
@@ -132,7 +132,7 @@ class HassTabsSubpage extends LitElement {
this.hass.language, this.hass.language,
this.narrow this.narrow
); );
const showTabs = tabs.length > 1 || !this.narrow;
return html` return html`
<div class="toolbar"> <div class="toolbar">
${this.mainPage ${this.mainPage
@@ -152,7 +152,7 @@ class HassTabsSubpage extends LitElement {
${this.narrow ${this.narrow
? html` <div class="main-title"><slot name="header"></slot></div> ` ? html` <div class="main-title"><slot name="header"></slot></div> `
: ""} : ""}
${tabs.length > 1 || !this.narrow ${showTabs
? html` ? html`
<div id="tabbar" class=${classMap({ "bottom-bar": this.narrow })}> <div id="tabbar" class=${classMap({ "bottom-bar": this.narrow })}>
${tabs} ${tabs}
@@ -163,10 +163,15 @@ class HassTabsSubpage extends LitElement {
<slot name="toolbar-icon"></slot> <slot name="toolbar-icon"></slot>
</div> </div>
</div> </div>
<div class="content" @scroll=${this._saveScrollPos}> <div
class="content ${classMap({ tabs: showTabs })}"
@scroll=${this._saveScrollPos}
>
<slot></slot> <slot></slot>
</div> </div>
<div id="fab"><slot name="fab"></slot></div> <div id="fab" class="${classMap({ tabs: showTabs })}">
<slot name="fab"></slot>
</div>
`; `;
} }
@@ -274,12 +279,13 @@ class HassTabsSubpage extends LitElement {
margin-left: env(safe-area-inset-left); margin-left: env(safe-area-inset-left);
margin-right: env(safe-area-inset-right); margin-right: env(safe-area-inset-right);
height: calc(100% - 65px); height: calc(100% - 65px);
height: calc(100% - 65px - env(safe-area-inset-bottom));
overflow-y: auto; overflow-y: auto;
overflow: auto; overflow: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
} }
:host([narrow]) .content { :host([narrow]) .content.tabs {
height: calc(100% - 128px); height: calc(100% - 128px);
height: calc(100% - 128px - env(safe-area-inset-bottom)); height: calc(100% - 128px - env(safe-area-inset-bottom));
} }
@@ -290,7 +296,7 @@ class HassTabsSubpage extends LitElement {
bottom: calc(16px + env(safe-area-inset-bottom)); bottom: calc(16px + env(safe-area-inset-bottom));
z-index: 1; z-index: 1;
} }
:host([narrow]) #fab { :host([narrow]) #fab.tabs {
bottom: calc(84px + env(safe-area-inset-bottom)); bottom: calc(84px + env(safe-area-inset-bottom));
} }
#fab[is-wide] { #fab[is-wide] {

View File

@@ -24,6 +24,7 @@ const NON_SWIPABLE_PANELS = ["map"];
declare global { declare global {
// for fire event // for fire event
interface HASSDomEvents { interface HASSDomEvents {
"hass-open-menu": undefined;
"hass-toggle-menu": undefined; "hass-toggle-menu": undefined;
"hass-show-notifications": undefined; "hass-show-notifications": undefined;
} }
@@ -92,6 +93,17 @@ class HomeAssistantMain extends LitElement {
protected firstUpdated() { protected firstUpdated() {
import(/* webpackChunkName: "ha-sidebar" */ "../components/ha-sidebar"); import(/* webpackChunkName: "ha-sidebar" */ "../components/ha-sidebar");
this.addEventListener("hass-open-menu", () => {
if (this._sidebarNarrow) {
this.drawer.open();
} else {
fireEvent(this, "hass-dock-sidebar", {
dock: "docked",
});
setTimeout(() => this.appLayout.resetLayout());
}
});
this.addEventListener("hass-toggle-menu", () => { this.addEventListener("hass-toggle-menu", () => {
if (this._sidebarNarrow) { if (this._sidebarNarrow) {
if (this.drawer.opened) { if (this.drawer.opened) {

View File

@@ -1,6 +1,13 @@
import { PolymerElement } from "@polymer/polymer"; import { PolymerElement } from "@polymer/polymer";
import {
STATE_NOT_RUNNING,
STATE_RUNNING,
STATE_STARTING,
} from "home-assistant-js-websocket";
import { customElement, property, PropertyValues } from "lit-element"; import { customElement, property, PropertyValues } from "lit-element";
import { deepActiveElement } from "../common/dom/deep-active-element";
import { deepEqual } from "../common/util/deep-equal"; import { deepEqual } from "../common/util/deep-equal";
import { CustomPanelInfo } from "../data/panel_custom";
import { HomeAssistant, Panels } from "../types"; import { HomeAssistant, Panels } from "../types";
import { removeInitSkeleton } from "../util/init-skeleton"; import { removeInitSkeleton } from "../util/init-skeleton";
import { import {
@@ -8,13 +15,6 @@ import {
RouteOptions, RouteOptions,
RouterOptions, RouterOptions,
} from "./hass-router-page"; } from "./hass-router-page";
import {
STATE_STARTING,
STATE_NOT_RUNNING,
STATE_RUNNING,
} from "home-assistant-js-websocket";
import { CustomPanelInfo } from "../data/panel_custom";
import { deepActiveElement } from "../common/dom/deep-active-element";
const CACHE_URL_PATHS = ["lovelace", "developer-tools"]; const CACHE_URL_PATHS = ["lovelace", "developer-tools"];
const COMPONENTS = { const COMPONENTS = {
@@ -64,6 +64,10 @@ const COMPONENTS = {
import( import(
/* webpackChunkName: "panel-shopping-list" */ "../panels/shopping-list/ha-panel-shopping-list" /* webpackChunkName: "panel-shopping-list" */ "../panels/shopping-list/ha-panel-shopping-list"
), ),
"media-browser": () =>
import(
/* webpackChunkName: "panel-media-browser" */ "../panels/media-browser/ha-panel-media-browser"
),
}; };
const getRoutes = (panels: Panels): RouterOptions => { const getRoutes = (panels: Panels): RouterOptions => {

View File

@@ -6,9 +6,9 @@ import {
CSSResult, CSSResult,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";

View File

@@ -4,9 +4,9 @@ import {
CSSResult, CSSResult,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { ifDefined } from "lit-html/directives/if-defined"; import { ifDefined } from "lit-html/directives/if-defined";
@@ -175,8 +175,8 @@ class HaConfigAreaPage extends LitElement {
</a> </a>
${!state.attributes.id ${!state.attributes.id
? html` ? html`
<paper-tooltip <paper-tooltip animation-delay="0">
>${this.hass.localize( ${this.hass.localize(
"ui.panel.config.devices.cant_edit" "ui.panel.config.devices.cant_edit"
)} )}
</paper-tooltip> </paper-tooltip>
@@ -228,8 +228,8 @@ class HaConfigAreaPage extends LitElement {
</a> </a>
${!state.attributes.id ${!state.attributes.id
? html` ? html`
<paper-tooltip <paper-tooltip animation-delay="0">
>${this.hass.localize( ${this.hass.localize(
"ui.panel.config.devices.cant_edit" "ui.panel.config.devices.cant_edit"
)} )}
</paper-tooltip> </paper-tooltip>

View File

@@ -1,9 +1,8 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-icon-button"; import "@material/mwc-icon-button";
import "../../../../components/ha-button-menu"; import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "../../../../components/ha-svg-icon"; import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical, mdiArrowUp, mdiArrowDown } from "@mdi/js"; import { mdiArrowDown, mdiArrowUp, mdiDotsVertical } from "@mdi/js";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox"; import "@polymer/paper-listbox/paper-listbox";
import type { PaperListboxElement } from "@polymer/paper-listbox/paper-listbox"; import type { PaperListboxElement } from "@polymer/paper-listbox/paper-listbox";
@@ -12,29 +11,31 @@ import {
CSSResult, CSSResult,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
internalProperty,
PropertyValues, PropertyValues,
} from "lit-element"; } from "lit-element";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive"; import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import "../../../../components/ha-svg-icon";
import type { Action } from "../../../../data/script"; import type { Action } from "../../../../data/script";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { handleStructError } from "../../../lovelace/common/structs/handle-errors";
import "./types/ha-automation-action-choose";
import "./types/ha-automation-action-condition"; import "./types/ha-automation-action-condition";
import "./types/ha-automation-action-delay"; import "./types/ha-automation-action-delay";
import "./types/ha-automation-action-device_id"; import "./types/ha-automation-action-device_id";
import "./types/ha-automation-action-event"; import "./types/ha-automation-action-event";
import "./types/ha-automation-action-repeat";
import "./types/ha-automation-action-scene"; import "./types/ha-automation-action-scene";
import "./types/ha-automation-action-service"; import "./types/ha-automation-action-service";
import "./types/ha-automation-action-wait_for_trigger";
import "./types/ha-automation-action-wait_template"; import "./types/ha-automation-action-wait_template";
import "./types/ha-automation-action-repeat";
import "./types/ha-automation-action-choose";
import { handleStructError } from "../../../lovelace/common/structs/handle-errors";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import { haStyle } from "../../../../resources/styles";
const OPTIONS = [ const OPTIONS = [
"condition", "condition",
@@ -44,6 +45,7 @@ const OPTIONS = [
"scene", "scene",
"service", "service",
"wait_template", "wait_template",
"wait_for_trigger",
"repeat", "repeat",
"choose", "choose",
]; ];
@@ -166,12 +168,12 @@ export default class HaAutomationActionRow extends LitElement {
"ui.panel.config.automation.editor.edit_yaml" "ui.panel.config.automation.editor.edit_yaml"
)} )}
</mwc-list-item> </mwc-list-item>
<mwc-list-item disabled> <mwc-list-item>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate" "ui.panel.config.automation.editor.actions.duplicate"
)} )}
</mwc-list-item> </mwc-list-item>
<mwc-list-item> <mwc-list-item class="warning">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete" "ui.panel.config.automation.editor.actions.delete"
)} )}
@@ -261,6 +263,7 @@ export default class HaAutomationActionRow extends LitElement {
this._switchYamlMode(); this._switchYamlMode();
break; break;
case 1: case 1:
fireEvent(this, "duplicate");
break; break;
case 2: case 2:
this._onDelete(); this._onDelete();
@@ -333,7 +336,6 @@ export default class HaAutomationActionRow extends LitElement {
--mdc-theme-text-primary-on-background: var(--disabled-text-color); --mdc-theme-text-primary-on-background: var(--disabled-text-color);
} }
.warning { .warning {
color: var(--warning-color);
margin-bottom: 8px; margin-bottom: 8px;
} }
.warning ul { .warning ul {

View File

@@ -28,6 +28,7 @@ export default class HaAutomationAction extends LitElement {
.index=${idx} .index=${idx}
.totalActions=${this.actions.length} .totalActions=${this.actions.length}
.action=${action} .action=${action}
@duplicate=${this._duplicateAction}
@move-action=${this._move} @move-action=${this._move}
@value-changed=${this._actionChanged} @value-changed=${this._actionChanged}
.hass=${this.hass} .hass=${this.hass}
@@ -78,6 +79,14 @@ export default class HaAutomationAction extends LitElement {
fireEvent(this, "value-changed", { value: actions }); fireEvent(this, "value-changed", { value: actions });
} }
private _duplicateAction(ev: CustomEvent) {
ev.stopPropagation();
const index = (ev.target as any).index;
fireEvent(this, "value-changed", {
value: this.actions.concat(this.actions[index]),
});
}
static get styles(): CSSResult { static get styles(): CSSResult {
return css` return css`
ha-automation-action-row, ha-automation-action-row,

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