Compare commits

...

123 Commits

Author SHA1 Message Date
Paulus Schoutsen
94215dc50b dev tools dialog 2021-12-05 17:40:15 -08:00
Philip Allgaier
8f5751d5bb Use correct label in area card editor (#10799) 2021-12-05 12:09:06 -06:00
Philip Allgaier
4095450476 Add switch to input row domains to fix mobile focus issue (#10792) 2021-12-04 11:43:14 +01:00
Bram Kragten
e61f587c51 Bumped version to 20211203.0 2021-12-03 18:07:07 +01:00
Bram Kragten
d43d19190e Fix entity marker (#10787) 2021-12-03 17:04:50 +00:00
Bram Kragten
a283acaabf safari doesnt support overflow-wrap: anywhere 2021-12-03 18:04:03 +01:00
Philip Allgaier
ea18fc0078 Ensure we always have an active theme name (fixes dark theme issues) (#10780)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-12-03 17:02:54 +00:00
Bram Kragten
1df11e9bf1 Use groupBy (#10786) 2021-12-03 08:42:23 -08:00
Bram Kragten
c71b2e6b9d Add provisioned device overview to zwave js (#10785) 2021-12-03 08:34:26 -08:00
Bram Kragten
db4aa05bf4 Differentiate between assigned and targeting scene/automations/script (#10781) 2021-12-03 08:21:26 -08:00
Bram Kragten
a54a2a54f8 Add support for local only users (#10784)
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
2021-12-03 16:34:34 +01:00
Philip Allgaier
0bcb4d0e09 Restore flex alignment for select and input-select rows (#10783) 2021-12-03 16:19:00 +01:00
Bram Kragten
95dbc811d3 Allow overriding device class (#10777) 2021-12-03 16:07:49 +01:00
Philip Allgaier
e28a11964e Use correct styling for cloud certificate dialog (#10782) 2021-12-03 15:08:49 +01:00
Bram Kragten
46a9e36516 Guard for non numeric states (#10775)
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
2021-12-03 12:53:50 +01:00
Paulus Schoutsen
e99f20c4f3 Tweak ZJS dashboard (#10772) 2021-12-03 10:51:50 +01:00
Joakim Sørensen
2100603cdc Remove handling of the supervisor panel from the sidebar (#10773) 2021-12-03 10:49:42 +01:00
Joakim Sørensen
da4942aca3 Add default icons for button entities (#10774) 2021-12-03 09:11:29 +01:00
Paulus Schoutsen
7c78fb314e Show add devices fab on devices page for ZJS (#10771) 2021-12-03 08:42:39 +01:00
Paulus Schoutsen
5bc2468cbc Bumped version to 20211202.0 2021-12-02 14:31:09 -08:00
Bram Kragten
a580904c52 Use chips for button rows (#10770) 2021-12-02 23:29:52 +01:00
Bram Kragten
48d12ceafe Group entities in area card by domain (#10767)
* Group entities in area card by domain

* Update hui-area-card.ts

* Update

* Add background color when no image

* Add camera support

* exclude unavailable states

* Update hui-area-card.ts
2021-12-02 23:15:18 +01:00
Carlos Garcia Saura
60ce805b3b Update hui-graph-header-footer.ts (#10476) 2021-12-02 13:32:38 -08:00
Paulus Schoutsen
251416b51d Add missing translation (#10769) 2021-12-02 13:01:19 -08:00
Bram Kragten
c41c6eedd8 Remove thingtalk cleanup create new automation dialog (#10748)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-12-02 11:26:41 -08:00
Joakim Sørensen
6877fd9e00 Hide updates for dev as well (#10761) 2021-12-02 17:32:18 +01:00
Joakim Sørensen
4cc104a99f Use add-ons for mobile header (#10760) 2021-12-02 17:31:41 +01:00
Joakim Sørensen
6494177821 Fix SU sidebar issues (#10757) 2021-12-02 17:31:09 +01:00
Joakim Sørensen
cea1a62867 handle ha-radio and ha-checkbox in ha-formfield (#10759) 2021-12-02 17:30:10 +01:00
rianadon
a6b5262d02 Use unit system definitions for weather units (#10657) 2021-12-02 17:27:23 +01:00
Joakim Sørensen
2a5fc5181e Fix create backup checkbox (#10756) 2021-12-02 11:54:05 +01:00
Joakim Sørensen
2fe8f5ff27 Use puzzle for addons and blur entries on click (#10755) 2021-12-02 11:05:14 +01:00
Philip Allgaier
0c75d5afc9 Make graph colors themable (#10698) 2021-12-02 10:49:46 +01:00
Philip Allgaier
cf062bf0f4 Fix pointer/more-info inconsistencies for entity rows (#10025)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-12-02 10:48:30 +01:00
Paulus Schoutsen
acf4d59fde Bumped version to 20211201.0 2021-12-01 14:47:17 -08:00
Paulus Schoutsen
05333ac2d9 Show disabled entity names on the device page (#10743)
* Show disabled entity names on the device page

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

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

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-12-01 14:46:40 -08:00
Bram Kragten
4b49da58b1 Add SmartStart/QR scan support for Z-Wave JS (#10726) 2021-12-01 14:12:52 -08:00
Joakim Sørensen
68373e6372 Focus Add-ons & Backups in config panel when clicking Supervisor in sidebar (#10745)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-12-01 08:46:38 -08:00
Matthias de Baat
01049e8eb8 Updated text (#10747)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-12-01 14:10:32 +00:00
Joakim Sørensen
87f7981144 Fix faded element in change log (#10737) 2021-12-01 13:55:01 +01:00
Paulus Schoutsen
ceac9834b9 Change the area of scenes in editor (#10731) 2021-12-01 13:54:28 +01:00
Joakim Sørensen
ac8f748656 Hide ha-icon-next if narrow (#10746) 2021-12-01 09:23:13 +01:00
Joakim Sørensen
1d97d8dca9 Handle 0 updates and show back on supervisor panels (#10744) 2021-11-30 23:30:38 -08:00
Bram Kragten
fd6785b593 Use backend for day month stats in energy dashboard (#10728) 2021-11-30 09:22:06 -08:00
Joakim Sørensen
d5fc751da6 Revert 10711 (#10736) 2021-11-30 18:02:02 +01:00
Joakim Sørensen
933fd72629 Fix typo (#10734) 2021-11-30 11:41:59 -05:00
Joakim Sørensen
0611133065 Move companion app config from sidebar to configuration dashboard (#10733)
* Move companion app config from sidebar to configuration dashboard

* Remove translation refrence
2021-11-30 08:03:10 -08:00
Allen Porter
02644b923f Improve hls stream view error handling (#10714)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-11-30 08:48:24 +01:00
Paulus Schoutsen
67f06112c6 Bumped version to 20211130.0 2021-11-29 16:57:58 -08:00
Paulus Schoutsen
49e39644f3 Tweak how scenes behave in generated lovelace (#10730) 2021-11-29 16:56:08 -08:00
Joakim Sørensen
990ad1bb67 Dashboard tweaks (#10729) 2021-11-29 23:56:59 +01:00
Philip Allgaier
dbbf246060 Installation type property during onboarding was misspelled (#10721) 2021-11-29 14:41:21 -08:00
amitfin
d2c20837a5 Fixed invalid hour handling in AMPM mode (#10717)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-11-29 18:49:33 +00:00
Philip Allgaier
e91d1777d0 Ensure conditional rows getting state_color value (#10708)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-11-29 12:30:08 +01:00
Joakim Sørensen
a5be143c3b Fix chip text color variable overrides (#10722) 2021-11-29 11:31:49 +01:00
Philip Allgaier
0ef07e4835 Ensure markdown card input is a string (#10705) 2021-11-29 10:50:08 +01:00
Philip Allgaier
9361e4cf9c Ensure required translations are loaded in safe-mode (#10709) 2021-11-29 10:34:25 +01:00
Luca Cavalli
e7fd75703f Fixed ellipsis usage on graph legend entries. (#10707) 2021-11-29 10:30:27 +01:00
Philip Allgaier
2c0b2f4bc5 Convert cover UI to Lit + ensure proper tilt rendering (#10671) 2021-11-29 10:30:14 +01:00
Philip Allgaier
faec09f0d1 Filter out disabled entities in the statistics dev tools (#10677) 2021-11-29 10:19:33 +01:00
Nathan Orick
b79c06ad71 Default to yaml editing when there are multiple states in condition (#10481) 2021-11-29 10:14:09 +01:00
Philip Allgaier
5614e0d29c Make "Energy distribution today" translatable (#10696) 2021-11-29 10:09:54 +01:00
Philip Allgaier
0b7fc177f9 Prevent errors in more-info-climate if no modes are provided despite support flags (#10694) 2021-11-29 10:03:30 +01:00
Philip Allgaier
367322415e Use ha-icon-button in ha-icon-overflow-menu (#10692) 2021-11-29 09:58:34 +01:00
Joakim Sørensen
117b50f3ea Add ha-faded (#10651) 2021-11-28 22:27:53 -08:00
Philip Allgaier
366aa8aed1 Fix typo on config page + adjust icon color (#10713) 2021-11-28 17:52:39 +01:00
Joakim Sørensen
43011179eb Finish up config changes (#10710)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-11-26 17:24:30 +01:00
Joakim Sørensen
6177d2b416 Use app-header-text-color (#10711) 2021-11-26 17:11:06 +01:00
Joakim Sørensen
f70485bc49 Don't make button disabled on error (#10699) 2021-11-25 16:56:57 +01:00
Erik Montnemery
921763b5f1 Improve device information when via device is unknown (#10685) 2021-11-24 09:09:21 +01:00
Joakim Sørensen
5fd4315789 Fix addon slug (#10693) 2021-11-23 08:53:17 -08:00
Joakim Sørensen
ed291b57d0 Render update card on add-on page (#10681) 2021-11-23 08:18:40 -08:00
Joakim Sørensen
f833701e7c Update background colors of navigation icons (#10691) 2021-11-23 14:36:11 +01:00
Paulus Schoutsen
8533b90957 Bumped version to 20211123.0 2021-11-22 17:28:13 -08:00
Laszlo Magyar
c95a54c6f3 Fixing typo in #10626 (#10686) 2021-11-22 18:59:35 +01:00
Joakim Sørensen
a991640f52 Remove first part of the update description (#10669) 2021-11-22 09:09:23 -08:00
Joakim Sørensen
3d99b92c07 Limit setting up supervisor subscriptions to the supervisor panel (#10680) 2021-11-22 08:59:28 -08:00
Philip Allgaier
d28ad17135 Use component to ensure relative-time in Glance card gets updated (#10666) 2021-11-22 11:12:04 +01:00
Philip Allgaier
3c67fc96b1 Make "Show more" show everything starting from yesterday (#10533) 2021-11-22 10:56:40 +01:00
Joakim Sørensen
4719636176 Fix dark main-content and split gallery demo (#10675) 2021-11-21 21:01:51 -08:00
Paulus Schoutsen
45efee28b8 Add scenes and scripts as buttons in footer of area cards (#10673)
* Add scenes and scripts as chips in footer of area cards

* Remove unused chips config type

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

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

* Fix typing

Co-authored-by: Zack Barett <arnett.zackary@gmail.com>
2021-11-21 20:59:56 -08:00
Joakim Sørensen
3bcf225380 Fix color overlay in ha-alert content (#10674) 2021-11-21 20:16:19 +01:00
Joakim Sørensen
2e81f843ce Use white for icons with backgound (#10672) 2021-11-21 18:07:55 +00:00
Joakim Sørensen
a430142296 Add iconColor to ha-config-navigation entries (#10658) 2021-11-21 09:52:58 -08:00
Joakim Sørensen
6335b13c5e Remove core note on update page (#10661) 2021-11-21 09:16:06 -08:00
Joakim Sørensen
6c4e987a24 Make ha-chip-set slot-able (#10647) 2021-11-21 09:15:38 -08:00
Joakim Sørensen
1a5c43d72a Fix color over slotted image in ha-alert (#10652) 2021-11-21 09:13:48 -08:00
epenet
91dbfca899 Add frequency device class for sensor (#10621) 2021-11-21 05:05:32 +01:00
Bram Kragten
96f103644a Send error message to sender (#10660) 2021-11-19 13:22:49 -08:00
Paulus Schoutsen
5304e5a670 Always render groups/areas in a single column (#10655) 2021-11-19 13:16:43 -08:00
Lasse Rosenow
390e5b3881 Simplify launch screen svg (#10643) 2021-11-18 16:20:45 -08:00
Joakim Sørensen
9f5756c9fa Use ha-formfield around backup checkbox (#10653) 2021-11-18 16:09:39 -08:00
Joakim Sørensen
0ca35d7012 Remove ha-alert actionText (#10646) 2021-11-18 16:09:13 -08:00
Joakim Sørensen
0d19f4792f Fix active tab (#10654) 2021-11-18 19:21:19 +00:00
Joakim Sørensen
91b009af79 Fix back button color (#10650) 2021-11-18 18:57:15 +01:00
Paulus Schoutsen
1ebd2fb9f1 Bumped version to 20211117.0 2021-11-17 10:54:08 -08:00
Zack Barett
4684979ae7 Area Card (#10141)
Co-authored-by: Philip Allgaier <mail@spacegaier.de>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-11-17 19:43:41 +01:00
Joakim Sørensen
a567312bdb Show updates on dashboard for dev (#10637) 2021-11-17 18:39:16 +00:00
Joakim Sørensen
1e851e0e8c Remove customize UI (#10632) 2021-11-17 10:34:20 -08:00
Bram Kragten
7d94615f47 Cast fixes (#10598) 2021-11-17 10:33:15 -08:00
Bram Kragten
582fab7ea1 Update Lovelace Cast app ID (#10592) 2021-11-17 10:32:15 -08:00
Philip Allgaier
822590ec8a Add correct button label to "no_state" statistics fix dialog (#10628) 2021-11-17 10:22:34 -08:00
Joakim Sørensen
e9f0967578 Move updates (#10626) 2021-11-17 10:21:27 -08:00
Joakim Sørensen
481da19c74 Fix datatable checkbox width (#10631) 2021-11-16 19:46:41 +01:00
Joakim Sørensen
b969db0c0f Use ha-form for onboarding-create-user (#10604) 2021-11-15 14:21:29 -08:00
Joakim Sørensen
a6b98fc3c3 Add markers-updated to ha-locations-editor (#10601) 2021-11-15 14:11:42 -08:00
Joakim Sørensen
87c2046ab5 Remove add-on store tab (#10624) 2021-11-15 09:15:20 -08:00
David F. Mulcahey
4b992fb0c4 Correct ZHA LQI sort in device children dialog (#10616) 2021-11-15 09:11:31 -08:00
Lasse Rosenow
3154011c65 Improve startup experience by removing AppBar skeleton (#10569) 2021-11-15 07:54:59 -08:00
Bram Kragten
4e68383cf7 Remove deprecated icons (#10622) 2021-11-15 11:54:59 +01:00
Michael Irigoyen
db6ef22ebb Update MDI to v6.5.95 (#10618) 2021-11-15 09:49:53 +01:00
Allen Porter
c238c7dbbc WebRTC fix for Safari (#10602) 2021-11-11 10:48:56 +01:00
Bram Kragten
d04823b4c5 Update image-cropper-dialog.ts 2021-11-10 22:55:05 +01:00
Bram Kragten
4cb45d6313 Add picture uploader to area (#10544) 2021-11-10 21:42:43 +01:00
Bram Kragten
6623e5f017 Fix thingktalk dialog (#10600) 2021-11-10 19:36:18 +00:00
Bram Kragten
6518aefb7f Prevent cast timeout after 10 mins, show current shown Lovelace view (#10586) 2021-11-09 21:53:40 +01:00
Bram Kragten
d5600b7c08 Bumped version to 20211109.0 2021-11-09 21:42:04 +01:00
Philip Allgaier
4789295d32 Add CSS var for ha-dialog border radius (#10424) 2021-11-09 17:24:39 +01:00
Philip Allgaier
70d54aa855 Ensure theme picker row uses correct theme name (#10589) 2021-11-09 17:22:48 +01:00
Bram Kragten
77549efc47 Bump codemirror (#10588) 2021-11-09 16:10:42 +01:00
Bram Kragten
00299bc74d Fix multi select ha-form (#10585) 2021-11-09 16:10:26 +01:00
Philip Allgaier
b74fc5578d Consistently show a close button for config dialogs (#10587) 2021-11-09 13:52:56 +00:00
Bram Kragten
9018d4cc18 Update translations 2021-11-08 19:58:29 +01:00
256 changed files with 7845 additions and 5294 deletions

View File

@@ -79,6 +79,11 @@ function copyFonts(staticDir) {
); );
} }
function copyQrScannerWorker(staticDir) {
const staticPath = genStaticPath(staticDir);
copyFileDir(npmPath("qr-scanner/qr-scanner-worker.min.js"), staticPath("js"));
}
function copyMapPanel(staticDir) { function copyMapPanel(staticDir) {
const staticPath = genStaticPath(staticDir); const staticPath = genStaticPath(staticDir);
copyFileDir( copyFileDir(
@@ -125,6 +130,9 @@ gulp.task("copy-static-app", async () => {
// Panel assets // Panel assets
copyMapPanel(staticDir); copyMapPanel(staticDir);
// Qr Scanner assets
copyQrScannerWorker(staticDir);
}); });
gulp.task("copy-static-demo", async () => { gulp.task("copy-static-demo", async () => {

File diff suppressed because one or more lines are too long

View File

@@ -11,6 +11,7 @@
--progress-color: #03a9f4; --progress-color: #03a9f4;
--splash-image: url('https://home-assistant.io/images/cast/splash.png'); --splash-image: url('https://home-assistant.io/images/cast/splash.png');
--splash-size: cover; --splash-size: cover;
--background-color: #41bdf5;
} }
</style> </style>
<script> <script>

View File

@@ -1,7 +1,4 @@
import { CastReceiverContext } from "chromecast-caf-receiver/cast.framework"; const castContext = cast.framework.CastReceiverContext.getInstance();
const castContext =
cast.framework.CastContext.getInstance() as unknown as CastReceiverContext;
const playerManager = castContext.getPlayerManager(); const playerManager = castContext.getPlayerManager();

View File

@@ -8,6 +8,9 @@ import { ReceivedMessage } from "./types";
const lovelaceController = new HcMain(); const lovelaceController = new HcMain();
document.body.append(lovelaceController); document.body.append(lovelaceController);
lovelaceController.addEventListener("cast-view-changed", (ev) => {
playDummyMedia(ev.detail.title);
});
const mediaPlayer = document.createElement("cast-media-player"); const mediaPlayer = document.createElement("cast-media-player");
mediaPlayer.style.display = "none"; mediaPlayer.style.display = "none";
@@ -28,21 +31,29 @@ const setTouchControlsVisibility = (visible: boolean) => {
} }
}; };
const playDummyMedia = () => { let timeOut: number | undefined;
const playerManager = castContext.getPlayerManager();
const playDummyMedia = (viewTitle?: string) => {
const loadRequestData = new cast.framework.messages.LoadRequestData(); const loadRequestData = new cast.framework.messages.LoadRequestData();
loadRequestData.autoplay = true; loadRequestData.autoplay = true;
loadRequestData.media = new cast.framework.messages.MediaInformation(); loadRequestData.media = new cast.framework.messages.MediaInformation();
loadRequestData.media.contentId = loadRequestData.media.contentId =
"https://www.home-assistant.io/images/blog/2018-09-thinking-big/social.png"; "https://cast.home-assistant.io/images/google-nest-hub.png";
loadRequestData.media.contentType = "image/jpeg"; loadRequestData.media.contentType = "image/jpeg";
loadRequestData.media.streamType = cast.framework.messages.StreamType.NONE; loadRequestData.media.streamType = cast.framework.messages.StreamType.NONE;
const metadata = new cast.framework.messages.GenericMediaMetadata(); const metadata = new cast.framework.messages.GenericMediaMetadata();
metadata.title = "Home Assistant Lovelace"; metadata.title = viewTitle;
loadRequestData.media.metadata = metadata; loadRequestData.media.metadata = metadata;
loadRequestData.requestId = 0; loadRequestData.requestId = 0;
playerManager.load(loadRequestData); playerManager.load(loadRequestData);
if (timeOut) {
clearTimeout(timeOut);
timeOut = undefined;
}
if (castContext.getDeviceCapabilities().touch_input_supported) {
timeOut = window.setTimeout(() => playDummyMedia(viewTitle), 540000); // repeat every 9 minutes to keep it active (gets deactivated after 10 minutes)
}
}; };
const showLovelaceController = () => { const showLovelaceController = () => {
@@ -50,7 +61,6 @@ const showLovelaceController = () => {
lovelaceController.style.display = "initial"; lovelaceController.style.display = "initial";
document.body.setAttribute("style", "overflow-y: auto !important"); document.body.setAttribute("style", "overflow-y: auto !important");
setTouchControlsVisibility(false); setTouchControlsVisibility(false);
playDummyMedia();
}; };
const showMediaPlayer = () => { const showMediaPlayer = () => {
@@ -69,6 +79,7 @@ const showMediaPlayer = () => {
--progress-color: #03a9f4; --progress-color: #03a9f4;
--splash-image: url('https://home-assistant.io/images/cast/splash.png'); --splash-image: url('https://home-assistant.io/images/cast/splash.png');
--splash-size: cover; --splash-size: cover;
--background-color: #41bdf5;
} }
`; `;
document.head.appendChild(style); document.head.appendChild(style);
@@ -81,22 +92,6 @@ options.customNamespaces = {
[CAST_NS]: cast.framework.system.MessageType.JSON, [CAST_NS]: cast.framework.system.MessageType.JSON,
}; };
// The docs say we need to set options.touchScreenOptimizeApp = true
// https://developers.google.com/cast/docs/caf_receiver/customize_ui#accessing_ui_controls
// This doesn't work.
// @ts-ignore
options.touchScreenOptimizedApp = true;
// The class reference say we can set a uiConfig in options to set it
// https://developers.google.com/cast/docs/reference/caf_receiver/cast.framework.CastReceiverOptions#uiConfig
// This doesn't work either.
// @ts-ignore
options.uiConfig = new cast.framework.ui.UiConfig();
// @ts-ignore
options.uiConfig.touchScreenOptimizedApp = true;
castContext.setInactivityTimeout(86400); // 1 day
castContext.addCustomMessageListener( castContext.addCustomMessageListener(
CAST_NS, CAST_NS,
// @ts-ignore // @ts-ignore
@@ -123,7 +118,7 @@ playerManager.setMessageInterceptor(
(loadRequestData) => { (loadRequestData) => {
if ( if (
loadRequestData.media.contentId === loadRequestData.media.contentId ===
"https://www.home-assistant.io/images/blog/2018-09-thinking-big/social.png" "https://cast.home-assistant.io/images/google-nest-hub.png"
) { ) {
return loadRequestData; return loadRequestData;
} }

View File

@@ -1,5 +1,6 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { LovelaceConfig } from "../../../../src/data/lovelace"; import { LovelaceConfig } from "../../../../src/data/lovelace";
import { Lovelace } from "../../../../src/panels/lovelace/types"; import { Lovelace } from "../../../../src/panels/lovelace/types";
import "../../../../src/panels/lovelace/views/hui-view"; import "../../../../src/panels/lovelace/views/hui-view";
@@ -14,7 +15,7 @@ class HcLovelace extends LitElement {
@property() public viewPath?: string | number; @property() public viewPath?: string | number;
public urlPath?: string | null; @property() public urlPath: string | null = null;
protected render(): TemplateResult { protected render(): TemplateResult {
const index = this._viewIndex; const index = this._viewIndex;
@@ -30,7 +31,7 @@ class HcLovelace extends LitElement {
config: this.lovelaceConfig, config: this.lovelaceConfig,
rawConfig: this.lovelaceConfig, rawConfig: this.lovelaceConfig,
editMode: false, editMode: false,
urlPath: this.urlPath!, urlPath: this.urlPath,
enableFullEditMode: () => undefined, enableFullEditMode: () => undefined,
mode: "storage", mode: "storage",
locale: this.hass.locale, locale: this.hass.locale,
@@ -54,6 +55,21 @@ class HcLovelace extends LitElement {
const index = this._viewIndex; const index = this._viewIndex;
if (index !== undefined) { if (index !== undefined) {
const dashboardTitle = this.lovelaceConfig.title || this.urlPath;
const viewTitle =
this.lovelaceConfig.views[index].title ||
this.lovelaceConfig.views[index].path;
fireEvent(this, "cast-view-changed", {
title:
dashboardTitle || viewTitle
? `${dashboardTitle || ""}${
dashboardTitle && viewTitle ? ": " : ""
}${viewTitle || ""}`
: undefined,
});
const configBackground = const configBackground =
this.lovelaceConfig.views[index].background || this.lovelaceConfig.views[index].background ||
this.lovelaceConfig.background; this.lovelaceConfig.background;
@@ -101,8 +117,15 @@ class HcLovelace extends LitElement {
} }
} }
export interface CastViewChanged {
title: string | undefined;
}
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"hc-lovelace": HcLovelace; "hc-lovelace": HcLovelace;
} }
interface HASSDomEvents {
"cast-view-changed": CastViewChanged;
}
} }

View File

@@ -13,7 +13,11 @@ import {
ShowDemoMessage, ShowDemoMessage,
ShowLovelaceViewMessage, ShowLovelaceViewMessage,
} from "../../../../src/cast/receiver_messages"; } from "../../../../src/cast/receiver_messages";
import { ReceiverStatusMessage } from "../../../../src/cast/sender_messages"; import {
ReceiverErrorCode,
ReceiverErrorMessage,
ReceiverStatusMessage,
} from "../../../../src/cast/sender_messages";
import { atLeastVersion } from "../../../../src/common/config/version"; import { atLeastVersion } from "../../../../src/common/config/version";
import { isNavigationClick } from "../../../../src/common/dom/is-navigation-click"; import { isNavigationClick } from "../../../../src/common/dom/is-navigation-click";
import { import {
@@ -40,9 +44,9 @@ export class HcMain extends HassElement {
@state() private _error?: string; @state() private _error?: string;
private _unsubLovelace?: UnsubscribeFunc; @state() private _urlPath?: string | null;
private _urlPath?: string | null; private _unsubLovelace?: UnsubscribeFunc;
public processIncomingMessage(msg: HassMessage) { public processIncomingMessage(msg: HassMessage) {
if (msg.type === "connect") { if (msg.type === "connect") {
@@ -68,8 +72,10 @@ export class HcMain extends HassElement {
!this._lovelaceConfig || !this._lovelaceConfig ||
this._lovelacePath === null || this._lovelacePath === null ||
// Guard against part of HA not being loaded yet. // Guard against part of HA not being loaded yet.
(this.hass && !this.hass ||
(!this.hass.states || !this.hass.config || !this.hass.services)) !this.hass.states ||
!this.hass.config ||
!this.hass.services
) { ) {
return html` return html`
<hc-launch-screen <hc-launch-screen
@@ -119,7 +125,7 @@ export class HcMain extends HassElement {
if (this.hass) { if (this.hass) {
status.hassUrl = this.hass.auth.data.hassUrl; status.hassUrl = this.hass.auth.data.hassUrl;
status.lovelacePath = this._lovelacePath!; status.lovelacePath = this._lovelacePath;
status.urlPath = this._urlPath; status.urlPath = this._urlPath;
} }
@@ -132,6 +138,26 @@ export class HcMain extends HassElement {
} }
} }
private _sendError(
error_code: number,
error_message: string,
senderId?: string
) {
const error: ReceiverErrorMessage = {
type: "receiver_error",
error_code,
error_message,
};
if (senderId) {
this.sendMessage(senderId, error);
} else {
for (const sender of castContext.getSenders()) {
this.sendMessage(sender.id, error);
}
}
}
private _dialogClosed = () => { private _dialogClosed = () => {
document.body.setAttribute("style", "overflow-y: auto !important"); document.body.setAttribute("style", "overflow-y: auto !important");
}; };
@@ -154,14 +180,18 @@ export class HcMain extends HassElement {
}), }),
}); });
} catch (err: any) { } catch (err: any) {
this._error = this._getErrorMessage(err); const errorMessage = this._getErrorMessage(err);
this._error = errorMessage;
this._sendError(err, errorMessage);
return; return;
} }
let connection; let connection;
try { try {
connection = await createConnection({ auth }); connection = await createConnection({ auth });
} catch (err: any) { } catch (err: any) {
this._error = this._getErrorMessage(err); const errorMessage = this._getErrorMessage(err);
this._error = errorMessage;
this._sendError(err, errorMessage);
return; return;
} }
if (this.hass) { if (this.hass) {
@@ -173,24 +203,29 @@ export class HcMain extends HassElement {
} }
private async _handleShowLovelaceMessage(msg: ShowLovelaceViewMessage) { private async _handleShowLovelaceMessage(msg: ShowLovelaceViewMessage) {
this._showDemo = false;
// We should not get this command before we are connected. // We should not get this command before we are connected.
// Means a client got out of sync. Let's send status to them. // Means a client got out of sync. Let's send status to them.
if (!this.hass) { if (!this.hass) {
this._sendStatus(msg.senderId!); this._sendStatus(msg.senderId!);
this._error = "Cannot show Lovelace because we're not connected."; this._error = "Cannot show Lovelace because we're not connected.";
this._sendError(ReceiverErrorCode.NOT_CONNECTED, this._error);
return; return;
} }
this._error = undefined;
if (msg.urlPath === "lovelace") { if (msg.urlPath === "lovelace") {
msg.urlPath = null; msg.urlPath = null;
} }
this._lovelacePath = msg.viewPath;
if (!this._unsubLovelace || this._urlPath !== msg.urlPath) { if (!this._unsubLovelace || this._urlPath !== msg.urlPath) {
this._urlPath = msg.urlPath; this._urlPath = msg.urlPath;
this._lovelaceConfig = undefined;
if (this._unsubLovelace) { if (this._unsubLovelace) {
this._unsubLovelace(); this._unsubLovelace();
} }
const llColl = atLeastVersion(this.hass.connection.haVersion, 0, 107) const llColl = atLeastVersion(this.hass.connection.haVersion, 0, 107)
? getLovelaceCollection(this.hass!.connection, msg.urlPath) ? getLovelaceCollection(this.hass.connection, msg.urlPath)
: getLegacyLovelaceCollection(this.hass!.connection); : getLegacyLovelaceCollection(this.hass.connection);
// We first do a single refresh because we need to check if there is LL // We first do a single refresh because we need to check if there is LL
// configuration. // configuration.
try { try {
@@ -199,8 +234,16 @@ export class HcMain extends HassElement {
this._handleNewLovelaceConfig(lovelaceConfig) this._handleNewLovelaceConfig(lovelaceConfig)
); );
} catch (err: any) { } catch (err: any) {
if (
atLeastVersion(this.hass.connection.haVersion, 0, 107) &&
err.code !== "config_not_found"
) {
// eslint-disable-next-line // eslint-disable-next-line
console.log("Error fetching Lovelace configuration", err, msg); console.log("Error fetching Lovelace configuration", err, msg);
this._error = `Error fetching Lovelace configuration: ${err.message}`;
this._sendError(ReceiverErrorCode.FETCH_CONFIG_FAILED, this._error);
return;
}
// Generate a Lovelace config. // Generate a Lovelace config.
this._unsubLovelace = () => undefined; this._unsubLovelace = () => undefined;
await this._generateLovelaceConfig(); await this._generateLovelaceConfig();
@@ -215,8 +258,6 @@ export class HcMain extends HassElement {
loadLovelaceResources(resources, this.hass!.auth.data.hassUrl); loadLovelaceResources(resources, this.hass!.auth.data.hassUrl);
} }
} }
this._showDemo = false;
this._lovelacePath = msg.viewPath;
this._sendStatus(); this._sendStatus();
} }
@@ -237,7 +278,7 @@ export class HcMain extends HassElement {
} }
private _handleNewLovelaceConfig(lovelaceConfig: LovelaceConfig) { private _handleNewLovelaceConfig(lovelaceConfig: LovelaceConfig) {
castContext.setApplicationState(lovelaceConfig.title!); castContext.setApplicationState(lovelaceConfig.title || "");
this._lovelaceConfig = lovelaceConfig; this._lovelaceConfig = lovelaceConfig;
} }

File diff suppressed because one or more lines are too long

View File

@@ -82,6 +82,9 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
], ],
})); }));
hass.mockWS("energy/info", () => ({ cost_sensors: [] })); hass.mockWS("energy/info", () => ({ cost_sensors: [] }));
hass.mockWS("energy/fossil_energy_consumption", ({ period }) => ({
start: period === "month" ? 500 : period === "day" ? 20 : 5,
}));
const todayString = format(startOfToday(), "yyyy-MM-dd"); const todayString = format(startOfToday(), "yyyy-MM-dd");
const tomorrowString = format(startOfTomorrow(), "yyyy-MM-dd"); const tomorrowString = format(startOfTomorrow(), "yyyy-MM-dd");
hass.mockWS( hass.mockWS(

View File

@@ -1,4 +1,10 @@
import { addHours, differenceInHours, endOfDay } from "date-fns"; import {
addDays,
addHours,
addMonths,
differenceInHours,
endOfDay,
} from "date-fns";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { StatisticValue } from "../../../src/data/history"; import { StatisticValue } from "../../../src/data/history";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
@@ -70,6 +76,7 @@ const generateMeanStatistics = (
id: string, id: string,
start: Date, start: Date,
end: Date, end: Date,
period: "5minute" | "hour" | "day" | "month" = "hour",
initValue: number, initValue: number,
maxDiff: number maxDiff: number
) => { ) => {
@@ -84,6 +91,7 @@ const generateMeanStatistics = (
statistics.push({ statistics.push({
statistic_id: id, statistic_id: id,
start: currentDate.toISOString(), start: currentDate.toISOString(),
end: currentDate.toISOString(),
mean, mean,
min: mean - Math.random() * maxDiff, min: mean - Math.random() * maxDiff,
max: mean + Math.random() * maxDiff, max: mean + Math.random() * maxDiff,
@@ -92,7 +100,12 @@ const generateMeanStatistics = (
sum: null, sum: null,
}); });
lastVal = mean; lastVal = mean;
currentDate = addHours(currentDate, 1); currentDate =
period === "day"
? addDays(currentDate, 1)
: period === "month"
? addMonths(currentDate, 1)
: addHours(currentDate, 1);
} }
return statistics; return statistics;
}; };
@@ -101,6 +114,7 @@ const generateSumStatistics = (
id: string, id: string,
start: Date, start: Date,
end: Date, end: Date,
period: "5minute" | "hour" | "day" | "month" = "hour",
initValue: number, initValue: number,
maxDiff: number maxDiff: number
) => { ) => {
@@ -115,6 +129,7 @@ const generateSumStatistics = (
statistics.push({ statistics.push({
statistic_id: id, statistic_id: id,
start: currentDate.toISOString(), start: currentDate.toISOString(),
end: currentDate.toISOString(),
mean: null, mean: null,
min: null, min: null,
max: null, max: null,
@@ -122,7 +137,12 @@ const generateSumStatistics = (
state: initValue + sum, state: initValue + sum,
sum, sum,
}); });
currentDate = addHours(currentDate, 1); currentDate =
period === "day"
? addDays(currentDate, 1)
: period === "month"
? addMonths(currentDate, 1)
: addHours(currentDate, 1);
} }
return statistics; return statistics;
}; };
@@ -131,6 +151,7 @@ const generateCurvedStatistics = (
id: string, id: string,
start: Date, start: Date,
end: Date, end: Date,
_period: "5minute" | "hour" | "day" | "month" = "hour",
initValue: number, initValue: number,
maxDiff: number, maxDiff: number,
metered: boolean metered: boolean
@@ -149,6 +170,7 @@ const generateCurvedStatistics = (
statistics.push({ statistics.push({
statistic_id: id, statistic_id: id,
start: currentDate.toISOString(), start: currentDate.toISOString(),
end: currentDate.toISOString(),
mean: null, mean: null,
min: null, min: null,
max: null, max: null,
@@ -167,11 +189,38 @@ const generateCurvedStatistics = (
const statisticsFunctions: Record< const statisticsFunctions: Record<
string, string,
(id: string, start: Date, end: Date) => StatisticValue[] (
id: string,
start: Date,
end: Date,
period: "5minute" | "hour" | "day" | "month"
) => StatisticValue[]
> = { > = {
"sensor.energy_consumption_tarif_1": (id: string, start: Date, end: Date) => { "sensor.energy_consumption_tarif_1": (
id: string,
start: Date,
end: Date,
period = "hour"
) => {
if (period !== "hour") {
return generateSumStatistics(
id,
start,
end,
period,
0,
period === "day" ? 17 : 504
);
}
const morningEnd = new Date(start.getTime() + 10 * 60 * 60 * 1000); const morningEnd = new Date(start.getTime() + 10 * 60 * 60 * 1000);
const morningLow = generateSumStatistics(id, start, morningEnd, 0, 0.7); const morningLow = generateSumStatistics(
id,
start,
morningEnd,
period,
0,
0.7
);
const eveningStart = new Date(start.getTime() + 20 * 60 * 60 * 1000); const eveningStart = new Date(start.getTime() + 20 * 60 * 60 * 1000);
const morningFinalVal = morningLow.length const morningFinalVal = morningLow.length
? morningLow[morningLow.length - 1].sum! ? morningLow[morningLow.length - 1].sum!
@@ -180,6 +229,7 @@ const statisticsFunctions: Record<
id, id,
morningEnd, morningEnd,
eveningStart, eveningStart,
period,
morningFinalVal, morningFinalVal,
0 0
); );
@@ -187,39 +237,71 @@ const statisticsFunctions: Record<
id, id,
eveningStart, eveningStart,
end, end,
period,
morningFinalVal, morningFinalVal,
0.7 0.7
); );
return [...morningLow, ...empty, ...eveningLow]; return [...morningLow, ...empty, ...eveningLow];
}, },
"sensor.energy_consumption_tarif_2": (id: string, start: Date, end: Date) => { "sensor.energy_consumption_tarif_2": (
id: string,
start: Date,
end: Date,
period = "hour"
) => {
if (period !== "hour") {
return generateSumStatistics(
id,
start,
end,
period,
0,
period === "day" ? 17 : 504
);
}
const morningEnd = new Date(start.getTime() + 9 * 60 * 60 * 1000); const morningEnd = new Date(start.getTime() + 9 * 60 * 60 * 1000);
const eveningStart = new Date(start.getTime() + 20 * 60 * 60 * 1000); const eveningStart = new Date(start.getTime() + 20 * 60 * 60 * 1000);
const highTarif = generateSumStatistics( const highTarif = generateSumStatistics(
id, id,
morningEnd, morningEnd,
eveningStart, eveningStart,
period,
0, 0,
0.3 0.3
); );
const highTarifFinalVal = highTarif.length const highTarifFinalVal = highTarif.length
? highTarif[highTarif.length - 1].sum! ? highTarif[highTarif.length - 1].sum!
: 0; : 0;
const morning = generateSumStatistics(id, start, morningEnd, 0, 0); const morning = generateSumStatistics(id, start, morningEnd, period, 0, 0);
const evening = generateSumStatistics( const evening = generateSumStatistics(
id, id,
eveningStart, eveningStart,
end, end,
period,
highTarifFinalVal, highTarifFinalVal,
0 0
); );
return [...morning, ...highTarif, ...evening]; return [...morning, ...highTarif, ...evening];
}, },
"sensor.energy_production_tarif_1": (id, start, end) => "sensor.energy_production_tarif_1": (id, start, end, period = "hour") =>
generateSumStatistics(id, start, end, 0, 0), generateSumStatistics(id, start, end, period, 0, 0),
"sensor.energy_production_tarif_1_compensation": (id, start, end) => "sensor.energy_production_tarif_1_compensation": (
generateSumStatistics(id, start, end, 0, 0), id,
"sensor.energy_production_tarif_2": (id, start, end) => { start,
end,
period = "hour"
) => generateSumStatistics(id, start, end, period, 0, 0),
"sensor.energy_production_tarif_2": (id, start, end, period = "hour") => {
if (period !== "hour") {
return generateSumStatistics(
id,
start,
end,
period,
0,
period === "day" ? 17 : 504
);
}
const productionStart = new Date(start.getTime() + 9 * 60 * 60 * 1000); const productionStart = new Date(start.getTime() + 9 * 60 * 60 * 1000);
const productionEnd = new Date(start.getTime() + 21 * 60 * 60 * 1000); const productionEnd = new Date(start.getTime() + 21 * 60 * 60 * 1000);
const dayEnd = new Date(endOfDay(productionEnd)); const dayEnd = new Date(endOfDay(productionEnd));
@@ -227,6 +309,7 @@ const statisticsFunctions: Record<
id, id,
productionStart, productionStart,
productionEnd, productionEnd,
period,
0, 0,
0.15, 0.15,
true true
@@ -234,18 +317,43 @@ const statisticsFunctions: Record<
const productionFinalVal = production.length const productionFinalVal = production.length
? production[production.length - 1].sum! ? production[production.length - 1].sum!
: 0; : 0;
const morning = generateSumStatistics(id, start, productionStart, 0, 0); const morning = generateSumStatistics(
id,
start,
productionStart,
period,
0,
0
);
const evening = generateSumStatistics( const evening = generateSumStatistics(
id, id,
productionEnd, productionEnd,
dayEnd, dayEnd,
period,
productionFinalVal, productionFinalVal,
0 0
); );
const rest = generateSumStatistics(id, dayEnd, end, productionFinalVal, 1); const rest = generateSumStatistics(
id,
dayEnd,
end,
period,
productionFinalVal,
1
);
return [...morning, ...production, ...evening, ...rest]; return [...morning, ...production, ...evening, ...rest];
}, },
"sensor.solar_production": (id, start, end) => { "sensor.solar_production": (id, start, end, period = "hour") => {
if (period !== "hour") {
return generateSumStatistics(
id,
start,
end,
period,
0,
period === "day" ? 17 : 504
);
}
const productionStart = new Date(start.getTime() + 7 * 60 * 60 * 1000); const productionStart = new Date(start.getTime() + 7 * 60 * 60 * 1000);
const productionEnd = new Date(start.getTime() + 23 * 60 * 60 * 1000); const productionEnd = new Date(start.getTime() + 23 * 60 * 60 * 1000);
const dayEnd = new Date(endOfDay(productionEnd)); const dayEnd = new Date(endOfDay(productionEnd));
@@ -253,6 +361,7 @@ const statisticsFunctions: Record<
id, id,
productionStart, productionStart,
productionEnd, productionEnd,
period,
0, 0,
0.3, 0.3,
true true
@@ -260,19 +369,32 @@ const statisticsFunctions: Record<
const productionFinalVal = production.length const productionFinalVal = production.length
? production[production.length - 1].sum! ? production[production.length - 1].sum!
: 0; : 0;
const morning = generateSumStatistics(id, start, productionStart, 0, 0); const morning = generateSumStatistics(
id,
start,
productionStart,
period,
0,
0
);
const evening = generateSumStatistics( const evening = generateSumStatistics(
id, id,
productionEnd, productionEnd,
dayEnd, dayEnd,
period,
productionFinalVal, productionFinalVal,
0 0
); );
const rest = generateSumStatistics(id, dayEnd, end, productionFinalVal, 2); const rest = generateSumStatistics(
id,
dayEnd,
end,
period,
productionFinalVal,
2
);
return [...morning, ...production, ...evening, ...rest]; return [...morning, ...production, ...evening, ...rest];
}, },
"sensor.grid_fossil_fuel_percentage": (id, start, end) =>
generateMeanStatistics(id, start, end, 35, 1.3),
}; };
export const mockHistory = (mockHass: MockHomeAssistant) => { export const mockHistory = (mockHass: MockHomeAssistant) => {
@@ -347,7 +469,7 @@ export const mockHistory = (mockHass: MockHomeAssistant) => {
mockHass.mockWS("history/list_statistic_ids", () => []); mockHass.mockWS("history/list_statistic_ids", () => []);
mockHass.mockWS( mockHass.mockWS(
"history/statistics_during_period", "history/statistics_during_period",
({ statistic_ids, start_time, end_time }, hass) => { ({ statistic_ids, start_time, end_time, period }, hass) => {
const start = new Date(start_time); const start = new Date(start_time);
const end = end_time ? new Date(end_time) : new Date(); const end = end_time ? new Date(end_time) : new Date();
@@ -355,7 +477,7 @@ export const mockHistory = (mockHass: MockHomeAssistant) => {
statistic_ids.forEach((id: string) => { statistic_ids.forEach((id: string) => {
if (id in statisticsFunctions) { if (id in statisticsFunctions) {
statistics[id] = statisticsFunctions[id](id, start, end); statistics[id] = statisticsFunctions[id](id, start, end, period);
} else { } else {
const entityState = hass.states[id]; const entityState = hass.states[id];
const state = entityState ? Number(entityState.state) : 1; const state = entityState ? Number(entityState.state) : 1;
@@ -365,6 +487,7 @@ export const mockHistory = (mockHass: MockHomeAssistant) => {
id, id,
start, start,
end, end,
period,
state, state,
state * (state > 80 ? 0.01 : 0.05) state * (state > 80 ? 0.01 : 0.05)
) )
@@ -372,6 +495,7 @@ export const mockHistory = (mockHass: MockHomeAssistant) => {
id, id,
start, start,
end, end,
period,
state, state,
state * (state > 80 ? 0.05 : 0.1) state * (state > 80 ? 0.05 : 0.1)
); );

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

View File

@@ -52,17 +52,13 @@ class DemoBlackWhiteRow extends LitElement {
firstUpdated(changedProps) { firstUpdated(changedProps) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
applyThemesOnElement( applyThemesOnElement(this.shadowRoot!.querySelector(".dark"), {
this.shadowRoot!.querySelector(".dark"),
{
default_theme: "default", default_theme: "default",
default_dark_theme: "default", default_dark_theme: "default",
themes: {}, themes: {},
darkMode: false, darkMode: true,
}, theme: "default",
"default", });
{ dark: true }
);
} }
handleSubmit(ev) { handleSubmit(ev) {

View File

@@ -1,15 +1,19 @@
import { html, css, LitElement, TemplateResult } from "lit"; import "@material/mwc-button/mwc-button";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
import "../../../src/components/ha-alert"; import "../../../src/components/ha-alert";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import "../../../src/components/ha-logo-svg";
const alerts: { const alerts: {
title?: string; title?: string;
description: string | TemplateResult; description: string | TemplateResult;
type: "info" | "warning" | "error" | "success"; type: "info" | "warning" | "error" | "success";
dismissable?: boolean; dismissable?: boolean;
action?: string;
rtl?: boolean; rtl?: boolean;
iconSlot?: TemplateResult;
actionSlot?: TemplateResult;
}[] = [ }[] = [
{ {
title: "Test info alert", title: "Test info alert",
@@ -73,13 +77,35 @@ const alerts: {
title: "Error with action", title: "Error with action",
description: "This is a test error alert with action", description: "This is a test error alert with action",
type: "error", type: "error",
action: "restart", actionSlot: html`<mwc-button slot="action" label="restart"></mwc-button>`,
}, },
{ {
title: "Unsaved data", title: "Unsaved data",
description: "You have unsaved data", description: "You have unsaved data",
type: "warning", type: "warning",
action: "save", actionSlot: html`<mwc-button slot="action" label="save"></mwc-button>`,
},
{
title: "Slotted icon",
description: "Alert with slotted icon",
type: "warning",
iconSlot: html`<span slot="icon" class="image">
<ha-logo-svg></ha-logo-svg>
</span>`,
},
{
title: "Slotted image",
description: "Alert with slotted image",
type: "warning",
iconSlot: html`<span slot="icon" class="image"
><img src="https://www.home-assistant.io/images/home-assistant-logo.svg"
/></span>`,
},
{
title: "Slotted action",
description: "Alert with slotted action",
type: "info",
actionSlot: html`<mwc-button slot="action" label="action"></mwc-button>`,
}, },
{ {
description: "Dismissable information (RTL)", description: "Dismissable information (RTL)",
@@ -91,7 +117,7 @@ const alerts: {
title: "Error with action", title: "Error with action",
description: "This is a test error alert with action (RTL)", description: "This is a test error alert with action (RTL)",
type: "error", type: "error",
action: "restart", actionSlot: html`<mwc-button slot="action" label="restart"></mwc-button>`,
rtl: true, rtl: true,
}, },
{ {
@@ -106,7 +132,10 @@ const alerts: {
export class DemoHaAlert extends LitElement { export class DemoHaAlert extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<ha-card header="ha-alert demo"> ${["light", "dark"].map(
(mode) => html`
<div class=${mode}>
<ha-card header="ha-alert ${mode} demo">
<div class="card-content"> <div class="card-content">
${alerts.map( ${alerts.map(
(alert) => html` (alert) => html`
@@ -114,22 +143,45 @@ export class DemoHaAlert extends LitElement {
.title=${alert.title || ""} .title=${alert.title || ""}
.alertType=${alert.type} .alertType=${alert.type}
.dismissable=${alert.dismissable || false} .dismissable=${alert.dismissable || false}
.actionText=${alert.action || ""}
.rtl=${alert.rtl || false} .rtl=${alert.rtl || false}
> >
${alert.description} ${alert.iconSlot} ${alert.description} ${alert.actionSlot}
</ha-alert> </ha-alert>
` `
)} )}
</div> </div>
</ha-card> </ha-card>
</div>
`
)}
`; `;
} }
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(this.shadowRoot!.querySelector(".dark"), {
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: true,
theme: "default",
});
}
static get styles() { static get styles() {
return css` return css`
:host {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.dark,
.light {
display: block;
background-color: var(--primary-background-color);
padding: 0 50px;
}
ha-card { ha-card {
max-width: 600px;
margin: 24px auto; margin: 24px auto;
} }
ha-alert { ha-alert {
@@ -142,8 +194,17 @@ export class DemoHaAlert extends LitElement {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
} }
span { .image {
margin-right: 16px; display: inline-flex;
height: 100%;
align-items: center;
}
img {
max-height: 24px;
width: 24px;
}
mwc-button {
--mdc-theme-primary: var(--primary-text-color);
} }
`; `;
} }

View File

@@ -3,6 +3,7 @@ import { css, html, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import "../../../src/components/ha-chip"; import "../../../src/components/ha-chip";
import "../../../src/components/ha-chip-set";
import "../../../src/components/ha-svg-icon"; import "../../../src/components/ha-svg-icon";
const chips: { const chips: {
@@ -22,8 +23,8 @@ const chips: {
}, },
]; ];
@customElement("demo-ha-chip") @customElement("demo-ha-chips")
export class DemoHaChip extends LitElement { export class DemoHaChips extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<ha-card header="ha-chip demo"> <ha-card header="ha-chip demo">
@@ -41,6 +42,23 @@ export class DemoHaChip extends LitElement {
)} )}
</div> </div>
</ha-card> </ha-card>
<ha-card header="ha-chip-set demo">
<div class="card-content">
<ha-chip-set>
${chips.map(
(chip) => html`
<ha-chip .hasIcon=${chip.icon !== undefined}>
${chip.icon
? html`<ha-svg-icon slot="icon" .path=${chip.icon}>
</ha-svg-icon>`
: ""}
${chip.content}
</ha-chip>
`
)}
</ha-chip-set>
</div>
</ha-card>
`; `;
} }
@@ -50,12 +68,19 @@ export class DemoHaChip extends LitElement {
max-width: 600px; max-width: 600px;
margin: 24px auto; margin: 24px auto;
} }
ha-chip {
margin-bottom: 4px;
}
.card-content {
display: flex;
flex-direction: column;
}
`; `;
} }
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"demo-ha-chip": DemoHaChip; "demo-ha-chips": DemoHaChips;
} }
} }

View File

@@ -0,0 +1,88 @@
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import "../../../src/components/ha-card";
import "../../../src/components/ha-faded";
import "../../../src/components/ha-markdown";
const LONG_TEXT = `
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc laoreet velit ut elit volutpat, eget ultrices odio lacinia. In imperdiet malesuada est, nec sagittis metus ultricies quis. Sed nisl ex, convallis porttitor ante quis, hendrerit tristique justo. Mauris pharetra venenatis augue, eu maximus sem cursus in. Quisque sed consequat risus. Suspendisse facilisis ligula a odio consectetur condimentum. Curabitur vehicula elit nec augue mollis, et volutpat massa dictum.
Nam pellentesque auctor rutrum. Suspendisse elit est, sodales vel diam nec, porttitor faucibus massa. Ut pretium ac orci eu pharetra. Praesent in nibh at magna viverra rutrum eu vitae tortor. Etiam eget sem ex. Fusce tristique odio nec lacus mattis, vitae tempor nunc malesuada. Maecenas faucibus magna vel libero maximus egestas. Vestibulum luctus semper velit, in lobortis risus tempus non. Curabitur bibendum ornare commodo. Quisque commodo neque sit amet tincidunt lacinia. Proin elementum ante velit, eu congue nulla semper quis. Pellentesque consequat vel nunc at scelerisque. Mauris sit amet venenatis diam, blandit viverra leo. Integer commodo laoreet orci.
Curabitur ipsum tortor, sodales ut augue sed, commodo porttitor libero. Pellentesque molestie vitae mi consectetur tempor. In sed lectus consequat, lobortis neque non, semper ipsum. Etiam eget ex et nibh sagittis pulvinar lacinia ac mauris. Aenean ligula eros, viverra ac nibh at, venenatis semper quam. Sed interdum ligula sit amet massa tincidunt tincidunt. Suspendisse potenti. Aliquam egestas facilisis est, sed faucibus erat scelerisque id. Duis dolor quam, viverra vitae orci euismod, laoreet pellentesque justo. Nunc malesuada non erat at ullamcorper. Mauris eget posuere odio. Vestibulum turpis nunc, pharetra eget ante in, feugiat mollis justo. Proin porttitor, diam nec vulputate pretium, tellus arcu rhoncus turpis, a blandit nisi nulla quis arcu. Nunc ac ullamcorper ligula, nec facilisis leo.
In vitae eros sollicitudin, iaculis ex eget, egestas orci. Etiam sed pretium lorem. Nam nisi enim, consectetur sit amet semper ac, semper pharetra diam. In pulvinar neque sapien, ac ullamcorper est lacinia a. Etiam tincidunt velit sed diam malesuada, eu ornare ex consectetur. Phasellus in imperdiet tellus. Sed bibendum, dui sit amet fringilla aliquet, enim odio sollicitudin lorem, vel semper turpis mauris vel mauris. Aenean congue magna ac massa cursus, in dictum orci commodo. Pellentesque mollis velit in sollicitudin tincidunt. Vestibulum et efficitur nulla.
Quisque posuere, velit sed porttitor dapibus, neque augue fringilla felis, eu luctus nisi nisl nec ipsum. Curabitur pellentesque ac lectus eget ultricies. Vestibulum est dolor, lacinia pharetra vulputate a, facilisis a magna. Nam vitae arcu nibh. Praesent finibus blandit ante, ac gravida ex mollis eget. Donec quam est, pulvinar vitae neque ut, bibendum aliquam erat. Nullam mollis arcu at sem tincidunt, in tristique lectus facilisis. Aenean ut lacus vel nisl finibus iaculis non a turpis. Integer eget ipsum ante. Donec nunc neque, vestibulum ac magna ac, posuere scelerisque dui. Pellentesque massa nibh, rhoncus id dolor quis, placerat posuere turpis. Donec aliquet augue nisi, eu finibus dui auctor et. Vestibulum eu varius lorem. Quisque lectus ante, malesuada pretium risus eget, interdum mattis enim.
`;
const SMALL_TEXT = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
@customElement("demo-ha-faded")
export class DemoHaFaded extends LitElement {
protected render(): TemplateResult {
return html`
<ha-card header="ha-faded demo">
<div class="card-content">
<h3>Long text directly as slotted content</h3>
<ha-faded>${LONG_TEXT}</ha-faded>
<h3>Long text with slotted element</h3>
<ha-faded><span>${LONG_TEXT}</span></ha-faded>
<h3>No text</h3>
<ha-faded><span></span></ha-faded>
<h3>Smal text</h3>
<ha-faded><span>${SMALL_TEXT}</span></ha-faded>
<h3>Long text in markdown</h3>
<ha-faded>
<ha-markdown .content=${LONG_TEXT}> </ha-markdown>
</ha-faded>
<h3>Missing 1px from hiding</h3>
<ha-faded faded-height="87">
<span>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc
laoreet velit ut elit volutpat, eget ultrices odio lacinia. In
imperdiet malesuada est, nec sagittis metus ultricies quis. Sed
nisl ex, convallis porttitor ante quis, hendrerit tristique justo.
Mauris pharetra venenatis augue, eu maximus sem cursus in. Quisque
sed consequat risus. Suspendisse facilisis ligula a odio
consectetur condimentum. Curabitur vehicula elit nec augue mollis,
et volutpat massa dictum. Nam pellentesque auctor rutrum.
Suspendisse elit est, sodales vel diam nec, porttitor faucibus
massa. Ut pretium ac orci eu pharetra.
</span>
</ha-faded>
<h3>1px over hiding point</h3>
<ha-faded faded-height="85">
<span>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc
laoreet velit ut elit volutpat, eget ultrices odio lacinia. In
imperdiet malesuada est, nec sagittis metus ultricies quis. Sed
nisl ex, convallis porttitor ante quis, hendrerit tristique justo.
Mauris pharetra venenatis augue, eu maximus sem cursus in. Quisque
sed consequat risus. Suspendisse facilisis ligula a odio
consectetur condimentum. Curabitur vehicula elit nec augue mollis,
et volutpat massa dictum. Nam pellentesque auctor rutrum.
Suspendisse elit est, sodales vel diam nec, porttitor faucibus
massa. Ut pretium ac orci eu pharetra.
</span>
</ha-faded>
</div>
</ha-card>
`;
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-ha-faded": DemoHaFaded;
}
}

View File

@@ -0,0 +1,156 @@
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, query } from "lit/decorators";
import { getEntity } from "../../../src/fake_data/entity";
import { provideHass } from "../../../src/fake_data/provide_hass";
import "../components/demo-cards";
const ENTITIES = [
getEntity("light", "bed_light", "on", {
friendly_name: "Bed Light",
}),
getEntity("switch", "bed_ac", "on", {
friendly_name: "Ecobee",
}),
getEntity("sensor", "bed_temp", "72", {
friendly_name: "Bedroom Temp",
device_class: "temperature",
unit_of_measurement: "°F",
}),
getEntity("light", "living_room_light", "off", {
friendly_name: "Living Room Light",
}),
getEntity("fan", "living_room", "on", {
friendly_name: "Living Room Fan",
}),
getEntity("sensor", "office_humidity", "73", {
friendly_name: "Office Humidity",
device_class: "humidity",
unit_of_measurement: "%",
}),
getEntity("light", "office", "on", {
friendly_name: "Office Light",
}),
getEntity("fan", "kitchen", "on", {
friendly_name: "Second Office Fan",
}),
getEntity("binary_sensor", "kitchen_door", "on", {
friendly_name: "Office Door",
device_class: "door",
}),
];
// TODO: Update image here
const CONFIGS = [
{
heading: "Bedroom",
config: `
- type: area
area: bedroom
image: "/images/bed.png"
`,
},
{
heading: "Living Room",
config: `
- type: area
area: living_room
image: "/images/living_room.png"
`,
},
{
heading: "Office",
config: `
- type: area
area: office
image: "/images/office.jpg"
`,
},
{
heading: "Kitchen",
config: `
- type: area
area: kitchen
image: "/images/kitchen.png"
`,
},
];
@customElement("demo-hui-area-card")
class DemoArea extends LitElement {
@query("#demos") private _demoRoot!: HTMLElement;
protected render(): TemplateResult {
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
}
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");
hass.updateTranslations("lovelace", "en");
hass.addEntities(ENTITIES);
hass.mockWS("config/area_registry/list", () => [
{
name: "Bedroom",
area_id: "bedroom",
},
{
name: "Living Room",
area_id: "living_room",
},
{
name: "Office",
area_id: "office",
},
{
name: "Second Office",
area_id: "kitchen",
},
]);
hass.mockWS("config/device_registry/list", () => []);
hass.mockWS("config/entity_registry/list", () => [
{
area_id: "bedroom",
entity_id: "light.bed_light",
},
{
area_id: "bedroom",
entity_id: "switch.bed_ac",
},
{
area_id: "bedroom",
entity_id: "sensor.bed_temp",
},
{
area_id: "living_room",
entity_id: "light.living_room_light",
},
{
area_id: "living_room",
entity_id: "fan.living_room",
},
{
area_id: "office",
entity_id: "light.office",
},
{
area_id: "office",
entity_id: "sensor.office_humidity",
},
{
area_id: "kitchen",
entity_id: "fan.kitchen",
},
{
area_id: "kitchen",
entity_id: "binary_sensor.kitchen_door",
},
]);
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-hui-area-card": DemoArea;
}
}

View File

@@ -0,0 +1,164 @@
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators";
import "../../../src/components/ha-card";
import {
SUPPORT_OPEN,
SUPPORT_STOP,
SUPPORT_CLOSE,
SUPPORT_SET_POSITION,
SUPPORT_OPEN_TILT,
SUPPORT_STOP_TILT,
SUPPORT_CLOSE_TILT,
SUPPORT_SET_TILT_POSITION,
} from "../../../src/data/cover";
import "../../../src/dialogs/more-info/more-info-content";
import { getEntity } from "../../../src/fake_data/entity";
import {
MockHomeAssistant,
provideHass,
} from "../../../src/fake_data/provide_hass";
import "../components/demo-more-infos";
const ENTITIES = [
getEntity("cover", "position_buttons", "on", {
friendly_name: "Position Buttons",
supported_features: SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE,
}),
getEntity("cover", "position_slider_half", "on", {
friendly_name: "Position Half-Open",
supported_features:
SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE + SUPPORT_SET_POSITION,
current_position: 50,
}),
getEntity("cover", "position_slider_open", "on", {
friendly_name: "Position Open",
supported_features:
SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE + SUPPORT_SET_POSITION,
current_position: 100,
}),
getEntity("cover", "position_slider_closed", "on", {
friendly_name: "Position Closed",
supported_features:
SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE + SUPPORT_SET_POSITION,
current_position: 0,
}),
getEntity("cover", "tilt_buttons", "on", {
friendly_name: "Tilt Buttons",
supported_features:
SUPPORT_OPEN_TILT + SUPPORT_STOP_TILT + SUPPORT_CLOSE_TILT,
}),
getEntity("cover", "tilt_slider_half", "on", {
friendly_name: "Tilt Half-Open",
supported_features:
SUPPORT_OPEN_TILT +
SUPPORT_STOP_TILT +
SUPPORT_CLOSE_TILT +
SUPPORT_SET_TILT_POSITION,
current_tilt_position: 50,
}),
getEntity("cover", "tilt_slider_open", "on", {
friendly_name: "Tilt Open",
supported_features:
SUPPORT_OPEN_TILT +
SUPPORT_STOP_TILT +
SUPPORT_CLOSE_TILT +
SUPPORT_SET_TILT_POSITION,
current_tilt_position: 100,
}),
getEntity("cover", "tilt_slider_closed", "on", {
friendly_name: "Tilt Closed",
supported_features:
SUPPORT_OPEN_TILT +
SUPPORT_STOP_TILT +
SUPPORT_CLOSE_TILT +
SUPPORT_SET_TILT_POSITION,
current_tilt_position: 0,
}),
getEntity("cover", "position_slider_tilt_slider", "on", {
friendly_name: "Both Sliders",
supported_features:
SUPPORT_OPEN +
SUPPORT_STOP +
SUPPORT_CLOSE +
SUPPORT_SET_POSITION +
SUPPORT_OPEN_TILT +
SUPPORT_STOP_TILT +
SUPPORT_CLOSE_TILT +
SUPPORT_SET_TILT_POSITION,
current_position: 30,
current_tilt_position: 70,
}),
getEntity("cover", "position_tilt_slider", "on", {
friendly_name: "Position & Tilt Slider",
supported_features:
SUPPORT_OPEN +
SUPPORT_STOP +
SUPPORT_CLOSE +
SUPPORT_OPEN_TILT +
SUPPORT_STOP_TILT +
SUPPORT_CLOSE_TILT +
SUPPORT_SET_TILT_POSITION,
current_tilt_position: 70,
}),
getEntity("cover", "position_slider_tilt", "on", {
friendly_name: "Position Slider & Tilt",
supported_features:
SUPPORT_OPEN +
SUPPORT_STOP +
SUPPORT_CLOSE +
SUPPORT_SET_POSITION +
SUPPORT_OPEN_TILT +
SUPPORT_STOP_TILT +
SUPPORT_CLOSE_TILT,
current_position: 30,
}),
getEntity("cover", "position_slider_only_tilt_slider", "on", {
friendly_name: "Position Slider Only & Tilt Buttons",
supported_features:
SUPPORT_SET_POSITION +
SUPPORT_OPEN_TILT +
SUPPORT_STOP_TILT +
SUPPORT_CLOSE_TILT,
current_position: 30,
}),
getEntity("cover", "position_slider_only_tilt", "on", {
friendly_name: "Position Slider Only & Tilt",
supported_features:
SUPPORT_SET_POSITION +
SUPPORT_OPEN_TILT +
SUPPORT_STOP_TILT +
SUPPORT_CLOSE_TILT +
SUPPORT_SET_TILT_POSITION,
current_position: 30,
current_tilt_position: 70,
}),
];
@customElement("demo-more-info-cover")
class DemoMoreInfoCover extends LitElement {
@property() public hass!: MockHomeAssistant;
@query("demo-more-infos") private _demoRoot!: HTMLElement;
protected render(): TemplateResult {
return html`
<demo-more-infos
.hass=${this.hass}
.entities=${ENTITIES.map((ent) => ent.entityId)}
></demo-more-infos>
`;
}
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");
hass.addEntities(ENTITIES);
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-more-info-cover": DemoMoreInfoCover;
}
}

View File

@@ -176,11 +176,6 @@ class HaGallery extends PolymerElement {
this.addEventListener("alert-dismissed-clicked", () => this.addEventListener("alert-dismissed-clicked", () =>
this.$.notifications.showDialog({ message: "Alert dismissed clicked" }) this.$.notifications.showDialog({ message: "Alert dismissed clicked" })
); );
this.addEventListener("alert-action-clicked", () =>
this.$.notifications.showDialog({ message: "Alert action clicked" })
);
this.addEventListener("hass-more-info", (ev) => { this.addEventListener("hass-more-info", (ev) => {
if (ev.detail.entityId) { if (ev.detail.entityId) {
this.$.notifications.showDialog({ this.$.notifications.showDialog({

View File

@@ -25,11 +25,10 @@ import {
} from "../../../src/data/hassio/addon"; } from "../../../src/data/hassio/addon";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import "../../../src/layouts/hass-loading-screen"; import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-tabs-subpage"; import "../../../src/layouts/hass-subpage";
import { HomeAssistant, Route } from "../../../src/types"; import { HomeAssistant, Route } from "../../../src/types";
import { showRegistriesDialog } from "../dialogs/registries/show-dialog-registries"; import { showRegistriesDialog } from "../dialogs/registries/show-dialog-registries";
import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories"; import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories";
import { supervisorTabs } from "../hassio-tabs";
import "./hassio-addon-repository"; import "./hassio-addon-repository";
const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => { const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => {
@@ -76,16 +75,12 @@ class HassioAddonStore extends LitElement {
} }
return html` return html`
<hass-tabs-subpage <hass-subpage
.hass=${this.hass} .hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow} .narrow=${this.narrow}
.route=${this.route} .route=${this.route}
.tabs=${supervisorTabs} .header=${this.supervisor.localize("panel.store")}
main-page
supervisor
> >
<span slot="header"> ${this.supervisor.localize("panel.store")} </span>
<ha-button-menu <ha-button-menu
corner="BOTTOM_START" corner="BOTTOM_START"
slot="toolbar-icon" slot="toolbar-icon"
@@ -133,7 +128,7 @@ class HassioAddonStore extends LitElement {
</div> </div>
` `
: ""} : ""}
</hass-tabs-subpage> </hass-subpage>
`; `;
} }

View File

@@ -108,7 +108,6 @@ class HassioAddonDashboard extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.localizeFunc=${this.supervisor.localize} .localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow} .narrow=${this.narrow}
.backPath=${this.addon.version ? "/hassio/dashboard" : "/hassio/store"}
.route=${route} .route=${route}
.tabs=${addonTabs} .tabs=${addonTabs}
supervisor supervisor

View File

@@ -4,7 +4,7 @@ import "../../../../src/components/ha-circular-progress";
import { HassioAddonDetails } from "../../../../src/data/hassio/addon"; import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import { Supervisor } from "../../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles"; import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types"; import { HomeAssistant, Route } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style"; import { hassioStyle } from "../../resources/hassio-style";
import "./hassio-addon-info"; import "./hassio-addon-info";
@@ -12,6 +12,8 @@ import "./hassio-addon-info";
class HassioAddonInfoDashboard extends LitElement { class HassioAddonInfoDashboard extends LitElement {
@property({ type: Boolean }) public narrow!: boolean; @property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor; @property({ attribute: false }) public supervisor!: Supervisor;
@@ -27,6 +29,7 @@ class HassioAddonInfoDashboard extends LitElement {
<div class="content"> <div class="content">
<hassio-addon-info <hassio-addon-info
.narrow=${this.narrow} .narrow=${this.narrow}
.route=${this.route}
.hass=${this.hass} .hass=${this.hass}
.supervisor=${this.supervisor} .supervisor=${this.supervisor}
.addon=${this.addon} .addon=${this.addon}

View File

@@ -1,6 +1,5 @@
import "@material/mwc-button"; import "@material/mwc-button";
import { import {
mdiArrowUpBoldCircle,
mdiCheckCircle, mdiCheckCircle,
mdiChip, mdiChip,
mdiCircle, mdiCircle,
@@ -49,7 +48,6 @@ import {
startHassioAddon, startHassioAddon,
stopHassioAddon, stopHassioAddon,
uninstallHassioAddon, uninstallHassioAddon,
updateHassioAddon,
validateHassioAddonOption, validateHassioAddonOption,
} from "../../../../src/data/hassio/addon"; } from "../../../../src/data/hassio/addon";
import { import {
@@ -64,14 +62,14 @@ import {
showConfirmationDialog, showConfirmationDialog,
} from "../../../../src/dialogs/generic/show-dialog-box"; } 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, Route } from "../../../../src/types";
import { bytesToString } from "../../../../src/util/bytes-to-string"; import { bytesToString } from "../../../../src/util/bytes-to-string";
import "../../components/hassio-card-content"; import "../../components/hassio-card-content";
import "../../components/supervisor-metric"; import "../../components/supervisor-metric";
import { showHassioMarkdownDialog } from "../../dialogs/markdown/show-dialog-hassio-markdown"; import { showHassioMarkdownDialog } from "../../dialogs/markdown/show-dialog-hassio-markdown";
import { showDialogSupervisorUpdate } from "../../dialogs/update/show-dialog-update";
import { hassioStyle } from "../../resources/hassio-style"; import { hassioStyle } from "../../resources/hassio-style";
import { addonArchIsSupported } from "../../util/addon"; import "../../update-available/update-available-card";
import { addonArchIsSupported, extractChangelog } from "../../util/addon";
const STAGE_ICON = { const STAGE_ICON = {
stable: mdiCheckCircle, stable: mdiCheckCircle,
@@ -92,6 +90,8 @@ const RATING_ICON = {
class HassioAddonInfo extends LitElement { class HassioAddonInfo extends LitElement {
@property({ type: Boolean }) public narrow!: boolean; @property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public addon!: HassioAddonDetails; @property({ attribute: false }) public addon!: HassioAddonDetails;
@@ -128,69 +128,12 @@ class HassioAddonInfo extends LitElement {
return html` return html`
${this.addon.update_available ${this.addon.update_available
? html` ? html`
<ha-card <update-available-card
.header="${this.supervisor.localize(
"common.update_available",
"count",
1
)}🎉"
>
<div class="card-content">
<hassio-card-content
.hass=${this.hass} .hass=${this.hass}
.title=${this.supervisor.localize( .narrow=${this.narrow}
"addon.dashboard.new_update_available", .supervisor=${this.supervisor}
"name", .addonSlug=${this.addon.slug}
this.addon.name, ></update-available-card>
"version",
this.addon.version_latest
)}
.description=${this.supervisor.localize(
"common.running_version",
"version",
this.addon.version
)}
icon=${mdiArrowUpBoldCircle}
iconClass="update"
></hassio-card-content>
${!this.addon.available && addonStoreInfo
? !addonArchIsSupported(
this.supervisor.info.supported_arch,
this.addon.arch
)
? html`
<ha-alert alert-type="warning">
${this.supervisor.localize(
"addon.dashboard.not_available_arch"
)}
</ha-alert>
`
: html`
<ha-alert alert-type="warning">
${this.supervisor.localize(
"addon.dashboard.not_available_arch",
"core_version_installed",
this.supervisor.core.version,
"core_version_needed",
addonStoreInfo.homeassistant
)}
</ha-alert>
`
: ""}
</div>
<div class="card-actions">
${this.addon.changelog
? html`
<mwc-button @click=${this._openChangelog}>
${this.supervisor.localize("addon.dashboard.changelog")}
</mwc-button>
`
: html`<span></span>`}
<mwc-button @click=${this._updateClicked}>
${this.supervisor.localize("common.update")}
</mwc-button>
</div>
</ha-card>
` `
: ""} : ""}
${!this.addon.protected ${!this.addon.protected
@@ -200,14 +143,18 @@ class HassioAddonInfo extends LitElement {
.title=${this.supervisor.localize( .title=${this.supervisor.localize(
"addon.dashboard.protection_mode.title" "addon.dashboard.protection_mode.title"
)} )}
.actionText=${this.supervisor.localize(
"addon.dashboard.protection_mode.enable"
)}
@alert-action-clicked=${this._protectionToggled}
> >
${this.supervisor.localize( ${this.supervisor.localize(
"addon.dashboard.protection_mode.content" "addon.dashboard.protection_mode.content"
)} )}
<mwc-button
slot="action"
.label=${this.supervisor.localize(
"addon.dashboard.protection_mode.enable"
)}
@click=${this._protectionToggled}
>
</mwc-button>
</ha-alert> </ha-alert>
` `
: ""} : ""}
@@ -899,22 +846,14 @@ class HassioAddonInfo extends LitElement {
private async _openChangelog(): Promise<void> { private async _openChangelog(): Promise<void> {
try { try {
let content = await fetchHassioAddonChangelog(this.hass, this.addon.slug); const content = await fetchHassioAddonChangelog(
if ( this.hass,
content.includes(`# ${this.addon.version}`) && this.addon.slug
content.includes(`# ${this.addon.version_latest}`) );
) {
const newcontent = content.split(`# ${this.addon.version}`)[0];
if (newcontent.includes(`# ${this.addon.version_latest}`)) {
// Only change the content if the new version still exist
// if the changelog does not have the newests version on top
// this will not be true, and we don't modify the content
content = newcontent;
}
}
showHassioMarkdownDialog(this, { showHassioMarkdownDialog(this, {
title: this.supervisor.localize("addon.dashboard.changelog"), title: this.supervisor.localize("addon.dashboard.changelog"),
content, content: extractChangelog(this.addon, content),
}); });
} catch (err: any) { } catch (err: any) {
showAlertDialog(this, { showAlertDialog(this, {
@@ -989,33 +928,6 @@ class HassioAddonInfo extends LitElement {
button.progress = false; button.progress = false;
} }
private async _updateClicked(): Promise<void> {
showDialogSupervisorUpdate(this, {
supervisor: this.supervisor,
name: this.addon.name,
version: this.addon.version_latest,
backupParams: {
name: `addon_${this.addon.slug}_${this.addon.version}`,
addons: [this.addon.slug],
homeassistant: false,
},
updateHandler: async () => this._updateAddon(),
});
}
private async _updateAddon(): Promise<void> {
await updateHassioAddon(this.hass, this.addon.slug);
fireEvent(this, "supervisor-collection-refresh", {
collection: "addon",
});
const eventdata = {
success: true,
response: undefined,
path: "update",
};
fireEvent(this, "hass-api-called", eventdata);
}
private async _startClicked(ev: CustomEvent): Promise<void> { private async _startClicked(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any; const button = ev.currentTarget as any;
button.progress = true; button.progress = true;
@@ -1244,6 +1156,17 @@ class HassioAddonInfo extends LitElement {
align-self: end; align-self: end;
} }
ha-alert mwc-button {
--mdc-theme-primary: var(--primary-text-color);
}
a {
text-decoration: none;
}
update-available-card {
padding-bottom: 16px;
}
@media (max-width: 720px) { @media (max-width: 720px) {
ha-chip { ha-chip {
line-height: 36px; line-height: 36px;

View File

@@ -158,7 +158,7 @@ export class HassioBackups extends LitElement {
} }
return html` return html`
<hass-tabs-subpage-data-table <hass-tabs-subpage-data-table
.tabs=${supervisorTabs} .tabs=${supervisorTabs(this.hass)}
.hass=${this.hass} .hass=${this.hass}
.localizeFunc=${this.supervisor.localize} .localizeFunc=${this.supervisor.localize}
.searchLabel=${this.supervisor.localize("search")} .searchLabel=${this.supervisor.localize("search")}
@@ -173,7 +173,8 @@ export class HassioBackups extends LitElement {
clickable clickable
selectable selectable
hasFab hasFab
main-page .mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
back-path="/config"
supervisor supervisor
> >
<ha-button-menu <ha-button-menu

View File

@@ -20,7 +20,9 @@ class HassioAddons extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div class="content"> <div class="content">
<h1>${this.supervisor.localize("dashboard.addons")}</h1> ${!atLeastVersion(this.hass.config.version, 2021, 12)
? html` <h1>${this.supervisor.localize("dashboard.addons")}</h1> `
: ""}
<div class="card-group"> <div class="card-group">
${!this.supervisor.supervisor.addons?.length ${!this.supervisor.supervisor.addons?.length
? html` ? html`

View File

@@ -1,5 +1,8 @@
import { mdiStorePlus } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version";
import "../../../src/components/ha-fab";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/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";
@@ -25,23 +28,41 @@ class HassioDashboard extends LitElement {
.localizeFunc=${this.supervisor.localize} .localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow} .narrow=${this.narrow}
.route=${this.route} .route=${this.route}
.tabs=${supervisorTabs} .tabs=${supervisorTabs(this.hass)}
main-page .mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
back-path="/config"
supervisor supervisor
hasFab
> >
<span slot="header"> <span slot="header">
${this.supervisor.localize("panel.dashboard")} ${this.supervisor.localize(
atLeastVersion(this.hass.config.version, 2021, 12)
? "panel.addons"
: "panel.dashboard"
)}
</span> </span>
<div class="content"> <div class="content">
${!atLeastVersion(this.hass.config.version, 2021, 12)
? html`
<hassio-update <hassio-update
.hass=${this.hass} .hass=${this.hass}
.supervisor=${this.supervisor} .supervisor=${this.supervisor}
></hassio-update> ></hassio-update>
`
: ""}
<hassio-addons <hassio-addons
.hass=${this.hass} .hass=${this.hass}
.supervisor=${this.supervisor} .supervisor=${this.supervisor}
></hassio-addons> ></hassio-addons>
</div> </div>
<a href="/hassio/store" slot="fab">
<ha-fab .label=${this.supervisor.localize("panel.store")} extended>
<ha-svg-icon
slot="icon"
.path=${mdiStorePlus}
></ha-svg-icon> </ha-fab
></a>
</hass-tabs-subpage> </hass-tabs-subpage>
`; `;
} }

View File

@@ -3,34 +3,18 @@ import { mdiHomeAssistant } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row"; import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-svg-icon"; import "../../../src/components/ha-svg-icon";
import {
extractApiErrorMessage,
HassioResponse,
ignoreSupervisorError,
} 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 { updateCore } from "../../../src/data/supervisor/core"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import {
Supervisor,
supervisorApiWsRequest,
} from "../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles"; import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types"; import { HomeAssistant } from "../../../src/types";
import { showDialogSupervisorUpdate } from "../dialogs/update/show-dialog-update";
import { hassioStyle } from "../resources/hassio-style"; import { hassioStyle } from "../resources/hassio-style";
const computeVersion = (key: string, version: string): string => const computeVersion = (key: string, version: string): string =>
@@ -73,26 +57,18 @@ export class HassioUpdate extends LitElement {
${this._renderUpdateCard( ${this._renderUpdateCard(
"Home Assistant Core", "Home Assistant Core",
"core", "core",
this.supervisor.core, this.supervisor.core
"hassio/homeassistant/update",
`https://${
this.supervisor.core.version_latest.includes("b") ? "rc" : "www"
}.home-assistant.io/latest-release-notes/`
)} )}
${this._renderUpdateCard( ${this._renderUpdateCard(
"Supervisor", "Supervisor",
"supervisor", "supervisor",
this.supervisor.supervisor, this.supervisor.supervisor
"hassio/supervisor/update",
`https://github.com//home-assistant/hassio/releases/tag/${this.supervisor.supervisor.version_latest}`
)} )}
${this.supervisor.host.features.includes("haos") ${this.supervisor.host.features.includes("haos")
? this._renderUpdateCard( ? this._renderUpdateCard(
"Operating System", "Operating System",
"os", "os",
this.supervisor.os, this.supervisor.os
"hassio/os/update",
`https://github.com//home-assistant/hassos/releases/tag/${this.supervisor.os.version_latest}`
) )
: ""} : ""}
</div> </div>
@@ -103,9 +79,7 @@ export class HassioUpdate extends LitElement {
private _renderUpdateCard( private _renderUpdateCard(
name: string, name: string,
key: string, key: string,
object: HassioHomeAssistantInfo | HassioSupervisorInfo | HassioHassOSInfo, object: HassioHomeAssistantInfo | HassioSupervisorInfo | HassioHassOSInfo
apiPath: string,
releaseNotesUrl: string
): TemplateResult { ): TemplateResult {
if (!object.update_available) { if (!object.update_available) {
return html``; return html``;
@@ -136,96 +110,15 @@ export class HassioUpdate extends LitElement {
</ha-settings-row> </ha-settings-row>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<a href=${releaseNotesUrl} target="_blank" rel="noreferrer"> <a href="/hassio/update-available/${key}">
<mwc-button> <mwc-button .label=${this.supervisor.localize("common.show")}>
${this.supervisor.localize("common.release_notes")}
</mwc-button> </mwc-button>
</a> </a>
<ha-progress-button
.apiPath=${apiPath}
.name=${name}
.key=${key}
.version=${object.version_latest}
@click=${this._confirmUpdate}
>
${this.supervisor.localize("common.update")}
</ha-progress-button>
</div> </div>
</ha-card> </ha-card>
`; `;
} }
private async _confirmUpdate(ev): Promise<void> {
const item = ev.currentTarget;
if (item.key === "core") {
showDialogSupervisorUpdate(this, {
supervisor: this.supervisor,
name: "Home Assistant Core",
version: this.supervisor.core.version_latest,
backupParams: {
name: `core_${this.supervisor.core.version}`,
folders: ["homeassistant"],
homeassistant: true,
},
updateHandler: async () => this._updateCore(),
});
return;
}
item.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize(
"confirm.update.title",
"name",
item.name
),
text: this.supervisor.localize(
"confirm.update.text",
"name",
item.name,
"version",
computeVersion(item.key, item.version)
),
confirmText: this.supervisor.localize("common.update"),
dismissText: this.supervisor.localize("common.cancel"),
});
if (!confirmed) {
item.progress = false;
return;
}
try {
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
await supervisorApiWsRequest(this.hass.connection, {
method: "post",
endpoint: item.apiPath.replace("hassio", ""),
timeout: null,
});
} else {
await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath);
}
fireEvent(this, "supervisor-collection-refresh", {
collection: item.key,
});
} catch (err: any) {
// Only show an error if the status code was not expected (user behind proxy)
// or no status at all(connection terminated)
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
showAlertDialog(this, {
title: this.supervisor.localize("common.error.update_failed"),
text: extractApiErrorMessage(err),
});
}
}
item.progress = false;
}
private async _updateCore(): Promise<void> {
await updateCore(this.hass);
fireEvent(this, "supervisor-collection-refresh", {
collection: "core",
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,

View File

@@ -1,203 +0,0 @@
import "@material/mwc-button/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-switch";
import {
extractApiErrorMessage,
ignoreSupervisorError,
} from "../../../../src/data/hassio/common";
import { createHassioPartialBackup } from "../../../../src/data/hassio/backup";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { SupervisorDialogSupervisorUpdateParams } from "./show-dialog-update";
@customElement("dialog-supervisor-update")
class DialogSupervisorUpdate extends LitElement {
public hass!: HomeAssistant;
@state() private _opened = false;
@state() private _createBackup = true;
@state() private _action: "backup" | "update" | null = null;
@state() private _error?: string;
@state()
private _dialogParams?: SupervisorDialogSupervisorUpdateParams;
public async showDialog(
params: SupervisorDialogSupervisorUpdateParams
): Promise<void> {
this._opened = true;
this._dialogParams = params;
await this.updateComplete;
}
public closeDialog(): void {
this._action = null;
this._createBackup = true;
this._error = undefined;
this._dialogParams = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
public focus(): void {
this.updateComplete.then(() =>
(
this.shadowRoot?.querySelector("[dialogInitialFocus]") as HTMLElement
)?.focus()
);
}
protected render(): TemplateResult {
if (!this._dialogParams) {
return html``;
}
return html`
<ha-dialog .open=${this._opened} scrimClickAction escapeKeyAction>
${this._action === null
? html`<slot name="heading">
<h2 id="title" class="header_title">
${this._dialogParams.supervisor.localize(
"confirm.update.title",
"name",
this._dialogParams.name
)}
</h2>
</slot>
<div>
${this._dialogParams.supervisor.localize(
"confirm.update.text",
"name",
this._dialogParams.name,
"version",
this._dialogParams.version
)}
</div>
<ha-settings-row>
<span slot="heading">
${this._dialogParams.supervisor.localize(
"dialog.update.backup"
)}
</span>
<span slot="description">
${this._dialogParams.supervisor.localize(
"dialog.update.create_backup",
"name",
this._dialogParams.name
)}
</span>
<ha-switch
.checked=${this._createBackup}
haptic
@click=${this._toggleBackup}
>
</ha-switch>
</ha-settings-row>
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
${this._dialogParams.supervisor.localize("common.cancel")}
</mwc-button>
<mwc-button
.disabled=${this._error !== undefined}
@click=${this._update}
slot="primaryAction"
>
${this._dialogParams.supervisor.localize("common.update")}
</mwc-button>`
: html`<ha-circular-progress alt="Updating" size="large" active>
</ha-circular-progress>
<p class="progress-text">
${this._action === "update"
? this._dialogParams.supervisor.localize(
"dialog.update.updating",
"name",
this._dialogParams.name,
"version",
this._dialogParams.version
)
: this._dialogParams.supervisor.localize(
"dialog.update.creating_backup",
"name",
this._dialogParams.name
)}
</p>`}
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
</ha-dialog>
`;
}
private _toggleBackup() {
this._createBackup = !this._createBackup;
}
private async _update() {
if (this._createBackup) {
this._action = "backup";
try {
await createHassioPartialBackup(
this.hass,
this._dialogParams!.backupParams
);
} catch (err: any) {
this._error = extractApiErrorMessage(err);
this._action = null;
return;
}
}
this._action = "update";
try {
await this._dialogParams!.updateHandler!();
} catch (err: any) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
this._error = extractApiErrorMessage(err);
this._action = null;
}
return;
}
this.closeDialog();
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
.form {
color: var(--primary-text-color);
}
ha-settings-row {
margin-top: 32px;
padding: 0;
}
ha-circular-progress {
display: block;
margin: 32px;
text-align: center;
}
.progress-text {
text-align: center;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-supervisor-update": DialogSupervisorUpdate;
}
}

View File

@@ -1,21 +0,0 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface SupervisorDialogSupervisorUpdateParams {
supervisor: Supervisor;
name: string;
version: string;
backupParams: any;
updateHandler: () => Promise<void>;
}
export const showDialogSupervisorUpdate = (
element: HTMLElement,
dialogParams: SupervisorDialogSupervisorUpdateParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-supervisor-update",
dialogImport: () => import("./dialog-supervisor-update"),
dialogParams,
});
};

View File

@@ -10,7 +10,7 @@ import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
import { Supervisor } from "../../src/data/supervisor/supervisor"; import { Supervisor } from "../../src/data/supervisor/supervisor";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager"; import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import "../../src/layouts/hass-loading-screen"; import "../../src/layouts/hass-loading-screen";
import { HomeAssistant, Route } from "../../src/types"; import { HomeAssistant } from "../../src/types";
import "./hassio-router"; import "./hassio-router";
import { SupervisorBaseElement } from "./supervisor-base-element"; import { SupervisorBaseElement } from "./supervisor-base-element";
@@ -24,8 +24,6 @@ export class HassioMain extends SupervisorBaseElement {
@property({ type: Boolean }) public narrow!: boolean; @property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public route?: Route;
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);

View File

@@ -34,6 +34,9 @@ const REDIRECTS: Redirects = {
supervisor_store: { supervisor_store: {
redirect: "/hassio/store", redirect: "/hassio/store",
}, },
supervisor_addons: {
redirect: "/hassio/dashboard",
},
supervisor_addon: { supervisor_addon: {
redirect: "/hassio/addon", redirect: "/hassio/addon",
params: { params: {

View File

@@ -35,6 +35,10 @@ class HassioRouter extends HassRouterPage {
backups: "dashboard", backups: "dashboard",
store: "dashboard", store: "dashboard",
system: "dashboard", system: "dashboard",
"update-available": {
tag: "update-available-dashboard",
load: () => import("./update-available/update-available-dashboard"),
},
addon: { addon: {
tag: "hassio-addon-dashboard", tag: "hassio-addon-dashboard",
load: () => import("./addon-view/hassio-addon-dashboard"), load: () => import("./addon-view/hassio-addon-dashboard"),

View File

@@ -1,16 +1,22 @@
import { mdiBackupRestore, mdiCogs, mdiStore, mdiViewDashboard } from "@mdi/js"; import {
mdiBackupRestore,
mdiCogs,
mdiPuzzle,
mdiViewDashboard,
} from "@mdi/js";
import { atLeastVersion } from "../../src/common/config/version";
import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage"; import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage";
import { HomeAssistant } from "../../src/types";
export const supervisorTabs: PageNavigation[] = [ export const supervisorTabs = (hass: HomeAssistant): PageNavigation[] => [
{ {
translationKey: "panel.dashboard", translationKey: atLeastVersion(hass.config.version, 2021, 12)
? "panel.addons"
: "panel.dashboard",
path: `/hassio/dashboard`, path: `/hassio/dashboard`,
iconPath: mdiViewDashboard, iconPath: atLeastVersion(hass.config.version, 2021, 12)
}, ? mdiPuzzle
{ : mdiViewDashboard,
translationKey: "panel.store",
path: `/hassio/store`,
iconPath: mdiStore,
}, },
{ {
translationKey: "panel.backups", translationKey: "panel.backups",

View File

@@ -25,7 +25,7 @@ import {
} from "../../src/data/supervisor/supervisor"; } from "../../src/data/supervisor/supervisor";
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin"; import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
import { urlSyncMixin } from "../../src/state/url-sync-mixin"; import { urlSyncMixin } from "../../src/state/url-sync-mixin";
import { HomeAssistant } from "../../src/types"; import { HomeAssistant, Route } from "../../src/types";
import { getTranslation } from "../../src/util/common-translation"; import { getTranslation } from "../../src/util/common-translation";
declare global { declare global {
@@ -38,6 +38,8 @@ declare global {
export class SupervisorBaseElement extends urlSyncMixin( export class SupervisorBaseElement extends urlSyncMixin(
ProvideHassLitMixin(LitElement) ProvideHassLitMixin(LitElement)
) { ) {
@property({ attribute: false }) public route?: Route;
@property({ attribute: false }) public supervisor: Partial<Supervisor> = { @property({ attribute: false }) public supervisor: Partial<Supervisor> = {
localize: () => "", localize: () => "",
}; };
@@ -108,8 +110,10 @@ export class SupervisorBaseElement extends urlSyncMixin(
this._language = this.hass.language; this._language = this.hass.language;
} }
this._initializeLocalize(); this._initializeLocalize();
if (this.route?.prefix === "/hassio") {
this._initSupervisor(); this._initSupervisor();
} }
}
private async _initializeLocalize() { private async _initializeLocalize() {
const { language, data } = await getTranslation( const { language, data } = await getTranslation(

View File

@@ -2,7 +2,7 @@ import "@material/mwc-button";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../src/common/dom/fire_event"; import { atLeastVersion } from "../../../src/common/config/version";
import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-button-menu"; import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
@@ -12,7 +12,7 @@ import {
fetchHassioStats, fetchHassioStats,
HassioStats, HassioStats,
} from "../../../src/data/hassio/common"; } from "../../../src/data/hassio/common";
import { restartCore, updateCore } from "../../../src/data/supervisor/core"; import { restartCore } from "../../../src/data/supervisor/core";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { import {
showAlertDialog, showAlertDialog,
@@ -22,7 +22,6 @@ import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types"; import { HomeAssistant } from "../../../src/types";
import { bytesToString } from "../../../src/util/bytes-to-string"; import { bytesToString } from "../../../src/util/bytes-to-string";
import "../components/supervisor-metric"; import "../components/supervisor-metric";
import { showDialogSupervisorUpdate } from "../dialogs/update/show-dialog-update";
import { hassioStyle } from "../resources/hassio-style"; import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-core-info") @customElement("hassio-core-info")
@@ -67,14 +66,15 @@ class HassioCoreInfo extends LitElement {
<span slot="description"> <span slot="description">
core-${this.supervisor.core.version_latest} core-${this.supervisor.core.version_latest}
</span> </span>
${this.supervisor.core.update_available ${!atLeastVersion(this.hass.config.version, 2021, 12) &&
this.supervisor.core.update_available
? html` ? html`
<ha-progress-button <a href="/hassio/update-available/core">
.title=${this.supervisor.localize("common.update")} <mwc-button
@click=${this._coreUpdate} .label=${this.supervisor.localize("common.show")}
> >
${this.supervisor.localize("common.update")} </mwc-button>
</ha-progress-button> </a>
` `
: ""} : ""}
</ha-settings-row> </ha-settings-row>
@@ -160,27 +160,6 @@ class HassioCoreInfo extends LitElement {
} }
} }
private async _coreUpdate(): Promise<void> {
showDialogSupervisorUpdate(this, {
supervisor: this.supervisor,
name: "Home Assistant Core",
version: this.supervisor.core.version_latest,
backupParams: {
name: `core_${this.supervisor.core.version}`,
folders: ["homeassistant"],
homeassistant: true,
},
updateHandler: async () => this._updateCore(),
});
}
private async _updateCore(): Promise<void> {
await updateCore(this.hass);
fireEvent(this, "supervisor-collection-refresh", {
collection: "core",
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,
@@ -239,6 +218,9 @@ class HassioCoreInfo extends LitElement {
mwc-list-item ha-svg-icon { mwc-list-item ha-svg-icon {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
a {
text-decoration: none;
}
`, `,
]; ];
} }

View File

@@ -21,7 +21,6 @@ import {
configSyncOS, configSyncOS,
rebootHost, rebootHost,
shutdownHost, shutdownHost,
updateOS,
} from "../../../src/data/hassio/host"; } from "../../../src/data/hassio/host";
import { import {
fetchNetworkInfo, fetchNetworkInfo,
@@ -106,11 +105,15 @@ class HassioHostInfo extends LitElement {
<span slot="description"> <span slot="description">
${this.supervisor.host.operating_system} ${this.supervisor.host.operating_system}
</span> </span>
${this.supervisor.os.update_available ${!atLeastVersion(this.hass.config.version, 2021, 12) &&
this.supervisor.os.update_available
? html` ? html`
<ha-progress-button @click=${this._osUpdate}> <a href="/hassio/update-available/os">
${this.supervisor.localize("commmon.update")} <mwc-button
</ha-progress-button> .label=${this.supervisor.localize("common.show")}
>
</mwc-button>
</a>
` `
: ""} : ""}
</ha-settings-row> </ha-settings-row>
@@ -333,50 +336,6 @@ class HassioHostInfo extends LitElement {
button.progress = false; 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: this.supervisor.localize(
"confirm.update.title",
"name",
"Home Assistant Operating System"
),
text: this.supervisor.localize(
"confirm.update.text",
"name",
"Home Assistant Operating System",
"version",
this.supervisor.os.version_latest
),
confirmText: this.supervisor.localize("common.update"),
dismissText: "no",
});
if (!confirmed) {
button.progress = false;
return;
}
try {
await updateOS(this.hass);
fireEvent(this, "supervisor-collection-refresh", { collection: "os" });
} catch (err: any) {
if (this.hass.connection.connected) {
showAlertDialog(this, {
title: this.supervisor.localize(
"common.failed_to_update_name",
"name",
"Home Assistant Operating System"
),
text: extractApiErrorMessage(err),
});
}
}
button.progress = false;
}
private async _changeNetworkClicked(): Promise<void> { private async _changeNetworkClicked(): Promise<void> {
showNetworkDialog(this, { showNetworkDialog(this, {
supervisor: this.supervisor, supervisor: this.supervisor,
@@ -494,6 +453,9 @@ class HassioHostInfo extends LitElement {
mwc-list-item ha-svg-icon { mwc-list-item ha-svg-icon {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
a {
text-decoration: none;
}
`, `,
]; ];
} }

View File

@@ -17,7 +17,6 @@ import {
restartSupervisor, restartSupervisor,
setSupervisorOption, setSupervisorOption,
SupervisorOptions, SupervisorOptions,
updateSupervisor,
} from "../../../src/data/hassio/supervisor"; } from "../../../src/data/hassio/supervisor";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { import {
@@ -77,16 +76,15 @@ class HassioSupervisorInfo extends LitElement {
<span slot="description"> <span slot="description">
supervisor-${this.supervisor.supervisor.version_latest} supervisor-${this.supervisor.supervisor.version_latest}
</span> </span>
${this.supervisor.supervisor.update_available ${!atLeastVersion(this.hass.config.version, 2021, 12) &&
this.supervisor.supervisor.update_available
? html` ? html`
<ha-progress-button <a href="/hassio/update-available/supervisor">
.title=${this.supervisor.localize( <mwc-button
"system.supervisor.update_supervisor" .label=${this.supervisor.localize("common.show")}
)}
@click=${this._supervisorUpdate}
> >
${this.supervisor.localize("common.update")} </mwc-button>
</ha-progress-button> </a>
` `
: ""} : ""}
</ha-settings-row> </ha-settings-row>
@@ -153,24 +151,28 @@ class HassioSupervisorInfo extends LitElement {
></ha-switch> ></ha-switch>
</ha-settings-row>` </ha-settings-row>`
: "" : ""
: html`<ha-alert : html`<ha-alert alert-type="warning">
alert-type="warning"
.actionText=${this.supervisor.localize("common.learn_more")}
@alert-action-clicked=${this._unsupportedDialog}
>
${this.supervisor.localize( ${this.supervisor.localize(
"system.supervisor.unsupported_title" "system.supervisor.unsupported_title"
)} )}
<mwc-button
slot="action"
.label=${this.supervisor.localize("common.learn_more")}
@click=${this._unsupportedDialog}
>
</mwc-button>
</ha-alert>`} </ha-alert>`}
${!this.supervisor.supervisor.healthy ${!this.supervisor.supervisor.healthy
? html`<ha-alert ? html`<ha-alert alert-type="error">
alert-type="error"
.actionText=${this.supervisor.localize("common.learn_more")}
@alert-action-clicked=${this._unhealthyDialog}
>
${this.supervisor.localize( ${this.supervisor.localize(
"system.supervisor.unhealthy_title" "system.supervisor.unhealthy_title"
)} )}
<mwc-button
slot="action"
.label=${this.supervisor.localize("common.learn_more")}
@click=${this._unhealthyDialog}
>
</mwc-button>
</ha-alert>` </ha-alert>`
: ""} : ""}
</div> </div>
@@ -337,51 +339,6 @@ class HassioSupervisorInfo extends LitElement {
} }
} }
private async _supervisorUpdate(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize(
"confirm.update.title",
"name",
"Supervisor"
),
text: this.supervisor.localize(
"confirm.update.text",
"name",
"Supervisor",
"version",
this.supervisor.supervisor.version_latest
),
confirmText: this.supervisor.localize("common.update"),
dismissText: this.supervisor.localize("common.cancel"),
});
if (!confirmed) {
button.progress = false;
return;
}
try {
await updateSupervisor(this.hass);
fireEvent(this, "supervisor-collection-refresh", {
collection: "supervisor",
});
} catch (err: any) {
showAlertDialog(this, {
title: this.supervisor.localize(
"common.failed_to_update_name",
"name",
"Supervisor"
),
text: extractApiErrorMessage(err),
});
} finally {
button.progress = false;
}
}
private async _diagnosticsInformationDialog(): Promise<void> { private async _diagnosticsInformationDialog(): Promise<void> {
await showAlertDialog(this, { await showAlertDialog(this, {
title: this.supervisor.localize( title: this.supervisor.localize(
@@ -513,6 +470,12 @@ class HassioSupervisorInfo extends LitElement {
white-space: normal; white-space: normal;
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
ha-alert mwc-button {
--mdc-theme-primary: var(--primary-text-color);
}
a {
text-decoration: none;
}
`, `,
]; ];
} }

View File

@@ -1,5 +1,6 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/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";
@@ -28,8 +29,9 @@ class HassioSystem extends LitElement {
.localizeFunc=${this.supervisor.localize} .localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow} .narrow=${this.narrow}
.route=${this.route} .route=${this.route}
.tabs=${supervisorTabs} .tabs=${supervisorTabs(this.hass)}
main-page .mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
back-path="/config"
supervisor supervisor
> >
<span slot="header"> ${this.supervisor.localize("panel.system")} </span> <span slot="header"> ${this.supervisor.localize("panel.system")} </span>

View File

@@ -0,0 +1,402 @@
import "@material/mwc-list/mwc-list-item";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/common/search/search-input";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-alert";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card";
import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-faded";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-markdown";
import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-svg-icon";
import "../../../src/components/ha-switch";
import {
fetchHassioAddonChangelog,
fetchHassioAddonInfo,
HassioAddonDetails,
updateHassioAddon,
} from "../../../src/data/hassio/addon";
import {
createHassioPartialBackup,
HassioPartialBackupCreateParams,
} from "../../../src/data/hassio/backup";
import {
extractApiErrorMessage,
ignoreSupervisorError,
} from "../../../src/data/hassio/common";
import { updateOS } from "../../../src/data/hassio/host";
import { updateSupervisor } from "../../../src/data/hassio/supervisor";
import { updateCore } from "../../../src/data/supervisor/core";
import { StoreAddon } from "../../../src/data/supervisor/store";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-subpage";
import "../../../src/layouts/hass-tabs-subpage";
import { SUPERVISOR_UPDATE_NAMES } from "../../../src/panels/config/dashboard/ha-config-updates";
import { HomeAssistant, Route } from "../../../src/types";
import { documentationUrl } from "../../../src/util/documentation-url";
import { addonArchIsSupported, extractChangelog } from "../util/addon";
declare global {
interface HASSDomEvents {
"update-complete": undefined;
}
}
type updateType = "os" | "supervisor" | "core" | "addon";
const changelogUrl = (
hass: HomeAssistant,
entry: updateType,
version: string
): string | undefined => {
if (entry === "addon") {
return undefined;
}
if (entry === "core") {
return version?.includes("dev")
? "https://github.com/home-assistant/core/commits/dev"
: documentationUrl(hass, "/latest-release-notes/");
}
if (entry === "os") {
return version?.includes("dev")
? "https://github.com/home-assistant/operating-system/commits/dev"
: `https://github.com/home-assistant/operating-system/releases/tag/${version}`;
}
if (entry === "supervisor") {
return version?.includes("dev")
? "https://github.com/home-assistant/supervisor/commits/main"
: `https://github.com/home-assistant/supervisor/releases/tag/${version}`;
}
return undefined;
};
@customElement("update-available-card")
class UpdateAvailableCard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public addonSlug?: string;
@state() private _updateType?: updateType;
@state() private _changelogContent?: string;
@state() private _addonInfo?: HassioAddonDetails;
@state() private _action: "backup" | "update" | null = null;
@state() private _error?: string;
private _addonStoreInfo = memoizeOne(
(slug: string, storeAddons: StoreAddon[]) =>
storeAddons.find((addon) => addon.slug === slug)
);
protected render(): TemplateResult {
if (
!this._updateType ||
(this._updateType === "addon" && !this._addonInfo)
) {
return html``;
}
const changelog = changelogUrl(this.hass, this._updateType, this._version);
return html`
<ha-card
.header=${this.supervisor.localize("update_available.update_name", {
name: this._name,
})}
>
<div class="card-content">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
${this._action === null
? html`
${this._changelogContent
? html`
<ha-faded>
<ha-markdown .content=${this._changelogContent}>
</ha-markdown>
</ha-faded>
`
: ""}
<div class="versions">
<p>
${this.supervisor.localize("update_available.description", {
name: this._name,
version: this._version,
newest_version: this._version_latest,
})}
</p>
</div>
${["core", "addon"].includes(this._updateType)
? html`
<ha-formfield
.label=${this.supervisor.localize(
"update_available.create_backup"
)}
>
<ha-checkbox checked></ha-checkbox>
</ha-formfield>
`
: ""}
`
: html`<ha-circular-progress alt="Updating" size="large" active>
</ha-circular-progress>
<p class="progress-text">
${this._action === "update"
? this.supervisor.localize("update_available.updating", {
name: this._name,
version: this._version_latest,
})
: this.supervisor.localize(
"update_available.creating_backup",
{ name: this._name }
)}
</p>`}
</div>
${this._action === null
? html`
<div class="card-actions">
${changelog
? html`<a .href=${changelog} target="_blank" rel="noreferrer">
<mwc-button
.label=${this.supervisor.localize(
"update_available.open_release_notes"
)}
>
</mwc-button>
</a>`
: ""}
<span></span>
<ha-progress-button
.disabled=${!this._version ||
(this._shouldCreateBackup &&
this.supervisor.info?.state !== "running")}
@click=${this._update}
raised
>
${this.supervisor.localize("common.update")}
</ha-progress-button>
</div>
`
: ""}
</ha-card>
`;
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
const pathPart = this.route?.path.substring(1, this.route.path.length);
const updateType = ["core", "os", "supervisor"].includes(pathPart)
? pathPart
: "addon";
this._updateType = updateType as updateType;
if (updateType === "addon") {
if (!this.addonSlug) {
this.addonSlug = pathPart;
}
this._loadAddonData();
}
}
get _shouldCreateBackup(): boolean {
const checkbox = this.shadowRoot?.querySelector("ha-checkbox");
if (checkbox) {
return checkbox.checked;
}
return true;
}
get _version(): string {
return this._updateType
? this._updateType === "addon"
? this._addonInfo!.version
: this.supervisor[this._updateType]?.version || ""
: "";
}
get _version_latest(): string {
return this._updateType
? this._updateType === "addon"
? this._addonInfo!.version_latest
: this.supervisor[this._updateType]?.version_latest || ""
: "";
}
get _name(): string {
return this._updateType
? this._updateType === "addon"
? this._addonInfo!.name
: SUPERVISOR_UPDATE_NAMES[this._updateType]
: "";
}
private async _loadAddonData() {
try {
this._addonInfo = await fetchHassioAddonInfo(this.hass, this.addonSlug!);
} catch (err) {
showAlertDialog(this, {
title: this._updateType,
text: extractApiErrorMessage(err),
});
return;
}
const addonStoreInfo =
!this._addonInfo.detached && !this._addonInfo.available
? this._addonStoreInfo(
this._addonInfo.slug,
this.supervisor.store.addons
)
: undefined;
if (this._addonInfo.changelog) {
try {
const content = await fetchHassioAddonChangelog(
this.hass,
this.addonSlug!
);
this._changelogContent = extractChangelog(this._addonInfo, content);
} catch (err) {
this._error = extractApiErrorMessage(err);
return;
}
}
if (!this._addonInfo.available && addonStoreInfo) {
if (
!addonArchIsSupported(
this.supervisor.info.supported_arch,
this._addonInfo.arch
)
) {
this._error = this.supervisor.localize(
"addon.dashboard.not_available_arch"
);
} else {
this._error = this.supervisor.localize(
"addon.dashboard.not_available_version",
{
core_version_installed: this.supervisor.core.version,
core_version_needed: addonStoreInfo.homeassistant,
}
);
}
}
}
private async _update() {
this._error = undefined;
if (this._shouldCreateBackup) {
let backupArgs: HassioPartialBackupCreateParams;
if (this._updateType === "addon") {
backupArgs = {
name: `addon_${this.addonSlug}_${this._version}`,
addons: [this.addonSlug!],
homeassistant: false,
};
} else {
backupArgs = {
name: `${this._updateType}_${this._version}`,
folders: ["homeassistant"],
homeassistant: true,
};
}
this._action = "backup";
try {
await createHassioPartialBackup(this.hass, backupArgs);
} catch (err: any) {
this._error = extractApiErrorMessage(err);
this._action = null;
return;
}
}
this._action = "update";
try {
if (this._updateType === "addon") {
await updateHassioAddon(this.hass, this.addonSlug!);
} else if (this._updateType === "core") {
await updateCore(this.hass);
} else if (this._updateType === "os") {
await updateOS(this.hass);
} else if (this._updateType === "supervisor") {
await updateSupervisor(this.hass);
}
} catch (err: any) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
this._error = extractApiErrorMessage(err);
this._action = null;
return;
}
}
fireEvent(this, "update-complete");
}
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
}
ha-card {
margin: auto;
}
a {
text-decoration: none;
color: var(--primary-text-color);
}
ha-settings-row {
padding: 0;
}
.card-actions {
display: flex;
justify-content: space-between;
border-top: none;
padding: 0 8px 8px;
}
ha-circular-progress {
display: block;
margin: 32px;
text-align: center;
}
.progress-text {
text-align: center;
}
ha-markdown {
padding-bottom: 8px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"update-available-card": UpdateAvailableCard;
}
}

View File

@@ -0,0 +1,59 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import "../../../src/layouts/hass-subpage";
import { HomeAssistant, Route } from "../../../src/types";
import "./update-available-card";
@customElement("update-available-dashboard")
class UpdateAvailableDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public route!: Route;
protected render(): TemplateResult {
return html`
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
>
<update-available-card
.hass=${this.hass}
.supervisor=${this.supervisor}
.route=${this.route}
.narrow=${this.narrow}
@update-complete=${this._updateComplete}
></update-available-card>
</hass-subpage>
`;
}
private _updateComplete() {
history.back();
}
static get styles(): CSSResultGroup {
return css`
hass-subpage {
--app-header-background-color: var(--primary-background-color);
--app-header-text-color: var(--sidebar-text-color);
}
update-available-card {
margin: auto;
margin-top: 16px;
max-width: 600px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"update-available-dashboard": UpdateAvailableDashboard;
}
}

View File

@@ -1,7 +1,30 @@
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { HassioAddonDetails } from "../../../src/data/hassio/addon";
import { SupervisorArch } from "../../../src/data/supervisor/supervisor"; import { SupervisorArch } from "../../../src/data/supervisor/supervisor";
export const addonArchIsSupported = memoizeOne( export const addonArchIsSupported = memoizeOne(
(supported_archs: SupervisorArch[], addon_archs: SupervisorArch[]) => (supported_archs: SupervisorArch[], addon_archs: SupervisorArch[]) =>
addon_archs.some((arch) => supported_archs.includes(arch)) addon_archs.some((arch) => supported_archs.includes(arch))
); );
export const extractChangelog = (
addon: HassioAddonDetails,
content: string
): string => {
if (content.startsWith("# Changelog")) {
content = content.substr(12, content.length);
}
if (
content.includes(`# ${addon.version}`) &&
content.includes(`# ${addon.version_latest}`)
) {
const newcontent = content.split(`# ${addon.version}`)[0];
if (newcontent.includes(`# ${addon.version_latest}`)) {
// Only change the content if the new version still exist
// if the changelog does not have the newests version on top
// this will not be true, and we don't modify the content
content = newcontent;
}
}
return content;
};

View File

@@ -23,16 +23,16 @@
"dependencies": { "dependencies": {
"@braintree/sanitize-url": "^5.0.2", "@braintree/sanitize-url": "^5.0.2",
"@codemirror/commands": "^0.19.5", "@codemirror/commands": "^0.19.5",
"@codemirror/gutter": "^0.19.3", "@codemirror/gutter": "^0.19.4",
"@codemirror/highlight": "^0.19.6", "@codemirror/highlight": "^0.19.6",
"@codemirror/history": "^0.19.0", "@codemirror/history": "^0.19.0",
"@codemirror/legacy-modes": "^0.19.0", "@codemirror/legacy-modes": "^0.19.0",
"@codemirror/rectangular-selection": "^0.19.1", "@codemirror/rectangular-selection": "^0.19.1",
"@codemirror/search": "^0.19.2", "@codemirror/search": "^0.19.2",
"@codemirror/state": "^0.19.2", "@codemirror/state": "^0.19.4",
"@codemirror/stream-parser": "^0.19.2", "@codemirror/stream-parser": "^0.19.2",
"@codemirror/text": "^0.19.4", "@codemirror/text": "^0.19.5",
"@codemirror/view": "^0.19.9", "@codemirror/view": "^0.19.15",
"@formatjs/intl-datetimeformat": "^4.2.5", "@formatjs/intl-datetimeformat": "^4.2.5",
"@formatjs/intl-getcanonicallocales": "^1.8.0", "@formatjs/intl-getcanonicallocales": "^1.8.0",
"@formatjs/intl-locale": "^2.4.40", "@formatjs/intl-locale": "^2.4.40",
@@ -67,8 +67,8 @@
"@material/mwc-tab-bar": "0.25.3", "@material/mwc-tab-bar": "0.25.3",
"@material/mwc-textfield": "0.25.3", "@material/mwc-textfield": "0.25.3",
"@material/top-app-bar": "14.0.0-canary.261f2db59.0", "@material/top-app-bar": "14.0.0-canary.261f2db59.0",
"@mdi/js": "6.4.95", "@mdi/js": "6.5.95",
"@mdi/svg": "6.4.95", "@mdi/svg": "6.5.95",
"@polymer/app-layout": "^3.1.0", "@polymer/app-layout": "^3.1.0",
"@polymer/iron-flex-layout": "^3.0.1", "@polymer/iron-flex-layout": "^3.0.1",
"@polymer/iron-icon": "^3.0.1", "@polymer/iron-icon": "^3.0.1",
@@ -102,7 +102,7 @@
"fuse.js": "^6.0.0", "fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2", "google-timezones-json": "^1.0.2",
"hls.js": "^1.0.11", "hls.js": "^1.0.11",
"home-assistant-js-websocket": "^5.11.1", "home-assistant-js-websocket": "^5.11.3",
"idb-keyval": "^5.1.3", "idb-keyval": "^5.1.3",
"intl-messageformat": "^9.9.1", "intl-messageformat": "^9.9.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
@@ -115,6 +115,7 @@
"node-vibrant": "3.2.1-alpha.1", "node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "^0.3.2", "proxy-polyfill": "^0.3.2",
"punycode": "^2.1.1", "punycode": "^2.1.1",
"qr-scanner": "^1.3.0",
"qrcode": "^1.4.4", "qrcode": "^1.4.4",
"regenerator-runtime": "^0.13.8", "regenerator-runtime": "^0.13.8",
"resize-observer-polyfill": "^1.5.1", "resize-observer-polyfill": "^1.5.1",

View File

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

View File

@@ -101,17 +101,13 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
this._fetchAuthProviders(); this._fetchAuthProviders();
if (matchMedia("(prefers-color-scheme: dark)").matches) { if (matchMedia("(prefers-color-scheme: dark)").matches) {
applyThemesOnElement( applyThemesOnElement(document.documentElement, {
document.documentElement,
{
default_theme: "default", default_theme: "default",
default_dark_theme: null, default_dark_theme: null,
themes: {}, themes: {},
darkMode: false, darkMode: true,
}, theme: "default",
"default", });
{ dark: true }
);
} }
if (!this.redirectUri) { if (!this.redirectUri) {

View File

@@ -3,5 +3,5 @@ import { CAST_DEV_APP_ID } from "./dev_const";
// Guard dev mode with `__dev__` so it can only ever be enabled in dev mode. // Guard dev mode with `__dev__` so it can only ever be enabled in dev mode.
export const CAST_DEV = __DEV__ && true; export const CAST_DEV = __DEV__ && true;
export const CAST_APP_ID = CAST_DEV ? CAST_DEV_APP_ID : "B12CE3CA"; export const CAST_APP_ID = CAST_DEV ? CAST_DEV_APP_ID : "A078F6B0";
export const CAST_NS = "urn:x-cast:com.nabucasa.hast"; export const CAST_NS = "urn:x-cast:com.nabucasa.hast";

View File

@@ -11,4 +11,20 @@ export interface ReceiverStatusMessage extends BaseCastMessage {
urlPath?: string | null; urlPath?: string | null;
} }
export interface ReceiverErrorMessage extends BaseCastMessage {
type: "receiver_error";
error_code: ReceiverErrorCode;
error_message: string;
}
export const enum ReceiverErrorCode {
CONNECTION_FAILED = 1,
AUTHENTICATION_FAILED = 2,
CONNECTION_LOST = 3,
HASS_URL_MISSING = 4,
NO_HTTPS = 5,
NOT_CONNECTED = 21,
FETCH_CONFIG_FAILED = 22,
}
export type SenderMessage = ReceiverStatusMessage; export type SenderMessage = ReceiverStatusMessage;

View File

@@ -61,3 +61,14 @@ export const COLORS = [
export function getColorByIndex(index: number) { export function getColorByIndex(index: number) {
return COLORS[index % COLORS.length]; return COLORS[index % COLORS.length];
} }
export function getGraphColorByIndex(
index: number,
style: CSSStyleDeclaration
) {
// The CSS vars for the colors use range 1..n, so we need to adjust the index from the internal 0..n color index range.
return (
style.getPropertyValue(`--graph-color-${index + 1}`) ||
getColorByIndex(index)
);
}

View File

@@ -7,7 +7,13 @@ export const canShowPage = (hass: HomeAssistant, page: PageNavigation) =>
!hideAdvancedPage(hass, page); !hideAdvancedPage(hass, page);
const isLoadedIntegration = (hass: HomeAssistant, page: PageNavigation) => const isLoadedIntegration = (hass: HomeAssistant, page: PageNavigation) =>
!page.component || isComponentLoaded(hass, page.component); page.component
? isComponentLoaded(hass, page.component)
: page.components
? page.components.some((integration) =>
isComponentLoaded(hass, integration)
)
: true;
const isCore = (page: PageNavigation) => page.core; const isCore = (page: PageNavigation) => page.core;
const isAdvancedPage = (page: PageNavigation) => page.advancedOnly; const isAdvancedPage = (page: PageNavigation) => page.advancedOnly;
const userWantsAdvanced = (hass: HomeAssistant) => hass.userData?.showAdvanced; const userWantsAdvanced = (hass: HomeAssistant) => hass.userData?.showAdvanced;

View File

@@ -119,6 +119,7 @@ export const FIXED_DEVICE_CLASS_ICONS = {
current: mdiCurrentAc, current: mdiCurrentAc,
date: mdiCalendar, date: mdiCalendar,
energy: mdiLightningBolt, energy: mdiLightningBolt,
frequency: mdiSineWave,
gas: mdiGasCylinder, gas: mdiGasCylinder,
humidity: mdiWaterPercent, humidity: mdiWaterPercent,
illuminance: mdiBrightness5, illuminance: mdiBrightness5,
@@ -187,8 +188,9 @@ export const DOMAINS_WITH_MORE_INFO = [
"weather", "weather",
]; ];
/** Domains that show no more info dialog. */ /** Domains that do not show the default more info dialog content (e.g. the attribute section)
export const DOMAINS_HIDE_MORE_INFO = [ * and do not have a separate more info (so not in DOMAINS_WITH_MORE_INFO). */
export const DOMAINS_HIDE_DEFAULT_MORE_INFO = [
"input_number", "input_number",
"input_select", "input_select",
"input_text", "input_text",
@@ -197,6 +199,31 @@ export const DOMAINS_HIDE_MORE_INFO = [
"select", "select",
]; ];
/** Domains that render an input element instead of a text value when rendered in a row.
* Those rows should then not show a cursor pointer when hovered (which would normally
* be the default) unless the element itself enforces it (e.g. a button). Also those elements
* should not act as a click target to open the more info dialog (the row name and state icon
* still do of course) as the click might instead e.g. activate the input field that this row shows.
*/
export const DOMAINS_INPUT_ROW = [
"cover",
"fan",
"humidifier",
"input_boolean",
"input_datetime",
"input_number",
"input_select",
"input_text",
"light",
"lock",
"media_player",
"number",
"scene",
"script",
"select",
"switch",
];
/** Domains that should have the history hidden in the more info dialog. */ /** Domains that should have the history hidden in the more info dialog. */
export const DOMAINS_MORE_INFO_NO_HISTORY = ["camera", "configurator", "scene"]; export const DOMAINS_MORE_INFO_NO_HISTORY = ["camera", "configurator", "scene"];

View File

@@ -23,9 +23,9 @@ let PROCESSED_THEMES: Record<string, ProcessedTheme> = {};
* Apply a theme to an element by setting the CSS variables on it. * Apply a theme to an element by setting the CSS variables on it.
* *
* element: Element to apply theme on. * element: Element to apply theme on.
* themes: HASS theme information. * themes: HASS theme information (e.g. active dark mode and globally active theme name).
* selectedTheme: Selected theme. * selectedTheme: Selected theme (used to override the globally active theme for this element).
* themeSettings: Settings such as selected dark mode and colors. * themeSettings: Additional settings such as selected colors.
*/ */
export const applyThemesOnElement = ( export const applyThemesOnElement = (
element, element,
@@ -33,31 +33,33 @@ export const applyThemesOnElement = (
selectedTheme?: string, selectedTheme?: string,
themeSettings?: Partial<HomeAssistant["selectedTheme"]> themeSettings?: Partial<HomeAssistant["selectedTheme"]>
) => { ) => {
let cacheKey = selectedTheme; // If there is no explicitly desired theme provided, we automatically
let themeRules: Partial<ThemeVars> = {}; // use the active one from `themes`.
const themeToApply = selectedTheme || themes.theme;
// If there is no explicitly desired dark mode provided, we automatically // If there is no explicitly desired dark mode provided, we automatically
// use the active one from hass.themes. // use the active one from `themes`.
if (!themeSettings || themeSettings?.dark === undefined) { const darkMode =
themeSettings = { themeSettings && themeSettings?.dark !== undefined
...themeSettings, ? themeSettings?.dark
dark: themes.darkMode, : themes.darkMode;
};
}
if (themeSettings.dark) { let cacheKey = themeToApply;
let themeRules: Partial<ThemeVars> = {};
if (darkMode) {
cacheKey = `${cacheKey}__dark`; cacheKey = `${cacheKey}__dark`;
themeRules = { ...darkStyles }; themeRules = { ...darkStyles };
} }
if (selectedTheme === "default") { if (themeToApply === "default") {
// Determine the primary and accent colors from the current settings. // Determine the primary and accent colors from the current settings.
// Fallbacks are implicitly the HA default blue and orange or the // Fallbacks are implicitly the HA default blue and orange or the
// derived "darkStyles" values, depending on the light vs dark mode. // derived "darkStyles" values, depending on the light vs dark mode.
const primaryColor = themeSettings.primaryColor; const primaryColor = themeSettings?.primaryColor;
const accentColor = themeSettings.accentColor; const accentColor = themeSettings?.accentColor;
if (themeSettings.dark && primaryColor) { if (darkMode && primaryColor) {
themeRules["app-header-background-color"] = hexBlend( themeRules["app-header-background-color"] = hexBlend(
primaryColor, primaryColor,
"#121212", "#121212",
@@ -98,17 +100,17 @@ export const applyThemesOnElement = (
// Custom theme logic (not relevant for default theme, since it would override // Custom theme logic (not relevant for default theme, since it would override
// the derived calculations from above) // the derived calculations from above)
if ( if (
selectedTheme && themeToApply &&
selectedTheme !== "default" && themeToApply !== "default" &&
themes.themes[selectedTheme] themes.themes[themeToApply]
) { ) {
// Apply theme vars that are relevant for all modes (but extract the "modes" section first) // Apply theme vars that are relevant for all modes (but extract the "modes" section first)
const { modes, ...baseThemeRules } = themes.themes[selectedTheme]; const { modes, ...baseThemeRules } = themes.themes[themeToApply];
themeRules = { ...themeRules, ...baseThemeRules }; themeRules = { ...themeRules, ...baseThemeRules };
// Apply theme vars for the specific mode if available // Apply theme vars for the specific mode if available
if (modes) { if (modes) {
if (themeSettings?.dark) { if (darkMode) {
themeRules = { ...themeRules, ...modes.dark }; themeRules = { ...themeRules, ...modes.dark };
} else { } else {
themeRules = { ...themeRules, ...modes.light }; themeRules = { ...themeRules, ...modes.light };

View File

@@ -1,30 +1,33 @@
import { import {
mdiAccount, mdiAccount,
mdiAccountArrowRight, mdiAccountArrowRight,
mdiAirHumidifierOff,
mdiAirHumidifier, mdiAirHumidifier,
mdiFlash, mdiAirHumidifierOff,
mdiBluetooth, mdiBluetooth,
mdiBluetoothConnect, mdiBluetoothConnect,
mdiCalendar,
mdiCast,
mdiCastConnected,
mdiClock,
mdiEmoticonDead,
mdiFlash,
mdiGestureTapButton,
mdiLanConnect, mdiLanConnect,
mdiLanDisconnect, mdiLanDisconnect,
mdiLockOpen, mdiLock,
mdiLockAlert, mdiLockAlert,
mdiLockClock, mdiLockClock,
mdiLock, mdiLockOpen,
mdiCastConnected, mdiPackageUp,
mdiCast,
mdiEmoticonDead,
mdiPowerPlug, mdiPowerPlug,
mdiPowerPlugOff, mdiPowerPlugOff,
mdiRestart,
mdiSleep, mdiSleep,
mdiTimerSand, mdiTimerSand,
mdiToggleSwitch, mdiToggleSwitch,
mdiToggleSwitchOff, mdiToggleSwitchOff,
mdiZWave,
mdiClock,
mdiCalendar,
mdiWeatherNight, mdiWeatherNight,
mdiZWave,
} from "@mdi/js"; } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
/** /**
@@ -52,6 +55,16 @@ export const domainIcon = (
case "binary_sensor": case "binary_sensor":
return binarySensorIcon(compareState, stateObj); return binarySensorIcon(compareState, stateObj);
case "button":
switch (stateObj?.attributes.device_class) {
case "restart":
return mdiRestart;
case "update":
return mdiPackageUp;
default:
return mdiGestureTapButton;
}
case "cover": case "cover":
return coverIcon(compareState, stateObj); return coverIcon(compareState, stateObj);

View File

@@ -95,7 +95,7 @@ export default class HaChartBase extends LitElement {
borderColor: dataset.borderColor as string, borderColor: dataset.borderColor as string,
})} })}
></div> ></div>
${dataset.label} <div class="label">${dataset.label}</div>
</li>` </li>`
)} )}
</ul> </ul>
@@ -278,11 +278,9 @@ export default class HaChartBase extends LitElement {
} }
.chartLegend li { .chartLegend li {
cursor: pointer; cursor: pointer;
display: inline-flex; display: inline-grid;
grid-auto-flow: column;
padding: 0 8px; padding: 0 8px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
box-sizing: border-box; box-sizing: border-box;
align-items: center; align-items: center;
color: var(--secondary-text-color); color: var(--secondary-text-color);
@@ -290,6 +288,11 @@ export default class HaChartBase extends LitElement {
.chartLegend .hidden { .chartLegend .hidden {
text-decoration: line-through; text-decoration: line-through;
} }
.chartLegend .label {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.chartLegend .bullet, .chartLegend .bullet,
.chartTooltip .bullet { .chartTooltip .bullet {
border-width: 1px; border-width: 1px;

View File

@@ -1,7 +1,7 @@
import type { ChartData, ChartDataset, ChartOptions } from "chart.js"; import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
import { html, LitElement, PropertyValues } from "lit"; import { html, LitElement, PropertyValues } from "lit";
import { property, state } from "lit/decorators"; import { property, state } from "lit/decorators";
import { getColorByIndex } from "../../common/color/colors"; import { getGraphColorByIndex } from "../../common/color/colors";
import { import {
formatNumber, formatNumber,
numberFormatToLocale, numberFormatToLocale,
@@ -190,7 +190,7 @@ class StateHistoryChartLine extends LitElement {
color?: string color?: string
) => { ) => {
if (!color) { if (!color) {
color = getColorByIndex(colorIndex); color = getGraphColorByIndex(colorIndex, computedStyles);
colorIndex++; colorIndex++;
} }
data.push({ data.push({

View File

@@ -2,7 +2,7 @@ import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { getColorByIndex } from "../../common/color/colors"; import { getGraphColorByIndex } from "../../common/color/colors";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time"; import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import { computeDomain } from "../../common/entity/compute_domain"; import { computeDomain } from "../../common/entity/compute_domain";
import { numberFormatToLocale } from "../../common/number/format_number"; import { numberFormatToLocale } from "../../common/number/format_number";
@@ -71,7 +71,7 @@ const getColor = (
stateColorMap.set(stateString, color); stateColorMap.set(stateString, color);
return color; return color;
} }
const color = getColorByIndex(colorIndex); const color = getGraphColorByIndex(colorIndex, computedStyles);
colorIndex++; colorIndex++;
stateColorMap.set(stateString, color); stateColorMap.set(stateString, color);
return color; return color;

View File

@@ -13,7 +13,7 @@ import {
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { getColorByIndex } from "../../common/color/colors"; import { getGraphColorByIndex } from "../../common/color/colors";
import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { computeStateName } from "../../common/entity/compute_state_name"; import { computeStateName } from "../../common/entity/compute_state_name";
import { import {
@@ -59,6 +59,8 @@ class StatisticsChart extends LitElement {
@state() private _chartOptions?: ChartOptions; @state() private _chartOptions?: ChartOptions;
private _computedStyle?: CSSStyleDeclaration;
protected shouldUpdate(changedProps: PropertyValues): boolean { protected shouldUpdate(changedProps: PropertyValues): boolean {
return changedProps.size > 1 || !changedProps.has("hass"); return changedProps.size > 1 || !changedProps.has("hass");
} }
@@ -72,6 +74,10 @@ class StatisticsChart extends LitElement {
} }
} }
public firstUpdated() {
this._computedStyle = getComputedStyle(this);
}
protected render(): TemplateResult { protected render(): TemplateResult {
if (!isComponentLoaded(this.hass, "history")) { if (!isComponentLoaded(this.hass, "history")) {
return html`<div class="info"> return html`<div class="info">
@@ -280,7 +286,7 @@ class StatisticsChart extends LitElement {
prevValues = dataValues; prevValues = dataValues;
}; };
const color = getColorByIndex(colorIndex); const color = getGraphColorByIndex(colorIndex, this._computedStyle!);
colorIndex++; colorIndex++;
const statTypes: this["statTypes"] = []; const statTypes: this["statTypes"] = [];

View File

@@ -678,7 +678,7 @@ export class HaDataTable extends LitElement {
padding-left: 16px; padding-left: 16px;
/* @noflip */ /* @noflip */
padding-right: 0; padding-right: 0;
width: 56px; width: 60px;
} }
:host([dir="rtl"]) .mdc-data-table__header-cell--checkbox, :host([dir="rtl"]) .mdc-data-table__header-cell--checkbox,
:host([dir="rtl"]) .mdc-data-table__cell--checkbox { :host([dir="rtl"]) .mdc-data-table__cell--checkbox {

View File

@@ -1,4 +1,3 @@
import "@material/mwc-button/mwc-button";
import { import {
mdiAlertCircleOutline, mdiAlertCircleOutline,
mdiAlertOutline, mdiAlertOutline,
@@ -23,7 +22,6 @@ const ALERT_ICONS = {
declare global { declare global {
interface HASSDomEvents { interface HASSDomEvents {
"alert-dismissed-clicked": undefined; "alert-dismissed-clicked": undefined;
"alert-action-clicked": undefined;
} }
} }
@@ -37,8 +35,6 @@ class HaAlert extends LitElement {
| "error" | "error"
| "success" = "info"; | "success" = "info";
@property({ attribute: "action-text" }) public actionText = "";
@property({ type: Boolean }) public dismissable = false; @property({ type: Boolean }) public dismissable = false;
@property({ type: Boolean }) public rtl = false; @property({ type: Boolean }) public rtl = false;
@@ -52,7 +48,9 @@ class HaAlert extends LitElement {
})}" })}"
> >
<div class="icon ${this.title ? "" : "no-title"}"> <div class="icon ${this.title ? "" : "no-title"}">
<slot name="icon">
<ha-svg-icon .path=${ALERT_ICONS[this.alertType]}></ha-svg-icon> <ha-svg-icon .path=${ALERT_ICONS[this.alertType]}></ha-svg-icon>
</slot>
</div> </div>
<div class="content"> <div class="content">
<div class="main-content"> <div class="main-content">
@@ -60,18 +58,15 @@ class HaAlert extends LitElement {
<slot></slot> <slot></slot>
</div> </div>
<div class="action"> <div class="action">
${this.actionText <slot name="action">
? html`<mwc-button ${this.dismissable
@click=${this._action_clicked}
.label=${this.actionText}
></mwc-button>`
: this.dismissable
? html`<ha-icon-button ? html`<ha-icon-button
@click=${this._dismiss_clicked} @click=${this._dismiss_clicked}
label="Dismiss alert" label="Dismiss alert"
.path=${mdiClose} .path=${mdiClose}
></ha-icon-button>` ></ha-icon-button>`
: ""} : ""}
</slot>
</div> </div>
</div> </div>
</div> </div>
@@ -82,10 +77,6 @@ class HaAlert extends LitElement {
fireEvent(this, "alert-dismissed-clicked"); fireEvent(this, "alert-dismissed-clicked");
} }
private _action_clicked() {
fireEvent(this, "alert-action-clicked");
}
static styles = css` static styles = css`
.issue-type { .issue-type {
position: relative; position: relative;
@@ -96,7 +87,7 @@ class HaAlert extends LitElement {
.issue-type.rtl { .issue-type.rtl {
flex-direction: row-reverse; flex-direction: row-reverse;
} }
.issue-type::before { .issue-type::after {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
@@ -108,17 +99,11 @@ class HaAlert extends LitElement {
border-radius: 4px; border-radius: 4px;
} }
.icon { .icon {
margin-right: 8px; z-index: 1;
width: 24px;
} }
.icon.no-title { .icon.no-title {
align-self: center; align-self: center;
} }
.issue-type.rtl > .icon {
margin-right: 0px;
margin-left: 8px;
width: 24px;
}
.issue-type.rtl > .content { .issue-type.rtl > .content {
flex-direction: row-reverse; flex-direction: row-reverse;
text-align: right; text-align: right;
@@ -129,44 +114,55 @@ class HaAlert extends LitElement {
align-items: center; align-items: center;
width: 100%; width: 100%;
} }
.action {
z-index: 1;
width: min-content;
--mdc-theme-primary: var(--primary-text-color);
}
.main-content { .main-content {
overflow-wrap: anywhere; overflow-wrap: anywhere;
word-break: break-word;
margin-left: 8px;
margin-right: 0;
}
.issue-type.rtl > .content > .main-content {
margin-left: 0;
margin-right: 8px;
} }
.title { .title {
margin-top: 2px; margin-top: 2px;
font-weight: bold; font-weight: bold;
} }
mwc-button { .action mwc-button,
.action ha-icon-button {
--mdc-theme-primary: var(--primary-text-color); --mdc-theme-primary: var(--primary-text-color);
}
ha-icon-button {
--mdc-icon-button-size: 36px; --mdc-icon-button-size: 36px;
} }
.issue-type.info > .icon { .issue-type.info > .icon {
color: var(--info-color); color: var(--info-color);
} }
.issue-type.info::before { .issue-type.info::after {
background-color: var(--info-color); background-color: var(--info-color);
} }
.issue-type.warning > .icon { .issue-type.warning > .icon {
color: var(--warning-color); color: var(--warning-color);
} }
.issue-type.warning::before { .issue-type.warning::after {
background-color: var(--warning-color); background-color: var(--warning-color);
} }
.issue-type.error > .icon { .issue-type.error > .icon {
color: var(--error-color); color: var(--error-color);
} }
.issue-type.error::before { .issue-type.error::after {
background-color: var(--error-color); background-color: var(--error-color);
} }
.issue-type.success > .icon { .issue-type.success > .icon {
color: var(--success-color); color: var(--success-color);
} }
.issue-type.success::before { .issue-type.success::after {
background-color: var(--success-color); background-color: var(--success-color);
} }
`; `;

View File

@@ -172,6 +172,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
{ {
area_id: "", area_id: "",
name: this.hass.localize("ui.components.area-picker.no_areas"), name: this.hass.localize("ui.components.area-picker.no_areas"),
picture: null,
}, },
]; ];
} }
@@ -295,6 +296,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
{ {
area_id: "", area_id: "",
name: this.hass.localize("ui.components.area-picker.no_match"), name: this.hass.localize("ui.components.area-picker.no_match"),
picture: null,
}, },
]; ];
} }
@@ -306,6 +308,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
{ {
area_id: "add_new", area_id: "add_new",
name: this.hass.localize("ui.components.area-picker.add_new"), name: this.hass.localize("ui.components.area-picker.add_new"),
picture: null,
}, },
]; ];
} }
@@ -340,7 +343,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
item-value-path="area_id" item-value-path="area_id"
item-id-path="area_id" item-id-path="area_id"
item-label-path="name" item-label-path="name"
.value=${this._value} .value=${this.value}
.disabled=${this.disabled} .disabled=${this.disabled}
${comboBoxRenderer(rowRenderer)} ${comboBoxRenderer(rowRenderer)}
@opened-changed=${this._openedChanged} @opened-changed=${this._openedChanged}
@@ -431,12 +434,24 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
name, name,
}); });
this._areas = [...this._areas!, area]; this._areas = [...this._areas!, area];
(this.comboBox as any).items = this._getAreas(
this._areas!,
this._devices!,
this._entities!,
this.includeDomains,
this.excludeDomains,
this.includeDeviceClasses,
this.deviceFilter,
this.entityFilter,
this.noAdd
);
this._setValue(area.area_id); this._setValue(area.area_id);
} catch (err: any) { } catch (err: any) {
showAlertDialog(this, { showAlertDialog(this, {
text: this.hass.localize( title: this.hass.localize(
"ui.components.area-picker.add_dialog.failed_create_area" "ui.components.area-picker.add_dialog.failed_create_area"
), ),
text: err.message,
}); });
} }
}, },

View File

@@ -23,6 +23,10 @@ class HaBluePrintPicker extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
public open() {
this.shadowRoot!.querySelector("paper-dropdown-menu-light")!.open();
}
private _processedBlueprints = memoizeOne((blueprints?: Blueprints) => { private _processedBlueprints = memoizeOne((blueprints?: Blueprints) => {
if (!blueprints) { if (!blueprints) {
return []; return [];

View File

@@ -8,52 +8,31 @@ import {
TemplateResult, TemplateResult,
unsafeCSS, unsafeCSS,
} from "lit"; } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-chip";
declare global {
// for fire event
interface HASSDomEvents {
"chip-clicked": { index: string };
}
}
@customElement("ha-chip-set") @customElement("ha-chip-set")
export class HaChipSet extends LitElement { export class HaChipSet extends LitElement {
@property() public items = [];
protected render(): TemplateResult { protected render(): TemplateResult {
if (this.items.length === 0) {
return html``;
}
return html` return html`
<div class="mdc-chip-set"> <div class="mdc-chip-set">
${this.items.map( <slot></slot>
(item, idx) =>
html`
<ha-chip .index=${idx} @click=${this._handleClick}>
${item}
</ha-chip>
`
)}
</div> </div>
`; `;
} }
private _handleClick(ev): void {
fireEvent(this, "chip-clicked", {
index: ev.currentTarget.index,
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
${unsafeCSS(chipStyles)} ${unsafeCSS(chipStyles)}
ha-chip { slot::slotted(ha-chip) {
margin: 4px; margin: 4px;
} }
slot::slotted(ha-chip:first-of-type) {
margin-left: -4px;
}
slot::slotted(ha-chip:last-of-type) {
margin-right: -4px;
}
`; `;
} }
} }

View File

@@ -10,24 +10,21 @@ import {
} from "lit"; } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
declare global {
// for fire event
interface HASSDomEvents {
"chip-clicked": { index: string };
}
}
@customElement("ha-chip") @customElement("ha-chip")
export class HaChip extends LitElement { export class HaChip extends LitElement {
@property() public index = 0;
@property({ type: Boolean }) public hasIcon = false; @property({ type: Boolean }) public hasIcon = false;
@property({ type: Boolean }) public noText = false;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div class="mdc-chip" .index=${this.index}> <div class="mdc-chip">
${this.hasIcon ${this.hasIcon
? html`<div class="mdc-chip__icon mdc-chip__icon--leading"> ? html`<div
class="mdc-chip__icon mdc-chip__icon--leading ${this.noText
? "no-text"
: ""}"
>
<slot name="icon"></slot> <slot name="icon"></slot>
</div>` </div>`
: null} : null}
@@ -60,6 +57,10 @@ export class HaChip extends LitElement {
--mdc-icon-size: 20px; --mdc-icon-size: 20px;
color: var(--ha-chip-icon-color, var(--ha-chip-text-color)); color: var(--ha-chip-icon-color, var(--ha-chip-text-color));
} }
.mdc-chip
.mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden).no-text {
margin-right: -4px;
}
`; `;
} }
} }

View File

@@ -1,39 +1,30 @@
import { mdiStop } from "@mdi/js"; import { mdiStop } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { import { customElement, property } from "lit/decorators";
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { computeCloseIcon, computeOpenIcon } from "../common/entity/cover_icon"; import { computeCloseIcon, computeOpenIcon } from "../common/entity/cover_icon";
import {
CoverEntity,
isClosing,
isFullyClosed,
isFullyOpen,
isOpening,
supportsClose,
supportsOpen,
supportsStop,
} from "../data/cover";
import { UNAVAILABLE } from "../data/entity"; import { UNAVAILABLE } from "../data/entity";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import CoverEntity from "../util/cover-model";
import "./ha-icon-button"; import "./ha-icon-button";
@customElement("ha-cover-controls") @customElement("ha-cover-controls")
class HaCoverControls extends LitElement { class HaCoverControls extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: HassEntity; @property({ attribute: false }) public stateObj!: CoverEntity;
@state() private _entityObj?: CoverEntity;
public willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (changedProperties.has("stateObj")) {
this._entityObj = new CoverEntity(this.hass, this.stateObj);
}
}
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._entityObj) { if (!this.stateObj) {
return html``; return html``;
} }
@@ -41,7 +32,7 @@ class HaCoverControls extends LitElement {
<div class="state"> <div class="state">
<ha-icon-button <ha-icon-button
class=${classMap({ class=${classMap({
hidden: !this._entityObj.supportsOpen, hidden: !supportsOpen(this.stateObj),
})} })}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.dialogs.more_info_control.open_cover" "ui.dialogs.more_info_control.open_cover"
@@ -53,7 +44,7 @@ class HaCoverControls extends LitElement {
</ha-icon-button> </ha-icon-button>
<ha-icon-button <ha-icon-button
class=${classMap({ class=${classMap({
hidden: !this._entityObj.supportsStop, hidden: !supportsStop(this.stateObj),
})} })}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.dialogs.more_info_control.stop_cover" "ui.dialogs.more_info_control.stop_cover"
@@ -64,7 +55,7 @@ class HaCoverControls extends LitElement {
></ha-icon-button> ></ha-icon-button>
<ha-icon-button <ha-icon-button
class=${classMap({ class=${classMap({
hidden: !this._entityObj.supportsClose, hidden: !supportsClose(this.stateObj),
})} })}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.dialogs.more_info_control.close_cover" "ui.dialogs.more_info_control.close_cover"
@@ -84,8 +75,7 @@ class HaCoverControls extends LitElement {
} }
const assumedState = this.stateObj.attributes.assumed_state === true; const assumedState = this.stateObj.attributes.assumed_state === true;
return ( return (
(this._entityObj.isFullyOpen || this._entityObj.isOpening) && (isFullyOpen(this.stateObj) || isOpening(this.stateObj)) && !assumedState
!assumedState
); );
} }
@@ -95,24 +85,30 @@ class HaCoverControls extends LitElement {
} }
const assumedState = this.stateObj.attributes.assumed_state === true; const assumedState = this.stateObj.attributes.assumed_state === true;
return ( return (
(this._entityObj.isFullyClosed || this._entityObj.isClosing) && (isFullyClosed(this.stateObj) || isClosing(this.stateObj)) &&
!assumedState !assumedState
); );
} }
private _onOpenTap(ev): void { private _onOpenTap(ev): void {
ev.stopPropagation(); ev.stopPropagation();
this._entityObj.openCover(); this.hass.callService("cover", "open_cover", {
entity_id: this.stateObj.entity_id,
});
} }
private _onCloseTap(ev): void { private _onCloseTap(ev): void {
ev.stopPropagation(); ev.stopPropagation();
this._entityObj.closeCover(); this.hass.callService("cover", "close_cover", {
entity_id: this.stateObj.entity_id,
});
} }
private _onStopTap(ev): void { private _onStopTap(ev): void {
ev.stopPropagation(); ev.stopPropagation();
this._entityObj.stopCover(); this.hass.callService("cover", "stop_cover", {
entity_id: this.stateObj.entity_id,
});
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@@ -1,44 +1,33 @@
import { mdiArrowBottomLeft, mdiArrowTopRight, mdiStop } from "@mdi/js"; import { mdiArrowBottomLeft, mdiArrowTopRight, mdiStop } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { import { customElement, property } from "lit/decorators";
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import {
CoverEntity,
isFullyClosedTilt,
isFullyOpenTilt,
supportsCloseTilt,
supportsOpenTilt,
supportsStopTilt,
} from "../data/cover";
import { UNAVAILABLE } from "../data/entity"; import { UNAVAILABLE } from "../data/entity";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import CoverEntity from "../util/cover-model";
import "./ha-icon-button"; import "./ha-icon-button";
@customElement("ha-cover-tilt-controls") @customElement("ha-cover-tilt-controls")
class HaCoverTiltControls extends LitElement { class HaCoverTiltControls extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) stateObj!: HassEntity; @property({ attribute: false }) stateObj!: CoverEntity;
@state() private _entityObj?: CoverEntity;
public willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (changedProperties.has("stateObj")) {
this._entityObj = new CoverEntity(this.hass, this.stateObj);
}
}
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._entityObj) { if (!this.stateObj) {
return html``; return html``;
} }
return html` <ha-icon-button return html` <ha-icon-button
class=${classMap({ class=${classMap({
invisible: !this._entityObj.supportsOpenTilt, invisible: !supportsOpenTilt(this.stateObj),
})} })}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.dialogs.more_info_control.open_tilt_cover" "ui.dialogs.more_info_control.open_tilt_cover"
@@ -49,7 +38,7 @@ class HaCoverTiltControls extends LitElement {
></ha-icon-button> ></ha-icon-button>
<ha-icon-button <ha-icon-button
class=${classMap({ class=${classMap({
invisible: !this._entityObj.supportsStopTilt, invisible: !supportsStopTilt(this.stateObj),
})} })}
.label=${this.hass.localize("ui.dialogs.more_info_control.stop_cover")} .label=${this.hass.localize("ui.dialogs.more_info_control.stop_cover")}
.path=${mdiStop} .path=${mdiStop}
@@ -58,7 +47,7 @@ class HaCoverTiltControls extends LitElement {
></ha-icon-button> ></ha-icon-button>
<ha-icon-button <ha-icon-button
class=${classMap({ class=${classMap({
invisible: !this._entityObj.supportsCloseTilt, invisible: !supportsCloseTilt(this.stateObj),
})} })}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.dialogs.more_info_control.close_tilt_cover" "ui.dialogs.more_info_control.close_tilt_cover"
@@ -74,7 +63,7 @@ class HaCoverTiltControls extends LitElement {
return true; return true;
} }
const assumedState = this.stateObj.attributes.assumed_state === true; const assumedState = this.stateObj.attributes.assumed_state === true;
return this._entityObj.isFullyOpenTilt && !assumedState; return isFullyOpenTilt(this.stateObj) && !assumedState;
} }
private _computeClosedDisabled(): boolean { private _computeClosedDisabled(): boolean {
@@ -82,22 +71,28 @@ class HaCoverTiltControls extends LitElement {
return true; return true;
} }
const assumedState = this.stateObj.attributes.assumed_state === true; const assumedState = this.stateObj.attributes.assumed_state === true;
return this._entityObj.isFullyClosedTilt && !assumedState; return isFullyClosedTilt(this.stateObj) && !assumedState;
} }
private _onOpenTiltTap(ev): void { private _onOpenTiltTap(ev): void {
ev.stopPropagation(); ev.stopPropagation();
this._entityObj.openCoverTilt(); this.hass.callService("cover", "open_cover_tilt", {
entity_id: this.stateObj.entity_id,
});
} }
private _onCloseTiltTap(ev): void { private _onCloseTiltTap(ev): void {
ev.stopPropagation(); ev.stopPropagation();
this._entityObj.closeCoverTilt(); this.hass.callService("cover", "close_cover_tilt", {
entity_id: this.stateObj.entity_id,
});
} }
private _onStopTiltTap(ev): void { private _onStopTiltTap(ev): void {
ev.stopPropagation(); ev.stopPropagation();
this._entityObj.stopCoverTilt(); this.hass.callService("cover", "stop_cover_tilt", {
entity_id: this.stateObj.entity_id,
});
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@@ -72,6 +72,10 @@ export class HaDialog extends Dialog {
position: var(--dialog-surface-position, relative); position: var(--dialog-surface-position, relative);
top: var(--dialog-surface-top); top: var(--dialog-surface-top);
min-height: var(--mdc-dialog-min-height, auto); min-height: var(--mdc-dialog-min-height, auto);
border-radius: var(
--ha-dialog-border-radius,
var(--ha-card-border-radius, 4px)
);
} }
:host([flexContent]) .mdc-dialog .mdc-dialog__content { :host([flexContent]) .mdc-dialog .mdc-dialog__content {
display: flex; display: flex;

View File

@@ -0,0 +1,82 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@customElement("ha-faded")
class HaFaded extends LitElement {
@property({ type: Number, attribute: "faded-height" })
public fadedHeight = 102;
@state() _contentShown = false;
protected render(): TemplateResult {
return html`
<div
class="container ${classMap({ faded: !this._contentShown })}"
style=${!this._contentShown ? `max-height: ${this.fadedHeight}px` : ""}
@click=${this._showContent}
>
<slot
@iron-resize=${
// ha-markdown-element fire this when render is complete
this._setShowContent
}
></slot>
</div>
`;
}
get _slottedHeight(): number {
return (
(
this.shadowRoot!.querySelector(".container")
?.firstElementChild as HTMLSlotElement
)
.assignedElements()
.reduce(
(partial, element) => partial + (element as HTMLElement).offsetHeight,
0
) || 0
);
}
private _setShowContent() {
const height = this._slottedHeight;
this._contentShown = height !== 0 && height <= this.fadedHeight + 50;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._setShowContent();
}
private _showContent(): void {
this._contentShown = true;
}
static get styles(): CSSResultGroup {
return css`
.container {
display: block;
height: auto;
cursor: default;
}
.faded {
cursor: pointer;
-webkit-mask-image: linear-gradient(
to bottom,
black 25%,
transparent 100%
);
mask-image: linear-gradient(to bottom, black 25%, transparent 100%);
overflow-y: hidden;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-faded": HaFaded;
}
}

View File

@@ -52,7 +52,9 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
const options = Object.entries(this.schema.options); const options = Array.isArray(this.schema.options)
? this.schema.options
: Object.entries(this.schema.options);
const data = this.data || []; const data = this.data || [];
const renderedOptions = options.map((item: string | [string, string]) => { const renderedOptions = options.map((item: string | [string, string]) => {

View File

@@ -38,7 +38,7 @@ export interface HaFormSelectSchema extends HaFormBaseSchema {
export interface HaFormMultiSelectSchema extends HaFormBaseSchema { export interface HaFormMultiSelectSchema extends HaFormBaseSchema {
type: "multi_select"; type: "multi_select";
options: Record<string, string>; options: Record<string, string> | string[];
} }
export interface HaFormFloatSchema extends HaFormBaseSchema { export interface HaFormFloatSchema extends HaFormBaseSchema {

View File

@@ -5,6 +5,22 @@ import { customElement } from "lit/decorators";
@customElement("ha-formfield") @customElement("ha-formfield")
// @ts-expect-error // @ts-expect-error
export class HaFormfield extends Formfield { export class HaFormfield extends Formfield {
protected _labelClick() {
const input = this.input;
if (input) {
input.focus();
switch (input.tagName) {
case "HA-CHECKBOX":
case "HA-RADIO":
(input as any).checked = !(input as any).checked;
break;
default:
input.click();
break;
}
}
}
protected static get styles(): CSSResultGroup { protected static get styles(): CSSResultGroup {
return [ return [
Formfield.styles, Formfield.styles,

View File

@@ -7,10 +7,11 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { nextRender } from "../common/util/render-status"; import { nextRender } from "../common/util/render-status";
import { getExternalConfig } from "../external_app/external_config"; import { getExternalConfig } from "../external_app/external_config";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./ha-alert";
type HlsLite = Omit< type HlsLite = Omit<
HlsType, HlsType,
@@ -41,6 +42,8 @@ class HaHLSPlayer extends LitElement {
// don't cache this, as we remove it on disconnects // don't cache this, as we remove it on disconnects
@query("video") private _videoEl!: HTMLVideoElement; @query("video") private _videoEl!: HTMLVideoElement;
@state() private _error?: string;
private _hlsPolyfillInstance?: HlsLite; private _hlsPolyfillInstance?: HlsLite;
private _exoPlayer = false; private _exoPlayer = false;
@@ -58,6 +61,9 @@ class HaHLSPlayer extends LitElement {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (this._error) {
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
}
return html` return html`
<video <video
?autoplay=${this.autoPlay} ?autoplay=${this.autoPlay}
@@ -90,6 +96,8 @@ class HaHLSPlayer extends LitElement {
} }
private async _startHls(): Promise<void> { private async _startHls(): Promise<void> {
this._error = undefined;
const videoEl = this._videoEl; const videoEl = this._videoEl;
const useExoPlayerPromise = this._getUseExoPlayer(); const useExoPlayerPromise = this._getUseExoPlayer();
const masterPlaylistPromise = fetch(this.url); const masterPlaylistPromise = fetch(this.url);
@@ -109,7 +117,7 @@ class HaHLSPlayer extends LitElement {
} }
if (!hlsSupported) { if (!hlsSupported) {
videoEl.innerHTML = this.hass.localize( this._error = this.hass.localize(
"ui.components.media-browser.video_not_supported" "ui.components.media-browser.video_not_supported"
); );
return; return;
@@ -196,6 +204,44 @@ class HaHLSPlayer extends LitElement {
hls.on(Hls.Events.MEDIA_ATTACHED, () => { hls.on(Hls.Events.MEDIA_ATTACHED, () => {
hls.loadSource(url); hls.loadSource(url);
}); });
hls.on(Hls.Events.ERROR, (_, data: any) => {
if (!data.fatal) {
return;
}
if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
switch (data.details) {
case Hls.ErrorDetails.MANIFEST_LOAD_ERROR: {
let error = "Error starting stream, see logs for details";
if (
data.response !== undefined &&
data.response.code !== undefined
) {
if (data.response.code >= 500) {
error += " (Server failure)";
} else if (data.response.code >= 400) {
error += " (Stream never started)";
} else {
error += " (" + data.response.code + ")";
}
}
this._error = error;
return;
}
case Hls.ErrorDetails.MANIFEST_LOAD_TIMEOUT:
this._error = "Timeout while starting stream";
return;
default:
this._error = "Unknown stream network error (" + data.details + ")";
return;
}
this._error = "Error with media stream contents (" + data.details + ")";
} else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
this._error = "Error with media stream contents (" + data.details + ")";
} else {
this._error =
"Unknown error with stream (" + data.type + ", " + data.details + ")";
}
});
} }
private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) { private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) {
@@ -231,6 +277,11 @@ class HaHLSPlayer extends LitElement {
width: 100%; width: 100%;
max-height: var(--video-max-height, calc(100vh - 97px)); max-height: var(--video-max-height, calc(100vh - 97px));
} }
ha-alert {
display: block;
padding: 100px 16px;
}
`; `;
} }
} }

View File

@@ -1,12 +1,12 @@
import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip";
import { css, html, LitElement, TemplateResult } from "lit"; import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "./ha-button-menu";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-icon-button";
import "./ha-svg-icon";
import { mdiDotsVertical } from "@mdi/js";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "@polymer/paper-tooltip/paper-tooltip"; import "./ha-button-menu";
import "./ha-icon-button";
import "./ha-svg-icon";
export interface IconOverflowMenuItem { export interface IconOverflowMenuItem {
[key: string]: any; [key: string]: any;
@@ -37,13 +37,11 @@ export class HaIconOverflowMenu extends LitElement {
corner="BOTTOM_START" corner="BOTTOM_START"
absolute absolute
> >
<mwc-icon-button <ha-icon-button
.title=${this.hass.localize("ui.common.menu")}
.label=${this.hass.localize("ui.common.overflow_menu")} .label=${this.hass.localize("ui.common.overflow_menu")}
.path=${mdiDotsVertical}
slot="trigger" slot="trigger"
> ></ha-icon-button>
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
${this.items.map( ${this.items.map(
(item) => html` (item) => html`

View File

@@ -29,315 +29,7 @@ interface DeprecatedIcon {
}; };
} }
const mdiDeprecatedIcons: DeprecatedIcon = { const mdiDeprecatedIcons: DeprecatedIcon = {};
"adobe-acrobat": {
removeIn: "2021.12",
},
adobe: {
removeIn: "2021.12",
},
"amazon-alexa": {
removeIn: "2021.12",
},
amazon: {
removeIn: "2021.12",
},
"android-auto": {
removeIn: "2021.12",
},
"android-debug-bridge": {
removeIn: "2021.12",
},
"apple-airplay": {
newName: "cast-variant",
removeIn: "2021.12",
},
bandcamp: {
removeIn: "2021.12",
},
battlenet: {
removeIn: "2021.12",
},
blogger: {
removeIn: "2021.12",
},
"bolnisi-cross": {
newName: "cross-bolnisi",
removeIn: "2021.12",
},
"boom-gate-down": {
newName: "boom-gate-arrow-down",
removeIn: "2021.12",
},
"boom-gate-down-outline": {
newName: "boom-gate-arrow-down-outline",
removeIn: "2021.12",
},
buddhism: {
newName: "dharmachakra",
removeIn: "2021.12",
},
buffer: {
removeIn: "2021.12",
},
"cash-usd-outline": {
removeIn: "2021.12",
},
"cash-usd": {
removeIn: "2021.12",
},
"cellphone-android": {
newName: "cellphone",
removeIn: "2021.12",
},
"cellphone-erase": {
newName: "cellphone-remove",
removeIn: "2021.12",
},
"cellphone-iphone": {
newName: "cellphone",
removeIn: "2021.12",
},
"celtic-cross": {
newName: "cross-celtic",
removeIn: "2021.12",
},
christianity: {
newName: "cross",
removeIn: "2021.12",
},
"christianity-outline": {
newName: "cross-outline",
removeIn: "2021.12",
},
"concourse-ci": {
removeIn: "2021.12",
},
"currency-usd-circle": {
removeIn: "2021.12",
},
"currency-usd-circle-outline": {
removeIn: "2021.12",
},
"do-not-disturb-off": {
newName: "minus-circle-off",
removeIn: "2021.12",
},
"do-not-disturb": {
newName: "minus-circle",
removeIn: "2021.12",
},
douban: {
removeIn: "2021.12",
},
face: {
newName: "face-man",
removeIn: "2021.12",
},
"face-outline": {
newName: "face-man-outline",
removeIn: "2021.12",
},
"face-profile-woman": {
newName: "face-woman-profile",
removeIn: "2021.12",
},
"face-shimmer": {
newName: "face-man-shimmer",
removeIn: "2021.12",
},
"face-shimmer-outline": {
newName: "face-man-shimmer-outline",
removeIn: "2021.12",
},
"file-pdf": {
newName: "file-pdf-box",
removeIn: "2021.12",
},
"file-pdf-outline": {
newName: "file-pdf-box",
removeIn: "2021.12",
},
"file-pdf-box-outline": {
newName: "file-pdf-box",
removeIn: "2021.12",
},
"flash-circle": {
newName: "lightning-bolt-circle",
removeIn: "2021.12",
},
"floor-lamp-variant": {
newName: "floor-lamp-torchiere-variant",
removeIn: "2021.12",
},
gif: {
newName: "file-gif-box",
removeIn: "2021.12",
},
"google-photos": {
removeIn: "2021.12",
},
gradient: {
newName: "gradient-vertical",
removeIn: "2021.12",
},
hand: {
newName: "hand-front-right",
removeIn: "2021.12",
},
"hand-left": {
newName: "hand-back-left",
removeIn: "2021.12",
},
"hand-right": {
newName: "hand-back-right",
removeIn: "2021.12",
},
hinduism: {
newName: "om",
removeIn: "2021.12",
},
"home-currency-usd": {
removeIn: "2021.12",
},
iframe: {
newName: "application-brackets",
removeIn: "2021.12",
},
"iframe-outline": {
newName: "application-brackets-outline",
removeIn: "2021.12",
},
"iframe-array": {
newName: "application-array",
removeIn: "2021.12",
},
"iframe-array-outline": {
newName: "application-array-outline",
removeIn: "2021.12",
},
"iframe-braces": {
newName: "application-braces",
removeIn: "2021.12",
},
"iframe-braces-outline": {
newName: "application-braces-outline",
removeIn: "2021.12",
},
"iframe-parentheses": {
newName: "application-parentheses",
removeIn: "2021.12",
},
"iframe-parentheses-outline": {
newName: "application-parentheses-outline",
removeIn: "2021.12",
},
"iframe-variable": {
newName: "application-variable",
removeIn: "2021.12",
},
"iframe-variable-outline": {
newName: "application-variable-outline",
removeIn: "2021.12",
},
islam: {
newName: "star-crescent",
removeIn: "2021.12",
},
judaism: {
newName: "star-david",
removeIn: "2021.12",
},
"laptop-chromebook": {
newName: "laptop",
removeIn: "2021.12",
},
"laptop-mac": {
newName: "laptop",
removeIn: "2021.12",
},
"laptop-windows": {
newName: "laptop",
removeIn: "2021.12",
},
"microsoft-edge-legacy": {
removeIn: "2021.12",
},
"microsoft-yammer": {
removeIn: "2021.12",
},
"monitor-clean": {
newName: "monitor-shimmer",
removeIn: "2021.12",
},
"pdf-box": {
newName: "file-pdf-box",
removeIn: "2021.12",
},
pharmacy: {
newName: "mortar-pestle-plus",
removeIn: "2021.12",
},
"plus-one": {
newName: "numeric-positive-1",
removeIn: "2021.12",
},
"poll-box": {
newName: "chart-box",
removeIn: "2021.12",
},
"poll-box-outline": {
newName: "chart-box-outline",
removeIn: "2021.12",
},
sparkles: {
newName: "shimmer",
removeIn: "2021.12",
},
"tablet-ipad": {
newName: "tablet",
removeIn: "2021.12",
},
teach: {
newName: "human-male-board",
removeIn: "2021.12",
},
telegram: {
removeIn: "2021.12",
},
"television-clean": {
newName: "television-shimmer",
removeIn: "2021.12",
},
"text-subject": {
newName: "text-long",
removeIn: "2021.12",
},
"twitter-retweet": {
newName: "repeat-variant",
removeIn: "2021.12",
},
untappd: {
removeIn: "2021.12",
},
vk: {
removeIn: "2021.12",
},
"voice-off": {
newName: "account-voice-off",
removeIn: "2021.12",
},
"xamarian-outline": {
newName: "xamarian",
removeIn: "2021.12",
},
xing: {
removeIn: "2021.12",
},
"y-combinator": {
removeIn: "2021.12",
},
};
const chunks: Chunks = {}; const chunks: Chunks = {};

File diff suppressed because one or more lines are too long

View File

@@ -24,7 +24,7 @@ class HaMarkdownElement extends ReactiveElement {
private async _render() { private async _render() {
this.innerHTML = await renderMarkdown( this.innerHTML = await renderMarkdown(
this.content, String(this.content),
{ {
breaks: this.breaks, breaks: this.breaks,
gfm: true, gfm: true,

View File

@@ -0,0 +1,162 @@
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select";
import type { Select } from "@material/mwc-select/mwc-select";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import type QrScanner from "qr-scanner";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { LocalizeFunc } from "../common/translations/localize";
import "./ha-alert";
@customElement("ha-qr-scanner")
class HaQrScanner extends LitElement {
@property() localize!: LocalizeFunc;
@state() private _cameras?: QrScanner.Camera[];
@state() private _error?: string;
private _qrScanner?: QrScanner;
private _qrNotFoundCount = 0;
@query("video", true) private _video!: HTMLVideoElement;
@query("#canvas-container", true) private _canvasContainer!: HTMLDivElement;
public disconnectedCallback(): void {
super.disconnectedCallback();
this._qrNotFoundCount = 0;
if (this._qrScanner) {
this._qrScanner.stop();
this._qrScanner.destroy();
this._qrScanner = undefined;
}
while (this._canvasContainer.lastChild) {
this._canvasContainer.removeChild(this._canvasContainer.lastChild);
}
}
public connectedCallback(): void {
super.connectedCallback();
if (this.hasUpdated && navigator.mediaDevices) {
this._loadQrScanner();
}
}
protected firstUpdated() {
if (navigator.mediaDevices) {
this._loadQrScanner();
}
}
protected updated(changedProps: PropertyValues) {
if (changedProps.has("_error") && this._error) {
fireEvent(this, "qr-code-error", { message: this._error });
}
}
protected render(): TemplateResult {
return html`${this._cameras && this._cameras.length > 1
? html`<mwc-select
.label=${this.localize(
"ui.panel.config.zwave_js.add_node.select_camera"
)}
fixedMenuPosition
naturalMenuWidth
@closed=${stopPropagation}
@selected=${this._cameraChanged}
>
${this._cameras!.map(
(camera) => html`
<mwc-list-item .value=${camera.id}>${camera.label}</mwc-list-item>
`
)}
</mwc-select>`
: ""}
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
${navigator.mediaDevices
? html`<video></video>
<div id="canvas-container"></div>`
: html`<ha-alert alert-type="warning"
>${!window.isSecureContext
? "You can only use your camera to scan a QR core when using HTTPS."
: "Your browser doesn't support QR scanning."}</ha-alert
>`}`;
}
private async _loadQrScanner() {
const QrScanner = (await import("qr-scanner")).default;
if (!(await QrScanner.hasCamera())) {
this._error = "No camera found";
return;
}
QrScanner.WORKER_PATH = "/static/js/qr-scanner-worker.min.js";
this._listCameras(QrScanner);
this._qrScanner = new QrScanner(
this._video,
this._qrCodeScanned,
this._qrCodeError
);
// @ts-ignore
const canvas = this._qrScanner.$canvas;
this._canvasContainer.appendChild(canvas);
canvas.style.display = "block";
try {
await this._qrScanner.start();
} catch (err: any) {
this._error = err;
}
}
private async _listCameras(qrScanner: typeof QrScanner): Promise<void> {
this._cameras = await qrScanner.listCameras(true);
}
private _qrCodeError = (err: any) => {
if (err === "No QR code found") {
this._qrNotFoundCount++;
if (this._qrNotFoundCount === 250) {
this._error = err;
}
return;
}
this._error = err.message || err;
// eslint-disable-next-line no-console
console.log(err);
};
private _qrCodeScanned = async (qrCodeString: string): Promise<void> => {
this._qrNotFoundCount = 0;
fireEvent(this, "qr-code-scanned", { value: qrCodeString });
};
private _cameraChanged(ev: CustomEvent): void {
this._qrScanner?.setCamera((ev.target as Select).value);
}
static styles = css`
canvas {
width: 100%;
}
mwc-select {
width: 100%;
margin-bottom: 16px;
}
`;
}
declare global {
// for fire event
interface HASSDomEvents {
"qr-code-scanned": { value: string };
"qr-code-error": { message: string };
}
interface HTMLElementTagNameMap {
"ha-qr-scanner": HaQrScanner;
}
}

View File

@@ -3,13 +3,11 @@ import {
mdiBell, mdiBell,
mdiCalendar, mdiCalendar,
mdiCart, mdiCart,
mdiCellphoneCog,
mdiChartBox, mdiChartBox,
mdiClose, mdiClose,
mdiCog, mdiCog,
mdiFormatListBulletedType, mdiFormatListBulletedType,
mdiHammer, mdiHammer,
mdiHomeAssistant,
mdiLightningBolt, mdiLightningBolt,
mdiMenu, mdiMenu,
mdiMenuOpen, mdiMenuOpen,
@@ -45,20 +43,16 @@ import {
PersistentNotification, PersistentNotification,
subscribeNotifications, subscribeNotifications,
} from "../data/persistent_notification"; } from "../data/persistent_notification";
import {
ExternalConfig,
getExternalConfig,
} from "../external_app/external_config";
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive"; import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant, PanelInfo } from "../types"; import type { HomeAssistant, PanelInfo, Route } from "../types";
import "./ha-icon"; import "./ha-icon";
import "./ha-icon-button"; import "./ha-icon-button";
import "./ha-menu-button"; import "./ha-menu-button";
import "./ha-svg-icon"; import "./ha-svg-icon";
import "./user/ha-user-badge"; import "./user/ha-user-badge";
const SHOW_AFTER_SPACER = ["config", "developer-tools", "hassio"]; const SHOW_AFTER_SPACER = ["config", "developer-tools"];
const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body; const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body;
@@ -68,7 +62,6 @@ const SORT_VALUE_URL_PATHS = {
logbook: 3, logbook: 3,
history: 4, history: 4,
"developer-tools": 9, "developer-tools": 9,
hassio: 10,
config: 11, config: 11,
}; };
@@ -77,7 +70,6 @@ const PANEL_ICONS = {
config: mdiCog, config: mdiCog,
"developer-tools": mdiHammer, "developer-tools": mdiHammer,
energy: mdiLightningBolt, energy: mdiLightningBolt,
hassio: mdiHomeAssistant,
history: mdiChartBox, history: mdiChartBox,
logbook: mdiFormatListBulletedType, logbook: mdiFormatListBulletedType,
lovelace: mdiViewDashboard, lovelace: mdiViewDashboard,
@@ -189,12 +181,12 @@ class HaSidebar extends LitElement {
@property({ type: Boolean, reflect: true }) public narrow!: boolean; @property({ type: Boolean, reflect: true }) public narrow!: boolean;
@property() public route!: Route;
@property({ type: Boolean }) public alwaysExpand = false; @property({ type: Boolean }) public alwaysExpand = false;
@property({ type: Boolean }) public editMode = false; @property({ type: Boolean }) public editMode = false;
@state() private _externalConfig?: ExternalConfig;
@state() private _notifications?: PersistentNotification[]; @state() private _notifications?: PersistentNotification[];
@state() private _renderEmptySortable = false; @state() private _renderEmptySortable = false;
@@ -241,7 +233,6 @@ class HaSidebar extends LitElement {
changedProps.has("expanded") || changedProps.has("expanded") ||
changedProps.has("narrow") || changedProps.has("narrow") ||
changedProps.has("alwaysExpand") || changedProps.has("alwaysExpand") ||
changedProps.has("_externalConfig") ||
changedProps.has("_notifications") || changedProps.has("_notifications") ||
changedProps.has("editMode") || changedProps.has("editMode") ||
changedProps.has("_renderEmptySortable") || changedProps.has("_renderEmptySortable") ||
@@ -272,11 +263,6 @@ class HaSidebar extends LitElement {
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
if (this.hass && this.hass.auth.external) {
getExternalConfig(this.hass.auth.external).then((conf) => {
this._externalConfig = conf;
});
}
subscribeNotifications(this.hass.connection, (notifications) => { subscribeNotifications(this.hass.connection, (notifications) => {
this._notifications = notifications; this._notifications = notifications;
}); });
@@ -351,12 +337,17 @@ class HaSidebar extends LitElement {
this._hiddenPanels this._hiddenPanels
); );
// Show the supervisor as beeing part of configuration
const selectedPanel = this.route.path?.startsWith("/hassio/")
? "config"
: this.hass.panelUrl;
// prettier-ignore // prettier-ignore
return html` return html`
<paper-listbox <paper-listbox
attr-for-selected="data-panel" attr-for-selected="data-panel"
class="ha-scrollbar" class="ha-scrollbar"
.selected=${this.hass.panelUrl} .selected=${selectedPanel}
@focusin=${this._listboxFocusIn} @focusin=${this._listboxFocusIn}
@focusout=${this._listboxFocusOut} @focusout=${this._listboxFocusOut}
@scroll=${this._listboxScroll} @scroll=${this._listboxScroll}
@@ -367,7 +358,6 @@ class HaSidebar extends LitElement {
: this._renderPanels(beforeSpacer)} : this._renderPanels(beforeSpacer)}
${this._renderSpacer()} ${this._renderSpacer()}
${this._renderPanels(afterSpacer)} ${this._renderPanels(afterSpacer)}
${this._renderExternalConfiguration()}
</paper-listbox> </paper-listbox>
`; `;
} }
@@ -552,34 +542,6 @@ class HaSidebar extends LitElement {
</a>`; </a>`;
} }
private _renderExternalConfiguration() {
return html`${this._externalConfig && this._externalConfig.hasSettingsScreen
? html`
<a
aria-role="option"
aria-label=${this.hass.localize(
"ui.sidebar.external_app_configuration"
)}
href="#external-app-configuration"
tabindex="-1"
@click=${this._handleExternalAppConfiguration}
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item>
<ha-svg-icon
slot="item-icon"
.path=${mdiCellphoneCog}
></ha-svg-icon>
<span class="item-text">
${this.hass.localize("ui.sidebar.external_app_configuration")}
</span>
</paper-icon-item>
</a>
`
: ""}`;
}
private get _tooltip() { private get _tooltip() {
return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement; return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement;
} }
@@ -751,13 +713,6 @@ class HaSidebar extends LitElement {
fireEvent(this, "hass-show-notifications"); fireEvent(this, "hass-show-notifications");
} }
private _handleExternalAppConfiguration(ev: Event) {
ev.preventDefault();
this.hass.auth.external!.fireMessage({
type: "config_screen/show",
});
}
private _toggleSidebar(ev: CustomEvent) { private _toggleSidebar(ev: CustomEvent) {
if (ev.detail.action !== "tap") { if (ev.detail.action !== "tap") {
return; return;

View File

@@ -27,7 +27,7 @@ export class HaTimeInput extends LitElement {
const parts = this.value?.split(":") || []; const parts = this.value?.split(":") || [];
let hours = parts[0]; let hours = parts[0];
const numberHours = Number(parts[0]); const numberHours = Number(parts[0]);
if (numberHours && useAMPM && numberHours > 12) { if (numberHours && useAMPM && numberHours > 12 && numberHours < 24) {
hours = String(numberHours - 12).padStart(2, "0"); hours = String(numberHours - 12).padStart(2, "0");
} }
if (useAMPM && numberHours === 0) { if (useAMPM && numberHours === 0) {

View File

@@ -80,6 +80,9 @@ class HaWebRtcPlayer extends LitElement {
// Some cameras (such as nest) require a data channel to establish a stream // Some cameras (such as nest) require a data channel to establish a stream
// however, not used by any integrations. // however, not used by any integrations.
peerConnection.createDataChannel("dataSendChannel"); peerConnection.createDataChannel("dataSendChannel");
peerConnection.addTransceiver("audio", { direction: "recvonly" });
peerConnection.addTransceiver("video", { direction: "recvonly" });
const offerOptions: RTCOfferOptions = { const offerOptions: RTCOfferOptions = {
offerToReceiveAudio: true, offerToReceiveAudio: true,
offerToReceiveVideo: true, offerToReceiveVideo: true,

View File

@@ -2,11 +2,8 @@ import { LitElement, html, css } from "lit";
import { property } from "lit/decorators"; import { property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types";
class HaEntityMarker extends LitElement { class HaEntityMarker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "entity-id" }) public entityId?: string; @property({ attribute: "entity-id" }) public entityId?: string;
@property({ attribute: "entity-name" }) public entityName?: string; @property({ attribute: "entity-name" }) public entityName?: string;
@@ -67,3 +64,9 @@ class HaEntityMarker extends LitElement {
} }
customElements.define("ha-entity-marker", HaEntityMarker); customElements.define("ha-entity-marker", HaEntityMarker);
declare global {
interface HTMLElementTagNameMap {
"ha-entity-marker": HaEntityMarker;
}
}

View File

@@ -26,6 +26,7 @@ declare global {
// for fire event // for fire event
interface HASSDomEvents { interface HASSDomEvents {
"location-updated": { id: string; location: [number, number] }; "location-updated": { id: string; location: [number, number] };
"markers-updated": undefined;
"radius-updated": { id: string; radius: number }; "radius-updated": { id: string; radius: number };
"marker-clicked": { id: string }; "marker-clicked": { id: string };
} }
@@ -281,6 +282,7 @@ export class HaLocationsEditor extends LitElement {
}); });
this._circles = circles; this._circles = circles;
this._locationMarkers = locationMarkers; this._locationMarkers = locationMarkers;
fireEvent(this, "markers-updated");
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@@ -412,7 +412,9 @@ export class HaMap extends ReactiveElement {
<ha-entity-marker <ha-entity-marker
entity-id="${getEntityId(entity)}" entity-id="${getEntityId(entity)}"
entity-name="${entityName}" entity-name="${entityName}"
entity-picture="${entityPicture || ""}" entity-picture="${
entityPicture ? this.hass.hassUrl(entityPicture) : ""
}"
${ ${
typeof entity !== "string" typeof entity !== "string"
? `entity-color="${entity.color}"` ? `entity-color="${entity.color}"`

View File

@@ -7,10 +7,12 @@ import { HomeAssistant } from "../types";
export interface AreaRegistryEntry { export interface AreaRegistryEntry {
area_id: string; area_id: string;
name: string; name: string;
picture: string | null;
} }
export interface AreaRegistryEntryMutableParams { export interface AreaRegistryEntryMutableParams {
name: string; name: string;
picture?: string | null;
} }
export const createAreaRegistryEntry = ( export const createAreaRegistryEntry = (

View File

@@ -179,7 +179,7 @@ export interface StateCondition extends BaseCondition {
condition: "state"; condition: "state";
entity_id: string; entity_id: string;
attribute?: string; attribute?: string;
state: string | number; state: string | number | string[];
for?: string | number | ForDict; for?: string | number | ForDict;
} }

95
src/data/cover.ts Normal file
View File

@@ -0,0 +1,95 @@
import {
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
import { supportsFeature } from "../common/entity/supports-feature";
export const SUPPORT_OPEN = 1;
export const SUPPORT_CLOSE = 2;
export const SUPPORT_SET_POSITION = 4;
export const SUPPORT_STOP = 8;
export const SUPPORT_OPEN_TILT = 16;
export const SUPPORT_CLOSE_TILT = 32;
export const SUPPORT_STOP_TILT = 64;
export const SUPPORT_SET_TILT_POSITION = 128;
export const FEATURE_CLASS_NAMES = {
4: "has-set_position",
16: "has-open_tilt",
32: "has-close_tilt",
64: "has-stop_tilt",
128: "has-set_tilt_position",
};
export const supportsOpen = (stateObj) =>
supportsFeature(stateObj, SUPPORT_OPEN);
export const supportsClose = (stateObj) =>
supportsFeature(stateObj, SUPPORT_CLOSE);
export const supportsSetPosition = (stateObj) =>
supportsFeature(stateObj, SUPPORT_SET_POSITION);
export const supportsStop = (stateObj) =>
supportsFeature(stateObj, SUPPORT_STOP);
export const supportsOpenTilt = (stateObj) =>
supportsFeature(stateObj, SUPPORT_OPEN_TILT);
export const supportsCloseTilt = (stateObj) =>
supportsFeature(stateObj, SUPPORT_CLOSE_TILT);
export const supportsStopTilt = (stateObj) =>
supportsFeature(stateObj, SUPPORT_STOP_TILT);
export const supportsSetTiltPosition = (stateObj) =>
supportsFeature(stateObj, SUPPORT_SET_TILT_POSITION);
export function isFullyOpen(stateObj: CoverEntity) {
if (stateObj.attributes.current_position !== undefined) {
return stateObj.attributes.current_position === 100;
}
return stateObj.state === "open";
}
export function isFullyClosed(stateObj: CoverEntity) {
if (stateObj.attributes.current_position !== undefined) {
return stateObj.attributes.current_position === 0;
}
return stateObj.state === "closed";
}
export function isFullyOpenTilt(stateObj: CoverEntity) {
return stateObj.attributes.current_tilt_position === 100;
}
export function isFullyClosedTilt(stateObj: CoverEntity) {
return stateObj.attributes.current_tilt_position === 0;
}
export function isOpening(stateObj: CoverEntity) {
return stateObj.state === "opening";
}
export function isClosing(stateObj: CoverEntity) {
return stateObj.state === "closing";
}
export function isTiltOnly(stateObj: CoverEntity) {
const supportsCover =
supportsOpen(stateObj) || supportsClose(stateObj) || supportsStop(stateObj);
const supportsTilt =
supportsOpenTilt(stateObj) ||
supportsCloseTilt(stateObj) ||
supportsStopTilt(stateObj);
return supportsTilt && !supportsCover;
}
interface CoverEntityAttributes extends HassEntityAttributeBase {
current_position: number;
current_tilt_position: number;
}
export interface CoverEntity extends HassEntityBase {
attributes: CoverEntityAttributes;
}

View File

@@ -1,5 +1,6 @@
import { import {
addHours, addHours,
differenceInDays,
endOfToday, endOfToday,
endOfYesterday, endOfYesterday,
startOfToday, startOfToday,
@@ -191,6 +192,27 @@ export const saveEnergyPreferences = async (
return newPrefs; return newPrefs;
}; };
export interface FossilEnergyConsumption {
[date: string]: number;
}
export const getFossilEnergyConsumption = async (
hass: HomeAssistant,
startTime: Date,
energy_statistic_ids: string[],
co2_statistic_id: string,
endTime?: Date,
period: "5minute" | "hour" | "day" | "month" = "hour"
) =>
hass.callWS<FossilEnergyConsumption>({
type: "energy/fossil_energy_consumption",
start_time: startTime.toISOString(),
end_time: endTime?.toISOString(),
energy_statistic_ids,
co2_statistic_id,
period,
});
interface EnergySourceByType { interface EnergySourceByType {
grid?: GridSourceTypeEnergyPreference[]; grid?: GridSourceTypeEnergyPreference[];
solar?: SolarSourceTypeEnergyPreference[]; solar?: SolarSourceTypeEnergyPreference[];
@@ -209,6 +231,7 @@ export interface EnergyData {
stats: Statistics; stats: Statistics;
co2SignalConfigEntry?: ConfigEntry; co2SignalConfigEntry?: ConfigEntry;
co2SignalEntity?: string; co2SignalEntity?: string;
fossilEnergyConsumption?: FossilEnergyConsumption;
} }
const getEnergyData = async ( const getEnergyData = async (
@@ -246,12 +269,9 @@ const getEnergyData = async (
} }
} }
const consumptionStatIDs: string[] = [];
const statIDs: string[] = []; const statIDs: string[] = [];
if (co2SignalEntity !== undefined) {
statIDs.push(co2SignalEntity);
}
for (const source of prefs.energy_sources) { for (const source of prefs.energy_sources) {
if (source.type === "solar") { if (source.type === "solar") {
statIDs.push(source.stat_energy_from); statIDs.push(source.stat_energy_from);
@@ -278,6 +298,7 @@ const getEnergyData = async (
// grid source // grid source
for (const flowFrom of source.flow_from) { for (const flowFrom of source.flow_from) {
consumptionStatIDs.push(flowFrom.stat_energy_from);
statIDs.push(flowFrom.stat_energy_from); statIDs.push(flowFrom.stat_energy_from);
if (flowFrom.stat_cost) { if (flowFrom.stat_cost) {
statIDs.push(flowFrom.stat_cost); statIDs.push(flowFrom.stat_cost);
@@ -299,7 +320,44 @@ const getEnergyData = async (
} }
} }
const stats = await fetchStatistics(hass!, addHours(start, -1), end, statIDs); // Subtract 1 hour from start to get starting point data const dayDifference = differenceInDays(end || new Date(), start);
// Subtract 1 hour from start to get starting point data
const startMinHour = addHours(start, -1);
const stats = await fetchStatistics(
hass!,
startMinHour,
end,
statIDs,
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
);
let fossilEnergyConsumption: FossilEnergyConsumption | undefined;
if (co2SignalEntity !== undefined) {
fossilEnergyConsumption = await getFossilEnergyConsumption(
hass!,
start,
consumptionStatIDs,
co2SignalEntity,
end,
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
);
}
Object.values(stats).forEach((stat) => {
// if the start of the first value is after the requested period, we have the first data point, and should add a zero point
if (stat.length && new Date(stat[0].start) > startMinHour) {
stat.unshift({
...stat[0],
start: startMinHour.toISOString(),
end: startMinHour.toISOString(),
sum: 0,
state: 0,
});
}
});
const data = { const data = {
start, start,
@@ -309,6 +367,7 @@ const getEnergyData = async (
stats, stats,
co2SignalConfigEntry, co2SignalConfigEntry,
co2SignalEntity, co2SignalEntity,
fossilEnergyConsumption,
}; };
return data; return data;

View File

@@ -21,6 +21,8 @@ export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
capabilities: Record<string, unknown>; capabilities: Record<string, unknown>;
original_name?: string; original_name?: string;
original_icon?: string; original_icon?: string;
device_class?: string;
original_device_class?: string;
} }
export interface UpdateEntityRegistryEntryResult { export interface UpdateEntityRegistryEntryResult {
@@ -32,6 +34,7 @@ export interface UpdateEntityRegistryEntryResult {
export interface EntityRegistryEntryUpdateParams { export interface EntityRegistryEntryUpdateParams {
name?: string | null; name?: string | null;
icon?: string | null; icon?: string | null;
device_class?: string | null;
area_id?: string | null; area_id?: string | null;
disabled_by?: string | null; disabled_by?: string | null;
new_entity_id?: string; new_entity_id?: string;
@@ -66,7 +69,7 @@ export const computeEntityRegistryName = (
return entry.name; return entry.name;
} }
const state = hass.states[entry.entity_id]; const state = hass.states[entry.entity_id];
return state ? computeStateName(state) : null; return state ? computeStateName(state) : entry.entity_id;
}; };
export const getExtendedEntityRegistryEntry = ( export const getExtendedEntityRegistryEntry = (

View File

@@ -1,4 +1,3 @@
import { addDays, addMonths, startOfDay, startOfMonth } from "date-fns";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { computeStateDisplay } from "../common/entity/compute_state_display"; import { computeStateDisplay } from "../common/entity/compute_state_display";
import { computeStateDomain } from "../common/entity/compute_state_domain"; import { computeStateDomain } from "../common/entity/compute_state_domain";
@@ -63,6 +62,7 @@ export interface Statistics {
export interface StatisticValue { export interface StatisticValue {
statistic_id: string; statistic_id: string;
start: string; start: string;
end: string;
last_reset: string | null; last_reset: string | null;
max: number | null; max: number | null;
mean: number | null; mean: number | null;
@@ -350,7 +350,7 @@ export const fetchStatistics = (
startTime: Date, startTime: Date,
endTime?: Date, endTime?: Date,
statistic_ids?: string[], statistic_ids?: string[],
period: "hour" | "5minute" = "hour" period: "5minute" | "hour" | "day" | "month" = "hour"
) => ) =>
hass.callWS<Statistics>({ hass.callWS<Statistics>({
type: "history/statistics_during_period", type: "history/statistics_during_period",
@@ -428,151 +428,3 @@ export const statisticsHaveType = (
stats: StatisticValue[], stats: StatisticValue[],
type: StatisticType type: StatisticType
) => stats.some((stat) => stat[type] !== null); ) => stats.some((stat) => stat[type] !== null);
// Merge the growth of multiple sum statistics into one
const mergeSumGrowthStatistics = (stats: StatisticValue[][]) => {
const result = {};
stats.forEach((stat) => {
if (stat.length === 0) {
return;
}
let prevSum: number | null = null;
stat.forEach((statVal) => {
if (statVal.sum === null) {
return;
}
if (prevSum === null) {
prevSum = statVal.sum;
return;
}
const growth = statVal.sum - prevSum;
if (statVal.start in result) {
result[statVal.start] += growth;
} else {
result[statVal.start] = growth;
}
prevSum = statVal.sum;
});
});
return result;
};
/**
* Get the growth of a statistic over the given period while applying a
* per-period percentage.
*/
export const calculateStatisticsSumGrowthWithPercentage = (
percentageStat: StatisticValue[],
sumStats: StatisticValue[][]
): number | null => {
let sum: number | null = null;
if (sumStats.length === 0 || percentageStat.length === 0) {
return null;
}
const sumGrowthToProcess = mergeSumGrowthStatistics(sumStats);
percentageStat.forEach((percentageStatValue) => {
const sumGrowth = sumGrowthToProcess[percentageStatValue.start];
if (sumGrowth === undefined) {
return;
}
if (sum === null) {
sum = sumGrowth * (percentageStatValue.mean! / 100);
} else {
sum += sumGrowth * (percentageStatValue.mean! / 100);
}
});
return sum;
};
export const reduceSumStatisticsByDay = (
values: StatisticValue[]
): StatisticValue[] => {
if (!values?.length) {
return [];
}
const result: StatisticValue[] = [];
if (
values.length > 1 &&
new Date(values[0].start).getDate() === new Date(values[1].start).getDate()
) {
// add init value if the first value isn't end of previous period
result.push({
...values[0]!,
start: startOfDay(addDays(new Date(values[0].start), -1)).toISOString(),
});
}
let lastValue: StatisticValue;
let prevDate: number | undefined;
for (const value of values) {
const date = new Date(value.start).getDate();
if (prevDate === undefined) {
prevDate = date;
}
if (prevDate !== date) {
// Last value of the day
result.push({
...lastValue!,
start: startOfDay(new Date(lastValue!.start)).toISOString(),
});
prevDate = date;
}
lastValue = value;
}
// Add final value
result.push({
...lastValue!,
start: startOfDay(new Date(lastValue!.start)).toISOString(),
});
return result;
};
export const reduceSumStatisticsByMonth = (
values: StatisticValue[]
): StatisticValue[] => {
if (!values?.length) {
return [];
}
const result: StatisticValue[] = [];
if (
values.length > 1 &&
new Date(values[0].start).getMonth() ===
new Date(values[1].start).getMonth()
) {
// add init value if the first value isn't end of previous period
result.push({
...values[0]!,
start: startOfMonth(
addMonths(new Date(values[0].start), -1)
).toISOString(),
});
}
let lastValue: StatisticValue;
let prevMonth: number | undefined;
for (const value of values) {
const month = new Date(value.start).getMonth();
if (prevMonth === undefined) {
prevMonth = month;
}
if (prevMonth !== month) {
// Last value of the month
result.push({
...lastValue!,
start: startOfMonth(new Date(lastValue!.start)).toISOString(),
});
prevMonth = month;
}
lastValue = value;
}
// Add final value
result.push({
...lastValue!,
start: startOfMonth(new Date(lastValue!.start)).toISOString(),
});
return result;
};

View File

@@ -18,10 +18,15 @@ export const SCENE_IGNORED_DOMAINS = [
"zone", "zone",
]; ];
let inititialSceneEditorData: Partial<SceneConfig> | undefined; let inititialSceneEditorData:
| { config?: Partial<SceneConfig>; areaId?: string }
| undefined;
export const showSceneEditor = (data?: Partial<SceneConfig>) => { export const showSceneEditor = (
inititialSceneEditorData = data; config?: Partial<SceneConfig>,
areaId?: string
) => {
inititialSceneEditorData = { config, areaId };
navigate("/config/scene/edit/new"); navigate("/config/scene/edit/new");
}; };

View File

@@ -70,6 +70,42 @@ export interface Supervisor {
localize: LocalizeFunc; localize: LocalizeFunc;
} }
interface SupervisorBaseAvailableUpdates {
panel_path?: string;
update_type?: string;
version_latest?: string;
}
interface SupervisorAddonAvailableUpdates
extends SupervisorBaseAvailableUpdates {
update_type?: "addon";
icon?: string;
name?: string;
}
interface SupervisorCoreAvailableUpdates
extends SupervisorBaseAvailableUpdates {
update_type?: "core";
}
interface SupervisorOsAvailableUpdates extends SupervisorBaseAvailableUpdates {
update_type?: "os";
}
interface SupervisorSupervisorAvailableUpdates
extends SupervisorBaseAvailableUpdates {
update_type?: "supervisor";
}
export type SupervisorAvailableUpdates =
| SupervisorAddonAvailableUpdates
| SupervisorCoreAvailableUpdates
| SupervisorOsAvailableUpdates
| SupervisorSupervisorAvailableUpdates;
export interface SupervisorAvailableUpdatesResponse {
available_updates: SupervisorAvailableUpdates[];
}
export const supervisorApiWsRequest = <T>( export const supervisorApiWsRequest = <T>(
conn: Connection, conn: Connection,
request: supervisorApiRequest request: supervisorApiRequest
@@ -139,3 +175,14 @@ export const subscribeSupervisorEvents = (
getSupervisorEventCollection(hass.connection, key, endpoint).subscribe( getSupervisorEventCollection(hass.connection, key, endpoint).subscribe(
onChange onChange
); );
export const fetchSupervisorAvailableUpdates = async (
hass: HomeAssistant
): Promise<SupervisorAvailableUpdates[]> =>
(
await hass.callWS<SupervisorAvailableUpdatesResponse>({
type: "supervisor/api",
endpoint: "/supervisor/available_updates",
method: "get",
})
).available_updates;

View File

@@ -13,6 +13,7 @@ export interface User {
name: string; name: string;
is_owner: boolean; is_owner: boolean;
is_active: boolean; is_active: boolean;
local_only: boolean;
system_generated: boolean; system_generated: boolean;
group_ids: string[]; group_ids: string[];
credentials: Credential[]; credentials: Credential[];
@@ -22,6 +23,7 @@ export interface UpdateUserParams {
name?: User["name"]; name?: User["name"];
is_active?: User["is_active"]; is_active?: User["is_active"];
group_ids?: User["group_ids"]; group_ids?: User["group_ids"];
local_only?: boolean;
} }
export const fetchUsers = async (hass: HomeAssistant) => export const fetchUsers = async (hass: HomeAssistant) =>
@@ -33,12 +35,14 @@ export const createUser = async (
hass: HomeAssistant, hass: HomeAssistant,
name: string, name: string,
// eslint-disable-next-line: variable-name // eslint-disable-next-line: variable-name
group_ids?: User["group_ids"] group_ids?: User["group_ids"],
local_only?: boolean
) => ) =>
hass.callWS<{ user: User }>({ hass.callWS<{ user: User }>({
type: "config/auth/create", type: "config/auth/create",
name, name,
group_ids, group_ids,
local_only,
}); });
export const updateUser = async ( export const updateUser = async (

View File

@@ -152,17 +152,11 @@ export const getWeatherUnit = (
hass: HomeAssistant, hass: HomeAssistant,
measure: string measure: string
): string => { ): string => {
const lengthUnit = hass.config.unit_system.length || "";
switch (measure) { switch (measure) {
case "pressure":
return lengthUnit === "km" ? "hPa" : "inHg";
case "wind_speed":
return `${lengthUnit}/h`;
case "visibility": case "visibility":
case "length": return hass.config.unit_system.length || "";
return lengthUnit;
case "precipitation": case "precipitation":
return lengthUnit === "km" ? "mm" : "in"; return hass.config.unit_system.accumulated_precipitation || "";
case "humidity": case "humidity":
case "precipitation_probability": case "precipitation_probability":
return "%"; return "%";

View File

@@ -23,6 +23,8 @@ export interface Themes {
// in theme picker, this property will still contain either true or false based on // in theme picker, this property will still contain either true or false based on
// what has been determined via system preferences and support from the selected theme. // what has been determined via system preferences and support from the selected theme.
darkMode: boolean; darkMode: boolean;
// Currently globally active theme name
theme: string;
} }
const fetchThemes = (conn) => const fetchThemes = (conn) =>

View File

@@ -34,7 +34,7 @@ export interface ZHADevice {
export interface Neighbor { export interface Neighbor {
ieee: string; ieee: string;
nwk: string; nwk: string;
lqi: number; lqi: string;
} }
export interface ZHADeviceEndpoint { export interface ZHADeviceEndpoint {

View File

@@ -57,6 +57,45 @@ export enum SecurityClass {
S0_Legacy = 7, S0_Legacy = 7,
} }
/** A named list of Z-Wave features */
export enum ZWaveFeature {
// Available starting with Z-Wave SDK 6.81
SmartStart,
}
enum QRCodeVersion {
S2 = 0,
SmartStart = 1,
}
enum Protocols {
ZWave = 0,
ZWaveLongRange = 1,
}
export interface QRProvisioningInformation {
version: QRCodeVersion;
securityClasses: SecurityClass[];
dsk: string;
genericDeviceClass: number;
specificDeviceClass: number;
installerIconType: number;
manufacturerId: number;
productType: number;
productId: number;
applicationVersion: string;
maxInclusionRequestInterval?: number | undefined;
uuid?: string | undefined;
supportedProtocols?: Protocols[] | undefined;
}
export interface PlannedProvisioningEntry {
/** The device specific key (DSK) in the form aaaaa-bbbbb-ccccc-ddddd-eeeee-fffff-11111-22222 */
dsk: string;
security_classes: SecurityClass[];
}
export const MINIMUM_QR_STRING_LENGTH = 52;
export interface ZWaveJSNodeIdentifiers { export interface ZWaveJSNodeIdentifiers {
home_id: string; home_id: string;
node_id: number; node_id: number;
@@ -166,6 +205,16 @@ export const enum NodeStatus {
Alive, Alive,
} }
export interface ZwaveJSProvisioningEntry {
/** The device specific key (DSK) in the form aaaaa-bbbbb-ccccc-ddddd-eeeee-fffff-11111-22222 */
dsk: string;
securityClasses: SecurityClass[];
/**
* Additional properties to be stored in this provisioning entry, e.g. the device ID from a scanned QR code
*/
[prop: string]: any;
}
export interface RequestedGrant { export interface RequestedGrant {
/** /**
* An array of security classes that are requested or to be granted. * An array of security classes that are requested or to be granted.
@@ -197,7 +246,7 @@ export const migrateZwave = (
dry_run, dry_run,
}); });
export const fetchNetworkStatus = ( export const fetchZwaveNetworkStatus = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string entry_id: string
): Promise<ZWaveJSNetwork> => ): Promise<ZWaveJSNetwork> =>
@@ -206,7 +255,7 @@ export const fetchNetworkStatus = (
entry_id, entry_id,
}); });
export const fetchDataCollectionStatus = ( export const fetchZwaveDataCollectionStatus = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string entry_id: string
): Promise<ZWaveJSDataCollectionStatus> => ): Promise<ZWaveJSDataCollectionStatus> =>
@@ -215,7 +264,7 @@ export const fetchDataCollectionStatus = (
entry_id, entry_id,
}); });
export const setDataCollectionPreference = ( export const setZwaveDataCollectionPreference = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, entry_id: string,
opted_in: boolean opted_in: boolean
@@ -226,25 +275,40 @@ export const setDataCollectionPreference = (
opted_in, opted_in,
}); });
export const subscribeAddNode = ( export const fetchZwaveProvisioningEntries = (
hass: HomeAssistant,
entry_id: string
): Promise<any> =>
hass.callWS({
type: "zwave_js/get_provisioning_entries",
entry_id,
});
export const subscribeAddZwaveNode = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, entry_id: string,
callbackFunction: (message: any) => void, callbackFunction: (message: any) => void,
inclusion_strategy: InclusionStrategy = InclusionStrategy.Default inclusion_strategy: InclusionStrategy = InclusionStrategy.Default,
qr_provisioning_information?: QRProvisioningInformation,
qr_code_string?: string,
planned_provisioning_entry?: PlannedProvisioningEntry
): Promise<UnsubscribeFunc> => ): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage((message) => callbackFunction(message), { hass.connection.subscribeMessage((message) => callbackFunction(message), {
type: "zwave_js/add_node", type: "zwave_js/add_node",
entry_id: entry_id, entry_id: entry_id,
inclusion_strategy, inclusion_strategy,
qr_code_string,
qr_provisioning_information,
planned_provisioning_entry,
}); });
export const stopInclusion = (hass: HomeAssistant, entry_id: string) => export const stopZwaveInclusion = (hass: HomeAssistant, entry_id: string) =>
hass.callWS({ hass.callWS({
type: "zwave_js/stop_inclusion", type: "zwave_js/stop_inclusion",
entry_id, entry_id,
}); });
export const grantSecurityClasses = ( export const zwaveGrantSecurityClasses = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, entry_id: string,
security_classes: SecurityClass[], security_classes: SecurityClass[],
@@ -257,7 +321,7 @@ export const grantSecurityClasses = (
client_side_auth, client_side_auth,
}); });
export const validateDskAndEnterPin = ( export const zwaveValidateDskAndEnterPin = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, entry_id: string,
pin: string pin: string
@@ -268,7 +332,57 @@ export const validateDskAndEnterPin = (
pin, pin,
}); });
export const fetchNodeStatus = ( export const zwaveSupportsFeature = (
hass: HomeAssistant,
entry_id: string,
feature: ZWaveFeature
): Promise<{ supported: boolean }> =>
hass.callWS({
type: "zwave_js/supports_feature",
entry_id,
feature,
});
export const zwaveParseQrCode = (
hass: HomeAssistant,
entry_id: string,
qr_code_string: string
): Promise<QRProvisioningInformation> =>
hass.callWS({
type: "zwave_js/parse_qr_code_string",
entry_id,
qr_code_string,
});
export const provisionZwaveSmartStartNode = (
hass: HomeAssistant,
entry_id: string,
qr_provisioning_information?: QRProvisioningInformation,
qr_code_string?: string,
planned_provisioning_entry?: PlannedProvisioningEntry
): Promise<QRProvisioningInformation> =>
hass.callWS({
type: "zwave_js/provision_smart_start_node",
entry_id,
qr_code_string,
qr_provisioning_information,
planned_provisioning_entry,
});
export const unprovisionZwaveSmartStartNode = (
hass: HomeAssistant,
entry_id: string,
dsk?: string,
node_id?: number
): Promise<QRProvisioningInformation> =>
hass.callWS({
type: "zwave_js/unprovision_smart_start_node",
entry_id,
dsk,
node_id,
});
export const fetchZwaveNodeStatus = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, entry_id: string,
node_id: number node_id: number
@@ -279,7 +393,7 @@ export const fetchNodeStatus = (
node_id, node_id,
}); });
export const fetchNodeMetadata = ( export const fetchZwaveNodeMetadata = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, entry_id: string,
node_id: number node_id: number
@@ -290,7 +404,7 @@ export const fetchNodeMetadata = (
node_id, node_id,
}); });
export const fetchNodeConfigParameters = ( export const fetchZwaveNodeConfigParameters = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, entry_id: string,
node_id: number node_id: number
@@ -301,7 +415,7 @@ export const fetchNodeConfigParameters = (
node_id, node_id,
}); });
export const setNodeConfigParameter = ( export const setZwaveNodeConfigParameter = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, entry_id: string,
node_id: number, node_id: number,
@@ -320,7 +434,7 @@ export const setNodeConfigParameter = (
return hass.callWS(data); return hass.callWS(data);
}; };
export const reinterviewNode = ( export const reinterviewZwaveNode = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, entry_id: string,
node_id: number, node_id: number,
@@ -335,7 +449,7 @@ export const reinterviewNode = (
} }
); );
export const healNode = ( export const healZwaveNode = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, entry_id: string,
node_id: number node_id: number
@@ -346,7 +460,7 @@ export const healNode = (
node_id, node_id,
}); });
export const removeFailedNode = ( export const removeFailedZwaveNode = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, entry_id: string,
node_id: number, node_id: number,
@@ -361,7 +475,7 @@ export const removeFailedNode = (
} }
); );
export const healNetwork = ( export const healZwaveNetwork = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string entry_id: string
): Promise<UnsubscribeFunc> => ): Promise<UnsubscribeFunc> =>
@@ -370,7 +484,7 @@ export const healNetwork = (
entry_id, entry_id,
}); });
export const stopHealNetwork = ( export const stopHealZwaveNetwork = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string entry_id: string
): Promise<UnsubscribeFunc> => ): Promise<UnsubscribeFunc> =>
@@ -379,7 +493,7 @@ export const stopHealNetwork = (
entry_id, entry_id,
}); });
export const subscribeNodeReady = ( export const subscribeZwaveNodeReady = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, entry_id: string,
node_id: number, node_id: number,
@@ -394,7 +508,7 @@ export const subscribeNodeReady = (
} }
); );
export const subscribeHealNetworkProgress = ( export const subscribeHealZwaveNetworkProgress = (
hass: HomeAssistant, hass: HomeAssistant,
entry_id: string, entry_id: string,
callbackFunction: (message: ZWaveJSHealNetworkStatusMessage) => void callbackFunction: (message: ZWaveJSHealNetworkStatusMessage) => void
@@ -407,7 +521,7 @@ export const subscribeHealNetworkProgress = (
} }
); );
export const getIdentifiersFromDevice = ( export const getZwaveJsIdentifiersFromDevice = (
device: DeviceRegistryEntry device: DeviceRegistryEntry
): ZWaveJSNodeIdentifiers | undefined => { ): ZWaveJSNodeIdentifiers | undefined => {
if (!device) { if (!device) {

View File

@@ -0,0 +1,138 @@
import { mdiClose } from "@mdi/js";
import "@polymer/paper-tabs";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant, Route } from "../../types";
import "../../components/ha-dialog";
import "../../components/ha-tabs";
import "../../components/ha-icon-button";
import "../../panels/developer-tools/developer-tools-router";
import type { HaDialog } from "../../components/ha-dialog";
import "@material/mwc-button/mwc-button";
@customElement("ha-developer-tools-dialog")
export class HaDeveloperToolsDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _opened = false;
@state() private _route: Route = {
prefix: "/developer-tools",
path: "/state",
};
@query("ha-dialog", true) private _dialog!: HaDialog;
public async showDialog(): Promise<void> {
this._opened = true;
}
public async closeDialog(): Promise<void> {
this._opened = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._opened) {
return html``;
}
return html`
<ha-dialog open @closed=${this.closeDialog}>
<div class="header">
<ha-tabs
scrollable
attr-for-selected="page-name"
.selected=${this._route.path.substr(1)}
@iron-activate=${this.handlePageSelected}
>
<paper-tab page-name="state">
${this.hass.localize(
"ui.panel.developer-tools.tabs.states.title"
)}
</paper-tab>
<paper-tab page-name="service">
${this.hass.localize(
"ui.panel.developer-tools.tabs.services.title"
)}
</paper-tab>
<paper-tab page-name="template">
${this.hass.localize(
"ui.panel.developer-tools.tabs.templates.title"
)}
</paper-tab>
<paper-tab page-name="event">
${this.hass.localize(
"ui.panel.developer-tools.tabs.events.title"
)}
</paper-tab>
<paper-tab page-name="statistics">
${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.title"
)}
</paper-tab>
</ha-tabs>
<ha-icon-button
.path=${mdiClose}
@click=${this.closeDialog}
></ha-icon-button>
</div>
<developer-tools-router
.route=${this._route}
.narrow=${document.body.clientWidth < 600}
.hass=${this.hass}
></developer-tools-router>
</ha-dialog>
`;
}
protected firstUpdated(changedProps: PropertyValues) {
super.updated(changedProps);
this.hass.loadBackendTranslation("title");
this.hass.loadFragmentTranslation("developer-tools");
}
private handlePageSelected(ev) {
const newPage = ev.detail.item.getAttribute("page-name");
if (newPage !== this._route.path.substr(1)) {
this._route = {
prefix: "/developer-tools",
path: `/${newPage}`,
};
} else {
// scrollTo(0, 0);
}
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-min-width: 100vw;
--mdc-dialog-min-height: 100vh;
}
.header {
display: flex;
}
ha-tabs {
flex: 1;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-developer-tools-dialog": HaDeveloperToolsDialog;
}
}

View File

@@ -0,0 +1,12 @@
import { fireEvent } from "../../common/dom/fire_event";
export const loadDeveloperToolDialog = () =>
import("./ha-developer-tools-dialog");
export const showDeveloperToolDialog = (element: HTMLElement): void => {
fireEvent(element, "show-dialog", {
dialogTag: "ha-developer-tools-dialog",
dialogImport: loadDeveloperToolDialog,
dialogParams: {},
});
};

View File

@@ -39,6 +39,7 @@ export class HaImagecropperDialog extends LitElement {
this._open = false; this._open = false;
this._params = undefined; this._params = undefined;
this._cropper?.destroy(); this._cropper?.destroy();
this._cropper = undefined;
} }
protected updated(changedProperties: PropertyValues) { protected updated(changedProperties: PropertyValues) {

View File

@@ -4,7 +4,7 @@ export interface CropOptions {
round: boolean; round: boolean;
type?: "image/jpeg" | "image/png"; type?: "image/jpeg" | "image/png";
quality?: number; quality?: number;
aspectRatio: number; aspectRatio?: number;
} }
export interface HaImageCropperDialogParams { export interface HaImageCropperDialogParams {

View File

@@ -192,7 +192,7 @@ class MoreInfoClimate extends LitElement {
</div> </div>
</div> </div>
${supportPresetMode ${supportPresetMode && stateObj.attributes.preset_modes
? html` ? html`
<div class="container-preset_modes"> <div class="container-preset_modes">
<ha-paper-dropdown-menu <ha-paper-dropdown-menu
@@ -220,7 +220,7 @@ class MoreInfoClimate extends LitElement {
</div> </div>
` `
: ""} : ""}
${supportFanMode ${supportFanMode && stateObj.attributes.fan_modes
? html` ? html`
<div class="container-fan_list"> <div class="container-fan_list">
<ha-paper-dropdown-menu <ha-paper-dropdown-menu
@@ -248,7 +248,7 @@ class MoreInfoClimate extends LitElement {
</div> </div>
` `
: ""} : ""}
${supportSwingMode ${supportSwingMode && stateObj.attributes.swing_modes
? html` ? html`
<div class="container-swing_list"> <div class="container-swing_list">
<ha-paper-dropdown-menu <ha-paper-dropdown-menu

View File

@@ -1,124 +0,0 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { attributeClassNames } from "../../../common/entity/attribute_class_names";
import { featureClassNames } from "../../../common/entity/feature_class_names";
import "../../../components/ha-cover-tilt-controls";
import "../../../components/ha-labeled-slider";
import LocalizeMixin from "../../../mixins/localize-mixin";
import CoverEntity from "../../../util/cover-model";
const FEATURE_CLASS_NAMES = {
4: "has-set_position",
128: "has-set_tilt_position",
};
class MoreInfoCover extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style include="iron-flex"></style>
<style>
.current_position,
.tilt {
max-height: 0px;
overflow: hidden;
}
.has-set_position .current_position,
.has-current_position .current_position,
.has-set_tilt_position .tilt,
.has-current_tilt_position .tilt {
max-height: 208px;
}
[invisible] {
visibility: hidden !important;
}
</style>
<div class$="[[computeClassNames(stateObj)]]">
<div class="current_position">
<ha-labeled-slider
caption="[[localize('ui.card.cover.position')]]"
pin=""
value="{{coverPositionSliderValue}}"
disabled="[[!entityObj.supportsSetPosition]]"
on-change="coverPositionSliderChanged"
></ha-labeled-slider>
</div>
<div class="tilt">
<ha-labeled-slider
caption="[[localize('ui.card.cover.tilt_position')]]"
pin=""
extra=""
value="{{coverTiltPositionSliderValue}}"
disabled="[[!entityObj.supportsSetTiltPosition]]"
on-change="coverTiltPositionSliderChanged"
>
<ha-cover-tilt-controls
slot="extra"
hidden$="[[entityObj.isTiltOnly]]"
hass="[[hass]]"
state-obj="[[stateObj]]"
></ha-cover-tilt-controls>
</ha-labeled-slider>
</div>
</div>
<ha-attributes
hass="[[hass]]"
state-obj="[[stateObj]]"
extra-filters="current_position,current_tilt_position"
></ha-attributes>
`;
}
static get properties() {
return {
hass: Object,
stateObj: {
type: Object,
observer: "stateObjChanged",
},
entityObj: {
type: Object,
computed: "computeEntityObj(hass, stateObj)",
},
coverPositionSliderValue: Number,
coverTiltPositionSliderValue: Number,
};
}
computeEntityObj(hass, stateObj) {
return new CoverEntity(hass, stateObj);
}
stateObjChanged(newVal) {
if (newVal) {
this.setProperties({
coverPositionSliderValue: newVal.attributes.current_position,
coverTiltPositionSliderValue: newVal.attributes.current_tilt_position,
});
}
}
computeClassNames(stateObj) {
const classes = [
attributeClassNames(stateObj, [
"current_position",
"current_tilt_position",
]),
featureClassNames(stateObj, FEATURE_CLASS_NAMES),
];
return classes.join(" ");
}
coverPositionSliderChanged(ev) {
this.entityObj.setCoverPosition(ev.target.value);
}
coverTiltPositionSliderChanged(ev) {
this.entityObj.setCoverTiltPosition(ev.target.value);
}
}
customElements.define("more-info-cover", MoreInfoCover);

View File

@@ -0,0 +1,140 @@
import { css, CSSResult, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { attributeClassNames } from "../../../common/entity/attribute_class_names";
import { featureClassNames } from "../../../common/entity/feature_class_names";
import "../../../components/ha-attributes";
import "../../../components/ha-cover-tilt-controls";
import "../../../components/ha-labeled-slider";
import {
CoverEntity,
FEATURE_CLASS_NAMES,
isTiltOnly,
supportsSetPosition,
supportsSetTiltPosition,
} from "../../../data/cover";
import { HomeAssistant } from "../../../types";
@customElement("more-info-cover")
class MoreInfoCover extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: CoverEntity;
protected render(): TemplateResult {
if (!this.stateObj) {
return html``;
}
const _isTiltOnly = isTiltOnly(this.stateObj);
return html`
<div class=${this._computeClassNames(this.stateObj)}>
<div class="current_position">
<ha-labeled-slider
.caption=${this.hass.localize("ui.card.cover.position")}
pin=""
.value=${this.stateObj.attributes.current_position}
.disabled=${!supportsSetPosition(this.stateObj)}
@change=${this._coverPositionSliderChanged}
></ha-labeled-slider>
</div>
<div class="tilt">
${supportsSetTiltPosition(this.stateObj)
? // Either render the labeled slider and put the tilt buttons into its slot
// or (if tilt position is not supported and therefore no slider is shown)
// render a title <div> (same style as for a labeled slider) and directly put
// the tilt controls on the more-info.
html` <ha-labeled-slider
.caption=${this.hass.localize("ui.card.cover.tilt_position")}
pin=""
extra=""
.value=${this.stateObj.attributes.current_tilt_position}
@change=${this._coverTiltPositionSliderChanged}
>
${!_isTiltOnly
? html`<ha-cover-tilt-controls
.hass=${this.hass}
slot="extra"
.stateObj=${this.stateObj}
></ha-cover-tilt-controls> `
: html``}
</ha-labeled-slider>`
: !_isTiltOnly
? html`
<div class="title">
${this.hass.localize("ui.card.cover.tilt_position")}
</div>
<ha-cover-tilt-controls
.hass=${this.hass}
.stateObj=${this.stateObj}
></ha-cover-tilt-controls>
`
: html``}
</div>
</div>
<ha-attributes
.hass=${this.hass}
.stateObj=${this.stateObj}
extra-filters="current_position,current_tilt_position"
></ha-attributes>
`;
}
private _computeClassNames(stateObj) {
const classes = [
attributeClassNames(stateObj, [
"current_position",
"current_tilt_position",
]),
featureClassNames(stateObj, FEATURE_CLASS_NAMES),
];
return classes.join(" ");
}
private _coverPositionSliderChanged(ev) {
this.hass.callService("cover", "set_cover_position", {
entity_id: this.stateObj.entity_id,
position: ev.target.value,
});
}
private _coverTiltPositionSliderChanged(ev) {
this.hass.callService("cover", "set_cover_tilt_position", {
entity_id: this.stateObj.entity_id,
tilt_position: ev.target.value,
});
}
static get styles(): CSSResult {
return css`
.current_position,
.tilt {
max-height: 0px;
overflow: hidden;
}
.has-set_position .current_position,
.has-current_position .current_position,
.has-open_tilt .tilt,
.has-close_tilt .tilt,
.has-stop_tilt .tilt,
.has-set_tilt_position .tilt,
.has-current_tilt_position .tilt {
max-height: 208px;
}
/* from ha-labeled-slider for consistent look */
.title {
margin: 5px 0 8px;
color: var(--primary-text-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"more-info-cover": MoreInfoCover;
}
}

View File

@@ -1,3 +1,4 @@
import { startOfYesterday } from "date-fns";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../common/config/is_component_loaded";
@@ -22,6 +23,8 @@ export class MoreInfoHistory extends LitElement {
@state() private _stateHistory?: HistoryResult; @state() private _stateHistory?: HistoryResult;
private _showMoreHref = "";
private _throttleGetStateHistory = throttle(() => { private _throttleGetStateHistory = throttle(() => {
this._getStateHistory(); this._getStateHistory();
}, 10000); }, 10000);
@@ -31,14 +34,12 @@ export class MoreInfoHistory extends LitElement {
return html``; return html``;
} }
const href = "/history?entity_id=" + this.entityId;
return html`${isComponentLoaded(this.hass, "history") return html`${isComponentLoaded(this.hass, "history")
? html` <div class="header"> ? html` <div class="header">
<div class="title"> <div class="title">
${this.hass.localize("ui.dialogs.more_info_control.history")} ${this.hass.localize("ui.dialogs.more_info_control.history")}
</div> </div>
<a href=${href} @click=${this._close} <a href=${this._showMoreHref} @click=${this._close}
>${this.hass.localize( >${this.hass.localize(
"ui.dialogs.more_info_control.show_more" "ui.dialogs.more_info_control.show_more"
)}</a )}</a
@@ -63,6 +64,10 @@ export class MoreInfoHistory extends LitElement {
return; return;
} }
this._showMoreHref = `/history?entity_id=${
this.entityId
}&start_date=${startOfYesterday().toISOString()}`;
this._throttleGetStateHistory(); this._throttleGetStateHistory();
return; return;
} }

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