Compare commits

...

78 Commits

Author SHA1 Message Date
Ludeeus
294967014d Add pointer cursor to ha-formfield in update card 2021-11-29 06:01:17 +00: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
Bram Kragten
fcdceba09d Bumped version to 20211108.0 2021-11-08 18:30:59 +01:00
Bram Kragten
06d4ccf344 Allow create zone without icon + add icon picker (#10447) 2021-11-08 09:29:10 -08:00
Bram Kragten
a268040ae7 Fix datetime polyfill for latest build (#10572) 2021-11-08 09:28:27 -08:00
Bram Kragten
67d79d618a Allow to input decimal in ha-form-float (#10575) 2021-11-08 09:28:17 -08:00
Philip Allgaier
0e8a06e24d Use correct darkMode flag for image variant selection (#10574) 2021-11-08 11:41:17 +01:00
rianadon
d7732ee850 Improve accessibility on login page (#9731) 2021-11-08 10:29:34 +01:00
H. Árkosi Róbert
729a928cfe Update connectivity icons (#10558) 2021-11-08 10:27:08 +01:00
Paulus Schoutsen
fe5a582a74 Filter out entities when expanding device in target (#10570) 2021-11-08 10:22:52 +01:00
Bram Kragten
c26a59d805 Format timestamp sensor states (#10525) 2021-11-05 12:01:14 +01:00
chriss158
ea331dbe0b Fix cut off slider value (#10250) 2021-11-05 11:55:03 +01:00
Bram Kragten
b97d6d7059 Play dummy media to prevent app from closing (#10531) 2021-11-04 10:12:07 -07:00
Bram Kragten
9425b943dd Add separate cast media entrypoint (with ES5) (#10527) 2021-11-04 10:09:21 -07:00
Joakim Sørensen
3fd0becfd4 Update registry dialog (#10524) 2021-11-04 13:47:53 +01:00
Joakim Sørensen
12ef191a0f Handle missing hass with backup upload during onboarding (#10523) 2021-11-04 13:47:30 +01:00
Marius
2bbb4acf3d change switch icon to mdiToggleSwitch (#10475)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-11-04 13:37:38 +01:00
Marius
77d54df007 Add Bluetooth source_type icon (#10507)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-11-04 12:18:36 +01:00
Zack Barett
1c35571ef0 Fix Device Page (#10513) 2021-11-04 10:41:30 +01:00
Bram Kragten
c8804160bf Add basic support for button entity (#10504) 2021-11-03 15:38:52 -07:00
Marius
0a6ffb6bc8 change device_tracker icon to reflect state (#10501)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-11-03 11:59:39 +01:00
191 changed files with 4953 additions and 3360 deletions

View File

@@ -165,6 +165,7 @@ module.exports.config = {
cast({ isProdBuild, latestBuild }) {
const entry = {
launcher: path.resolve(paths.cast_dir, "src/launcher/entrypoint.ts"),
media: path.resolve(paths.cast_dir, "src/media/entrypoint.ts"),
};
if (latestBuild) {

View File

@@ -154,6 +154,15 @@ gulp.task("gen-index-cast-dev", (done) => {
contentReceiver
);
const contentMedia = renderCastTemplate("media", {
latestMediaJS: "/frontend_latest/media.js",
es5MediaJS: "/frontend_es5/media.js",
});
fs.outputFileSync(
path.resolve(paths.cast_output_root, "media.html"),
contentMedia
);
const contentFAQ = renderCastTemplate("launcher-faq", {
latestLauncherJS: "/frontend_latest/launcher.js",
es5LauncherJS: "/frontend_es5/launcher.js",
@@ -192,6 +201,15 @@ gulp.task("gen-index-cast-prod", (done) => {
contentReceiver
);
const contentMedia = renderCastTemplate("media", {
latestMediaJS: latestManifest["media.js"],
es5MediaJS: es5Manifest["media.js"],
});
fs.outputFileSync(
path.resolve(paths.cast_output_root, "media.html"),
contentMedia
);
const contentFAQ = renderCastTemplate("launcher-faq", {
latestLauncherJS: latestManifest["launcher.js"],
es5LauncherJS: es5Manifest["launcher.js"],

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<script src="//www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js"></script>
<style>
body {
--logo-image: url('https://www.home-assistant.io/images/home-assistant-logo.svg');
--logo-repeat: no-repeat;
--playback-logo-image: url('https://www.home-assistant.io/images/home-assistant-logo.svg');
--theme-hue: 200;
--progress-color: #03a9f4;
--splash-image: url('https://home-assistant.io/images/cast/splash.png');
--splash-size: cover;
--background-color: #41bdf5;
}
</style>
<script>
var _gaq=[['_setAccount','UA-57927901-10'],['_trackPageview']];
(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
g.src=('https:'==location.protocol?'//ssl':'//www')+'.google-analytics.com/ga.js';
s.parentNode.insertBefore(g,s)}(document,'script'));
</script>
</head>
<body>
<%= renderTemplate('_js_base') %>
<cast-media-player></cast-media-player>
<script>
import("<%= latestMediaJS %>");
window.latestJS = true;
</script>
<script>
if (!window.latestJS) {
<% if (useRollup) { %>
_ls("/static/js/s.min.js").onload = function() {
System.import("<%= es5MediaJS %>");
};
<% } else { %>
_ls("<%= es5MediaJS %>");
<% } %>
}
</script>
</body>
</html>

View File

@@ -0,0 +1,22 @@
const castContext = cast.framework.CastReceiverContext.getInstance();
const playerManager = castContext.getPlayerManager();
playerManager.setMessageInterceptor(
cast.framework.messages.MessageType.LOAD,
(loadRequestData) => {
const media = loadRequestData.media;
// Special handling if it came from Google Assistant
if (media.entity) {
media.contentId = media.entity;
media.streamType = cast.framework.messages.StreamType.LIVE;
media.contentType = "application/vnd.apple.mpegurl";
// @ts-ignore
media.hlsVideoSegmentFormat =
cast.framework.messages.HlsVideoSegmentFormat.FMP4;
}
return loadRequestData;
}
);
castContext.start();

View File

@@ -8,6 +8,9 @@ import { ReceivedMessage } from "./types";
const lovelaceController = new HcMain();
document.body.append(lovelaceController);
lovelaceController.addEventListener("cast-view-changed", (ev) => {
playDummyMedia(ev.detail.title);
});
const mediaPlayer = document.createElement("cast-media-player");
mediaPlayer.style.display = "none";
@@ -28,6 +31,31 @@ const setTouchControlsVisibility = (visible: boolean) => {
}
};
let timeOut: number | undefined;
const playDummyMedia = (viewTitle?: string) => {
const loadRequestData = new cast.framework.messages.LoadRequestData();
loadRequestData.autoplay = true;
loadRequestData.media = new cast.framework.messages.MediaInformation();
loadRequestData.media.contentId =
"https://cast.home-assistant.io/images/google-nest-hub.png";
loadRequestData.media.contentType = "image/jpeg";
loadRequestData.media.streamType = cast.framework.messages.StreamType.NONE;
const metadata = new cast.framework.messages.GenericMediaMetadata();
metadata.title = viewTitle;
loadRequestData.media.metadata = metadata;
loadRequestData.requestId = 0;
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 = () => {
mediaPlayer.style.display = "none";
lovelaceController.style.display = "initial";
@@ -51,6 +79,7 @@ const showMediaPlayer = () => {
--progress-color: #03a9f4;
--splash-image: url('https://home-assistant.io/images/cast/splash.png');
--splash-size: cover;
--background-color: #41bdf5;
}
`;
document.head.appendChild(style);
@@ -63,22 +92,6 @@ options.customNamespaces = {
[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(
CAST_NS,
// @ts-ignore
@@ -103,6 +116,12 @@ const playerManager = castContext.getPlayerManager();
playerManager.setMessageInterceptor(
cast.framework.messages.MessageType.LOAD,
(loadRequestData) => {
if (
loadRequestData.media.contentId ===
"https://cast.home-assistant.io/images/google-nest-hub.png"
) {
return loadRequestData;
}
// We received a play media command, hide Lovelace and show media player
showMediaPlayer();
const media = loadRequestData.media;

View File

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

View File

@@ -13,7 +13,11 @@ import {
ShowDemoMessage,
ShowLovelaceViewMessage,
} 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 { isNavigationClick } from "../../../../src/common/dom/is-navigation-click";
import {
@@ -40,9 +44,9 @@ export class HcMain extends HassElement {
@state() private _error?: string;
private _unsubLovelace?: UnsubscribeFunc;
@state() private _urlPath?: string | null;
private _urlPath?: string | null;
private _unsubLovelace?: UnsubscribeFunc;
public processIncomingMessage(msg: HassMessage) {
if (msg.type === "connect") {
@@ -68,8 +72,10 @@ export class HcMain extends HassElement {
!this._lovelaceConfig ||
this._lovelacePath === null ||
// Guard against part of HA not being loaded yet.
(this.hass &&
(!this.hass.states || !this.hass.config || !this.hass.services))
!this.hass ||
!this.hass.states ||
!this.hass.config ||
!this.hass.services
) {
return html`
<hc-launch-screen
@@ -107,6 +113,7 @@ export class HcMain extends HassElement {
this._sendStatus();
}
});
this.addEventListener("dialog-closed", this._dialogClosed);
}
private _sendStatus(senderId?: string) {
@@ -118,7 +125,7 @@ export class HcMain extends HassElement {
if (this.hass) {
status.hassUrl = this.hass.auth.data.hassUrl;
status.lovelacePath = this._lovelacePath!;
status.lovelacePath = this._lovelacePath;
status.urlPath = this._urlPath;
}
@@ -131,6 +138,30 @@ 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 = () => {
document.body.setAttribute("style", "overflow-y: auto !important");
};
private async _handleGetStatusMessage(msg: GetStatusMessage) {
this._sendStatus(msg.senderId!);
}
@@ -149,14 +180,18 @@ export class HcMain extends HassElement {
}),
});
} catch (err: any) {
this._error = this._getErrorMessage(err);
const errorMessage = this._getErrorMessage(err);
this._error = errorMessage;
this._sendError(err, errorMessage);
return;
}
let connection;
try {
connection = await createConnection({ auth });
} catch (err: any) {
this._error = this._getErrorMessage(err);
const errorMessage = this._getErrorMessage(err);
this._error = errorMessage;
this._sendError(err, errorMessage);
return;
}
if (this.hass) {
@@ -168,24 +203,29 @@ export class HcMain extends HassElement {
}
private async _handleShowLovelaceMessage(msg: ShowLovelaceViewMessage) {
this._showDemo = false;
// We should not get this command before we are connected.
// Means a client got out of sync. Let's send status to them.
if (!this.hass) {
this._sendStatus(msg.senderId!);
this._error = "Cannot show Lovelace because we're not connected.";
this._sendError(ReceiverErrorCode.NOT_CONNECTED, this._error);
return;
}
this._error = undefined;
if (msg.urlPath === "lovelace") {
msg.urlPath = null;
}
this._lovelacePath = msg.viewPath;
if (!this._unsubLovelace || this._urlPath !== msg.urlPath) {
this._urlPath = msg.urlPath;
this._lovelaceConfig = undefined;
if (this._unsubLovelace) {
this._unsubLovelace();
}
const llColl = atLeastVersion(this.hass.connection.haVersion, 0, 107)
? getLovelaceCollection(this.hass!.connection, msg.urlPath)
: getLegacyLovelaceCollection(this.hass!.connection);
? getLovelaceCollection(this.hass.connection, msg.urlPath)
: getLegacyLovelaceCollection(this.hass.connection);
// We first do a single refresh because we need to check if there is LL
// configuration.
try {
@@ -194,8 +234,16 @@ export class HcMain extends HassElement {
this._handleNewLovelaceConfig(lovelaceConfig)
);
} catch (err: any) {
if (
atLeastVersion(this.hass.connection.haVersion, 0, 107) &&
err.code !== "config_not_found"
) {
// eslint-disable-next-line
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.
this._unsubLovelace = () => undefined;
await this._generateLovelaceConfig();
@@ -210,8 +258,6 @@ export class HcMain extends HassElement {
loadLovelaceResources(resources, this.hass!.auth.data.hassUrl);
}
}
this._showDemo = false;
this._lovelacePath = msg.viewPath;
this._sendStatus();
}
@@ -232,7 +278,7 @@ export class HcMain extends HassElement {
}
private _handleNewLovelaceConfig(lovelaceConfig: LovelaceConfig) {
castContext.setApplicationState(lovelaceConfig.title!);
castContext.setApplicationState(lovelaceConfig.title || "");
this._lovelaceConfig = lovelaceConfig;
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

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 { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
import "../../../src/components/ha-alert";
import "../../../src/components/ha-card";
import "../../../src/components/ha-logo-svg";
const alerts: {
title?: string;
description: string | TemplateResult;
type: "info" | "warning" | "error" | "success";
dismissable?: boolean;
action?: string;
rtl?: boolean;
iconSlot?: TemplateResult;
actionSlot?: TemplateResult;
}[] = [
{
title: "Test info alert",
@@ -73,13 +77,35 @@ const alerts: {
title: "Error with action",
description: "This is a test error alert with action",
type: "error",
action: "restart",
actionSlot: html`<mwc-button slot="action" label="restart"></mwc-button>`,
},
{
title: "Unsaved data",
description: "You have unsaved data",
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)",
@@ -91,7 +117,7 @@ const alerts: {
title: "Error with action",
description: "This is a test error alert with action (RTL)",
type: "error",
action: "restart",
actionSlot: html`<mwc-button slot="action" label="restart"></mwc-button>`,
rtl: true,
},
{
@@ -106,7 +132,10 @@ const alerts: {
export class DemoHaAlert extends LitElement {
protected render(): TemplateResult {
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">
${alerts.map(
(alert) => html`
@@ -114,22 +143,49 @@ export class DemoHaAlert extends LitElement {
.title=${alert.title || ""}
.alertType=${alert.type}
.dismissable=${alert.dismissable || false}
.actionText=${alert.action || ""}
.rtl=${alert.rtl || false}
>
${alert.description}
${alert.iconSlot} ${alert.description} ${alert.actionSlot}
</ha-alert>
`
)}
</div>
</ha-card>
</div>
`
)}
`;
}
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: false,
},
"default",
{ dark: true }
);
}
static get styles() {
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 {
max-width: 600px;
margin: 24px auto;
}
ha-alert {
@@ -142,8 +198,17 @@ export class DemoHaAlert extends LitElement {
align-items: center;
justify-content: space-between;
}
span {
margin-right: 16px;
.image {
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 "../../../src/components/ha-card";
import "../../../src/components/ha-chip";
import "../../../src/components/ha-chip-set";
import "../../../src/components/ha-svg-icon";
const chips: {
@@ -22,8 +23,8 @@ const chips: {
},
];
@customElement("demo-ha-chip")
export class DemoHaChip extends LitElement {
@customElement("demo-ha-chips")
export class DemoHaChips extends LitElement {
protected render(): TemplateResult {
return html`
<ha-card header="ha-chip demo">
@@ -41,6 +42,23 @@ export class DemoHaChip extends LitElement {
)}
</div>
</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;
margin: 24px auto;
}
ha-chip {
margin-bottom: 4px;
}
.card-content {
display: flex;
flex-direction: column;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-ha-chip": DemoHaChip;
"demo-ha-chips": DemoHaChips;
}
}

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

@@ -176,11 +176,6 @@ class HaGallery extends PolymerElement {
this.addEventListener("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) => {
if (ev.detail.entityId) {
this.$.notifications.showDialog({

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
import "@material/mwc-button";
import {
mdiArrowUpBoldCircle,
mdiCheckCircle,
mdiChip,
mdiCircle,
@@ -49,7 +48,6 @@ import {
startHassioAddon,
stopHassioAddon,
uninstallHassioAddon,
updateHassioAddon,
validateHassioAddonOption,
} from "../../../../src/data/hassio/addon";
import {
@@ -64,14 +62,14 @@ import {
showConfirmationDialog,
} from "../../../../src/dialogs/generic/show-dialog-box";
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 "../../components/hassio-card-content";
import "../../components/supervisor-metric";
import { showHassioMarkdownDialog } from "../../dialogs/markdown/show-dialog-hassio-markdown";
import { showDialogSupervisorUpdate } from "../../dialogs/update/show-dialog-update";
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 = {
stable: mdiCheckCircle,
@@ -92,6 +90,8 @@ const RATING_ICON = {
class HassioAddonInfo extends LitElement {
@property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public addon!: HassioAddonDetails;
@@ -128,69 +128,12 @@ class HassioAddonInfo extends LitElement {
return html`
${this.addon.update_available
? html`
<ha-card
.header="${this.supervisor.localize(
"common.update_available",
"count",
1
)}🎉"
>
<div class="card-content">
<hassio-card-content
<update-available-card
.hass=${this.hass}
.title=${this.supervisor.localize(
"addon.dashboard.new_update_available",
"name",
this.addon.name,
"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>
.narrow=${this.narrow}
.supervisor=${this.supervisor}
.addonSlug=${this.addon.slug}
></update-available-card>
`
: ""}
${!this.addon.protected
@@ -200,14 +143,18 @@ class HassioAddonInfo extends LitElement {
.title=${this.supervisor.localize(
"addon.dashboard.protection_mode.title"
)}
.actionText=${this.supervisor.localize(
"addon.dashboard.protection_mode.enable"
)}
@alert-action-clicked=${this._protectionToggled}
>
${this.supervisor.localize(
"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>
`
: ""}
@@ -899,22 +846,14 @@ class HassioAddonInfo extends LitElement {
private async _openChangelog(): Promise<void> {
try {
let content = await fetchHassioAddonChangelog(this.hass, this.addon.slug);
if (
content.includes(`# ${this.addon.version}`) &&
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;
}
}
const content = await fetchHassioAddonChangelog(
this.hass,
this.addon.slug
);
showHassioMarkdownDialog(this, {
title: this.supervisor.localize("addon.dashboard.changelog"),
content,
content: extractChangelog(this.addon, content),
});
} catch (err: any) {
showAlertDialog(this, {
@@ -989,33 +928,6 @@ class HassioAddonInfo extends LitElement {
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> {
const button = ev.currentTarget as any;
button.progress = true;
@@ -1244,6 +1156,17 @@ class HassioAddonInfo extends LitElement {
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) {
ha-chip {
line-height: 36px;

View File

@@ -158,7 +158,7 @@ export class HassioBackups extends LitElement {
}
return html`
<hass-tabs-subpage-data-table
.tabs=${supervisorTabs}
.tabs=${supervisorTabs(this.hass)}
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.searchLabel=${this.supervisor.localize("search")}

View File

@@ -16,11 +16,9 @@ declare global {
}
}
const MAX_FILE_SIZE = 1 * 1024 * 1024 * 1024; // 1GB
@customElement("hassio-upload-backup")
export class HassioUploadBackup extends LitElement {
public hass!: HomeAssistant;
public hass?: HomeAssistant;
@state() public value: string | null = null;
@@ -43,20 +41,6 @@ export class HassioUploadBackup extends LitElement {
private async _uploadFile(ev) {
const file = ev.detail.files[0];
if (file.size > MAX_FILE_SIZE) {
showAlertDialog(this, {
title: "Backup file is too big",
text: html`The maximum allowed filesize is 1GB.<br />
<a
href="https://www.home-assistant.io/hassio/haos_common_tasks/#restoring-a-backup-on-a-new-install"
target="_blank"
>Have a look here on how to restore it.</a
>`,
confirmText: "ok",
});
return;
}
if (!["application/x-tar"].includes(file.type)) {
showAlertDialog(this, {
title: "Unsupported file format",

View File

@@ -20,7 +20,9 @@ class HassioAddons extends LitElement {
protected render(): TemplateResult {
return html`
<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">
${!this.supervisor.supervisor.addons?.length
? html`

View File

@@ -1,5 +1,8 @@
import { mdiStorePlus } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
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 "../../../src/layouts/hass-tabs-subpage";
import { haStyle } from "../../../src/resources/styles";
@@ -25,23 +28,37 @@ class HassioDashboard extends LitElement {
.localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${supervisorTabs}
.tabs=${supervisorTabs(this.hass)}
main-page
supervisor
hasFab
>
<span slot="header">
${this.supervisor.localize("panel.dashboard")}
</span>
<div class="content">
${this.hass.config.version.includes("dev") ||
!atLeastVersion(this.hass.config.version, 2021, 12)
? html`
<hassio-update
.hass=${this.hass}
.supervisor=${this.supervisor}
></hassio-update>
`
: ""}
<hassio-addons
.hass=${this.hass}
.supervisor=${this.supervisor}
></hassio-addons>
</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>
`;
}

View File

@@ -3,34 +3,18 @@ import { mdiHomeAssistant } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
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/ha-card";
import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-svg-icon";
import {
extractApiErrorMessage,
HassioResponse,
ignoreSupervisorError,
} from "../../../src/data/hassio/common";
import { HassioHassOSInfo } from "../../../src/data/hassio/host";
import {
HassioHomeAssistantInfo,
HassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor";
import { updateCore } from "../../../src/data/supervisor/core";
import {
Supervisor,
supervisorApiWsRequest,
} from "../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { showDialogSupervisorUpdate } from "../dialogs/update/show-dialog-update";
import { hassioStyle } from "../resources/hassio-style";
const computeVersion = (key: string, version: string): string =>
@@ -73,26 +57,18 @@ export class HassioUpdate extends LitElement {
${this._renderUpdateCard(
"Home Assistant Core",
"core",
this.supervisor.core,
"hassio/homeassistant/update",
`https://${
this.supervisor.core.version_latest.includes("b") ? "rc" : "www"
}.home-assistant.io/latest-release-notes/`
this.supervisor.core
)}
${this._renderUpdateCard(
"Supervisor",
"supervisor",
this.supervisor.supervisor,
"hassio/supervisor/update",
`https://github.com//home-assistant/hassio/releases/tag/${this.supervisor.supervisor.version_latest}`
this.supervisor.supervisor
)}
${this.supervisor.host.features.includes("haos")
? this._renderUpdateCard(
"Operating System",
"os",
this.supervisor.os,
"hassio/os/update",
`https://github.com//home-assistant/hassos/releases/tag/${this.supervisor.os.version_latest}`
this.supervisor.os
)
: ""}
</div>
@@ -103,9 +79,7 @@ export class HassioUpdate extends LitElement {
private _renderUpdateCard(
name: string,
key: string,
object: HassioHomeAssistantInfo | HassioSupervisorInfo | HassioHassOSInfo,
apiPath: string,
releaseNotesUrl: string
object: HassioHomeAssistantInfo | HassioSupervisorInfo | HassioHassOSInfo
): TemplateResult {
if (!object.update_available) {
return html``;
@@ -136,96 +110,15 @@ export class HassioUpdate extends LitElement {
</ha-settings-row>
</div>
<div class="card-actions">
<a href=${releaseNotesUrl} target="_blank" rel="noreferrer">
<mwc-button>
${this.supervisor.localize("common.release_notes")}
<a href="/hassio/update-available/${key}">
<mwc-button .label=${this.supervisor.localize("common.show")}>
</mwc-button>
</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>
</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 {
return [
haStyle,

View File

@@ -15,7 +15,7 @@ export class DialogHassioBackupUpload
extends LitElement
implements HassDialog<HassioBackupUploadDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _params?: HassioBackupUploadDialogParams;
@@ -54,7 +54,7 @@ export class DialogHassioBackupUpload
<ha-header-bar>
<span slot="title"> Upload backup </span>
<ha-icon-button
.label=${this.hass.localize("common.close")}
.label=${this.hass?.localize("common.close") || "close"}
.path=${mdiClose}
slot="actionItems"
dialogAction="cancel"

View File

@@ -35,7 +35,7 @@ class HassioBackupDialog
extends LitElement
implements HassDialog<HassioBackupDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _error?: string;
@@ -77,7 +77,7 @@ class HassioBackupDialog
<ha-header-bar>
<span slot="title">${this._backup.name}</span>
<ha-icon-button
.label=${this.hass.localize("common.close")}
.label=${this.hass?.localize("common.close") || "close"}
.path=${mdiClose}
slot="actionItems"
dialogAction="cancel"
@@ -114,7 +114,7 @@ class HassioBackupDialog
@closed=${stopPropagation}
>
<ha-icon-button
.label=${this.hass.localize("common.menu")}
.label=${this.hass!.localize("common.menu")}
.path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>
@@ -192,18 +192,16 @@ class HassioBackupDialog
}
if (!this._dialogParams?.onboarding) {
this.hass
.callApi(
this.hass!.callApi(
"POST",
`hassio/${
atLeastVersion(this.hass.config.version, 2021, 9)
atLeastVersion(this.hass!.config.version, 2021, 9)
? "backups"
: "snapshots"
}/${this._backup!.slug}/restore/partial`,
backupDetails
)
.then(
).then(
() => {
this.closeDialog();
},
@@ -244,17 +242,15 @@ class HassioBackupDialog
}
if (!this._dialogParams?.onboarding) {
this.hass
.callApi(
this.hass!.callApi(
"POST",
`hassio/${
atLeastVersion(this.hass.config.version, 2021, 9)
atLeastVersion(this.hass!.config.version, 2021, 9)
? "backups"
: "snapshots"
}/${this._backup!.slug}/restore/full`,
backupDetails
)
.then(
).then(
() => {
this.closeDialog();
},
@@ -283,17 +279,14 @@ class HassioBackupDialog
return;
}
this.hass
.callApi(
atLeastVersion(this.hass.config.version, 2021, 9) ? "DELETE" : "POST",
this.hass!.callApi(
atLeastVersion(this.hass!.config.version, 2021, 9) ? "DELETE" : "POST",
`hassio/${
atLeastVersion(this.hass.config.version, 2021, 9)
atLeastVersion(this.hass!.config.version, 2021, 9)
? `backups/${this._backup!.slug}`
: `snapshots/${this._backup!.slug}/remove`
}`
)
.then(
).then(
() => {
if (this._dialogParams!.onDelete) {
this._dialogParams!.onDelete();
@@ -310,9 +303,9 @@ class HassioBackupDialog
let signedPath: { path: string };
try {
signedPath = await getSignedPath(
this.hass,
this.hass!,
`/api/hassio/${
atLeastVersion(this.hass.config.version, 2021, 9)
atLeastVersion(this.hass!.config.version, 2021, 9)
? "backups"
: "snapshots"
}/${this._backup!.slug}/download`

View File

@@ -1,12 +1,12 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list-item";
import { mdiDelete } from "@mdi/js";
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../src/components/ha-circular-progress";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-form/ha-form";
import { HaFormSchema } from "../../../../src/components/ha-form/types";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-settings-row";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import {
addHassioDockerRegistry,
@@ -19,22 +19,41 @@ import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { RegistriesDialogParams } from "./show-dialog-registries";
const SCHEMA = [
{
type: "string",
name: "registry",
required: true,
},
{
type: "string",
name: "username",
required: true,
},
{
type: "string",
name: "password",
required: true,
format: "password",
},
];
@customElement("dialog-hassio-registries")
class HassioRegistriesDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) private _registries?: {
@state() private _registries?: {
registry: string;
username: string;
}[];
@state() private _registry?: string;
@state() private _username?: string;
@state() private _password?: string;
@state() private _input: {
registry?: string;
username?: string;
password?: string;
} = {};
@state() private _opened = false;
@@ -47,6 +66,7 @@ class HassioRegistriesDialog extends LitElement {
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
hideActions
.heading=${createCloseHeading(
this.hass,
this._addingRegistry
@@ -54,99 +74,77 @@ class HassioRegistriesDialog extends LitElement {
: this.supervisor.localize("dialog.registries.title_manage")
)}
>
<div class="form">
${this._addingRegistry
? html`
<paper-input
@value-changed=${this._inputChanged}
class="flex-auto"
name="registry"
.label=${this.supervisor.localize(
"dialog.registries.registry"
)}
required
auto-validate
></paper-input>
<paper-input
@value-changed=${this._inputChanged}
class="flex-auto"
name="username"
.label=${this.supervisor.localize(
"dialog.registries.username"
)}
required
auto-validate
></paper-input>
<paper-input
@value-changed=${this._inputChanged}
class="flex-auto"
name="password"
.label=${this.supervisor.localize(
"dialog.registries.password"
)}
type="password"
required
auto-validate
></paper-input>
<ha-form
.data=${this._input}
.schema=${SCHEMA}
@value-changed=${this._valueChanged}
.computeLabel=${this._computeLabel}
></ha-form>
<div class="action">
<mwc-button
?disabled=${Boolean(
!this._registry || !this._username || !this._password
!this._input.registry ||
!this._input.username ||
!this._input.password
)}
@click=${this._addNewRegistry}
>
${this.supervisor.localize("dialog.registries.add_registry")}
</mwc-button>
</div>
`
: html`${this._registries?.length
? this._registries.map(
(entry) => html`
<mwc-list-item class="option" hasMeta twoline>
<span>${entry.registry}</span>
<span slot="secondary"
>${this.supervisor.localize(
<ha-settings-row class="registry">
<span slot="heading"> ${entry.registry} </span>
<span slot="description">
${this.supervisor.localize(
"dialog.registries.username"
)}:
${entry.username}</span
>
${entry.username}
</span>
<ha-icon-button
.entry=${entry}
.label=${this.supervisor.localize(
"dialog.registries.remove"
)}
.path=${mdiDelete}
slot="meta"
@click=${this._removeRegistry}
></ha-icon-button>
</mwc-list-item>
</ha-settings-row>
`
)
: html`
<mwc-list-item>
<span
>${this.supervisor.localize(
<ha-alert>
${this.supervisor.localize(
"dialog.registries.no_registries"
)}</span
>
</mwc-list-item>
)}
</ha-alert>
`}
<div class="action">
<mwc-button @click=${this._addRegistry}>
${this.supervisor.localize(
"dialog.registries.add_new_registry"
)}
</mwc-button> `}
</div>
</mwc-button>
</div> `}
</ha-dialog>
`;
}
private _inputChanged(ev: Event) {
const target = ev.currentTarget as PaperInputElement;
this[`_${target.name}`] = target.value;
private _computeLabel = (schema: HaFormSchema) =>
this.supervisor.localize(`dialog.registries.${schema.name}`) || schema.name;
private _valueChanged(ev: CustomEvent) {
this._input = ev.detail.value;
}
public async showDialog(dialogParams: RegistriesDialogParams): Promise<void> {
this._opened = true;
this._input = {};
this.supervisor = dialogParams.supervisor;
await this._loadRegistries();
await this.updateComplete;
@@ -155,6 +153,7 @@ class HassioRegistriesDialog extends LitElement {
public closeDialog(): void {
this._addingRegistry = false;
this._opened = false;
this._input = {};
}
public focus(): void {
@@ -179,15 +178,16 @@ class HassioRegistriesDialog extends LitElement {
private async _addNewRegistry(): Promise<void> {
const data = {};
data[this._registry!] = {
username: this._username,
password: this._password,
data[this._input.registry!] = {
username: this._input.username,
password: this._input.password,
};
try {
await addHassioDockerRegistry(this.hass, data);
await this._loadRegistries();
this._addingRegistry = false;
this._input = {};
} catch (err: any) {
showAlertDialog(this, {
title: this.supervisor.localize("dialog.registries.failed_to_add"),
@@ -215,32 +215,20 @@ class HassioRegistriesDialog extends LitElement {
haStyle,
haStyleDialog,
css`
ha-dialog.button-left {
--justify-action-buttons: flex-start;
}
paper-icon-item {
cursor: pointer;
}
.form {
color: var(--primary-text-color);
}
.option {
.registry {
border: 1px solid var(--divider-color);
border-radius: 4px;
margin-top: 4px;
}
mwc-button {
margin-left: 8px;
.action {
margin-top: 24px;
width: 100%;
display: flex;
justify-content: flex-end;
}
ha-icon-button {
color: var(--error-color);
margin: -10px;
}
mwc-list-item {
cursor: default;
}
mwc-list-item span[slot="secondary"] {
color: var(--secondary-text-color);
margin-right: -10px;
}
`,
];

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

View File

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

View File

@@ -35,6 +35,10 @@ class HassioRouter extends HassRouterPage {
backups: "dashboard",
store: "dashboard",
system: "dashboard",
"update-available": {
tag: "update-available-dashboard",
load: () => import("./update-available/update-available-dashboard"),
},
addon: {
tag: "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 { 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`,
iconPath: mdiViewDashboard,
},
{
translationKey: "panel.store",
path: `/hassio/store`,
iconPath: mdiStore,
iconPath: atLeastVersion(hass.config.version, 2021, 12)
? mdiPuzzle
: mdiViewDashboard,
},
{
translationKey: "panel.backups",

View File

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

View File

@@ -2,7 +2,7 @@ import "@material/mwc-button";
import "@material/mwc-list/mwc-list-item";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
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/ha-button-menu";
import "../../../src/components/ha-card";
@@ -12,7 +12,7 @@ import {
fetchHassioStats,
HassioStats,
} 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 {
showAlertDialog,
@@ -22,7 +22,6 @@ import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { bytesToString } from "../../../src/util/bytes-to-string";
import "../components/supervisor-metric";
import { showDialogSupervisorUpdate } from "../dialogs/update/show-dialog-update";
import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-core-info")
@@ -67,14 +66,15 @@ class HassioCoreInfo extends LitElement {
<span slot="description">
core-${this.supervisor.core.version_latest}
</span>
${this.supervisor.core.update_available
${!atLeastVersion(this.hass.config.version, 2021, 12) &&
this.supervisor.core.update_available
? html`
<ha-progress-button
.title=${this.supervisor.localize("common.update")}
@click=${this._coreUpdate}
<a href="/hassio/update-available/core">
<mwc-button
.label=${this.supervisor.localize("common.show")}
>
${this.supervisor.localize("common.update")}
</ha-progress-button>
</mwc-button>
</a>
`
: ""}
</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 {
return [
haStyle,
@@ -239,6 +218,9 @@ class HassioCoreInfo extends LitElement {
mwc-list-item ha-svg-icon {
color: var(--secondary-text-color);
}
a {
text-decoration: none;
}
`,
];
}

View File

@@ -21,7 +21,6 @@ import {
configSyncOS,
rebootHost,
shutdownHost,
updateOS,
} from "../../../src/data/hassio/host";
import {
fetchNetworkInfo,
@@ -106,11 +105,15 @@ class HassioHostInfo extends LitElement {
<span slot="description">
${this.supervisor.host.operating_system}
</span>
${this.supervisor.os.update_available
${!atLeastVersion(this.hass.config.version, 2021, 12) &&
this.supervisor.os.update_available
? html`
<ha-progress-button @click=${this._osUpdate}>
${this.supervisor.localize("commmon.update")}
</ha-progress-button>
<a href="/hassio/update-available/os">
<mwc-button
.label=${this.supervisor.localize("common.show")}
>
</mwc-button>
</a>
`
: ""}
</ha-settings-row>
@@ -333,50 +336,6 @@ class HassioHostInfo extends LitElement {
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> {
showNetworkDialog(this, {
supervisor: this.supervisor,
@@ -494,6 +453,9 @@ class HassioHostInfo extends LitElement {
mwc-list-item ha-svg-icon {
color: var(--secondary-text-color);
}
a {
text-decoration: none;
}
`,
];
}

View File

@@ -17,7 +17,6 @@ import {
restartSupervisor,
setSupervisorOption,
SupervisorOptions,
updateSupervisor,
} from "../../../src/data/hassio/supervisor";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import {
@@ -77,16 +76,15 @@ class HassioSupervisorInfo extends LitElement {
<span slot="description">
supervisor-${this.supervisor.supervisor.version_latest}
</span>
${this.supervisor.supervisor.update_available
${!atLeastVersion(this.hass.config.version, 2021, 12) &&
this.supervisor.supervisor.update_available
? html`
<ha-progress-button
.title=${this.supervisor.localize(
"system.supervisor.update_supervisor"
)}
@click=${this._supervisorUpdate}
<a href="/hassio/update-available/supervisor">
<mwc-button
.label=${this.supervisor.localize("common.show")}
>
${this.supervisor.localize("common.update")}
</ha-progress-button>
</mwc-button>
</a>
`
: ""}
</ha-settings-row>
@@ -153,24 +151,28 @@ class HassioSupervisorInfo extends LitElement {
></ha-switch>
</ha-settings-row>`
: ""
: html`<ha-alert
alert-type="warning"
.actionText=${this.supervisor.localize("common.learn_more")}
@alert-action-clicked=${this._unsupportedDialog}
>
: html`<ha-alert alert-type="warning">
${this.supervisor.localize(
"system.supervisor.unsupported_title"
)}
<mwc-button
slot="action"
.label=${this.supervisor.localize("common.learn_more")}
@click=${this._unsupportedDialog}
>
</mwc-button>
</ha-alert>`}
${!this.supervisor.supervisor.healthy
? html`<ha-alert
alert-type="error"
.actionText=${this.supervisor.localize("common.learn_more")}
@alert-action-clicked=${this._unhealthyDialog}
>
? html`<ha-alert alert-type="error">
${this.supervisor.localize(
"system.supervisor.unhealthy_title"
)}
<mwc-button
slot="action"
.label=${this.supervisor.localize("common.learn_more")}
@click=${this._unhealthyDialog}
>
</mwc-button>
</ha-alert>`
: ""}
</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> {
await showAlertDialog(this, {
title: this.supervisor.localize(
@@ -513,6 +470,12 @@ class HassioSupervisorInfo extends LitElement {
white-space: normal;
color: var(--secondary-text-color);
}
ha-alert mwc-button {
--mdc-theme-primary: var(--primary-text-color);
}
a {
text-decoration: none;
}
`,
];
}

View File

@@ -28,7 +28,7 @@ class HassioSystem extends LitElement {
.localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${supervisorTabs}
.tabs=${supervisorTabs(this.hass)}
main-page
supervisor
>

View File

@@ -0,0 +1,401 @@
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-expansion-panel";
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-expansion-panel header="Changelog" outlined>
<ha-markdown .content=${this._changelogContent}>
</ha-markdown>
</ha-expansion-panel>
`
: ""}
<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 {
return this.shadowRoot?.querySelector("ha-checkbox")?.checked || 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;
}
ha-formfield {
cursor: pointer;
}
`;
}
}
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 { HassioAddonDetails } from "../../../src/data/hassio/addon";
import { SupervisorArch } from "../../../src/data/supervisor/supervisor";
export const addonArchIsSupported = memoizeOne(
(supported_archs: SupervisorArch[], addon_archs: SupervisorArch[]) =>
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": {
"@braintree/sanitize-url": "^5.0.2",
"@codemirror/commands": "^0.19.5",
"@codemirror/gutter": "^0.19.3",
"@codemirror/gutter": "^0.19.4",
"@codemirror/highlight": "^0.19.6",
"@codemirror/history": "^0.19.0",
"@codemirror/legacy-modes": "^0.19.0",
"@codemirror/rectangular-selection": "^0.19.1",
"@codemirror/search": "^0.19.2",
"@codemirror/state": "^0.19.2",
"@codemirror/state": "^0.19.4",
"@codemirror/stream-parser": "^0.19.2",
"@codemirror/text": "^0.19.4",
"@codemirror/view": "^0.19.9",
"@codemirror/text": "^0.19.5",
"@codemirror/view": "^0.19.15",
"@formatjs/intl-datetimeformat": "^4.2.5",
"@formatjs/intl-getcanonicallocales": "^1.8.0",
"@formatjs/intl-locale": "^2.4.40",
@@ -67,8 +67,8 @@
"@material/mwc-tab-bar": "0.25.3",
"@material/mwc-textfield": "0.25.3",
"@material/top-app-bar": "14.0.0-canary.261f2db59.0",
"@mdi/js": "6.4.95",
"@mdi/svg": "6.4.95",
"@mdi/js": "6.5.95",
"@mdi/svg": "6.5.95",
"@polymer/app-layout": "^3.1.0",
"@polymer/iron-flex-layout": "^3.0.1",
"@polymer/iron-icon": "^3.0.1",

View File

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

View File

@@ -21,7 +21,11 @@ class HaPickAuthProvider extends litLocalizeLiteMixin(LitElement) {
<p>${this.localize("ui.panel.page-authorize.pick_auth_provider")}:</p>
${this.authProviders.map(
(provider) => html`
<paper-item .auth_provider=${provider} @click=${this._handlePick}>
<paper-item
role="button"
.auth_provider=${provider}
@click=${this._handlePick}
>
<paper-item-body>${provider.name}</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>

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.
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";

View File

@@ -11,4 +11,20 @@ export interface ReceiverStatusMessage extends BaseCastMessage {
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;

View File

@@ -7,7 +7,13 @@ export const canShowPage = (hass: HomeAssistant, page: PageNavigation) =>
!hideAdvancedPage(hass, page);
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 isAdvancedPage = (page: PageNavigation) => page.advancedOnly;
const userWantsAdvanced = (hass: HomeAssistant) => hass.userData?.showAdvanced;

View File

@@ -76,7 +76,6 @@ export const FIXED_DOMAIN_ICONS = {
configurator: mdiCog,
conversation: mdiTextToSpeech,
counter: mdiCounter,
device_tracker: mdiAccount,
fan: mdiFan,
google_assistant: mdiGoogleAssistant,
group: mdiGoogleCirclesCommunities,
@@ -104,7 +103,6 @@ export const FIXED_DOMAIN_ICONS = {
siren: mdiBullhorn,
simple_alarm: mdiBell,
sun: mdiWhiteBalanceSunny,
switch: mdiFlash,
timer: mdiTimerOutline,
updater: mdiCloudUpload,
vacuum: mdiRobotVacuum,
@@ -121,6 +119,7 @@ export const FIXED_DEVICE_CLASS_ICONS = {
current: mdiCurrentAc,
date: mdiCalendar,
energy: mdiLightningBolt,
frequency: mdiSineWave,
gas: mdiGasCylinder,
humidity: mdiWaterPercent,
illuminance: mdiBrightness5,
@@ -145,6 +144,7 @@ export const FIXED_DEVICE_CLASS_ICONS = {
/** Domains that have a state card. */
export const DOMAINS_WITH_CARD = [
"button",
"climate",
"cover",
"configurator",

View File

@@ -6,6 +6,8 @@ import {
mdiBrightness5,
mdiBrightness7,
mdiCheckboxMarkedCircle,
mdiCheckNetworkOutline,
mdiCloseNetworkOutline,
mdiCheckCircle,
mdiCropPortrait,
mdiDoorClosed,
@@ -26,8 +28,6 @@ import {
mdiPowerPlugOff,
mdiRadioboxBlank,
mdiRun,
mdiServerNetwork,
mdiServerNetworkOff,
mdiSmoke,
mdiSnowflake,
mdiSquare,
@@ -55,7 +55,7 @@ export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => {
case "cold":
return is_off ? mdiThermometer : mdiSnowflake;
case "connectivity":
return is_off ? mdiServerNetworkOff : mdiServerNetwork;
return is_off ? mdiCloseNetworkOutline : mdiCheckNetworkOutline;
case "door":
return is_off ? mdiDoorClosed : mdiDoorOpen;
case "garage_door":

View File

@@ -116,6 +116,14 @@ export const computeStateDisplay = (
return formatNumber(compareState, locale);
}
// state of button is a timestamp
if (
domain === "button" ||
(domain === "sensor" && stateObj.attributes.device_class === "timestamp")
) {
return formatDateTime(new Date(compareState), locale);
}
return (
// Return device class translation
(stateObj.attributes.device_class &&

View File

@@ -1,6 +1,13 @@
import {
mdiAccount,
mdiAccountArrowRight,
mdiAirHumidifierOff,
mdiAirHumidifier,
mdiFlash,
mdiBluetooth,
mdiBluetoothConnect,
mdiLanConnect,
mdiLanDisconnect,
mdiLockOpen,
mdiLockAlert,
mdiLockClock,
@@ -8,8 +15,12 @@ import {
mdiCastConnected,
mdiCast,
mdiEmoticonDead,
mdiPowerPlug,
mdiPowerPlugOff,
mdiSleep,
mdiTimerSand,
mdiToggleSwitch,
mdiToggleSwitchOff,
mdiZWave,
mdiClock,
mdiCalendar,
@@ -44,6 +55,17 @@ export const domainIcon = (
case "cover":
return coverIcon(compareState, stateObj);
case "device_tracker":
if (stateObj?.attributes.source_type === "router") {
return compareState === "home" ? mdiLanConnect : mdiLanDisconnect;
}
if (
["bluetooth", "bluetooth_le"].includes(stateObj?.attributes.source_type)
) {
return compareState === "home" ? mdiBluetoothConnect : mdiBluetooth;
}
return compareState === "not_home" ? mdiAccountArrowRight : mdiAccount;
case "humidifier":
return state && state === "off" ? mdiAirHumidifierOff : mdiAirHumidifier;
@@ -63,6 +85,16 @@ export const domainIcon = (
case "media_player":
return compareState === "playing" ? mdiCastConnected : mdiCast;
case "switch":
switch (stateObj?.attributes.device_class) {
case "outlet":
return state === "on" ? mdiPowerPlug : mdiPowerPlugOff;
case "switch":
return state === "on" ? mdiToggleSwitch : mdiToggleSwitchOff;
default:
return mdiFlash;
}
case "zwave":
switch (compareState) {
case "dead":

View File

@@ -32,6 +32,7 @@ if (__BUILD__ === "latest") {
}
if (shouldPolyfillDateTime()) {
polyfills.push(import("@formatjs/intl-datetimeformat/polyfill"));
polyfills.push(import("@formatjs/intl-datetimeformat/add-all-tz"));
}
}

View File

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

View File

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

View File

@@ -172,6 +172,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
{
area_id: "",
name: this.hass.localize("ui.components.area-picker.no_areas"),
picture: null,
},
];
}
@@ -295,6 +296,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
{
area_id: "",
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",
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-id-path="area_id"
item-label-path="name"
.value=${this._value}
.value=${this.value}
.disabled=${this.disabled}
${comboBoxRenderer(rowRenderer)}
@opened-changed=${this._openedChanged}
@@ -431,12 +434,24 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
name,
});
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);
} catch (err: any) {
showAlertDialog(this, {
text: this.hass.localize(
title: this.hass.localize(
"ui.components.area-picker.add_dialog.failed_create_area"
),
text: err.message,
});
}
},

View File

@@ -8,52 +8,31 @@ import {
TemplateResult,
unsafeCSS,
} from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-chip";
declare global {
// for fire event
interface HASSDomEvents {
"chip-clicked": { index: string };
}
}
import { customElement } from "lit/decorators";
@customElement("ha-chip-set")
export class HaChipSet extends LitElement {
@property() public items = [];
protected render(): TemplateResult {
if (this.items.length === 0) {
return html``;
}
return html`
<div class="mdc-chip-set">
${this.items.map(
(item, idx) =>
html`
<ha-chip .index=${idx} @click=${this._handleClick}>
${item}
</ha-chip>
`
)}
<slot></slot>
</div>
`;
}
private _handleClick(ev): void {
fireEvent(this, "chip-clicked", {
index: ev.currentTarget.index,
});
}
static get styles(): CSSResultGroup {
return css`
${unsafeCSS(chipStyles)}
ha-chip {
slot::slotted(ha-chip) {
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,22 +10,13 @@ import {
} from "lit";
import { customElement, property } from "lit/decorators";
declare global {
// for fire event
interface HASSDomEvents {
"chip-clicked": { index: string };
}
}
@customElement("ha-chip")
export class HaChip extends LitElement {
@property() public index = 0;
@property({ type: Boolean }) public hasIcon = false;
protected render(): TemplateResult {
return html`
<div class="mdc-chip" .index=${this.index}>
<div class="mdc-chip">
${this.hasIcon
? html`<div class="mdc-chip__icon mdc-chip__icon--leading">
<slot name="icon"></slot>

View File

@@ -72,6 +72,10 @@ export class HaDialog extends Dialog {
position: var(--dialog-surface-position, relative);
top: var(--dialog-surface-top);
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 {
display: flex;

View File

@@ -17,7 +17,7 @@ declare global {
@customElement("ha-file-upload")
export class HaFileUpload extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public hass?: HomeAssistant;
@property() public accept!: string;
@@ -88,7 +88,8 @@ export class HaFileUpload extends LitElement {
<ha-icon-button
slot="suffix"
@click=${this._clearValue}
.label=${this.hass.localize("ui.common.close")}
.label=${this.hass?.localize("ui.common.close") ||
"close"}
.path=${mdiClose}
></ha-icon-button>
`

View File

@@ -47,12 +47,19 @@ export class HaFormFloat extends LitElement implements HaFormElement {
private _valueChanged(ev: Event) {
const source = ev.target as TextField;
const rawValue = source.value;
const rawValue = source.value.replace(",", ".");
let value: number | undefined;
if (rawValue.endsWith(".")) {
return;
}
if (rawValue !== "") {
value = parseFloat(rawValue);
if (isNaN(value)) {
value = undefined;
}
}
// Detect anything changed
@@ -61,7 +68,6 @@ export class HaFormFloat extends LitElement implements HaFormElement {
const newRawValue = value === undefined ? "" : String(value);
if (source.value !== newRawValue) {
source.value = newRawValue;
return;
}
return;
}

View File

@@ -52,7 +52,9 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
}
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 renderedOptions = options.map((item: string | [string, string]) => {

View File

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

View File

@@ -60,6 +60,8 @@ export class HaIconPicker extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public invalid = false;
@state() private _opened = false;
@query("vaadin-combo-box-light", true) private comboBox!: HTMLElement;
@@ -86,6 +88,8 @@ export class HaIconPicker extends LitElement {
autocomplete="off"
autocorrect="off"
spellcheck="false"
.errorMessage=${this.errorMessage}
.invalid=${this.invalid}
>
${this._value || this.placeholder
? html`

View File

@@ -29,315 +29,7 @@ interface 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 mdiDeprecatedIcons: DeprecatedIcon = {};
const chunks: Chunks = {};

File diff suppressed because one or more lines are too long

View File

@@ -51,7 +51,7 @@ import {
} from "../external_app/external_config";
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant, PanelInfo } from "../types";
import type { HomeAssistant, PanelInfo, Route } from "../types";
import "./ha-icon";
import "./ha-icon-button";
import "./ha-menu-button";
@@ -189,6 +189,8 @@ class HaSidebar extends LitElement {
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
@property() public route!: Route;
@property({ type: Boolean }) public alwaysExpand = false;
@property({ type: Boolean }) public editMode = false;
@@ -351,12 +353,19 @@ class HaSidebar extends LitElement {
this._hiddenPanels
);
// Show the update-available as beeing part of configuration
const selectedPanel = this.route.path?.startsWith(
"/hassio/update-available"
)
? "config"
: this.hass.panelUrl;
// prettier-ignore
return html`
<paper-listbox
attr-for-selected="data-panel"
class="ha-scrollbar"
.selected=${this.hass.panelUrl}
.selected=${selectedPanel}
@focusin=${this._listboxFocusIn}
@focusout=${this._listboxFocusOut}
@scroll=${this._listboxScroll}

View File

@@ -502,6 +502,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
}
private _entityRegMeetsFilter(entity: EntityRegistryEntry): boolean {
if (entity.entity_category) {
return false;
}
if (
this.includeDomains &&
!this.includeDomains.includes(computeDomain(entity.entity_id))

View File

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

View File

@@ -26,7 +26,9 @@ class HaEntityMarker extends LitElement {
? html`<div
class="entity-picture"
style=${styleMap({
"background-image": `url(${this.entityPicture})`,
"background-image": `url(${this.hass.hassUrl(
this.entityPicture
)})`,
})}
></div>`
: this.entityName}

View File

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

View File

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

View File

@@ -66,7 +66,7 @@ export const computeEntityRegistryName = (
return entry.name;
}
const state = hass.states[entry.entity_id];
return state ? computeStateName(state) : null;
return state ? computeStateName(state) : entry.entity_id;
};
export const getExtendedEntityRegistryEntry = (

View File

@@ -79,7 +79,7 @@ export const fetchHassioBackups = async (
};
export const fetchHassioBackupInfo = async (
hass: HomeAssistant,
hass: HomeAssistant | undefined,
backup: string
): Promise<HassioBackupDetail> => {
if (hass) {
@@ -202,7 +202,7 @@ export const createHassioPartialBackup = async (
};
export const uploadBackup = async (
hass: HomeAssistant,
hass: HomeAssistant | undefined,
file: File
): Promise<HassioResponse<HassioBackup>> => {
const fd = new FormData();

View File

@@ -70,6 +70,42 @@ export interface Supervisor {
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>(
conn: Connection,
request: supervisorApiRequest
@@ -139,3 +175,14 @@ export const subscribeSupervisorEvents = (
getSupervisorEventCollection(hass.connection, key, endpoint).subscribe(
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

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

View File

@@ -12,7 +12,7 @@ export interface Zone {
}
export interface ZoneMutableParams {
icon: string;
icon?: string;
latitude: number;
longitude: number;
name: string;

View File

@@ -45,7 +45,7 @@ class StepFlowPickFlow extends LitElement {
domain: flow.handler,
type: "icon",
useFallback: true,
darkOptimized: this.hass.selectedTheme?.dark,
darkOptimized: this.hass.themes?.darkMode,
})}
referrerpolicy="no-referrer"
/>

View File

@@ -102,7 +102,7 @@ class StepFlowPickHandler extends LitElement {
domain: handler.slug,
type: "icon",
useFallback: true,
darkOptimized: this.hass.selectedTheme?.dark,
darkOptimized: this.hass.themes?.darkMode,
})}
referrerpolicy="no-referrer"
/>

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
import { startOfYesterday } from "date-fns";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
@@ -30,6 +31,8 @@ export class MoreInfoLogbook extends LitElement {
private _error?: string;
private _showMoreHref = "";
private _throttleGetLogbookEntries = throttle(() => {
this._getLogBookData();
}, 10000);
@@ -44,8 +47,6 @@ export class MoreInfoLogbook extends LitElement {
return html``;
}
const href = "/logbook?entity_id=" + this.entityId;
return html`
${isComponentLoaded(this.hass, "logbook")
? this._error
@@ -67,7 +68,7 @@ export class MoreInfoLogbook extends LitElement {
<div class="title">
${this.hass.localize("ui.dialogs.more_info_control.logbook")}
</div>
<a href=${href} @click=${this._close}
<a href=${this._showMoreHref} @click=${this._close}
>${this.hass.localize(
"ui.dialogs.more_info_control.show_more"
)}</a
@@ -106,6 +107,10 @@ export class MoreInfoLogbook extends LitElement {
return;
}
this._showMoreHref = `/logbook?entity_id=${
this.entityId
}&start_date=${startOfYesterday().toISOString()}`;
this._throttleGetLogbookEntries();
return;
}

View File

@@ -33,7 +33,7 @@
<body>
<div class="content">
<div class="header">
<img src="/static/icons/favicon-192x192.png" height="52" />
<img src="/static/icons/favicon-192x192.png" height="52" alt="" />
Home Assistant
</div>
<ha-authorize><p>Initializing</p></ha-authorize>

File diff suppressed because one or more lines are too long

View File

@@ -64,7 +64,7 @@
<body id="particles">
<div class="content">
<div class="header">
<img src="/static/icons/favicon-192x192.png" height="52" width="52" />
<img src="/static/icons/favicon-192x192.png" height="52" width="52" alt="" />
Home Assistant
</div>

View File

@@ -1,26 +1,25 @@
import "@material/mwc-button";
import { css, CSSResultGroup, html, LitElement } from "lit";
import { property } from "lit/decorators";
import "../components/ha-circular-progress";
import { removeInitSkeleton } from "../util/init-skeleton";
import { property, state } from "lit/decorators";
class HaInitPage extends LitElement {
@property({ type: Boolean }) public error = false;
@state() showProgressIndicator = false;
private _showProgressIndicatorTimeout;
protected render() {
return html`
<div>
<img src="/static/icons/favicon-192x192.png" height="192" />
${this.error
return this.error
? html`
<p>Unable to connect to Home Assistant.</p>
<mwc-button @click=${this._retry}>Retry</mwc-button>
${location.host.includes("ui.nabu.casa")
? html`
<p>
It is possible that you are seeing this screen because
your Home Assistant is not currently connected. You can
ask it to come online from your
It is possible that you are seeing this screen because your
Home Assistant is not currently connected. You can ask it to
come online from your
<a href="https://account.nabucasa.com/"
>Naba Casa account page</a
>.
@@ -29,15 +28,25 @@ class HaInitPage extends LitElement {
: ""}
`
: html`
<ha-circular-progress active></ha-circular-progress>
<p>Loading data</p>
`}
<div id="progress-indicator-wrapper">
${this.showProgressIndicator
? html`<ha-circular-progress active></ha-circular-progress>`
: ""}
</div>
<div id="loading-text">Loading data</div>
`;
}
disconnectedCallback() {
super.disconnectedCallback();
clearTimeout(this._showProgressIndicatorTimeout);
}
protected firstUpdated() {
removeInitSkeleton();
this._showProgressIndicatorTimeout = setTimeout(async () => {
await import("../components/ha-circular-progress");
this.showProgressIndicator = true;
}, 5000);
}
private _retry() {
@@ -46,20 +55,23 @@ class HaInitPage extends LitElement {
static get styles(): CSSResultGroup {
return css`
div {
height: 100%;
:host {
flex: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
ha-circular-progress {
margin-top: 9px;
#progress-indicator-wrapper {
display: flex;
align-items: center;
margin: 25px 0;
height: 50px;
}
a {
color: var(--primary-color);
}
p {
p,
#loading-text {
max-width: 350px;
color: var(--primary-text-color);
}
@@ -68,3 +80,9 @@ class HaInitPage extends LitElement {
}
customElements.define("ha-init-page", HaInitPage);
declare global {
interface HTMLElementTagNameMap {
"ha-init-page": HaInitPage;
}
}

View File

@@ -13,6 +13,8 @@ class HassSubpage extends LitElement {
@property({ type: Boolean, attribute: "main-page" }) public mainPage = false;
@property({ type: String, attribute: "back-path" }) public backPath?: string;
@property({ type: Boolean, reflect: true }) public narrow = false;
@property({ type: Boolean }) public supervisor = false;
@@ -31,6 +33,14 @@ class HassSubpage extends LitElement {
.narrow=${this.narrow}
></ha-menu-button>
`
: this.backPath
? html`
<a href=${this.backPath}>
<ha-icon-button-arrow-prev
.hass=${this.hass}
></ha-icon-button-arrow-prev>
</a>
`
: html`
<ha-icon-button-arrow-prev
.hass=${this.hass}
@@ -80,6 +90,10 @@ class HassSubpage extends LitElement {
border-bottom: var(--app-header-border-bottom, none);
box-sizing: border-box;
}
.toolbar a {
color: var(--app-header-text-color);
text-decoration: none;
}
ha-menu-button,
ha-icon-button-arrow-prev,

View File

@@ -24,10 +24,13 @@ export interface PageNavigation {
path: string;
translationKey?: string;
component?: string;
components?: string[];
name?: string;
core?: boolean;
advancedOnly?: boolean;
iconPath?: string;
description?: string;
iconColor?: string;
info?: any;
}
@@ -85,7 +88,7 @@ class HassTabsSubpage extends LitElement {
<a href=${page.path}>
<ha-tab
.hass=${this.hass}
.active=${page === activeTab}
.active=${page.path === activeTab?.path}
.narrow=${this.narrow}
.name=${page.translationKey
? localizeFunc(page.translationKey)
@@ -224,7 +227,7 @@ class HassTabsSubpage extends LitElement {
box-sizing: border-box;
}
.toolbar a {
color: var(--sidebar-text-color);
color: var(--app-header-text-color);
text-decoration: none;
}
.bottom-bar a {

View File

@@ -88,6 +88,7 @@ class HomeAssistantMain extends LitElement {
<ha-sidebar
.hass=${hass}
.narrow=${sidebarNarrow}
.route=${this.route}
.editMode=${this._sidebarEditMode}
.alwaysExpand=${sidebarNarrow ||
this.hass.dockedSidebar === "docked"}

View File

@@ -8,6 +8,10 @@ import { HassElement } from "../state/hass-element";
import QuickBarMixin from "../state/quick-bar-mixin";
import { HomeAssistant, Route } from "../types";
import { storeState } from "../util/ha-pref-storage";
import {
renderLaunchScreenInfoBox,
removeLaunchScreen,
} from "../util/launch-screen";
import {
registerServiceWorker,
supportsServiceWorker,
@@ -40,6 +44,8 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
private _visiblePromiseResolve?: () => void;
private _visibleLaunchScreen = true;
constructor() {
super();
const path = curPath();
@@ -55,16 +61,26 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
}
protected render() {
const hass = this.hass;
return hass && hass.states && hass.config && hass.services
? html`
if (this._isHassComplete() && this.hass) {
return html`
<home-assistant-main
.hass=${this.hass}
.route=${this._route}
></home-assistant-main>
`
: html`<ha-init-page .error=${this._error}></ha-init-page>`;
`;
}
return "";
}
update(changedProps) {
super.update(changedProps);
// Remove launch screen if main gui is loaded
if (this._isHassComplete() && this._visibleLaunchScreen) {
this._visibleLaunchScreen = false;
removeLaunchScreen();
}
}
protected firstUpdated(changedProps) {
@@ -109,6 +125,13 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
navigate(href);
}
});
// Render launch screen info box (loading data / error message)
if (!this._isHassComplete() && this._visibleLaunchScreen) {
renderLaunchScreenInfoBox(
html`<ha-init-page .error=${this._error}></ha-init-page>`
);
}
}
protected updated(changedProps: PropertyValues): void {
@@ -229,6 +252,14 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
this._visiblePromiseResolve = undefined;
}
}
private _isHassComplete(): boolean {
if (this.hass?.states && this.hass.config && this.hass.services) {
return true;
}
return false;
}
}
declare global {

View File

@@ -11,7 +11,7 @@ import { deepEqual } from "../common/util/deep-equal";
import { getDefaultPanel } from "../data/panel";
import { CustomPanelInfo } from "../data/panel_custom";
import { HomeAssistant, Panels } from "../types";
import { removeInitSkeleton } from "../util/init-skeleton";
import { removeLaunchScreen } from "../util/launch-screen";
import {
HassRouterPage,
RouteOptions,
@@ -226,7 +226,7 @@ class PartialPanelResolver extends HassRouterPage {
) {
await this.rebuild();
await this.pageRendered;
removeInitSkeleton();
removeLaunchScreen();
}
}
}

View File

@@ -2,13 +2,16 @@ import "@material/mwc-button/mwc-button";
import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeFunc } from "../common/translations/localize";
import { createCurrencyListEl } from "../components/currency-datalist";
import "../components/map/ha-locations-editor";
import type { MarkerLocation } from "../components/map/ha-locations-editor";
import type {
HaLocationsEditor,
MarkerLocation,
} from "../components/map/ha-locations-editor";
import { createTimezoneListEl } from "../components/timezone-datalist";
import {
ConfigUpdateValues,
@@ -25,6 +28,7 @@ import type { HaRadio } from "../components/ha-radio";
const amsterdam: [number, number] = [52.3731339, 4.8903147];
const mql = matchMedia("(prefers-color-scheme: dark)");
const locationMarkerId = "location";
@customElement("onboarding-core-config")
class OnboardingCoreConfig extends LitElement {
@@ -46,6 +50,8 @@ class OnboardingCoreConfig extends LitElement {
@state() private _timeZone?: string;
@query("ha-locations-editor", true) private map!: HaLocationsEditor;
protected render(): TemplateResult {
return html`
<p>
@@ -268,7 +274,7 @@ class OnboardingCoreConfig extends LitElement {
private _markerLocation = memoizeOne(
(location: [number, number]): MarkerLocation[] => [
{
id: "location",
id: locationMarkerId,
latitude: location[0],
longitude: location[1],
location_editable: true,
@@ -304,6 +310,15 @@ class OnboardingCoreConfig extends LitElement {
const values = await detectCoreConfig(this.hass);
if (values.latitude && values.longitude) {
this.map.addEventListener(
"markers-updated",
() => {
this.map.fitMarker(locationMarkerId);
},
{
once: true,
}
);
this._location = [Number(values.latitude), Number(values.longitude)];
}
if (values.elevation) {

View File

@@ -1,5 +1,4 @@
import "@material/mwc-button";
import "@polymer/paper-input/paper-input";
import { genClientId } from "home-assistant-js-websocket";
import {
css,
@@ -9,168 +8,116 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-form/ha-form";
import type { HaForm } from "../components/ha-form/ha-form";
import { HaFormDataContainer, HaFormSchema } from "../components/ha-form/types";
import { onboardUserStep } from "../data/onboarding";
import { PolymerChangedEvent } from "../polymer-types";
const CREATE_USER_SCHEMA: HaFormSchema[] = [
{ type: "string", name: "name", required: true },
{ type: "string", name: "username", required: true },
{ type: "string", name: "password", required: true },
{ type: "string", name: "password_confirm", required: true },
];
@customElement("onboarding-create-user")
class OnboardingCreateUser extends LitElement {
@property() public localize!: LocalizeFunc;
@property() public language!: string;
@state() private _name = "";
@state() private _username = "";
@state() private _password = "";
@state() private _passwordConfirm = "";
@state() private _loading = false;
@state() private _errorMsg?: string = undefined;
@state() private _errorMsg?: string;
@state() private _formError: Record<string, string> = {};
@state() private _newUser: HaFormDataContainer = {};
@query("ha-form", true) private _form?: HaForm;
protected render(): TemplateResult {
return html`
<p>
${this.localize("ui.panel.page-onboarding.intro")}
</p>
<p>${this.localize("ui.panel.page-onboarding.intro")}</p>
<p>${this.localize("ui.panel.page-onboarding.user.intro")}</p>
<p>
${this.localize("ui.panel.page-onboarding.user.intro")}
</p>
${this._errorMsg
? html`<ha-alert alert-type="error">${this._errorMsg}</ha-alert>`
: ""}
${
this._errorMsg
? html`
<p class="error">
${this.localize(
`ui.panel.page-onboarding.user.error.${this._errorMsg}`
) || this._errorMsg}
</p>
`
: ""
}
<form>
<paper-input
name="name"
label=${this.localize("ui.panel.page-onboarding.user.data.name")}
.value=${this._name}
<ha-form
.computeLabel=${this._computeLabel(this.localize)}
.data=${this._newUser}
.disabled=${this._loading}
.error=${this._formError}
.schema=${CREATE_USER_SCHEMA}
@value-changed=${this._handleValueChanged}
required
auto-validate
autocapitalize='on'
.errorMessage=${this.localize(
"ui.panel.page-onboarding.user.required_field"
)}
@blur=${this._maybePopulateUsername}
></paper-input>
></ha-form>
<paper-input
name="username"
label=${this.localize("ui.panel.page-onboarding.user.data.username")}
value=${this._username}
@value-changed=${this._handleValueChanged}
required
auto-validate
autocapitalize='none'
.errorMessage=${this.localize(
"ui.panel.page-onboarding.user.required_field"
)}
></paper-input>
<paper-input
name="password"
label=${this.localize("ui.panel.page-onboarding.user.data.password")}
value=${this._password}
@value-changed=${this._handleValueChanged}
required
type='password'
auto-validate
.errorMessage=${this.localize(
"ui.panel.page-onboarding.user.required_field"
)}
></paper-input>
<paper-input
name="passwordConfirm"
label=${this.localize(
"ui.panel.page-onboarding.user.data.password_confirm"
)}
value=${this._passwordConfirm}
@value-changed=${this._handleValueChanged}
required
type='password'
.invalid=${
this._password !== "" &&
this._passwordConfirm !== "" &&
this._passwordConfirm !== this._password
}
.errorMessage=${this.localize(
"ui.panel.page-onboarding.user.error.password_not_match"
)}
></paper-input>
<p class="action">
<mwc-button
raised
@click=${this._submitForm}
.disabled=${this._loading}
.disabled=${this._loading ||
!this._newUser.name ||
!this._newUser.username ||
!this._newUser.password}
>
${this.localize("ui.panel.page-onboarding.user.create_account")}
</mwc-button>
</p>
</div>
</form>
`;
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
setTimeout(
() => this.shadowRoot!.querySelector("paper-input")!.focus(),
100
);
setTimeout(() => this._form?.focus(), 100);
this.addEventListener("keypress", (ev) => {
if (ev.keyCode === 13) {
if (
ev.keyCode === 13 &&
this._newUser.name &&
this._newUser.username &&
this._newUser.password &&
this._newUser.password_confirm
) {
this._submitForm(ev);
}
});
}
private _handleValueChanged(ev: PolymerChangedEvent<string>): void {
const name = (ev.target as any).name;
this[`_${name}`] = ev.detail.value;
private _computeLabel(localize) {
return (schema: HaFormSchema) =>
localize(`ui.panel.page-onboarding.user.data.${schema.name}`);
}
private _handleValueChanged(
ev: PolymerChangedEvent<HaFormDataContainer>
): void {
this._newUser = ev.detail.value;
this._maybePopulateUsername();
this._formError.password_confirm =
this._newUser.password !== this._newUser.password_confirm
? this.localize(
"ui.panel.page-onboarding.user.error.password_not_match"
)
: "";
}
private _maybePopulateUsername(): void {
if (this._username) {
if (!this._newUser.name || this._newUser.name === this._newUser.username) {
return;
}
const parts = this._name.split(" ");
const parts = String(this._newUser.name).split(" ");
if (parts.length) {
this._username = parts[0].toLowerCase();
this._newUser.username = parts[0].toLowerCase();
}
}
private async _submitForm(ev): Promise<void> {
ev.preventDefault();
if (!this._name || !this._username || !this._password) {
this._errorMsg = "required_fields";
return;
}
if (this._password !== this._passwordConfirm) {
this._errorMsg = "password_not_match";
return;
}
this._loading = true;
this._errorMsg = "";
@@ -179,9 +126,9 @@ class OnboardingCreateUser extends LitElement {
const result = await onboardUserStep({
client_id: clientId,
name: this._name,
username: this._username,
password: this._password,
name: String(this._newUser.name),
username: String(this._newUser.username),
password: String(this._newUser.password),
language: this.language,
});
@@ -199,13 +146,11 @@ class OnboardingCreateUser extends LitElement {
static get styles(): CSSResultGroup {
return css`
.error {
color: red;
}
.action {
margin: 32px 0 16px;
mwc-button {
margin: 32px 0 0;
text-align: center;
display: block;
text-align: right;
}
`;
}

View File

@@ -81,7 +81,7 @@ class OnboardingIntegrations extends LitElement {
.domain=${entry.domain}
.title=${title}
.badgeIcon=${mdiCheck}
.darkOptimizedIcon=${this.hass.selectedTheme?.dark}
.darkOptimizedIcon=${this.hass.themes?.darkMode}
></integration-badge>
`,
];
@@ -98,7 +98,7 @@ class OnboardingIntegrations extends LitElement {
clickable
.domain=${flow.handler}
.title=${title}
.darkOptimizedIcon=${this.hass.selectedTheme?.dark}
.darkOptimizedIcon=${this.hass.themes?.darkMode}
></integration-badge>
</button>
`,

View File

@@ -3,19 +3,31 @@ import "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { navigate } from "../../../common/navigate";
import "../../../components/ha-dialog";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-alert";
import "../../../components/ha-picture-upload";
import type { HaPictureUpload } from "../../../components/ha-picture-upload";
import { AreaRegistryEntryMutableParams } from "../../../data/area_registry";
import { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog";
import { PolymerChangedEvent } from "../../../polymer-types";
import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { AreaRegistryDetailDialogParams } from "./show-dialog-area-registry-detail";
const cropOptions: CropOptions = {
round: false,
type: "image/jpeg",
quality: 0.75,
aspectRatio: 1.78,
};
class DialogAreaDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _name!: string;
@state() private _picture!: string | null;
@state() private _error?: string;
@state() private _params?: AreaRegistryDetailDialogParams;
@@ -28,6 +40,7 @@ class DialogAreaDetail extends LitElement {
this._params = params;
this._error = undefined;
this._name = this._params.entry ? this._params.entry.name : "";
this._picture = this._params.entry?.picture || null;
await this.updateComplete;
}
@@ -47,12 +60,17 @@ class DialogAreaDetail extends LitElement {
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${entry
.heading=${createCloseHeading(
this.hass,
entry
? entry.name
: this.hass.localize("ui.panel.config.areas.editor.default_name")}
: this.hass.localize("ui.panel.config.areas.editor.default_name")
)}
>
<div>
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
${this._error
? html` <ha-alert alert-type="error">${this._error}</ha-alert> `
: ""}
<div class="form">
${entry
? html`
@@ -75,6 +93,13 @@ class DialogAreaDetail extends LitElement {
)}
.invalid=${nameInvalid}
></paper-input>
<ha-picture-upload
.hass=${this.hass}
.value=${this._picture}
crop
.cropOptions=${cropOptions}
@change=${this._pictureChanged}
></ha-picture-upload>
</div>
</div>
${entry
@@ -117,18 +142,24 @@ class DialogAreaDetail extends LitElement {
this._name = ev.detail.value;
}
private _pictureChanged(ev: PolymerChangedEvent<string | null>) {
this._error = undefined;
this._picture = (ev.target as HaPictureUpload).value;
}
private async _updateEntry() {
this._submitting = true;
try {
const values: AreaRegistryEntryMutableParams = {
name: this._name.trim(),
picture: this._picture,
};
if (this._params!.entry) {
await this._params!.updateEntry!(values);
} else {
await this._params!.createEntry!(values);
}
this._params = undefined;
this.closeDialog();
} catch (err: any) {
this._error =
err.message ||
@@ -142,13 +173,11 @@ class DialogAreaDetail extends LitElement {
this._submitting = true;
try {
if (await this._params!.removeEntry!()) {
this._params = undefined;
this.closeDialog();
}
} finally {
this._submitting = false;
}
navigate("/config/areas/dashboard");
}
static get styles(): CSSResultGroup {
@@ -158,9 +187,6 @@ class DialogAreaDetail extends LitElement {
.form {
padding-bottom: 24px;
}
.error {
color: var(--error-color);
}
`,
];
}

View File

@@ -1,13 +1,17 @@
import "@material/mwc-button";
import { mdiCog } from "@mdi/js";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import { mdiImagePlus, mdiPencil } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { afterNextRender } from "../../../common/util/render-status";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-next";
import {
AreaRegistryEntry,
deleteAreaRegistryEntry,
@@ -131,28 +135,62 @@ class HaConfigAreaPage extends LitElement {
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.tabs=${configSections.integrations}
.tabs=${configSections.devices}
.route=${this.route}
>
${this.narrow ? html` <span slot="header"> ${area.name} </span> ` : ""}
${this.narrow
? html`<span slot="header"> ${area.name} </span>
<ha-icon-button
slot="toolbar-icon"
.path=${mdiCog}
.path=${mdiPencil}
.entry=${area}
@click=${this._showSettings}
.label=${this.hass.localize("ui.panel.config.areas.edit_settings")}
></ha-icon-button>
slot="toolbar-icon"
.label=${this.hass.localize(
"ui.panel.config.areas.edit_settings"
)}
></ha-icon-button>`
: ""}
<div class="container">
${!this.narrow
? html`
<div class="fullwidth">
<h1>${area.name}</h1>
<h1>
${area.name}
<ha-icon-button
.path=${mdiPencil}
.entry=${area}
@click=${this._showSettings}
.label=${this.hass.localize(
"ui.panel.config.areas.edit_settings"
)}
></ha-icon-button>
</h1>
</div>
`
: ""}
<div class="column">
${area.picture
? html`<div class="img-container">
<img src=${area.picture} /><ha-icon-button
.path=${mdiPencil}
.entry=${area}
@click=${this._showSettings}
.label=${this.hass.localize(
"ui.panel.config.areas.edit_settings"
)}
class="img-edit-btn"
></ha-icon-button>
</div>`
: html`<mwc-button
.entry=${area}
@click=${this._showSettings}
.label=${this.hass.localize(
"ui.panel.config.areas.add_picture"
)}
>
<ha-svg-icon .path=${mdiImagePlus} slot="icon"></ha-svg-icon>
</mwc-button>`}
<ha-card
.header=${this.hass.localize("ui.panel.config.devices.caption")}
>${devices.length
@@ -181,7 +219,8 @@ class HaConfigAreaPage extends LitElement {
.header=${this.hass.localize(
"ui.panel.config.areas.editor.linked_entities_caption"
)}
>${entities.length
>
${entities.length
? entities.map(
(entity) =>
html`
@@ -390,6 +429,7 @@ class HaConfigAreaPage extends LitElement {
try {
await deleteAreaRegistryEntry(this.hass!, entry!.area_id);
afterNextRender(() => history.back());
return true;
} catch (err: any) {
return false;
@@ -403,7 +443,7 @@ class HaConfigAreaPage extends LitElement {
haStyle,
css`
h1 {
margin-top: 0;
margin: 0;
font-family: var(--paper-font-headline_-_font-family);
-webkit-font-smoothing: var(
--paper-font-headline_-_-webkit-font-smoothing
@@ -413,6 +453,13 @@ class HaConfigAreaPage extends LitElement {
letter-spacing: var(--paper-font-headline_-_letter-spacing);
line-height: var(--paper-font-headline_-_line-height);
opacity: var(--dark-primary-opacity);
display: flex;
align-items: center;
}
img {
border-radius: var(--ha-card-border-radius, 4px);
width: 100%;
}
.container {
@@ -458,6 +505,34 @@ class HaConfigAreaPage extends LitElement {
paper-item.no-link {
cursor: default;
}
ha-card > a:first-child {
display: block;
}
ha-card > *:first-child {
margin-top: -16px;
}
.img-container {
position: relative;
}
.img-edit-btn {
position: absolute;
top: 4px;
right: 4px;
display: none;
}
.img-container:hover .img-edit-btn {
display: block;
}
.img-edit-btn::before {
content: "";
position: absolute;
width: 100%;
height: 100%;
background-color: var(--card-background-color);
opacity: 0.5;
border-radius: 50%;
}
`,
];
}

View File

@@ -1,15 +1,8 @@
import { mdiHelpCircle, mdiPlus } from "@mdi/js";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { HASSDomEvent } from "../../../common/dom/fire_event";
import { navigate } from "../../../common/navigate";
import {
DataTableColumnContainer,
RowClickedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/ha-fab";
import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon";
@@ -21,7 +14,7 @@ import type { DeviceRegistryEntry } from "../../../data/device_registry";
import type { EntityRegistryEntry } from "../../../data/entity_registry";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-tabs-subpage-data-table";
import "../../../layouts/hass-tabs-subpage";
import { HomeAssistant, Route } from "../../../types";
import "../ha-config-section";
import { configSections } from "../ha-panel-config";
@@ -53,15 +46,22 @@ export class HaConfigAreasDashboard extends LitElement {
entities: EntityRegistryEntry[]
) =>
areas.map((area) => {
let noDevicesInArea = 0;
let noServicesInArea = 0;
let noEntitiesInArea = 0;
const devicesInArea = new Set();
for (const device of devices) {
if (device.area_id === area.area_id) {
devicesInArea.add(device.id);
if (device.entry_type === "service") {
noServicesInArea++;
} else {
noDevicesInArea++;
}
}
}
let entitiesInArea = 0;
for (const entity of entities) {
if (
@@ -69,81 +69,28 @@ export class HaConfigAreasDashboard extends LitElement {
? entity.area_id === area.area_id
: devicesInArea.has(entity.device_id)
) {
entitiesInArea++;
noEntitiesInArea++;
}
}
return {
...area,
devices: devicesInArea.size,
entities: entitiesInArea,
devices: noDevicesInArea,
services: noServicesInArea,
entities: noEntitiesInArea,
};
})
);
private _columns = memoizeOne(
(narrow: boolean): DataTableColumnContainer =>
narrow
? {
name: {
title: this.hass.localize(
"ui.panel.config.areas.data_table.area"
),
sortable: true,
filterable: true,
grows: true,
direction: "asc",
},
}
: {
name: {
title: this.hass.localize(
"ui.panel.config.areas.data_table.area"
),
sortable: true,
filterable: true,
grows: true,
direction: "asc",
},
devices: {
title: this.hass.localize(
"ui.panel.config.areas.data_table.devices"
),
sortable: true,
type: "numeric",
width: "20%",
direction: "asc",
},
entities: {
title: this.hass.localize(
"ui.panel.config.areas.data_table.entities"
),
sortable: true,
type: "numeric",
width: "20%",
direction: "asc",
},
}
);
protected render(): TemplateResult {
return html`
<hass-tabs-subpage-data-table
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.isWide=${this.isWide}
back-path="/config"
.tabs=${configSections.integrations}
.tabs=${configSections.devices}
.route=${this.route}
.columns=${this._columns(this.narrow)}
.data=${this._areas(this.areas, this.devices, this.entities)}
@row-click=${this._handleRowClicked}
.noDataText=${this.hass.localize(
"ui.panel.config.areas.picker.no_areas"
)}
id="area_id"
hasFab
clickable
>
<ha-icon-button
slot="toolbar-icon"
@@ -151,6 +98,58 @@ export class HaConfigAreasDashboard extends LitElement {
.path=${mdiHelpCircle}
@click=${this._showHelp}
></ha-icon-button>
<div class="container">
${this._areas(this.areas, this.devices, this.entities).map(
(area) =>
html`<a href=${`/config/areas/area/${area.area_id}`}
><ha-card outlined>
<div
style=${styleMap({
backgroundImage: area.picture
? `url(${area.picture})`
: undefined,
})}
class="picture ${!area.picture ? "placeholder" : ""}"
></div>
<h1 class="card-header">${area.name}</h1>
<div class="card-content">
<div>
${area.devices
? html`
${this.hass.localize(
"ui.panel.config.integrations.config_entry.devices",
"count",
area.devices
)}${area.services ? "," : ""}
`
: ""}
${area.services
? html`
${this.hass.localize(
"ui.panel.config.integrations.config_entry.services",
"count",
area.services
)}
`
: ""}
${(area.devices || area.services) && area.entities
? this.hass.localize("ui.common.and")
: ""}
${area.entities
? html`
${this.hass.localize(
"ui.panel.config.integrations.config_entry.entities",
"count",
area.entities
)}
`
: ""}
</div>
</div>
</ha-card></a
>`
)}
</div>
<ha-fab
slot="fab"
.label=${this.hass.localize(
@@ -161,7 +160,7 @@ export class HaConfigAreasDashboard extends LitElement {
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
</hass-tabs-subpage-data-table>
</hass-tabs-subpage>
`;
}
@@ -191,11 +190,6 @@ export class HaConfigAreasDashboard extends LitElement {
});
}
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
const areaId = ev.detail.id;
navigate(`/config/areas/area/${areaId}`);
}
private _openDialog(entry?: AreaRegistryEntry) {
showAreaRegistryDetailDialog(this, {
entry,
@@ -210,6 +204,51 @@ export class HaConfigAreasDashboard extends LitElement {
--app-header-background-color: var(--sidebar-background-color);
--app-header-text-color: var(--sidebar-text-color);
}
.container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-gap: 16px 16px;
padding: 8px 16px 16px;
margin: 0 auto 64px auto;
max-width: 1000px;
}
.container > * {
max-width: 500px;
}
ha-card {
overflow: hidden;
}
a {
text-decoration: none;
}
h1 {
padding-bottom: 0;
}
.picture {
height: 150px;
width: 100%;
background-size: cover;
background-position: center;
position: relative;
}
.picture.placeholder::before {
position: absolute;
content: "";
width: 100%;
height: 100%;
background-color: var(--sidebar-selected-icon-color);
opacity: 0.12;
}
.card-content {
min-height: 16px;
color: var(--secondary-text-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-areas-dashboard": HaConfigAreasDashboard;
}
}

View File

@@ -114,7 +114,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
.narrow=${this.narrow}
.route=${this.route}
.backCallback=${this._backTapped}
.tabs=${configSections.automation}
.tabs=${configSections.automations}
>
<ha-button-menu
corner="BOTTOM_START"

View File

@@ -225,7 +225,7 @@ class HaAutomationPicker extends LitElement {
back-path="/config"
id="entity_id"
.route=${this.route}
.tabs=${configSections.automation}
.tabs=${configSections.automations}
.activeFilters=${this._activeFilters}
.columns=${this._columns(this.narrow, this.hass.locale)}
.data=${this._automations(this.automations, this._filteredAutomations)}

View File

@@ -112,7 +112,7 @@ export class HaAutomationTrace extends LitElement {
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${configSections.automation}
.tabs=${configSections.automations}
>
${this.narrow
? html`<span slot="header">${title}</span>

View File

@@ -63,6 +63,13 @@ class DialogThingtalk extends LitElement {
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
public closeInitDialog() {
if (this._placeholders) {
return;
}
this.closeDialog();
}
protected render(): TemplateResult {
if (!this._params) {
return html``;
@@ -82,7 +89,7 @@ class DialogThingtalk extends LitElement {
return html`
<ha-dialog
open
@closed=${this.closeDialog}
@closed=${this.closeInitDialog}
.heading=${this.hass.localize(
`ui.panel.config.automation.thingtalk.task_selection.header`
)}

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