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) {
// eslint-disable-next-line
console.log("Error fetching Lovelace configuration", err, msg);
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,30 +132,60 @@ const alerts: {
export class DemoHaAlert extends LitElement {
protected render(): TemplateResult {
return html`
<ha-card header="ha-alert demo">
<div class="card-content">
${alerts.map(
(alert) => html`
<ha-alert
.title=${alert.title || ""}
.alertType=${alert.type}
.dismissable=${alert.dismissable || false}
.actionText=${alert.action || ""}
.rtl=${alert.rtl || false}
>
${alert.description}
</ha-alert>
`
)}
</div>
</ha-card>
${["light", "dark"].map(
(mode) => html`
<div class=${mode}>
<ha-card header="ha-alert ${mode} demo">
<div class="card-content">
${alerts.map(
(alert) => html`
<ha-alert
.title=${alert.title || ""}
.alertType=${alert.type}
.dismissable=${alert.dismissable || false}
.rtl=${alert.rtl || false}
>
${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
.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>
<update-available-card
.hass=${this.hass}
.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">
<hassio-update
.hass=${this.hass}
.supervisor=${this.supervisor}
></hassio-update>
${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,25 +192,23 @@ class HassioBackupDialog
}
if (!this._dialogParams?.onboarding) {
this.hass
.callApi(
"POST",
this.hass!.callApi(
"POST",
`hassio/${
atLeastVersion(this.hass.config.version, 2021, 9)
? "backups"
: "snapshots"
}/${this._backup!.slug}/restore/partial`,
backupDetails
)
.then(
() => {
this.closeDialog();
},
(error) => {
this._error = error.body.message;
}
);
`hassio/${
atLeastVersion(this.hass!.config.version, 2021, 9)
? "backups"
: "snapshots"
}/${this._backup!.slug}/restore/partial`,
backupDetails
).then(
() => {
this.closeDialog();
},
(error) => {
this._error = error.body.message;
}
);
} else {
fireEvent(this, "restoring");
fetch(`/api/hassio/backups/${this._backup!.slug}/restore/partial`, {
@@ -244,24 +242,22 @@ class HassioBackupDialog
}
if (!this._dialogParams?.onboarding) {
this.hass
.callApi(
"POST",
`hassio/${
atLeastVersion(this.hass.config.version, 2021, 9)
? "backups"
: "snapshots"
}/${this._backup!.slug}/restore/full`,
backupDetails
)
.then(
() => {
this.closeDialog();
},
(error) => {
this._error = error.body.message;
}
);
this.hass!.callApi(
"POST",
`hassio/${
atLeastVersion(this.hass!.config.version, 2021, 9)
? "backups"
: "snapshots"
}/${this._backup!.slug}/restore/full`,
backupDetails
).then(
() => {
this.closeDialog();
},
(error) => {
this._error = error.body.message;
}
);
} else {
fireEvent(this, "restoring");
fetch(`/api/hassio/backups/${this._backup!.slug}/restore/full`, {
@@ -283,36 +279,33 @@ class HassioBackupDialog
return;
}
this.hass
.callApi(
atLeastVersion(this.hass.config.version, 2021, 9) ? "DELETE" : "POST",
`hassio/${
atLeastVersion(this.hass.config.version, 2021, 9)
? `backups/${this._backup!.slug}`
: `snapshots/${this._backup!.slug}/remove`
}`
)
.then(
() => {
if (this._dialogParams!.onDelete) {
this._dialogParams!.onDelete();
}
this.closeDialog();
},
(error) => {
this._error = error.body.message;
this.hass!.callApi(
atLeastVersion(this.hass!.config.version, 2021, 9) ? "DELETE" : "POST",
`hassio/${
atLeastVersion(this.hass!.config.version, 2021, 9)
? `backups/${this._backup!.slug}`
: `snapshots/${this._backup!.slug}/remove`
}`
).then(
() => {
if (this._dialogParams!.onDelete) {
this._dialogParams!.onDelete();
}
);
this.closeDialog();
},
(error) => {
this._error = error.body.message;
}
);
}
private async _downloadClicked() {
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>
${this._addingRegistry
? html`
<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>
`
: 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(
"dialog.registries.username"
)}:
${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>
`
)
: html`
<mwc-list-item>
<span
>${this.supervisor.localize(
"dialog.registries.no_registries"
)}</span
>
</mwc-list-item>
`}
</div>
`
: html`${this._registries?.length
? this._registries.map(
(entry) => html`
<ha-settings-row class="registry">
<span slot="heading"> ${entry.registry} </span>
<span slot="description">
${this.supervisor.localize(
"dialog.registries.username"
)}:
${entry.username}
</span>
<ha-icon-button
.entry=${entry}
.label=${this.supervisor.localize(
"dialog.registries.remove"
)}
.path=${mdiDelete}
@click=${this._removeRegistry}
></ha-icon-button>
</ha-settings-row>
`
)
: html`
<ha-alert>
${this.supervisor.localize(
"dialog.registries.no_registries"
)}
</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,7 +110,9 @@ export class SupervisorBaseElement extends urlSyncMixin(
this._language = this.hass.language;
}
this._initializeLocalize();
this._initSupervisor();
if (this.route?.prefix === "/hassio") {
this._initSupervisor();
}
}
private async _initializeLocalize() {

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}
>
${this.supervisor.localize("common.update")}
</ha-progress-button>
<a href="/hassio/update-available/core">
<mwc-button
.label=${this.supervisor.localize("common.show")}
>
</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}
>
${this.supervisor.localize("common.update")}
</ha-progress-button>
<a href="/hassio/update-available/supervisor">
<mwc-button
.label=${this.supervisor.localize("common.show")}
>
</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"}">
<ha-svg-icon .path=${ALERT_ICONS[this.alertType]}></ha-svg-icon>
<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
? html`<ha-icon-button
@click=${this._dismiss_clicked}
label="Dismiss alert"
.path=${mdiClose}
></ha-icon-button>`
: ""}
<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>
@@ -70,4 +70,4 @@
})();
</script>
</body>
</html>
</html>

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,43 +1,52 @@
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
? 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
<a href="https://account.nabucasa.com/"
>Naba Casa account page</a
>.
</p>
`
: ""}
`
: html`
<ha-circular-progress active></ha-circular-progress>
<p>Loading data</p>
`}
</div>
`;
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
<a href="https://account.nabucasa.com/"
>Naba Casa account page</a
>.
</p>
`
: ""}
`
: html`
<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;
if (this._isHassComplete() && this.hass) {
return html`
<home-assistant-main
.hass=${this.hass}
.route=${this._route}
></home-assistant-main>
`;
}
return hass && hass.states && hass.config && hass.services
? 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}
>
${this.localize("ui.panel.page-onboarding.user.create_account")}
</mwc-button>
</p>
</div>
</form>
`;
<mwc-button
raised
@click=${this._submitForm}
.disabled=${this._loading ||
!this._newUser.name ||
!this._newUser.username ||
!this._newUser.password}
>
${this.localize("ui.panel.page-onboarding.user.create_account")}
</mwc-button>
`;
}
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
? entry.name
: this.hass.localize("ui.panel.config.areas.editor.default_name")}
.heading=${createCloseHeading(
this.hass,
entry
? entry.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> ` : ""}
<ha-icon-button
slot="toolbar-icon"
.path=${mdiCog}
.entry=${area}
@click=${this._showSettings}
.label=${this.hass.localize("ui.panel.config.areas.edit_settings")}
></ha-icon-button>
${this.narrow
? html`<span slot="header"> ${area.name} </span>
<ha-icon-button
.path=${mdiPencil}
.entry=${area}
@click=${this._showSettings}
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,97 +46,51 @@ 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 (
entity.area_id
? 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