mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-13 11:19:25 +00:00
Compare commits
171 Commits
20211026.0
...
dev-tools-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
94215dc50b | ||
![]() |
8f5751d5bb | ||
![]() |
4095450476 | ||
![]() |
e61f587c51 | ||
![]() |
d43d19190e | ||
![]() |
a283acaabf | ||
![]() |
ea18fc0078 | ||
![]() |
1df11e9bf1 | ||
![]() |
c71b2e6b9d | ||
![]() |
db4aa05bf4 | ||
![]() |
a54a2a54f8 | ||
![]() |
0bcb4d0e09 | ||
![]() |
95dbc811d3 | ||
![]() |
e28a11964e | ||
![]() |
46a9e36516 | ||
![]() |
e99f20c4f3 | ||
![]() |
2100603cdc | ||
![]() |
da4942aca3 | ||
![]() |
7c78fb314e | ||
![]() |
5bc2468cbc | ||
![]() |
a580904c52 | ||
![]() |
48d12ceafe | ||
![]() |
60ce805b3b | ||
![]() |
251416b51d | ||
![]() |
c41c6eedd8 | ||
![]() |
6877fd9e00 | ||
![]() |
4cc104a99f | ||
![]() |
6494177821 | ||
![]() |
cea1a62867 | ||
![]() |
a6b5262d02 | ||
![]() |
2a5fc5181e | ||
![]() |
2fe8f5ff27 | ||
![]() |
0c75d5afc9 | ||
![]() |
cf062bf0f4 | ||
![]() |
acf4d59fde | ||
![]() |
05333ac2d9 | ||
![]() |
4b49da58b1 | ||
![]() |
68373e6372 | ||
![]() |
01049e8eb8 | ||
![]() |
87f7981144 | ||
![]() |
ceac9834b9 | ||
![]() |
ac8f748656 | ||
![]() |
1d97d8dca9 | ||
![]() |
fd6785b593 | ||
![]() |
d5fc751da6 | ||
![]() |
933fd72629 | ||
![]() |
0611133065 | ||
![]() |
02644b923f | ||
![]() |
67f06112c6 | ||
![]() |
49e39644f3 | ||
![]() |
990ad1bb67 | ||
![]() |
dbbf246060 | ||
![]() |
d2c20837a5 | ||
![]() |
e91d1777d0 | ||
![]() |
a5be143c3b | ||
![]() |
0ef07e4835 | ||
![]() |
9361e4cf9c | ||
![]() |
e7fd75703f | ||
![]() |
2c0b2f4bc5 | ||
![]() |
faec09f0d1 | ||
![]() |
b79c06ad71 | ||
![]() |
5614e0d29c | ||
![]() |
0b7fc177f9 | ||
![]() |
367322415e | ||
![]() |
117b50f3ea | ||
![]() |
366aa8aed1 | ||
![]() |
43011179eb | ||
![]() |
6177d2b416 | ||
![]() |
f70485bc49 | ||
![]() |
921763b5f1 | ||
![]() |
5fd4315789 | ||
![]() |
ed291b57d0 | ||
![]() |
f833701e7c | ||
![]() |
8533b90957 | ||
![]() |
c95a54c6f3 | ||
![]() |
a991640f52 | ||
![]() |
3d99b92c07 | ||
![]() |
d28ad17135 | ||
![]() |
3c67fc96b1 | ||
![]() |
4719636176 | ||
![]() |
45efee28b8 | ||
![]() |
3bcf225380 | ||
![]() |
2e81f843ce | ||
![]() |
a430142296 | ||
![]() |
6335b13c5e | ||
![]() |
6c4e987a24 | ||
![]() |
1a5c43d72a | ||
![]() |
91dbfca899 | ||
![]() |
96f103644a | ||
![]() |
5304e5a670 | ||
![]() |
390e5b3881 | ||
![]() |
9f5756c9fa | ||
![]() |
0ca35d7012 | ||
![]() |
0d19f4792f | ||
![]() |
91b009af79 | ||
![]() |
1ebd2fb9f1 | ||
![]() |
4684979ae7 | ||
![]() |
a567312bdb | ||
![]() |
1e851e0e8c | ||
![]() |
7d94615f47 | ||
![]() |
582fab7ea1 | ||
![]() |
822590ec8a | ||
![]() |
e9f0967578 | ||
![]() |
481da19c74 | ||
![]() |
b969db0c0f | ||
![]() |
a6b98fc3c3 | ||
![]() |
87c2046ab5 | ||
![]() |
4b992fb0c4 | ||
![]() |
3154011c65 | ||
![]() |
4e68383cf7 | ||
![]() |
db6ef22ebb | ||
![]() |
c238c7dbbc | ||
![]() |
d04823b4c5 | ||
![]() |
4cb45d6313 | ||
![]() |
6623e5f017 | ||
![]() |
6518aefb7f | ||
![]() |
d5600b7c08 | ||
![]() |
4789295d32 | ||
![]() |
70d54aa855 | ||
![]() |
77549efc47 | ||
![]() |
00299bc74d | ||
![]() |
b74fc5578d | ||
![]() |
9018d4cc18 | ||
![]() |
fcdceba09d | ||
![]() |
06d4ccf344 | ||
![]() |
a268040ae7 | ||
![]() |
67d79d618a | ||
![]() |
0e8a06e24d | ||
![]() |
d7732ee850 | ||
![]() |
729a928cfe | ||
![]() |
fe5a582a74 | ||
![]() |
c26a59d805 | ||
![]() |
ea331dbe0b | ||
![]() |
b97d6d7059 | ||
![]() |
9425b943dd | ||
![]() |
3fd0becfd4 | ||
![]() |
12ef191a0f | ||
![]() |
2bbb4acf3d | ||
![]() |
77d54df007 | ||
![]() |
1c35571ef0 | ||
![]() |
c8804160bf | ||
![]() |
0a6ffb6bc8 | ||
![]() |
6984f19aa0 | ||
![]() |
cb8de53d74 | ||
![]() |
93680b9764 | ||
![]() |
3cf9b745b5 | ||
![]() |
5851fe26ff | ||
![]() |
b188c4ec81 | ||
![]() |
4624c3d75b | ||
![]() |
7d196b4b95 | ||
![]() |
6347e44d94 | ||
![]() |
719d9386c5 | ||
![]() |
bb734be4bc | ||
![]() |
7cadaf1dc3 | ||
![]() |
c30453a86f | ||
![]() |
c2e3d0188e | ||
![]() |
aabb8ea16f | ||
![]() |
df572d59c5 | ||
![]() |
5ef7a37c20 | ||
![]() |
4b44e197ae | ||
![]() |
8b5b21ae69 | ||
![]() |
f5417fad6f | ||
![]() |
7fa6317f5c | ||
![]() |
74533cebc6 | ||
![]() |
10986db7c6 | ||
![]() |
67648baca7 | ||
![]() |
dc9182e9ab | ||
![]() |
4a7a81ffdb | ||
![]() |
09ef72647e | ||
![]() |
da38e6f986 | ||
![]() |
bd1a9f2cb0 |
@@ -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) {
|
||||
|
@@ -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"],
|
||||
|
@@ -79,6 +79,11 @@ function copyFonts(staticDir) {
|
||||
);
|
||||
}
|
||||
|
||||
function copyQrScannerWorker(staticDir) {
|
||||
const staticPath = genStaticPath(staticDir);
|
||||
copyFileDir(npmPath("qr-scanner/qr-scanner-worker.min.js"), staticPath("js"));
|
||||
}
|
||||
|
||||
function copyMapPanel(staticDir) {
|
||||
const staticPath = genStaticPath(staticDir);
|
||||
copyFileDir(
|
||||
@@ -125,6 +130,9 @@ gulp.task("copy-static-app", async () => {
|
||||
|
||||
// Panel assets
|
||||
copyMapPanel(staticDir);
|
||||
|
||||
// Qr Scanner assets
|
||||
copyQrScannerWorker(staticDir);
|
||||
});
|
||||
|
||||
gulp.task("copy-static-demo", async () => {
|
||||
|
@@ -26,6 +26,7 @@ const getMeta = () => {
|
||||
path: svg.match(/ d="([^"]+)"/)[1],
|
||||
name: icon.name,
|
||||
tags: icon.tags,
|
||||
aliases: icon.aliases,
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -37,6 +38,7 @@ const addRemovedMeta = (meta) => {
|
||||
path: removeIcon.path,
|
||||
name: removeIcon.name,
|
||||
tags: [],
|
||||
aliases: [],
|
||||
}));
|
||||
const combinedMeta = [...meta, ...removedMeta];
|
||||
return combinedMeta.sort((a, b) => a.name.localeCompare(b.name));
|
||||
@@ -141,7 +143,15 @@ gulp.task("gen-icons-json", (done) => {
|
||||
|
||||
fs.writeFileSync(
|
||||
path.resolve(OUTPUT_DIR, "iconList.json"),
|
||||
JSON.stringify(orderMeta(meta).map((icon) => icon.name))
|
||||
JSON.stringify(
|
||||
orderMeta(meta).map((icon) => ({
|
||||
name: icon.name,
|
||||
keywords: [
|
||||
...icon.tags.map((t) => t.toLowerCase().replace(/\s\/\s/g, " ")),
|
||||
...icon.aliases,
|
||||
],
|
||||
}))
|
||||
)
|
||||
);
|
||||
|
||||
done();
|
||||
|
File diff suppressed because one or more lines are too long
46
cast/src/html/media.html.template
Normal file
46
cast/src/html/media.html.template
Normal 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>
|
22
cast/src/media/entrypoint.ts
Normal file
22
cast/src/media/entrypoint.ts
Normal 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();
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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
@@ -82,6 +82,9 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
|
||||
],
|
||||
}));
|
||||
hass.mockWS("energy/info", () => ({ cost_sensors: [] }));
|
||||
hass.mockWS("energy/fossil_energy_consumption", ({ period }) => ({
|
||||
start: period === "month" ? 500 : period === "day" ? 20 : 5,
|
||||
}));
|
||||
const todayString = format(startOfToday(), "yyyy-MM-dd");
|
||||
const tomorrowString = format(startOfTomorrow(), "yyyy-MM-dd");
|
||||
hass.mockWS(
|
||||
|
@@ -1,4 +1,10 @@
|
||||
import { addHours, differenceInHours, endOfDay } from "date-fns";
|
||||
import {
|
||||
addDays,
|
||||
addHours,
|
||||
addMonths,
|
||||
differenceInHours,
|
||||
endOfDay,
|
||||
} from "date-fns";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { StatisticValue } from "../../../src/data/history";
|
||||
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
@@ -70,6 +76,7 @@ const generateMeanStatistics = (
|
||||
id: string,
|
||||
start: Date,
|
||||
end: Date,
|
||||
period: "5minute" | "hour" | "day" | "month" = "hour",
|
||||
initValue: number,
|
||||
maxDiff: number
|
||||
) => {
|
||||
@@ -84,6 +91,7 @@ const generateMeanStatistics = (
|
||||
statistics.push({
|
||||
statistic_id: id,
|
||||
start: currentDate.toISOString(),
|
||||
end: currentDate.toISOString(),
|
||||
mean,
|
||||
min: mean - Math.random() * maxDiff,
|
||||
max: mean + Math.random() * maxDiff,
|
||||
@@ -92,7 +100,12 @@ const generateMeanStatistics = (
|
||||
sum: null,
|
||||
});
|
||||
lastVal = mean;
|
||||
currentDate = addHours(currentDate, 1);
|
||||
currentDate =
|
||||
period === "day"
|
||||
? addDays(currentDate, 1)
|
||||
: period === "month"
|
||||
? addMonths(currentDate, 1)
|
||||
: addHours(currentDate, 1);
|
||||
}
|
||||
return statistics;
|
||||
};
|
||||
@@ -101,6 +114,7 @@ const generateSumStatistics = (
|
||||
id: string,
|
||||
start: Date,
|
||||
end: Date,
|
||||
period: "5minute" | "hour" | "day" | "month" = "hour",
|
||||
initValue: number,
|
||||
maxDiff: number
|
||||
) => {
|
||||
@@ -115,6 +129,7 @@ const generateSumStatistics = (
|
||||
statistics.push({
|
||||
statistic_id: id,
|
||||
start: currentDate.toISOString(),
|
||||
end: currentDate.toISOString(),
|
||||
mean: null,
|
||||
min: null,
|
||||
max: null,
|
||||
@@ -122,7 +137,12 @@ const generateSumStatistics = (
|
||||
state: initValue + sum,
|
||||
sum,
|
||||
});
|
||||
currentDate = addHours(currentDate, 1);
|
||||
currentDate =
|
||||
period === "day"
|
||||
? addDays(currentDate, 1)
|
||||
: period === "month"
|
||||
? addMonths(currentDate, 1)
|
||||
: addHours(currentDate, 1);
|
||||
}
|
||||
return statistics;
|
||||
};
|
||||
@@ -131,6 +151,7 @@ const generateCurvedStatistics = (
|
||||
id: string,
|
||||
start: Date,
|
||||
end: Date,
|
||||
_period: "5minute" | "hour" | "day" | "month" = "hour",
|
||||
initValue: number,
|
||||
maxDiff: number,
|
||||
metered: boolean
|
||||
@@ -149,6 +170,7 @@ const generateCurvedStatistics = (
|
||||
statistics.push({
|
||||
statistic_id: id,
|
||||
start: currentDate.toISOString(),
|
||||
end: currentDate.toISOString(),
|
||||
mean: null,
|
||||
min: null,
|
||||
max: null,
|
||||
@@ -167,11 +189,38 @@ const generateCurvedStatistics = (
|
||||
|
||||
const statisticsFunctions: Record<
|
||||
string,
|
||||
(id: string, start: Date, end: Date) => StatisticValue[]
|
||||
(
|
||||
id: string,
|
||||
start: Date,
|
||||
end: Date,
|
||||
period: "5minute" | "hour" | "day" | "month"
|
||||
) => StatisticValue[]
|
||||
> = {
|
||||
"sensor.energy_consumption_tarif_1": (id: string, start: Date, end: Date) => {
|
||||
"sensor.energy_consumption_tarif_1": (
|
||||
id: string,
|
||||
start: Date,
|
||||
end: Date,
|
||||
period = "hour"
|
||||
) => {
|
||||
if (period !== "hour") {
|
||||
return generateSumStatistics(
|
||||
id,
|
||||
start,
|
||||
end,
|
||||
period,
|
||||
0,
|
||||
period === "day" ? 17 : 504
|
||||
);
|
||||
}
|
||||
const morningEnd = new Date(start.getTime() + 10 * 60 * 60 * 1000);
|
||||
const morningLow = generateSumStatistics(id, start, morningEnd, 0, 0.7);
|
||||
const morningLow = generateSumStatistics(
|
||||
id,
|
||||
start,
|
||||
morningEnd,
|
||||
period,
|
||||
0,
|
||||
0.7
|
||||
);
|
||||
const eveningStart = new Date(start.getTime() + 20 * 60 * 60 * 1000);
|
||||
const morningFinalVal = morningLow.length
|
||||
? morningLow[morningLow.length - 1].sum!
|
||||
@@ -180,6 +229,7 @@ const statisticsFunctions: Record<
|
||||
id,
|
||||
morningEnd,
|
||||
eveningStart,
|
||||
period,
|
||||
morningFinalVal,
|
||||
0
|
||||
);
|
||||
@@ -187,39 +237,71 @@ const statisticsFunctions: Record<
|
||||
id,
|
||||
eveningStart,
|
||||
end,
|
||||
period,
|
||||
morningFinalVal,
|
||||
0.7
|
||||
);
|
||||
return [...morningLow, ...empty, ...eveningLow];
|
||||
},
|
||||
"sensor.energy_consumption_tarif_2": (id: string, start: Date, end: Date) => {
|
||||
"sensor.energy_consumption_tarif_2": (
|
||||
id: string,
|
||||
start: Date,
|
||||
end: Date,
|
||||
period = "hour"
|
||||
) => {
|
||||
if (period !== "hour") {
|
||||
return generateSumStatistics(
|
||||
id,
|
||||
start,
|
||||
end,
|
||||
period,
|
||||
0,
|
||||
period === "day" ? 17 : 504
|
||||
);
|
||||
}
|
||||
const morningEnd = new Date(start.getTime() + 9 * 60 * 60 * 1000);
|
||||
const eveningStart = new Date(start.getTime() + 20 * 60 * 60 * 1000);
|
||||
const highTarif = generateSumStatistics(
|
||||
id,
|
||||
morningEnd,
|
||||
eveningStart,
|
||||
period,
|
||||
0,
|
||||
0.3
|
||||
);
|
||||
const highTarifFinalVal = highTarif.length
|
||||
? highTarif[highTarif.length - 1].sum!
|
||||
: 0;
|
||||
const morning = generateSumStatistics(id, start, morningEnd, 0, 0);
|
||||
const morning = generateSumStatistics(id, start, morningEnd, period, 0, 0);
|
||||
const evening = generateSumStatistics(
|
||||
id,
|
||||
eveningStart,
|
||||
end,
|
||||
period,
|
||||
highTarifFinalVal,
|
||||
0
|
||||
);
|
||||
return [...morning, ...highTarif, ...evening];
|
||||
},
|
||||
"sensor.energy_production_tarif_1": (id, start, end) =>
|
||||
generateSumStatistics(id, start, end, 0, 0),
|
||||
"sensor.energy_production_tarif_1_compensation": (id, start, end) =>
|
||||
generateSumStatistics(id, start, end, 0, 0),
|
||||
"sensor.energy_production_tarif_2": (id, start, end) => {
|
||||
"sensor.energy_production_tarif_1": (id, start, end, period = "hour") =>
|
||||
generateSumStatistics(id, start, end, period, 0, 0),
|
||||
"sensor.energy_production_tarif_1_compensation": (
|
||||
id,
|
||||
start,
|
||||
end,
|
||||
period = "hour"
|
||||
) => generateSumStatistics(id, start, end, period, 0, 0),
|
||||
"sensor.energy_production_tarif_2": (id, start, end, period = "hour") => {
|
||||
if (period !== "hour") {
|
||||
return generateSumStatistics(
|
||||
id,
|
||||
start,
|
||||
end,
|
||||
period,
|
||||
0,
|
||||
period === "day" ? 17 : 504
|
||||
);
|
||||
}
|
||||
const productionStart = new Date(start.getTime() + 9 * 60 * 60 * 1000);
|
||||
const productionEnd = new Date(start.getTime() + 21 * 60 * 60 * 1000);
|
||||
const dayEnd = new Date(endOfDay(productionEnd));
|
||||
@@ -227,6 +309,7 @@ const statisticsFunctions: Record<
|
||||
id,
|
||||
productionStart,
|
||||
productionEnd,
|
||||
period,
|
||||
0,
|
||||
0.15,
|
||||
true
|
||||
@@ -234,18 +317,43 @@ const statisticsFunctions: Record<
|
||||
const productionFinalVal = production.length
|
||||
? production[production.length - 1].sum!
|
||||
: 0;
|
||||
const morning = generateSumStatistics(id, start, productionStart, 0, 0);
|
||||
const morning = generateSumStatistics(
|
||||
id,
|
||||
start,
|
||||
productionStart,
|
||||
period,
|
||||
0,
|
||||
0
|
||||
);
|
||||
const evening = generateSumStatistics(
|
||||
id,
|
||||
productionEnd,
|
||||
dayEnd,
|
||||
period,
|
||||
productionFinalVal,
|
||||
0
|
||||
);
|
||||
const rest = generateSumStatistics(id, dayEnd, end, productionFinalVal, 1);
|
||||
const rest = generateSumStatistics(
|
||||
id,
|
||||
dayEnd,
|
||||
end,
|
||||
period,
|
||||
productionFinalVal,
|
||||
1
|
||||
);
|
||||
return [...morning, ...production, ...evening, ...rest];
|
||||
},
|
||||
"sensor.solar_production": (id, start, end) => {
|
||||
"sensor.solar_production": (id, start, end, period = "hour") => {
|
||||
if (period !== "hour") {
|
||||
return generateSumStatistics(
|
||||
id,
|
||||
start,
|
||||
end,
|
||||
period,
|
||||
0,
|
||||
period === "day" ? 17 : 504
|
||||
);
|
||||
}
|
||||
const productionStart = new Date(start.getTime() + 7 * 60 * 60 * 1000);
|
||||
const productionEnd = new Date(start.getTime() + 23 * 60 * 60 * 1000);
|
||||
const dayEnd = new Date(endOfDay(productionEnd));
|
||||
@@ -253,6 +361,7 @@ const statisticsFunctions: Record<
|
||||
id,
|
||||
productionStart,
|
||||
productionEnd,
|
||||
period,
|
||||
0,
|
||||
0.3,
|
||||
true
|
||||
@@ -260,19 +369,32 @@ const statisticsFunctions: Record<
|
||||
const productionFinalVal = production.length
|
||||
? production[production.length - 1].sum!
|
||||
: 0;
|
||||
const morning = generateSumStatistics(id, start, productionStart, 0, 0);
|
||||
const morning = generateSumStatistics(
|
||||
id,
|
||||
start,
|
||||
productionStart,
|
||||
period,
|
||||
0,
|
||||
0
|
||||
);
|
||||
const evening = generateSumStatistics(
|
||||
id,
|
||||
productionEnd,
|
||||
dayEnd,
|
||||
period,
|
||||
productionFinalVal,
|
||||
0
|
||||
);
|
||||
const rest = generateSumStatistics(id, dayEnd, end, productionFinalVal, 2);
|
||||
const rest = generateSumStatistics(
|
||||
id,
|
||||
dayEnd,
|
||||
end,
|
||||
period,
|
||||
productionFinalVal,
|
||||
2
|
||||
);
|
||||
return [...morning, ...production, ...evening, ...rest];
|
||||
},
|
||||
"sensor.grid_fossil_fuel_percentage": (id, start, end) =>
|
||||
generateMeanStatistics(id, start, end, 35, 1.3),
|
||||
};
|
||||
|
||||
export const mockHistory = (mockHass: MockHomeAssistant) => {
|
||||
@@ -347,7 +469,7 @@ export const mockHistory = (mockHass: MockHomeAssistant) => {
|
||||
mockHass.mockWS("history/list_statistic_ids", () => []);
|
||||
mockHass.mockWS(
|
||||
"history/statistics_during_period",
|
||||
({ statistic_ids, start_time, end_time }, hass) => {
|
||||
({ statistic_ids, start_time, end_time, period }, hass) => {
|
||||
const start = new Date(start_time);
|
||||
const end = end_time ? new Date(end_time) : new Date();
|
||||
|
||||
@@ -355,7 +477,7 @@ export const mockHistory = (mockHass: MockHomeAssistant) => {
|
||||
|
||||
statistic_ids.forEach((id: string) => {
|
||||
if (id in statisticsFunctions) {
|
||||
statistics[id] = statisticsFunctions[id](id, start, end);
|
||||
statistics[id] = statisticsFunctions[id](id, start, end, period);
|
||||
} else {
|
||||
const entityState = hass.states[id];
|
||||
const state = entityState ? Number(entityState.state) : 1;
|
||||
@@ -365,6 +487,7 @@ export const mockHistory = (mockHass: MockHomeAssistant) => {
|
||||
id,
|
||||
start,
|
||||
end,
|
||||
period,
|
||||
state,
|
||||
state * (state > 80 ? 0.01 : 0.05)
|
||||
)
|
||||
@@ -372,6 +495,7 @@ export const mockHistory = (mockHass: MockHomeAssistant) => {
|
||||
id,
|
||||
start,
|
||||
end,
|
||||
period,
|
||||
state,
|
||||
state * (state > 80 ? 0.05 : 0.1)
|
||||
);
|
||||
|
BIN
gallery/public/images/office.jpg
Normal file
BIN
gallery/public/images/office.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 147 KiB |
@@ -52,17 +52,13 @@ class DemoBlackWhiteRow extends LitElement {
|
||||
|
||||
firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector(".dark"),
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: false,
|
||||
},
|
||||
"default",
|
||||
{ dark: true }
|
||||
);
|
||||
applyThemesOnElement(this.shadowRoot!.querySelector(".dark"), {
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
});
|
||||
}
|
||||
|
||||
handleSubmit(ev) {
|
||||
|
@@ -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,56 @@ 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: true,
|
||||
theme: "default",
|
||||
});
|
||||
}
|
||||
|
||||
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 +194,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);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
88
gallery/src/demos/demo-ha-faded.ts
Normal file
88
gallery/src/demos/demo-ha-faded.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-faded";
|
||||
import "../../../src/components/ha-markdown";
|
||||
|
||||
const LONG_TEXT = `
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc laoreet velit ut elit volutpat, eget ultrices odio lacinia. In imperdiet malesuada est, nec sagittis metus ultricies quis. Sed nisl ex, convallis porttitor ante quis, hendrerit tristique justo. Mauris pharetra venenatis augue, eu maximus sem cursus in. Quisque sed consequat risus. Suspendisse facilisis ligula a odio consectetur condimentum. Curabitur vehicula elit nec augue mollis, et volutpat massa dictum.
|
||||
|
||||
Nam pellentesque auctor rutrum. Suspendisse elit est, sodales vel diam nec, porttitor faucibus massa. Ut pretium ac orci eu pharetra. Praesent in nibh at magna viverra rutrum eu vitae tortor. Etiam eget sem ex. Fusce tristique odio nec lacus mattis, vitae tempor nunc malesuada. Maecenas faucibus magna vel libero maximus egestas. Vestibulum luctus semper velit, in lobortis risus tempus non. Curabitur bibendum ornare commodo. Quisque commodo neque sit amet tincidunt lacinia. Proin elementum ante velit, eu congue nulla semper quis. Pellentesque consequat vel nunc at scelerisque. Mauris sit amet venenatis diam, blandit viverra leo. Integer commodo laoreet orci.
|
||||
|
||||
Curabitur ipsum tortor, sodales ut augue sed, commodo porttitor libero. Pellentesque molestie vitae mi consectetur tempor. In sed lectus consequat, lobortis neque non, semper ipsum. Etiam eget ex et nibh sagittis pulvinar lacinia ac mauris. Aenean ligula eros, viverra ac nibh at, venenatis semper quam. Sed interdum ligula sit amet massa tincidunt tincidunt. Suspendisse potenti. Aliquam egestas facilisis est, sed faucibus erat scelerisque id. Duis dolor quam, viverra vitae orci euismod, laoreet pellentesque justo. Nunc malesuada non erat at ullamcorper. Mauris eget posuere odio. Vestibulum turpis nunc, pharetra eget ante in, feugiat mollis justo. Proin porttitor, diam nec vulputate pretium, tellus arcu rhoncus turpis, a blandit nisi nulla quis arcu. Nunc ac ullamcorper ligula, nec facilisis leo.
|
||||
|
||||
In vitae eros sollicitudin, iaculis ex eget, egestas orci. Etiam sed pretium lorem. Nam nisi enim, consectetur sit amet semper ac, semper pharetra diam. In pulvinar neque sapien, ac ullamcorper est lacinia a. Etiam tincidunt velit sed diam malesuada, eu ornare ex consectetur. Phasellus in imperdiet tellus. Sed bibendum, dui sit amet fringilla aliquet, enim odio sollicitudin lorem, vel semper turpis mauris vel mauris. Aenean congue magna ac massa cursus, in dictum orci commodo. Pellentesque mollis velit in sollicitudin tincidunt. Vestibulum et efficitur nulla.
|
||||
|
||||
Quisque posuere, velit sed porttitor dapibus, neque augue fringilla felis, eu luctus nisi nisl nec ipsum. Curabitur pellentesque ac lectus eget ultricies. Vestibulum est dolor, lacinia pharetra vulputate a, facilisis a magna. Nam vitae arcu nibh. Praesent finibus blandit ante, ac gravida ex mollis eget. Donec quam est, pulvinar vitae neque ut, bibendum aliquam erat. Nullam mollis arcu at sem tincidunt, in tristique lectus facilisis. Aenean ut lacus vel nisl finibus iaculis non a turpis. Integer eget ipsum ante. Donec nunc neque, vestibulum ac magna ac, posuere scelerisque dui. Pellentesque massa nibh, rhoncus id dolor quis, placerat posuere turpis. Donec aliquet augue nisi, eu finibus dui auctor et. Vestibulum eu varius lorem. Quisque lectus ante, malesuada pretium risus eget, interdum mattis enim.
|
||||
`;
|
||||
|
||||
const SMALL_TEXT = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
|
||||
|
||||
@customElement("demo-ha-faded")
|
||||
export class DemoHaFaded extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-card header="ha-faded demo">
|
||||
<div class="card-content">
|
||||
<h3>Long text directly as slotted content</h3>
|
||||
<ha-faded>${LONG_TEXT}</ha-faded>
|
||||
<h3>Long text with slotted element</h3>
|
||||
<ha-faded><span>${LONG_TEXT}</span></ha-faded>
|
||||
<h3>No text</h3>
|
||||
<ha-faded><span></span></ha-faded>
|
||||
<h3>Smal text</h3>
|
||||
<ha-faded><span>${SMALL_TEXT}</span></ha-faded>
|
||||
<h3>Long text in markdown</h3>
|
||||
<ha-faded>
|
||||
<ha-markdown .content=${LONG_TEXT}> </ha-markdown>
|
||||
</ha-faded>
|
||||
<h3>Missing 1px from hiding</h3>
|
||||
<ha-faded faded-height="87">
|
||||
<span>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc
|
||||
laoreet velit ut elit volutpat, eget ultrices odio lacinia. In
|
||||
imperdiet malesuada est, nec sagittis metus ultricies quis. Sed
|
||||
nisl ex, convallis porttitor ante quis, hendrerit tristique justo.
|
||||
Mauris pharetra venenatis augue, eu maximus sem cursus in. Quisque
|
||||
sed consequat risus. Suspendisse facilisis ligula a odio
|
||||
consectetur condimentum. Curabitur vehicula elit nec augue mollis,
|
||||
et volutpat massa dictum. Nam pellentesque auctor rutrum.
|
||||
Suspendisse elit est, sodales vel diam nec, porttitor faucibus
|
||||
massa. Ut pretium ac orci eu pharetra.
|
||||
</span>
|
||||
</ha-faded>
|
||||
<h3>1px over hiding point</h3>
|
||||
<ha-faded faded-height="85">
|
||||
<span>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc
|
||||
laoreet velit ut elit volutpat, eget ultrices odio lacinia. In
|
||||
imperdiet malesuada est, nec sagittis metus ultricies quis. Sed
|
||||
nisl ex, convallis porttitor ante quis, hendrerit tristique justo.
|
||||
Mauris pharetra venenatis augue, eu maximus sem cursus in. Quisque
|
||||
sed consequat risus. Suspendisse facilisis ligula a odio
|
||||
consectetur condimentum. Curabitur vehicula elit nec augue mollis,
|
||||
et volutpat massa dictum. Nam pellentesque auctor rutrum.
|
||||
Suspendisse elit est, sodales vel diam nec, porttitor faucibus
|
||||
massa. Ut pretium ac orci eu pharetra.
|
||||
</span>
|
||||
</ha-faded>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 24px auto;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-ha-faded": DemoHaFaded;
|
||||
}
|
||||
}
|
@@ -222,6 +222,30 @@ const SCHEMAS: {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "OctoPrint",
|
||||
translations: {
|
||||
username: "Username",
|
||||
host: "Host",
|
||||
port: "Port Number",
|
||||
path: "Application Path",
|
||||
ssl: "Use SSL",
|
||||
},
|
||||
schema: [
|
||||
{ type: "string", name: "username", required: true, default: "" },
|
||||
{ type: "string", name: "host", required: true, default: "" },
|
||||
{
|
||||
type: "integer",
|
||||
valueMin: 1,
|
||||
valueMax: 65535,
|
||||
name: "port",
|
||||
optional: true,
|
||||
default: 80,
|
||||
},
|
||||
{ type: "string", name: "path", optional: true, default: "/" },
|
||||
{ type: "boolean", name: "ssl", optional: true, default: false },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-ha-form")
|
||||
|
156
gallery/src/demos/demo-hui-area-card.ts
Normal file
156
gallery/src/demos/demo-hui-area-card.ts
Normal 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;
|
||||
}
|
||||
}
|
164
gallery/src/demos/demo-more-info-cover.ts
Normal file
164
gallery/src/demos/demo-more-info-cover.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import "../../../src/components/ha-card";
|
||||
import {
|
||||
SUPPORT_OPEN,
|
||||
SUPPORT_STOP,
|
||||
SUPPORT_CLOSE,
|
||||
SUPPORT_SET_POSITION,
|
||||
SUPPORT_OPEN_TILT,
|
||||
SUPPORT_STOP_TILT,
|
||||
SUPPORT_CLOSE_TILT,
|
||||
SUPPORT_SET_TILT_POSITION,
|
||||
} from "../../../src/data/cover";
|
||||
import "../../../src/dialogs/more-info/more-info-content";
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
import {
|
||||
MockHomeAssistant,
|
||||
provideHass,
|
||||
} from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-more-infos";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("cover", "position_buttons", "on", {
|
||||
friendly_name: "Position Buttons",
|
||||
supported_features: SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE,
|
||||
}),
|
||||
getEntity("cover", "position_slider_half", "on", {
|
||||
friendly_name: "Position Half-Open",
|
||||
supported_features:
|
||||
SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE + SUPPORT_SET_POSITION,
|
||||
current_position: 50,
|
||||
}),
|
||||
getEntity("cover", "position_slider_open", "on", {
|
||||
friendly_name: "Position Open",
|
||||
supported_features:
|
||||
SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE + SUPPORT_SET_POSITION,
|
||||
current_position: 100,
|
||||
}),
|
||||
getEntity("cover", "position_slider_closed", "on", {
|
||||
friendly_name: "Position Closed",
|
||||
supported_features:
|
||||
SUPPORT_OPEN + SUPPORT_STOP + SUPPORT_CLOSE + SUPPORT_SET_POSITION,
|
||||
current_position: 0,
|
||||
}),
|
||||
getEntity("cover", "tilt_buttons", "on", {
|
||||
friendly_name: "Tilt Buttons",
|
||||
supported_features:
|
||||
SUPPORT_OPEN_TILT + SUPPORT_STOP_TILT + SUPPORT_CLOSE_TILT,
|
||||
}),
|
||||
getEntity("cover", "tilt_slider_half", "on", {
|
||||
friendly_name: "Tilt Half-Open",
|
||||
supported_features:
|
||||
SUPPORT_OPEN_TILT +
|
||||
SUPPORT_STOP_TILT +
|
||||
SUPPORT_CLOSE_TILT +
|
||||
SUPPORT_SET_TILT_POSITION,
|
||||
current_tilt_position: 50,
|
||||
}),
|
||||
getEntity("cover", "tilt_slider_open", "on", {
|
||||
friendly_name: "Tilt Open",
|
||||
supported_features:
|
||||
SUPPORT_OPEN_TILT +
|
||||
SUPPORT_STOP_TILT +
|
||||
SUPPORT_CLOSE_TILT +
|
||||
SUPPORT_SET_TILT_POSITION,
|
||||
current_tilt_position: 100,
|
||||
}),
|
||||
getEntity("cover", "tilt_slider_closed", "on", {
|
||||
friendly_name: "Tilt Closed",
|
||||
supported_features:
|
||||
SUPPORT_OPEN_TILT +
|
||||
SUPPORT_STOP_TILT +
|
||||
SUPPORT_CLOSE_TILT +
|
||||
SUPPORT_SET_TILT_POSITION,
|
||||
current_tilt_position: 0,
|
||||
}),
|
||||
getEntity("cover", "position_slider_tilt_slider", "on", {
|
||||
friendly_name: "Both Sliders",
|
||||
supported_features:
|
||||
SUPPORT_OPEN +
|
||||
SUPPORT_STOP +
|
||||
SUPPORT_CLOSE +
|
||||
SUPPORT_SET_POSITION +
|
||||
SUPPORT_OPEN_TILT +
|
||||
SUPPORT_STOP_TILT +
|
||||
SUPPORT_CLOSE_TILT +
|
||||
SUPPORT_SET_TILT_POSITION,
|
||||
current_position: 30,
|
||||
current_tilt_position: 70,
|
||||
}),
|
||||
getEntity("cover", "position_tilt_slider", "on", {
|
||||
friendly_name: "Position & Tilt Slider",
|
||||
supported_features:
|
||||
SUPPORT_OPEN +
|
||||
SUPPORT_STOP +
|
||||
SUPPORT_CLOSE +
|
||||
SUPPORT_OPEN_TILT +
|
||||
SUPPORT_STOP_TILT +
|
||||
SUPPORT_CLOSE_TILT +
|
||||
SUPPORT_SET_TILT_POSITION,
|
||||
current_tilt_position: 70,
|
||||
}),
|
||||
getEntity("cover", "position_slider_tilt", "on", {
|
||||
friendly_name: "Position Slider & Tilt",
|
||||
supported_features:
|
||||
SUPPORT_OPEN +
|
||||
SUPPORT_STOP +
|
||||
SUPPORT_CLOSE +
|
||||
SUPPORT_SET_POSITION +
|
||||
SUPPORT_OPEN_TILT +
|
||||
SUPPORT_STOP_TILT +
|
||||
SUPPORT_CLOSE_TILT,
|
||||
current_position: 30,
|
||||
}),
|
||||
getEntity("cover", "position_slider_only_tilt_slider", "on", {
|
||||
friendly_name: "Position Slider Only & Tilt Buttons",
|
||||
supported_features:
|
||||
SUPPORT_SET_POSITION +
|
||||
SUPPORT_OPEN_TILT +
|
||||
SUPPORT_STOP_TILT +
|
||||
SUPPORT_CLOSE_TILT,
|
||||
current_position: 30,
|
||||
}),
|
||||
getEntity("cover", "position_slider_only_tilt", "on", {
|
||||
friendly_name: "Position Slider Only & Tilt",
|
||||
supported_features:
|
||||
SUPPORT_SET_POSITION +
|
||||
SUPPORT_OPEN_TILT +
|
||||
SUPPORT_STOP_TILT +
|
||||
SUPPORT_CLOSE_TILT +
|
||||
SUPPORT_SET_TILT_POSITION,
|
||||
current_position: 30,
|
||||
current_tilt_position: 70,
|
||||
}),
|
||||
];
|
||||
|
||||
@customElement("demo-more-info-cover")
|
||||
class DemoMoreInfoCover extends LitElement {
|
||||
@property() public hass!: MockHomeAssistant;
|
||||
|
||||
@query("demo-more-infos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<demo-more-infos
|
||||
.hass=${this.hass}
|
||||
.entities=${ENTITIES.map((ent) => ent.entityId)}
|
||||
></demo-more-infos>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-more-info-cover": DemoMoreInfoCover;
|
||||
}
|
||||
}
|
@@ -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({
|
||||
|
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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}
|
||||
|
@@ -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;
|
||||
|
@@ -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")}
|
||||
@@ -173,7 +173,8 @@ export class HassioBackups extends LitElement {
|
||||
clickable
|
||||
selectable
|
||||
hasFab
|
||||
main-page
|
||||
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
|
||||
back-path="/config"
|
||||
supervisor
|
||||
>
|
||||
<ha-button-menu
|
||||
|
@@ -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",
|
||||
|
@@ -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`
|
||||
|
@@ -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,41 @@ class HassioDashboard extends LitElement {
|
||||
.localizeFunc=${this.supervisor.localize}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.tabs=${supervisorTabs}
|
||||
main-page
|
||||
.tabs=${supervisorTabs(this.hass)}
|
||||
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
|
||||
back-path="/config"
|
||||
supervisor
|
||||
hasFab
|
||||
>
|
||||
<span slot="header">
|
||||
${this.supervisor.localize("panel.dashboard")}
|
||||
${this.supervisor.localize(
|
||||
atLeastVersion(this.hass.config.version, 2021, 12)
|
||||
? "panel.addons"
|
||||
: "panel.dashboard"
|
||||
)}
|
||||
</span>
|
||||
<div class="content">
|
||||
<hassio-update
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this.supervisor}
|
||||
></hassio-update>
|
||||
${!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>
|
||||
`;
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -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"
|
||||
|
@@ -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`
|
||||
|
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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,
|
||||
});
|
||||
};
|
@@ -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);
|
||||
|
||||
|
@@ -34,6 +34,9 @@ const REDIRECTS: Redirects = {
|
||||
supervisor_store: {
|
||||
redirect: "/hassio/store",
|
||||
},
|
||||
supervisor_addons: {
|
||||
redirect: "/hassio/dashboard",
|
||||
},
|
||||
supervisor_addon: {
|
||||
redirect: "/hassio/addon",
|
||||
params: {
|
||||
|
@@ -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"),
|
||||
|
@@ -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",
|
||||
|
@@ -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() {
|
||||
|
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import "../../../src/layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
@@ -28,8 +29,9 @@ class HassioSystem extends LitElement {
|
||||
.localizeFunc=${this.supervisor.localize}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.tabs=${supervisorTabs}
|
||||
main-page
|
||||
.tabs=${supervisorTabs(this.hass)}
|
||||
.mainPage=${!atLeastVersion(this.hass.config.version, 2021, 12)}
|
||||
back-path="/config"
|
||||
supervisor
|
||||
>
|
||||
<span slot="header"> ${this.supervisor.localize("panel.system")} </span>
|
||||
|
402
hassio/src/update-available/update-available-card.ts
Normal file
402
hassio/src/update-available/update-available-card.ts
Normal file
@@ -0,0 +1,402 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/common/search/search-input";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-alert";
|
||||
import "../../../src/components/ha-button-menu";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-checkbox";
|
||||
import "../../../src/components/ha-faded";
|
||||
import "../../../src/components/ha-formfield";
|
||||
import "../../../src/components/ha-icon-button";
|
||||
import "../../../src/components/ha-markdown";
|
||||
import "../../../src/components/ha-settings-row";
|
||||
import "../../../src/components/ha-svg-icon";
|
||||
import "../../../src/components/ha-switch";
|
||||
import {
|
||||
fetchHassioAddonChangelog,
|
||||
fetchHassioAddonInfo,
|
||||
HassioAddonDetails,
|
||||
updateHassioAddon,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import {
|
||||
createHassioPartialBackup,
|
||||
HassioPartialBackupCreateParams,
|
||||
} from "../../../src/data/hassio/backup";
|
||||
import {
|
||||
extractApiErrorMessage,
|
||||
ignoreSupervisorError,
|
||||
} from "../../../src/data/hassio/common";
|
||||
import { updateOS } from "../../../src/data/hassio/host";
|
||||
import { updateSupervisor } from "../../../src/data/hassio/supervisor";
|
||||
import { updateCore } from "../../../src/data/supervisor/core";
|
||||
import { StoreAddon } from "../../../src/data/supervisor/store";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import "../../../src/layouts/hass-subpage";
|
||||
import "../../../src/layouts/hass-tabs-subpage";
|
||||
import { SUPERVISOR_UPDATE_NAMES } from "../../../src/panels/config/dashboard/ha-config-updates";
|
||||
import { HomeAssistant, Route } from "../../../src/types";
|
||||
import { documentationUrl } from "../../../src/util/documentation-url";
|
||||
import { addonArchIsSupported, extractChangelog } from "../util/addon";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"update-complete": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
type updateType = "os" | "supervisor" | "core" | "addon";
|
||||
|
||||
const changelogUrl = (
|
||||
hass: HomeAssistant,
|
||||
entry: updateType,
|
||||
version: string
|
||||
): string | undefined => {
|
||||
if (entry === "addon") {
|
||||
return undefined;
|
||||
}
|
||||
if (entry === "core") {
|
||||
return version?.includes("dev")
|
||||
? "https://github.com/home-assistant/core/commits/dev"
|
||||
: documentationUrl(hass, "/latest-release-notes/");
|
||||
}
|
||||
if (entry === "os") {
|
||||
return version?.includes("dev")
|
||||
? "https://github.com/home-assistant/operating-system/commits/dev"
|
||||
: `https://github.com/home-assistant/operating-system/releases/tag/${version}`;
|
||||
}
|
||||
if (entry === "supervisor") {
|
||||
return version?.includes("dev")
|
||||
? "https://github.com/home-assistant/supervisor/commits/main"
|
||||
: `https://github.com/home-assistant/supervisor/releases/tag/${version}`;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
@customElement("update-available-card")
|
||||
class UpdateAvailableCard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public supervisor!: Supervisor;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ attribute: false }) public addonSlug?: string;
|
||||
|
||||
@state() private _updateType?: updateType;
|
||||
|
||||
@state() private _changelogContent?: string;
|
||||
|
||||
@state() private _addonInfo?: HassioAddonDetails;
|
||||
|
||||
@state() private _action: "backup" | "update" | null = null;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _addonStoreInfo = memoizeOne(
|
||||
(slug: string, storeAddons: StoreAddon[]) =>
|
||||
storeAddons.find((addon) => addon.slug === slug)
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (
|
||||
!this._updateType ||
|
||||
(this._updateType === "addon" && !this._addonInfo)
|
||||
) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const changelog = changelogUrl(this.hass, this._updateType, this._version);
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
.header=${this.supervisor.localize("update_available.update_name", {
|
||||
name: this._name,
|
||||
})}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
${this._action === null
|
||||
? html`
|
||||
${this._changelogContent
|
||||
? html`
|
||||
<ha-faded>
|
||||
<ha-markdown .content=${this._changelogContent}>
|
||||
</ha-markdown>
|
||||
</ha-faded>
|
||||
`
|
||||
: ""}
|
||||
<div class="versions">
|
||||
<p>
|
||||
${this.supervisor.localize("update_available.description", {
|
||||
name: this._name,
|
||||
version: this._version,
|
||||
newest_version: this._version_latest,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
${["core", "addon"].includes(this._updateType)
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${this.supervisor.localize(
|
||||
"update_available.create_backup"
|
||||
)}
|
||||
>
|
||||
<ha-checkbox checked></ha-checkbox>
|
||||
</ha-formfield>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: html`<ha-circular-progress alt="Updating" size="large" active>
|
||||
</ha-circular-progress>
|
||||
<p class="progress-text">
|
||||
${this._action === "update"
|
||||
? this.supervisor.localize("update_available.updating", {
|
||||
name: this._name,
|
||||
version: this._version_latest,
|
||||
})
|
||||
: this.supervisor.localize(
|
||||
"update_available.creating_backup",
|
||||
{ name: this._name }
|
||||
)}
|
||||
</p>`}
|
||||
</div>
|
||||
${this._action === null
|
||||
? html`
|
||||
<div class="card-actions">
|
||||
${changelog
|
||||
? html`<a .href=${changelog} target="_blank" rel="noreferrer">
|
||||
<mwc-button
|
||||
.label=${this.supervisor.localize(
|
||||
"update_available.open_release_notes"
|
||||
)}
|
||||
>
|
||||
</mwc-button>
|
||||
</a>`
|
||||
: ""}
|
||||
<span></span>
|
||||
<ha-progress-button
|
||||
.disabled=${!this._version ||
|
||||
(this._shouldCreateBackup &&
|
||||
this.supervisor.info?.state !== "running")}
|
||||
@click=${this._update}
|
||||
raised
|
||||
>
|
||||
${this.supervisor.localize("common.update")}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
const pathPart = this.route?.path.substring(1, this.route.path.length);
|
||||
const updateType = ["core", "os", "supervisor"].includes(pathPart)
|
||||
? pathPart
|
||||
: "addon";
|
||||
this._updateType = updateType as updateType;
|
||||
|
||||
if (updateType === "addon") {
|
||||
if (!this.addonSlug) {
|
||||
this.addonSlug = pathPart;
|
||||
}
|
||||
this._loadAddonData();
|
||||
}
|
||||
}
|
||||
|
||||
get _shouldCreateBackup(): boolean {
|
||||
const checkbox = this.shadowRoot?.querySelector("ha-checkbox");
|
||||
if (checkbox) {
|
||||
return checkbox.checked;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
get _version(): string {
|
||||
return this._updateType
|
||||
? this._updateType === "addon"
|
||||
? this._addonInfo!.version
|
||||
: this.supervisor[this._updateType]?.version || ""
|
||||
: "";
|
||||
}
|
||||
|
||||
get _version_latest(): string {
|
||||
return this._updateType
|
||||
? this._updateType === "addon"
|
||||
? this._addonInfo!.version_latest
|
||||
: this.supervisor[this._updateType]?.version_latest || ""
|
||||
: "";
|
||||
}
|
||||
|
||||
get _name(): string {
|
||||
return this._updateType
|
||||
? this._updateType === "addon"
|
||||
? this._addonInfo!.name
|
||||
: SUPERVISOR_UPDATE_NAMES[this._updateType]
|
||||
: "";
|
||||
}
|
||||
|
||||
private async _loadAddonData() {
|
||||
try {
|
||||
this._addonInfo = await fetchHassioAddonInfo(this.hass, this.addonSlug!);
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: this._updateType,
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const addonStoreInfo =
|
||||
!this._addonInfo.detached && !this._addonInfo.available
|
||||
? this._addonStoreInfo(
|
||||
this._addonInfo.slug,
|
||||
this.supervisor.store.addons
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (this._addonInfo.changelog) {
|
||||
try {
|
||||
const content = await fetchHassioAddonChangelog(
|
||||
this.hass,
|
||||
this.addonSlug!
|
||||
);
|
||||
this._changelogContent = extractChangelog(this._addonInfo, content);
|
||||
} catch (err) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this._addonInfo.available && addonStoreInfo) {
|
||||
if (
|
||||
!addonArchIsSupported(
|
||||
this.supervisor.info.supported_arch,
|
||||
this._addonInfo.arch
|
||||
)
|
||||
) {
|
||||
this._error = this.supervisor.localize(
|
||||
"addon.dashboard.not_available_arch"
|
||||
);
|
||||
} else {
|
||||
this._error = this.supervisor.localize(
|
||||
"addon.dashboard.not_available_version",
|
||||
{
|
||||
core_version_installed: this.supervisor.core.version,
|
||||
core_version_needed: addonStoreInfo.homeassistant,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _update() {
|
||||
this._error = undefined;
|
||||
if (this._shouldCreateBackup) {
|
||||
let backupArgs: HassioPartialBackupCreateParams;
|
||||
if (this._updateType === "addon") {
|
||||
backupArgs = {
|
||||
name: `addon_${this.addonSlug}_${this._version}`,
|
||||
addons: [this.addonSlug!],
|
||||
homeassistant: false,
|
||||
};
|
||||
} else {
|
||||
backupArgs = {
|
||||
name: `${this._updateType}_${this._version}`,
|
||||
folders: ["homeassistant"],
|
||||
homeassistant: true,
|
||||
};
|
||||
}
|
||||
this._action = "backup";
|
||||
try {
|
||||
await createHassioPartialBackup(this.hass, backupArgs);
|
||||
} catch (err: any) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
this._action = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this._action = "update";
|
||||
try {
|
||||
if (this._updateType === "addon") {
|
||||
await updateHassioAddon(this.hass, this.addonSlug!);
|
||||
} else if (this._updateType === "core") {
|
||||
await updateCore(this.hass);
|
||||
} else if (this._updateType === "os") {
|
||||
await updateOS(this.hass);
|
||||
} else if (this._updateType === "supervisor") {
|
||||
await updateSupervisor(this.hass);
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
|
||||
this._error = extractApiErrorMessage(err);
|
||||
this._action = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
fireEvent(this, "update-complete");
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
ha-card {
|
||||
margin: auto;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-top: none;
|
||||
padding: 0 8px 8px;
|
||||
}
|
||||
|
||||
ha-circular-progress {
|
||||
display: block;
|
||||
margin: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
ha-markdown {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"update-available-card": UpdateAvailableCard;
|
||||
}
|
||||
}
|
59
hassio/src/update-available/update-available-dashboard.ts
Normal file
59
hassio/src/update-available/update-available-dashboard.ts
Normal 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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
};
|
||||
|
15
package.json
15
package.json
@@ -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",
|
||||
@@ -102,7 +102,7 @@
|
||||
"fuse.js": "^6.0.0",
|
||||
"google-timezones-json": "^1.0.2",
|
||||
"hls.js": "^1.0.11",
|
||||
"home-assistant-js-websocket": "^5.11.1",
|
||||
"home-assistant-js-websocket": "^5.11.3",
|
||||
"idb-keyval": "^5.1.3",
|
||||
"intl-messageformat": "^9.9.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
@@ -115,6 +115,7 @@
|
||||
"node-vibrant": "3.2.1-alpha.1",
|
||||
"proxy-polyfill": "^0.3.2",
|
||||
"punycode": "^2.1.1",
|
||||
"qr-scanner": "^1.3.0",
|
||||
"qrcode": "^1.4.4",
|
||||
"regenerator-runtime": "^0.13.8",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
|
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20211026.0",
|
||||
version="20211203.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/frontend",
|
||||
author="The Home Assistant Authors",
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import "@material/mwc-button";
|
||||
import { genClientId } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -8,18 +9,18 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import "../components/ha-alert";
|
||||
import "../components/ha-checkbox";
|
||||
import { computeInitialHaFormData } from "../components/ha-form/compute-initial-ha-form-data";
|
||||
import "../components/ha-form/ha-form";
|
||||
import "../components/ha-formfield";
|
||||
import "../components/ha-markdown";
|
||||
import "../components/ha-alert";
|
||||
import { AuthProvider } from "../data/auth";
|
||||
import {
|
||||
DataEntryFlowStep,
|
||||
DataEntryFlowStepForm,
|
||||
} from "../data/data_entry_flow";
|
||||
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
|
||||
import { computeInitialHaFormData } from "../components/ha-form/compute-initial-ha-form-data";
|
||||
import "./ha-password-manager-polyfill";
|
||||
|
||||
type State = "loading" | "error" | "step";
|
||||
@@ -205,7 +206,8 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
|
||||
.computeError=${this._computeErrorCallback(step)}
|
||||
@value-changed=${this._stepDataChanged}
|
||||
></ha-form>
|
||||
${this.clientId === window.location.origin && step.step_id !== "mfa"
|
||||
${this.clientId === genClientId() &&
|
||||
!["select_mfa_module", "mfa"].includes(step.step_id)
|
||||
? html`
|
||||
<ha-formfield
|
||||
class="store-token"
|
||||
|
@@ -101,17 +101,13 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
this._fetchAuthProviders();
|
||||
|
||||
if (matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||
applyThemesOnElement(
|
||||
document.documentElement,
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: null,
|
||||
themes: {},
|
||||
darkMode: false,
|
||||
},
|
||||
"default",
|
||||
{ dark: true }
|
||||
);
|
||||
applyThemesOnElement(document.documentElement, {
|
||||
default_theme: "default",
|
||||
default_dark_theme: null,
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.redirectUri) {
|
||||
|
@@ -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>
|
||||
|
@@ -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";
|
||||
|
@@ -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;
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { AuthData } from "home-assistant-js-websocket";
|
||||
import { extractSearchParam } from "../url/search-params";
|
||||
|
||||
const storage = window.localStorage || {};
|
||||
|
||||
@@ -31,10 +32,7 @@ export function askWrite() {
|
||||
export function saveTokens(tokens: AuthData | null) {
|
||||
tokenCache.tokens = tokens;
|
||||
|
||||
if (
|
||||
!tokenCache.writeEnabled &&
|
||||
new URLSearchParams(window.location.search).get("storeToken") === "true"
|
||||
) {
|
||||
if (!tokenCache.writeEnabled && extractSearchParam("storeToken") === "true") {
|
||||
tokenCache.writeEnabled = true;
|
||||
}
|
||||
|
||||
|
@@ -61,3 +61,14 @@ export const COLORS = [
|
||||
export function getColorByIndex(index: number) {
|
||||
return COLORS[index % COLORS.length];
|
||||
}
|
||||
|
||||
export function getGraphColorByIndex(
|
||||
index: number,
|
||||
style: CSSStyleDeclaration
|
||||
) {
|
||||
// The CSS vars for the colors use range 1..n, so we need to adjust the index from the internal 0..n color index range.
|
||||
return (
|
||||
style.getPropertyValue(`--graph-color-${index + 1}`) ||
|
||||
getColorByIndex(index)
|
||||
);
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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",
|
||||
@@ -188,8 +188,9 @@ export const DOMAINS_WITH_MORE_INFO = [
|
||||
"weather",
|
||||
];
|
||||
|
||||
/** Domains that show no more info dialog. */
|
||||
export const DOMAINS_HIDE_MORE_INFO = [
|
||||
/** Domains that do not show the default more info dialog content (e.g. the attribute section)
|
||||
* and do not have a separate more info (so not in DOMAINS_WITH_MORE_INFO). */
|
||||
export const DOMAINS_HIDE_DEFAULT_MORE_INFO = [
|
||||
"input_number",
|
||||
"input_select",
|
||||
"input_text",
|
||||
@@ -198,6 +199,31 @@ export const DOMAINS_HIDE_MORE_INFO = [
|
||||
"select",
|
||||
];
|
||||
|
||||
/** Domains that render an input element instead of a text value when rendered in a row.
|
||||
* Those rows should then not show a cursor pointer when hovered (which would normally
|
||||
* be the default) unless the element itself enforces it (e.g. a button). Also those elements
|
||||
* should not act as a click target to open the more info dialog (the row name and state icon
|
||||
* still do of course) as the click might instead e.g. activate the input field that this row shows.
|
||||
*/
|
||||
export const DOMAINS_INPUT_ROW = [
|
||||
"cover",
|
||||
"fan",
|
||||
"humidifier",
|
||||
"input_boolean",
|
||||
"input_datetime",
|
||||
"input_number",
|
||||
"input_select",
|
||||
"input_text",
|
||||
"light",
|
||||
"lock",
|
||||
"media_player",
|
||||
"number",
|
||||
"scene",
|
||||
"script",
|
||||
"select",
|
||||
"switch",
|
||||
];
|
||||
|
||||
/** Domains that should have the history hidden in the more info dialog. */
|
||||
export const DOMAINS_MORE_INFO_NO_HISTORY = ["camera", "configurator", "scene"];
|
||||
|
||||
|
@@ -23,9 +23,9 @@ let PROCESSED_THEMES: Record<string, ProcessedTheme> = {};
|
||||
* Apply a theme to an element by setting the CSS variables on it.
|
||||
*
|
||||
* element: Element to apply theme on.
|
||||
* themes: HASS theme information.
|
||||
* selectedTheme: Selected theme.
|
||||
* themeSettings: Settings such as selected dark mode and colors.
|
||||
* themes: HASS theme information (e.g. active dark mode and globally active theme name).
|
||||
* selectedTheme: Selected theme (used to override the globally active theme for this element).
|
||||
* themeSettings: Additional settings such as selected colors.
|
||||
*/
|
||||
export const applyThemesOnElement = (
|
||||
element,
|
||||
@@ -33,31 +33,33 @@ export const applyThemesOnElement = (
|
||||
selectedTheme?: string,
|
||||
themeSettings?: Partial<HomeAssistant["selectedTheme"]>
|
||||
) => {
|
||||
let cacheKey = selectedTheme;
|
||||
let themeRules: Partial<ThemeVars> = {};
|
||||
// If there is no explicitly desired theme provided, we automatically
|
||||
// use the active one from `themes`.
|
||||
const themeToApply = selectedTheme || themes.theme;
|
||||
|
||||
// If there is no explicitly desired dark mode provided, we automatically
|
||||
// use the active one from hass.themes.
|
||||
if (!themeSettings || themeSettings?.dark === undefined) {
|
||||
themeSettings = {
|
||||
...themeSettings,
|
||||
dark: themes.darkMode,
|
||||
};
|
||||
}
|
||||
// use the active one from `themes`.
|
||||
const darkMode =
|
||||
themeSettings && themeSettings?.dark !== undefined
|
||||
? themeSettings?.dark
|
||||
: themes.darkMode;
|
||||
|
||||
if (themeSettings.dark) {
|
||||
let cacheKey = themeToApply;
|
||||
let themeRules: Partial<ThemeVars> = {};
|
||||
|
||||
if (darkMode) {
|
||||
cacheKey = `${cacheKey}__dark`;
|
||||
themeRules = { ...darkStyles };
|
||||
}
|
||||
|
||||
if (selectedTheme === "default") {
|
||||
if (themeToApply === "default") {
|
||||
// Determine the primary and accent colors from the current settings.
|
||||
// Fallbacks are implicitly the HA default blue and orange or the
|
||||
// derived "darkStyles" values, depending on the light vs dark mode.
|
||||
const primaryColor = themeSettings.primaryColor;
|
||||
const accentColor = themeSettings.accentColor;
|
||||
const primaryColor = themeSettings?.primaryColor;
|
||||
const accentColor = themeSettings?.accentColor;
|
||||
|
||||
if (themeSettings.dark && primaryColor) {
|
||||
if (darkMode && primaryColor) {
|
||||
themeRules["app-header-background-color"] = hexBlend(
|
||||
primaryColor,
|
||||
"#121212",
|
||||
@@ -98,17 +100,17 @@ export const applyThemesOnElement = (
|
||||
// Custom theme logic (not relevant for default theme, since it would override
|
||||
// the derived calculations from above)
|
||||
if (
|
||||
selectedTheme &&
|
||||
selectedTheme !== "default" &&
|
||||
themes.themes[selectedTheme]
|
||||
themeToApply &&
|
||||
themeToApply !== "default" &&
|
||||
themes.themes[themeToApply]
|
||||
) {
|
||||
// Apply theme vars that are relevant for all modes (but extract the "modes" section first)
|
||||
const { modes, ...baseThemeRules } = themes.themes[selectedTheme];
|
||||
const { modes, ...baseThemeRules } = themes.themes[themeToApply];
|
||||
themeRules = { ...themeRules, ...baseThemeRules };
|
||||
|
||||
// Apply theme vars for the specific mode if available
|
||||
if (modes) {
|
||||
if (themeSettings?.dark) {
|
||||
if (darkMode) {
|
||||
themeRules = { ...themeRules, ...modes.dark };
|
||||
} else {
|
||||
themeRules = { ...themeRules, ...modes.light };
|
||||
|
@@ -1,2 +1,3 @@
|
||||
/** An empty image which can be set as src of an img element. */
|
||||
export default "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
||||
export const emptyImageBase64 =
|
||||
"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
||||
|
@@ -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":
|
||||
|
@@ -4,7 +4,7 @@ import { FrontendLocaleData } from "../../data/translation";
|
||||
import { formatDate } from "../datetime/format_date";
|
||||
import { formatDateTime } from "../datetime/format_date_time";
|
||||
import { formatTime } from "../datetime/format_time";
|
||||
import { formatNumber } from "../number/format_number";
|
||||
import { formatNumber, isNumericState } from "../number/format_number";
|
||||
import { LocalizeFunc } from "../translations/localize";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
|
||||
@@ -20,7 +20,8 @@ export const computeStateDisplay = (
|
||||
return localize(`state.default.${compareState}`);
|
||||
}
|
||||
|
||||
if (stateObj.attributes.unit_of_measurement) {
|
||||
// Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber`
|
||||
if (isNumericState(stateObj)) {
|
||||
if (stateObj.attributes.device_class === "monetary") {
|
||||
try {
|
||||
return formatNumber(compareState, locale, {
|
||||
@@ -31,8 +32,10 @@ export const computeStateDisplay = (
|
||||
// fallback to default
|
||||
}
|
||||
}
|
||||
return `${formatNumber(compareState, locale)} ${
|
||||
return `${formatNumber(compareState, locale)}${
|
||||
stateObj.attributes.unit_of_measurement
|
||||
? " " + stateObj.attributes.unit_of_measurement
|
||||
: ""
|
||||
}`;
|
||||
}
|
||||
|
||||
@@ -113,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 &&
|
||||
|
@@ -1,19 +1,33 @@
|
||||
import {
|
||||
mdiAirHumidifierOff,
|
||||
mdiAccount,
|
||||
mdiAccountArrowRight,
|
||||
mdiAirHumidifier,
|
||||
mdiLockOpen,
|
||||
mdiAirHumidifierOff,
|
||||
mdiBluetooth,
|
||||
mdiBluetoothConnect,
|
||||
mdiCalendar,
|
||||
mdiCast,
|
||||
mdiCastConnected,
|
||||
mdiClock,
|
||||
mdiEmoticonDead,
|
||||
mdiFlash,
|
||||
mdiGestureTapButton,
|
||||
mdiLanConnect,
|
||||
mdiLanDisconnect,
|
||||
mdiLock,
|
||||
mdiLockAlert,
|
||||
mdiLockClock,
|
||||
mdiLock,
|
||||
mdiCastConnected,
|
||||
mdiCast,
|
||||
mdiEmoticonDead,
|
||||
mdiLockOpen,
|
||||
mdiPackageUp,
|
||||
mdiPowerPlug,
|
||||
mdiPowerPlugOff,
|
||||
mdiRestart,
|
||||
mdiSleep,
|
||||
mdiTimerSand,
|
||||
mdiZWave,
|
||||
mdiClock,
|
||||
mdiCalendar,
|
||||
mdiToggleSwitch,
|
||||
mdiToggleSwitchOff,
|
||||
mdiWeatherNight,
|
||||
mdiZWave,
|
||||
} from "@mdi/js";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
/**
|
||||
@@ -41,9 +55,30 @@ export const domainIcon = (
|
||||
case "binary_sensor":
|
||||
return binarySensorIcon(compareState, stateObj);
|
||||
|
||||
case "button":
|
||||
switch (stateObj?.attributes.device_class) {
|
||||
case "restart":
|
||||
return mdiRestart;
|
||||
case "update":
|
||||
return mdiPackageUp;
|
||||
default:
|
||||
return mdiGestureTapButton;
|
||||
}
|
||||
|
||||
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 +98,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":
|
||||
|
@@ -1,6 +1,15 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { FrontendLocaleData, NumberFormat } from "../../data/translation";
|
||||
import { round } from "./round";
|
||||
|
||||
/**
|
||||
* Returns true if the entity is considered numeric based on the attributes it has
|
||||
* @param stateObj The entity state object
|
||||
*/
|
||||
export const isNumericState = (stateObj: HassEntity): boolean =>
|
||||
!!stateObj.attributes.unit_of_measurement ||
|
||||
!!stateObj.attributes.state_class;
|
||||
|
||||
export const numberFormatToLocale = (
|
||||
localeOptions: FrontendLocaleData
|
||||
): string | string[] | undefined => {
|
||||
|
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -95,7 +95,7 @@ export default class HaChartBase extends LitElement {
|
||||
borderColor: dataset.borderColor as string,
|
||||
})}
|
||||
></div>
|
||||
${dataset.label}
|
||||
<div class="label">${dataset.label}</div>
|
||||
</li>`
|
||||
)}
|
||||
</ul>
|
||||
@@ -278,11 +278,9 @@ export default class HaChartBase extends LitElement {
|
||||
}
|
||||
.chartLegend li {
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
display: inline-grid;
|
||||
grid-auto-flow: column;
|
||||
padding: 0 8px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
color: var(--secondary-text-color);
|
||||
@@ -290,6 +288,11 @@ export default class HaChartBase extends LitElement {
|
||||
.chartLegend .hidden {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.chartLegend .label {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
.chartLegend .bullet,
|
||||
.chartTooltip .bullet {
|
||||
border-width: 1px;
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
|
||||
import { html, LitElement, PropertyValues } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { getColorByIndex } from "../../common/color/colors";
|
||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||
import {
|
||||
formatNumber,
|
||||
numberFormatToLocale,
|
||||
@@ -164,7 +164,7 @@ class StateHistoryChartLine extends LitElement {
|
||||
const pushData = (timestamp: Date, datavalues: any[] | null) => {
|
||||
if (!datavalues) return;
|
||||
if (timestamp > endTime) {
|
||||
// Drop datapoints that are after the requested endTime. This could happen if
|
||||
// Drop data points that are after the requested endTime. This could happen if
|
||||
// endTime is "now" and client time is not in sync with server time.
|
||||
return;
|
||||
}
|
||||
@@ -190,7 +190,7 @@ class StateHistoryChartLine extends LitElement {
|
||||
color?: string
|
||||
) => {
|
||||
if (!color) {
|
||||
color = getColorByIndex(colorIndex);
|
||||
color = getGraphColorByIndex(colorIndex, computedStyles);
|
||||
colorIndex++;
|
||||
}
|
||||
data.push({
|
||||
|
@@ -2,7 +2,7 @@ import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { getColorByIndex } from "../../common/color/colors";
|
||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { numberFormatToLocale } from "../../common/number/format_number";
|
||||
@@ -24,7 +24,7 @@ const BINARY_SENSOR_DEVICE_CLASS_COLOR_NOT_INVERTED = new Set([
|
||||
"plug",
|
||||
"power",
|
||||
"presence",
|
||||
"update",
|
||||
"running",
|
||||
]);
|
||||
|
||||
const STATIC_STATE_COLORS = new Set([
|
||||
@@ -71,7 +71,7 @@ const getColor = (
|
||||
stateColorMap.set(stateString, color);
|
||||
return color;
|
||||
}
|
||||
const color = getColorByIndex(colorIndex);
|
||||
const color = getGraphColorByIndex(colorIndex, computedStyles);
|
||||
colorIndex++;
|
||||
stateColorMap.set(stateString, color);
|
||||
return color;
|
||||
|
@@ -13,7 +13,7 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { getColorByIndex } from "../../common/color/colors";
|
||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import {
|
||||
@@ -59,6 +59,8 @@ class StatisticsChart extends LitElement {
|
||||
|
||||
@state() private _chartOptions?: ChartOptions;
|
||||
|
||||
private _computedStyle?: CSSStyleDeclaration;
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
return changedProps.size > 1 || !changedProps.has("hass");
|
||||
}
|
||||
@@ -72,6 +74,10 @@ class StatisticsChart extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
public firstUpdated() {
|
||||
this._computedStyle = getComputedStyle(this);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!isComponentLoaded(this.hass, "history")) {
|
||||
return html`<div class="info">
|
||||
@@ -261,7 +267,7 @@ class StatisticsChart extends LitElement {
|
||||
) => {
|
||||
if (!dataValues) return;
|
||||
if (timestamp > endTime) {
|
||||
// Drop datapoints that are after the requested endTime. This could happen if
|
||||
// Drop data points that are after the requested endTime. This could happen if
|
||||
// endTime is "now" and client time is not in sync with server time.
|
||||
return;
|
||||
}
|
||||
@@ -280,7 +286,7 @@ class StatisticsChart extends LitElement {
|
||||
prevValues = dataValues;
|
||||
};
|
||||
|
||||
const color = getColorByIndex(colorIndex);
|
||||
const color = getGraphColorByIndex(colorIndex, this._computedStyle!);
|
||||
colorIndex++;
|
||||
|
||||
const statTypes: this["statTypes"] = [];
|
||||
|
@@ -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 {
|
||||
|
@@ -14,7 +14,10 @@ import secondsToDuration from "../../common/datetime/seconds_to_duration";
|
||||
import { computeStateDisplay } from "../../common/entity/compute_state_display";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { formatNumber } from "../../common/number/format_number";
|
||||
import {
|
||||
formatNumber,
|
||||
isNumericState,
|
||||
} from "../../common/number/format_number";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||
import { timerTimeRemaining } from "../../data/timer";
|
||||
import { HomeAssistant } from "../../types";
|
||||
@@ -145,7 +148,7 @@ export class HaStateLabelBadge extends LitElement {
|
||||
return entityState.state === UNKNOWN ||
|
||||
entityState.state === UNAVAILABLE
|
||||
? "-"
|
||||
: entityState.attributes.unit_of_measurement
|
||||
: isNumericState(entityState)
|
||||
? formatNumber(entityState.state, this.hass!.locale)
|
||||
: computeStateDisplay(
|
||||
this.hass!.localize,
|
||||
|
@@ -102,7 +102,12 @@ export class HaStatisticPicker extends LitElement {
|
||||
</style>
|
||||
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
|
||||
<paper-icon-item>
|
||||
<state-badge slot="item-icon" .stateObj=${item.state}></state-badge>
|
||||
${item.state
|
||||
? html`<state-badge
|
||||
slot="item-icon"
|
||||
.stateObj=${item.state}
|
||||
></state-badge>`
|
||||
: ""}
|
||||
<paper-item-body two-line="">
|
||||
${item.name}
|
||||
<span secondary
|
||||
@@ -153,7 +158,10 @@ export class HaStatisticPicker extends LitElement {
|
||||
const entityState = this.hass.states[meta.statistic_id];
|
||||
if (!entityState) {
|
||||
if (!entitiesOnly) {
|
||||
output.push({ id: meta.statistic_id, name: meta.statistic_id });
|
||||
output.push({
|
||||
id: meta.statistic_id,
|
||||
name: meta.name || meta.statistic_id,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@@ -80,16 +80,14 @@ export class StateBadge extends LitElement {
|
||||
|
||||
this._showIcon = true;
|
||||
|
||||
if (stateObj) {
|
||||
if (stateObj && this.overrideImage === undefined) {
|
||||
// hide icon if we have entity picture
|
||||
if (
|
||||
((stateObj.attributes.entity_picture_local ||
|
||||
(stateObj.attributes.entity_picture_local ||
|
||||
stateObj.attributes.entity_picture) &&
|
||||
!this.overrideIcon) ||
|
||||
this.overrideImage
|
||||
!this.overrideIcon
|
||||
) {
|
||||
let imageUrl =
|
||||
this.overrideImage ||
|
||||
stateObj.attributes.entity_picture_local ||
|
||||
stateObj.attributes.entity_picture;
|
||||
if (this.hass) {
|
||||
|
@@ -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,55 @@ 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;
|
||||
word-break: break-word;
|
||||
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);
|
||||
}
|
||||
`;
|
||||
|
@@ -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,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@@ -23,6 +23,10 @@ class HaBluePrintPicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
public open() {
|
||||
this.shadowRoot!.querySelector("paper-dropdown-menu-light")!.open();
|
||||
}
|
||||
|
||||
private _processedBlueprints = memoizeOne((blueprints?: Blueprints) => {
|
||||
if (!blueprints) {
|
||||
return [];
|
||||
|
@@ -86,7 +86,7 @@ class HaCameraStream extends LitElement {
|
||||
}
|
||||
if (this.stateObj.attributes.frontend_stream_type === STREAM_TYPE_HLS) {
|
||||
return this._url
|
||||
? html` <ha-hls-player
|
||||
? html`<ha-hls-player
|
||||
autoplay
|
||||
playsinline
|
||||
.allowExoPlayer=${this.allowExoPlayer}
|
||||
@@ -98,7 +98,7 @@ class HaCameraStream extends LitElement {
|
||||
: html``;
|
||||
}
|
||||
if (this.stateObj.attributes.frontend_stream_type === STREAM_TYPE_WEB_RTC) {
|
||||
return html` <ha-web-rtc-player
|
||||
return html`<ha-web-rtc-player
|
||||
autoplay
|
||||
playsinline
|
||||
.muted=${this.muted}
|
||||
@@ -115,23 +115,18 @@ class HaCameraStream extends LitElement {
|
||||
// Fallback when unable to fetch stream url
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
!isComponentLoaded(this.hass!, "stream") ||
|
||||
!supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)
|
||||
) {
|
||||
if (!supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)) {
|
||||
// Steaming is not supported by the camera so fallback to MJPEG stream
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
this.stateObj!.attributes.frontend_stream_type === STREAM_TYPE_WEB_RTC &&
|
||||
typeof RTCPeerConnection === "undefined"
|
||||
this.stateObj!.attributes.frontend_stream_type === STREAM_TYPE_WEB_RTC
|
||||
) {
|
||||
// Stream requires WebRTC but browser does not support, so fallback to
|
||||
// MJPEG stream.
|
||||
return true;
|
||||
// Browser support required for WebRTC
|
||||
return typeof RTCPeerConnection === "undefined";
|
||||
}
|
||||
// Render stream
|
||||
return false;
|
||||
// Server side stream component required for HLS
|
||||
return !isComponentLoaded(this.hass!, "stream");
|
||||
}
|
||||
|
||||
private async _getStreamUrl(): Promise<void> {
|
||||
|
@@ -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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -10,24 +10,21 @@ 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;
|
||||
|
||||
@property({ type: Boolean }) public noText = 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">
|
||||
? html`<div
|
||||
class="mdc-chip__icon mdc-chip__icon--leading ${this.noText
|
||||
? "no-text"
|
||||
: ""}"
|
||||
>
|
||||
<slot name="icon"></slot>
|
||||
</div>`
|
||||
: null}
|
||||
@@ -60,6 +57,10 @@ export class HaChip extends LitElement {
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--ha-chip-icon-color, var(--ha-chip-text-color));
|
||||
}
|
||||
.mdc-chip
|
||||
.mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden).no-text {
|
||||
margin-right: -4px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -1,39 +1,30 @@
|
||||
import { mdiStop } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { computeCloseIcon, computeOpenIcon } from "../common/entity/cover_icon";
|
||||
import {
|
||||
CoverEntity,
|
||||
isClosing,
|
||||
isFullyClosed,
|
||||
isFullyOpen,
|
||||
isOpening,
|
||||
supportsClose,
|
||||
supportsOpen,
|
||||
supportsStop,
|
||||
} from "../data/cover";
|
||||
import { UNAVAILABLE } from "../data/entity";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import CoverEntity from "../util/cover-model";
|
||||
import "./ha-icon-button";
|
||||
|
||||
@customElement("ha-cover-controls")
|
||||
class HaCoverControls extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: HassEntity;
|
||||
|
||||
@state() private _entityObj?: CoverEntity;
|
||||
|
||||
public willUpdate(changedProperties: PropertyValues): void {
|
||||
super.willUpdate(changedProperties);
|
||||
|
||||
if (changedProperties.has("stateObj")) {
|
||||
this._entityObj = new CoverEntity(this.hass, this.stateObj);
|
||||
}
|
||||
}
|
||||
@property({ attribute: false }) public stateObj!: CoverEntity;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._entityObj) {
|
||||
if (!this.stateObj) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
@@ -41,7 +32,7 @@ class HaCoverControls extends LitElement {
|
||||
<div class="state">
|
||||
<ha-icon-button
|
||||
class=${classMap({
|
||||
hidden: !this._entityObj.supportsOpen,
|
||||
hidden: !supportsOpen(this.stateObj),
|
||||
})}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.open_cover"
|
||||
@@ -53,7 +44,7 @@ class HaCoverControls extends LitElement {
|
||||
</ha-icon-button>
|
||||
<ha-icon-button
|
||||
class=${classMap({
|
||||
hidden: !this._entityObj.supportsStop,
|
||||
hidden: !supportsStop(this.stateObj),
|
||||
})}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.stop_cover"
|
||||
@@ -64,7 +55,7 @@ class HaCoverControls extends LitElement {
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
class=${classMap({
|
||||
hidden: !this._entityObj.supportsClose,
|
||||
hidden: !supportsClose(this.stateObj),
|
||||
})}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.close_cover"
|
||||
@@ -84,8 +75,7 @@ class HaCoverControls extends LitElement {
|
||||
}
|
||||
const assumedState = this.stateObj.attributes.assumed_state === true;
|
||||
return (
|
||||
(this._entityObj.isFullyOpen || this._entityObj.isOpening) &&
|
||||
!assumedState
|
||||
(isFullyOpen(this.stateObj) || isOpening(this.stateObj)) && !assumedState
|
||||
);
|
||||
}
|
||||
|
||||
@@ -95,24 +85,30 @@ class HaCoverControls extends LitElement {
|
||||
}
|
||||
const assumedState = this.stateObj.attributes.assumed_state === true;
|
||||
return (
|
||||
(this._entityObj.isFullyClosed || this._entityObj.isClosing) &&
|
||||
(isFullyClosed(this.stateObj) || isClosing(this.stateObj)) &&
|
||||
!assumedState
|
||||
);
|
||||
}
|
||||
|
||||
private _onOpenTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this._entityObj.openCover();
|
||||
this.hass.callService("cover", "open_cover", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private _onCloseTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this._entityObj.closeCover();
|
||||
this.hass.callService("cover", "close_cover", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private _onStopTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this._entityObj.stopCover();
|
||||
this.hass.callService("cover", "stop_cover", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@@ -1,44 +1,33 @@
|
||||
import { mdiArrowBottomLeft, mdiArrowTopRight, mdiStop } from "@mdi/js";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import {
|
||||
CoverEntity,
|
||||
isFullyClosedTilt,
|
||||
isFullyOpenTilt,
|
||||
supportsCloseTilt,
|
||||
supportsOpenTilt,
|
||||
supportsStopTilt,
|
||||
} from "../data/cover";
|
||||
import { UNAVAILABLE } from "../data/entity";
|
||||
import { HomeAssistant } from "../types";
|
||||
import CoverEntity from "../util/cover-model";
|
||||
import "./ha-icon-button";
|
||||
|
||||
@customElement("ha-cover-tilt-controls")
|
||||
class HaCoverTiltControls extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) stateObj!: HassEntity;
|
||||
|
||||
@state() private _entityObj?: CoverEntity;
|
||||
|
||||
public willUpdate(changedProperties: PropertyValues): void {
|
||||
super.willUpdate(changedProperties);
|
||||
|
||||
if (changedProperties.has("stateObj")) {
|
||||
this._entityObj = new CoverEntity(this.hass, this.stateObj);
|
||||
}
|
||||
}
|
||||
@property({ attribute: false }) stateObj!: CoverEntity;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._entityObj) {
|
||||
if (!this.stateObj) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html` <ha-icon-button
|
||||
class=${classMap({
|
||||
invisible: !this._entityObj.supportsOpenTilt,
|
||||
invisible: !supportsOpenTilt(this.stateObj),
|
||||
})}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.open_tilt_cover"
|
||||
@@ -49,7 +38,7 @@ class HaCoverTiltControls extends LitElement {
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
class=${classMap({
|
||||
invisible: !this._entityObj.supportsStopTilt,
|
||||
invisible: !supportsStopTilt(this.stateObj),
|
||||
})}
|
||||
.label=${this.hass.localize("ui.dialogs.more_info_control.stop_cover")}
|
||||
.path=${mdiStop}
|
||||
@@ -58,7 +47,7 @@ class HaCoverTiltControls extends LitElement {
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
class=${classMap({
|
||||
invisible: !this._entityObj.supportsCloseTilt,
|
||||
invisible: !supportsCloseTilt(this.stateObj),
|
||||
})}
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.close_tilt_cover"
|
||||
@@ -74,7 +63,7 @@ class HaCoverTiltControls extends LitElement {
|
||||
return true;
|
||||
}
|
||||
const assumedState = this.stateObj.attributes.assumed_state === true;
|
||||
return this._entityObj.isFullyOpenTilt && !assumedState;
|
||||
return isFullyOpenTilt(this.stateObj) && !assumedState;
|
||||
}
|
||||
|
||||
private _computeClosedDisabled(): boolean {
|
||||
@@ -82,22 +71,28 @@ class HaCoverTiltControls extends LitElement {
|
||||
return true;
|
||||
}
|
||||
const assumedState = this.stateObj.attributes.assumed_state === true;
|
||||
return this._entityObj.isFullyClosedTilt && !assumedState;
|
||||
return isFullyClosedTilt(this.stateObj) && !assumedState;
|
||||
}
|
||||
|
||||
private _onOpenTiltTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this._entityObj.openCoverTilt();
|
||||
this.hass.callService("cover", "open_cover_tilt", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private _onCloseTiltTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this._entityObj.closeCoverTilt();
|
||||
this.hass.callService("cover", "close_cover_tilt", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private _onStopTiltTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this._entityObj.stopCoverTilt();
|
||||
this.hass.callService("cover", "stop_cover_tilt", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@@ -38,6 +38,7 @@ export class HaDialog extends Dialog {
|
||||
.mdc-dialog {
|
||||
--mdc-dialog-scroll-divider-color: var(--divider-color);
|
||||
z-index: var(--dialog-z-index, 7);
|
||||
-webkit-backdrop-filter: var(--dialog-backdrop-filter, none);
|
||||
backdrop-filter: var(--dialog-backdrop-filter, none);
|
||||
}
|
||||
.mdc-dialog__actions {
|
||||
@@ -71,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;
|
||||
|
82
src/components/ha-faded.ts
Normal file
82
src/components/ha-faded.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
|
||||
@customElement("ha-faded")
|
||||
class HaFaded extends LitElement {
|
||||
@property({ type: Number, attribute: "faded-height" })
|
||||
public fadedHeight = 102;
|
||||
|
||||
@state() _contentShown = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div
|
||||
class="container ${classMap({ faded: !this._contentShown })}"
|
||||
style=${!this._contentShown ? `max-height: ${this.fadedHeight}px` : ""}
|
||||
@click=${this._showContent}
|
||||
>
|
||||
<slot
|
||||
@iron-resize=${
|
||||
// ha-markdown-element fire this when render is complete
|
||||
this._setShowContent
|
||||
}
|
||||
></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
get _slottedHeight(): number {
|
||||
return (
|
||||
(
|
||||
this.shadowRoot!.querySelector(".container")
|
||||
?.firstElementChild as HTMLSlotElement
|
||||
)
|
||||
.assignedElements()
|
||||
.reduce(
|
||||
(partial, element) => partial + (element as HTMLElement).offsetHeight,
|
||||
0
|
||||
) || 0
|
||||
);
|
||||
}
|
||||
|
||||
private _setShowContent() {
|
||||
const height = this._slottedHeight;
|
||||
this._contentShown = height !== 0 && height <= this.fadedHeight + 50;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._setShowContent();
|
||||
}
|
||||
|
||||
private _showContent(): void {
|
||||
this._contentShown = true;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
.container {
|
||||
display: block;
|
||||
height: auto;
|
||||
cursor: default;
|
||||
}
|
||||
.faded {
|
||||
cursor: pointer;
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to bottom,
|
||||
black 25%,
|
||||
transparent 100%
|
||||
);
|
||||
mask-image: linear-gradient(to bottom, black 25%, transparent 100%);
|
||||
overflow-y: hidden;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-faded": HaFaded;
|
||||
}
|
||||
}
|
@@ -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>
|
||||
`
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -36,7 +36,11 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if ("valueMin" in this.schema && "valueMax" in this.schema) {
|
||||
if (
|
||||
this.schema.valueMin !== undefined &&
|
||||
this.schema.valueMax !== undefined &&
|
||||
this.schema.valueMax - this.schema.valueMin < 256
|
||||
) {
|
||||
return html`
|
||||
<div>
|
||||
${this.label}
|
||||
@@ -96,10 +100,15 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
}
|
||||
|
||||
if (this.schema.optional) {
|
||||
return 0;
|
||||
return this.schema.valueMin || 0;
|
||||
}
|
||||
|
||||
return this.schema.description?.suggested_value || this.schema.default || 0;
|
||||
return (
|
||||
this.schema.description?.suggested_value ||
|
||||
this.schema.default ||
|
||||
this.schema.valueMin ||
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
private _handleCheckboxChange(ev: Event) {
|
||||
|
@@ -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]) => {
|
||||
|
@@ -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 {
|
||||
|
@@ -5,6 +5,22 @@ import { customElement } from "lit/decorators";
|
||||
@customElement("ha-formfield")
|
||||
// @ts-expect-error
|
||||
export class HaFormfield extends Formfield {
|
||||
protected _labelClick() {
|
||||
const input = this.input;
|
||||
if (input) {
|
||||
input.focus();
|
||||
switch (input.tagName) {
|
||||
case "HA-CHECKBOX":
|
||||
case "HA-RADIO":
|
||||
(input as any).checked = !(input as any).checked;
|
||||
break;
|
||||
default:
|
||||
input.click();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static get styles(): CSSResultGroup {
|
||||
return [
|
||||
Formfield.styles,
|
||||
|
@@ -7,10 +7,11 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
import { getExternalConfig } from "../external_app/external_config";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-alert";
|
||||
|
||||
type HlsLite = Omit<
|
||||
HlsType,
|
||||
@@ -41,6 +42,8 @@ class HaHLSPlayer extends LitElement {
|
||||
// don't cache this, as we remove it on disconnects
|
||||
@query("video") private _videoEl!: HTMLVideoElement;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _hlsPolyfillInstance?: HlsLite;
|
||||
|
||||
private _exoPlayer = false;
|
||||
@@ -58,6 +61,9 @@ class HaHLSPlayer extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (this._error) {
|
||||
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
|
||||
}
|
||||
return html`
|
||||
<video
|
||||
?autoplay=${this.autoPlay}
|
||||
@@ -90,6 +96,8 @@ class HaHLSPlayer extends LitElement {
|
||||
}
|
||||
|
||||
private async _startHls(): Promise<void> {
|
||||
this._error = undefined;
|
||||
|
||||
const videoEl = this._videoEl;
|
||||
const useExoPlayerPromise = this._getUseExoPlayer();
|
||||
const masterPlaylistPromise = fetch(this.url);
|
||||
@@ -109,7 +117,7 @@ class HaHLSPlayer extends LitElement {
|
||||
}
|
||||
|
||||
if (!hlsSupported) {
|
||||
videoEl.innerHTML = this.hass.localize(
|
||||
this._error = this.hass.localize(
|
||||
"ui.components.media-browser.video_not_supported"
|
||||
);
|
||||
return;
|
||||
@@ -196,6 +204,44 @@ class HaHLSPlayer extends LitElement {
|
||||
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
|
||||
hls.loadSource(url);
|
||||
});
|
||||
hls.on(Hls.Events.ERROR, (_, data: any) => {
|
||||
if (!data.fatal) {
|
||||
return;
|
||||
}
|
||||
if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
|
||||
switch (data.details) {
|
||||
case Hls.ErrorDetails.MANIFEST_LOAD_ERROR: {
|
||||
let error = "Error starting stream, see logs for details";
|
||||
if (
|
||||
data.response !== undefined &&
|
||||
data.response.code !== undefined
|
||||
) {
|
||||
if (data.response.code >= 500) {
|
||||
error += " (Server failure)";
|
||||
} else if (data.response.code >= 400) {
|
||||
error += " (Stream never started)";
|
||||
} else {
|
||||
error += " (" + data.response.code + ")";
|
||||
}
|
||||
}
|
||||
this._error = error;
|
||||
return;
|
||||
}
|
||||
case Hls.ErrorDetails.MANIFEST_LOAD_TIMEOUT:
|
||||
this._error = "Timeout while starting stream";
|
||||
return;
|
||||
default:
|
||||
this._error = "Unknown stream network error (" + data.details + ")";
|
||||
return;
|
||||
}
|
||||
this._error = "Error with media stream contents (" + data.details + ")";
|
||||
} else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
|
||||
this._error = "Error with media stream contents (" + data.details + ")";
|
||||
} else {
|
||||
this._error =
|
||||
"Unknown error with stream (" + data.type + ", " + data.details + ")";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) {
|
||||
@@ -229,6 +275,12 @@ class HaHLSPlayer extends LitElement {
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
max-height: var(--video-max-height, calc(100vh - 97px));
|
||||
}
|
||||
|
||||
ha-alert {
|
||||
display: block;
|
||||
padding: 100px 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "./ha-button-menu";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import "@material/mwc-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import "./ha-button-menu";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
export interface IconOverflowMenuItem {
|
||||
[key: string]: any;
|
||||
@@ -37,13 +37,11 @@ export class HaIconOverflowMenu extends LitElement {
|
||||
corner="BOTTOM_START"
|
||||
absolute
|
||||
>
|
||||
<mwc-icon-button
|
||||
.title=${this.hass.localize("ui.common.menu")}
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.overflow_menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
slot="trigger"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
></ha-icon-button>
|
||||
|
||||
${this.items.map(
|
||||
(item) => html`
|
||||
|
@@ -7,14 +7,19 @@ import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { ComboBoxLitRenderer, comboBoxRenderer } from "lit-vaadin-helpers";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { customIcons } from "../data/custom_icons";
|
||||
import { PolymerChangedEvent } from "../polymer-types";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
|
||||
let mdiIconList: string[] = [];
|
||||
type IconItem = {
|
||||
icon: string;
|
||||
keywords: string[];
|
||||
};
|
||||
let iconItems: IconItem[] = [];
|
||||
|
||||
// eslint-disable-next-line lit/prefer-static-styles
|
||||
const rowRenderer: ComboBoxLitRenderer<string> = (item) => html`<style>
|
||||
const rowRenderer: ComboBoxLitRenderer<IconItem> = (item) => html`<style>
|
||||
paper-icon-item {
|
||||
padding: 0;
|
||||
margin: -8px;
|
||||
@@ -37,8 +42,8 @@ const rowRenderer: ComboBoxLitRenderer<string> = (item) => html`<style>
|
||||
|
||||
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
|
||||
<paper-icon-item>
|
||||
<ha-icon .icon=${item} slot="item-icon"></ha-icon>
|
||||
<paper-item-body>${item}</paper-item-body>
|
||||
<ha-icon .icon=${item.icon} slot="item-icon"></ha-icon>
|
||||
<paper-item-body>${item.icon}</paper-item-body>
|
||||
</paper-icon-item>`;
|
||||
|
||||
@customElement("ha-icon-picker")
|
||||
@@ -55,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;
|
||||
@@ -66,7 +73,7 @@ export class HaIconPicker extends LitElement {
|
||||
item-label-path="icon"
|
||||
.value=${this._value}
|
||||
allow-custom-value
|
||||
.filteredItems=${mdiIconList}
|
||||
.filteredItems=${iconItems}
|
||||
${comboBoxRenderer(rowRenderer)}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._valueChanged}
|
||||
@@ -81,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`
|
||||
@@ -105,10 +114,38 @@ export class HaIconPicker extends LitElement {
|
||||
|
||||
private async _openedChanged(ev: PolymerChangedEvent<boolean>) {
|
||||
this._opened = ev.detail.value;
|
||||
if (this._opened && !mdiIconList.length) {
|
||||
if (this._opened && !iconItems.length) {
|
||||
const iconList = await import("../../build/mdi/iconList.json");
|
||||
mdiIconList = iconList.default.map((icon) => `mdi:${icon}`);
|
||||
(this.comboBox as any).filteredItems = mdiIconList;
|
||||
|
||||
iconItems = iconList.default.map((icon) => ({
|
||||
icon: `mdi:${icon.name}`,
|
||||
keywords: icon.keywords,
|
||||
}));
|
||||
|
||||
(this.comboBox as any).filteredItems = iconItems;
|
||||
|
||||
Object.keys(customIcons).forEach((iconSet) => {
|
||||
this._loadCustomIconItems(iconSet);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadCustomIconItems(iconsetPrefix: string) {
|
||||
try {
|
||||
const getIconList = customIcons[iconsetPrefix].getIconList;
|
||||
if (typeof getIconList !== "function") {
|
||||
return;
|
||||
}
|
||||
const iconList = await getIconList();
|
||||
const customIconItems = iconList.map((icon) => ({
|
||||
icon: `${iconsetPrefix}:${icon.name}`,
|
||||
keywords: icon.keywords ?? [],
|
||||
}));
|
||||
iconItems.push(...customIconItems);
|
||||
(this.comboBox as any).filteredItems = iconItems;
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line
|
||||
console.warn(`Unable to load icon list for ${iconsetPrefix} iconset`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,16 +170,30 @@ export class HaIconPicker extends LitElement {
|
||||
const filterString = ev.detail.value.toLowerCase();
|
||||
const characterCount = filterString.length;
|
||||
if (characterCount >= 2) {
|
||||
const filteredItems = mdiIconList.filter((icon) =>
|
||||
icon.includes(filterString)
|
||||
);
|
||||
const filteredItems: IconItem[] = [];
|
||||
const filteredItemsByKeywords: IconItem[] = [];
|
||||
|
||||
iconItems.forEach((item) => {
|
||||
if (item.icon.includes(filterString)) {
|
||||
filteredItems.push(item);
|
||||
return;
|
||||
}
|
||||
if (item.keywords.some((t) => t.includes(filterString))) {
|
||||
filteredItemsByKeywords.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
filteredItems.push(...filteredItemsByKeywords);
|
||||
|
||||
if (filteredItems.length > 0) {
|
||||
(this.comboBox as any).filteredItems = filteredItems;
|
||||
} else {
|
||||
(this.comboBox as any).filteredItems = [filterString];
|
||||
(this.comboBox as any).filteredItems = [
|
||||
{ icon: filterString, keywords: [] },
|
||||
];
|
||||
}
|
||||
} else {
|
||||
(this.comboBox as any).filteredItems = mdiIconList;
|
||||
(this.comboBox as any).filteredItems = iconItems;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -10,7 +10,7 @@ import {
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import { CustomIcon, customIconsets } from "../data/custom_iconsets";
|
||||
import { CustomIcon, customIcons } from "../data/custom_icons";
|
||||
import {
|
||||
checkCacheVersion,
|
||||
Chunks,
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -356,7 +48,7 @@ export class HaIcon extends LitElement {
|
||||
|
||||
@state() private _path?: string;
|
||||
|
||||
@state() private _viewBox?;
|
||||
@state() private _viewBox?: string;
|
||||
|
||||
@state() private _legacy = false;
|
||||
|
||||
@@ -386,6 +78,7 @@ export class HaIcon extends LitElement {
|
||||
if (!this.icon) {
|
||||
return;
|
||||
}
|
||||
const requestedIcon = this.icon;
|
||||
const [iconPrefix, origIconName] = this.icon.split(":", 2);
|
||||
|
||||
let iconName = origIconName;
|
||||
@@ -395,10 +88,10 @@ export class HaIcon extends LitElement {
|
||||
}
|
||||
|
||||
if (!MDI_PREFIXES.includes(iconPrefix)) {
|
||||
if (iconPrefix in customIconsets) {
|
||||
const customIconset = customIconsets[iconPrefix];
|
||||
if (customIconset) {
|
||||
this._setCustomPath(customIconset(iconName));
|
||||
if (iconPrefix in customIcons) {
|
||||
const customIcon = customIcons[iconPrefix];
|
||||
if (customIcon && typeof customIcon.getIcon === "function") {
|
||||
this._setCustomPath(customIcon.getIcon(iconName), requestedIcon);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -441,14 +134,16 @@ export class HaIcon extends LitElement {
|
||||
}
|
||||
|
||||
if (databaseIcon) {
|
||||
this._path = databaseIcon;
|
||||
if (this.icon === requestedIcon) {
|
||||
this._path = databaseIcon;
|
||||
}
|
||||
cachedIcons[iconName] = databaseIcon;
|
||||
return;
|
||||
}
|
||||
const chunk = findIconChunk(iconName);
|
||||
|
||||
if (chunk in chunks) {
|
||||
this._setPath(chunks[chunk], iconName);
|
||||
this._setPath(chunks[chunk], iconName, requestedIcon);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -456,19 +151,31 @@ export class HaIcon extends LitElement {
|
||||
response.json()
|
||||
);
|
||||
chunks[chunk] = iconPromise;
|
||||
this._setPath(iconPromise, iconName);
|
||||
this._setPath(iconPromise, iconName, requestedIcon);
|
||||
debouncedWriteCache();
|
||||
}
|
||||
|
||||
private async _setCustomPath(promise: Promise<CustomIcon>) {
|
||||
private async _setCustomPath(
|
||||
promise: Promise<CustomIcon>,
|
||||
requestedIcon: string
|
||||
) {
|
||||
const icon = await promise;
|
||||
if (this.icon !== requestedIcon) {
|
||||
return;
|
||||
}
|
||||
this._path = icon.path;
|
||||
this._viewBox = icon.viewBox;
|
||||
}
|
||||
|
||||
private async _setPath(promise: Promise<Icons>, iconName: string) {
|
||||
private async _setPath(
|
||||
promise: Promise<Icons>,
|
||||
iconName: string,
|
||||
requestedIcon: string
|
||||
) {
|
||||
const iconPack = await promise;
|
||||
this._path = iconPack[iconName];
|
||||
if (this.icon === requestedIcon) {
|
||||
this._path = iconPack[iconName];
|
||||
}
|
||||
cachedIcons[iconName] = iconPack[iconName];
|
||||
}
|
||||
|
||||
|
39
src/components/ha-logo-svg.ts
Normal file
39
src/components/ha-logo-svg.ts
Normal file
File diff suppressed because one or more lines are too long
@@ -24,7 +24,7 @@ class HaMarkdownElement extends ReactiveElement {
|
||||
|
||||
private async _render() {
|
||||
this.innerHTML = await renderMarkdown(
|
||||
this.content,
|
||||
String(this.content),
|
||||
{
|
||||
breaks: this.breaks,
|
||||
gfm: true,
|
||||
|
162
src/components/ha-qr-scanner.ts
Normal file
162
src/components/ha-qr-scanner.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import "@material/mwc-select/mwc-select";
|
||||
import type { Select } from "@material/mwc-select/mwc-select";
|
||||
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import type QrScanner from "qr-scanner";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { LocalizeFunc } from "../common/translations/localize";
|
||||
import "./ha-alert";
|
||||
|
||||
@customElement("ha-qr-scanner")
|
||||
class HaQrScanner extends LitElement {
|
||||
@property() localize!: LocalizeFunc;
|
||||
|
||||
@state() private _cameras?: QrScanner.Camera[];
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _qrScanner?: QrScanner;
|
||||
|
||||
private _qrNotFoundCount = 0;
|
||||
|
||||
@query("video", true) private _video!: HTMLVideoElement;
|
||||
|
||||
@query("#canvas-container", true) private _canvasContainer!: HTMLDivElement;
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._qrNotFoundCount = 0;
|
||||
if (this._qrScanner) {
|
||||
this._qrScanner.stop();
|
||||
this._qrScanner.destroy();
|
||||
this._qrScanner = undefined;
|
||||
}
|
||||
while (this._canvasContainer.lastChild) {
|
||||
this._canvasContainer.removeChild(this._canvasContainer.lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated && navigator.mediaDevices) {
|
||||
this._loadQrScanner();
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
if (navigator.mediaDevices) {
|
||||
this._loadQrScanner();
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("_error") && this._error) {
|
||||
fireEvent(this, "qr-code-error", { message: this._error });
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`${this._cameras && this._cameras.length > 1
|
||||
? html`<mwc-select
|
||||
.label=${this.localize(
|
||||
"ui.panel.config.zwave_js.add_node.select_camera"
|
||||
)}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@closed=${stopPropagation}
|
||||
@selected=${this._cameraChanged}
|
||||
>
|
||||
${this._cameras!.map(
|
||||
(camera) => html`
|
||||
<mwc-list-item .value=${camera.id}>${camera.label}</mwc-list-item>
|
||||
`
|
||||
)}
|
||||
</mwc-select>`
|
||||
: ""}
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
${navigator.mediaDevices
|
||||
? html`<video></video>
|
||||
<div id="canvas-container"></div>`
|
||||
: html`<ha-alert alert-type="warning"
|
||||
>${!window.isSecureContext
|
||||
? "You can only use your camera to scan a QR core when using HTTPS."
|
||||
: "Your browser doesn't support QR scanning."}</ha-alert
|
||||
>`}`;
|
||||
}
|
||||
|
||||
private async _loadQrScanner() {
|
||||
const QrScanner = (await import("qr-scanner")).default;
|
||||
if (!(await QrScanner.hasCamera())) {
|
||||
this._error = "No camera found";
|
||||
return;
|
||||
}
|
||||
QrScanner.WORKER_PATH = "/static/js/qr-scanner-worker.min.js";
|
||||
this._listCameras(QrScanner);
|
||||
this._qrScanner = new QrScanner(
|
||||
this._video,
|
||||
this._qrCodeScanned,
|
||||
this._qrCodeError
|
||||
);
|
||||
// @ts-ignore
|
||||
const canvas = this._qrScanner.$canvas;
|
||||
this._canvasContainer.appendChild(canvas);
|
||||
canvas.style.display = "block";
|
||||
try {
|
||||
await this._qrScanner.start();
|
||||
} catch (err: any) {
|
||||
this._error = err;
|
||||
}
|
||||
}
|
||||
|
||||
private async _listCameras(qrScanner: typeof QrScanner): Promise<void> {
|
||||
this._cameras = await qrScanner.listCameras(true);
|
||||
}
|
||||
|
||||
private _qrCodeError = (err: any) => {
|
||||
if (err === "No QR code found") {
|
||||
this._qrNotFoundCount++;
|
||||
if (this._qrNotFoundCount === 250) {
|
||||
this._error = err;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this._error = err.message || err;
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(err);
|
||||
};
|
||||
|
||||
private _qrCodeScanned = async (qrCodeString: string): Promise<void> => {
|
||||
this._qrNotFoundCount = 0;
|
||||
fireEvent(this, "qr-code-scanned", { value: qrCodeString });
|
||||
};
|
||||
|
||||
private _cameraChanged(ev: CustomEvent): void {
|
||||
this._qrScanner?.setCamera((ev.target as Select).value);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
canvas {
|
||||
width: 100%;
|
||||
}
|
||||
mwc-select {
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"qr-code-scanned": { value: string };
|
||||
"qr-code-error": { message: string };
|
||||
}
|
||||
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-qr-scanner": HaQrScanner;
|
||||
}
|
||||
}
|
@@ -3,13 +3,11 @@ import {
|
||||
mdiBell,
|
||||
mdiCalendar,
|
||||
mdiCart,
|
||||
mdiCellphoneCog,
|
||||
mdiChartBox,
|
||||
mdiClose,
|
||||
mdiCog,
|
||||
mdiFormatListBulletedType,
|
||||
mdiHammer,
|
||||
mdiHomeAssistant,
|
||||
mdiLightningBolt,
|
||||
mdiMenu,
|
||||
mdiMenuOpen,
|
||||
@@ -45,20 +43,16 @@ import {
|
||||
PersistentNotification,
|
||||
subscribeNotifications,
|
||||
} from "../data/persistent_notification";
|
||||
import {
|
||||
ExternalConfig,
|
||||
getExternalConfig,
|
||||
} 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";
|
||||
import "./ha-svg-icon";
|
||||
import "./user/ha-user-badge";
|
||||
|
||||
const SHOW_AFTER_SPACER = ["config", "developer-tools", "hassio"];
|
||||
const SHOW_AFTER_SPACER = ["config", "developer-tools"];
|
||||
|
||||
const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body;
|
||||
|
||||
@@ -68,7 +62,6 @@ const SORT_VALUE_URL_PATHS = {
|
||||
logbook: 3,
|
||||
history: 4,
|
||||
"developer-tools": 9,
|
||||
hassio: 10,
|
||||
config: 11,
|
||||
};
|
||||
|
||||
@@ -77,7 +70,6 @@ const PANEL_ICONS = {
|
||||
config: mdiCog,
|
||||
"developer-tools": mdiHammer,
|
||||
energy: mdiLightningBolt,
|
||||
hassio: mdiHomeAssistant,
|
||||
history: mdiChartBox,
|
||||
logbook: mdiFormatListBulletedType,
|
||||
lovelace: mdiViewDashboard,
|
||||
@@ -189,12 +181,12 @@ 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;
|
||||
|
||||
@state() private _externalConfig?: ExternalConfig;
|
||||
|
||||
@state() private _notifications?: PersistentNotification[];
|
||||
|
||||
@state() private _renderEmptySortable = false;
|
||||
@@ -241,7 +233,6 @@ class HaSidebar extends LitElement {
|
||||
changedProps.has("expanded") ||
|
||||
changedProps.has("narrow") ||
|
||||
changedProps.has("alwaysExpand") ||
|
||||
changedProps.has("_externalConfig") ||
|
||||
changedProps.has("_notifications") ||
|
||||
changedProps.has("editMode") ||
|
||||
changedProps.has("_renderEmptySortable") ||
|
||||
@@ -272,11 +263,6 @@ class HaSidebar extends LitElement {
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
if (this.hass && this.hass.auth.external) {
|
||||
getExternalConfig(this.hass.auth.external).then((conf) => {
|
||||
this._externalConfig = conf;
|
||||
});
|
||||
}
|
||||
subscribeNotifications(this.hass.connection, (notifications) => {
|
||||
this._notifications = notifications;
|
||||
});
|
||||
@@ -351,12 +337,17 @@ class HaSidebar extends LitElement {
|
||||
this._hiddenPanels
|
||||
);
|
||||
|
||||
// Show the supervisor as beeing part of configuration
|
||||
const selectedPanel = this.route.path?.startsWith("/hassio/")
|
||||
? "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}
|
||||
@@ -367,7 +358,6 @@ class HaSidebar extends LitElement {
|
||||
: this._renderPanels(beforeSpacer)}
|
||||
${this._renderSpacer()}
|
||||
${this._renderPanels(afterSpacer)}
|
||||
${this._renderExternalConfiguration()}
|
||||
</paper-listbox>
|
||||
`;
|
||||
}
|
||||
@@ -552,34 +542,6 @@ class HaSidebar extends LitElement {
|
||||
</a>`;
|
||||
}
|
||||
|
||||
private _renderExternalConfiguration() {
|
||||
return html`${this._externalConfig && this._externalConfig.hasSettingsScreen
|
||||
? html`
|
||||
<a
|
||||
aria-role="option"
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.sidebar.external_app_configuration"
|
||||
)}
|
||||
href="#external-app-configuration"
|
||||
tabindex="-1"
|
||||
@click=${this._handleExternalAppConfiguration}
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<paper-icon-item>
|
||||
<ha-svg-icon
|
||||
slot="item-icon"
|
||||
.path=${mdiCellphoneCog}
|
||||
></ha-svg-icon>
|
||||
<span class="item-text">
|
||||
${this.hass.localize("ui.sidebar.external_app_configuration")}
|
||||
</span>
|
||||
</paper-icon-item>
|
||||
</a>
|
||||
`
|
||||
: ""}`;
|
||||
}
|
||||
|
||||
private get _tooltip() {
|
||||
return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement;
|
||||
}
|
||||
@@ -751,13 +713,6 @@ class HaSidebar extends LitElement {
|
||||
fireEvent(this, "hass-show-notifications");
|
||||
}
|
||||
|
||||
private _handleExternalAppConfiguration(ev: Event) {
|
||||
ev.preventDefault();
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "config_screen/show",
|
||||
});
|
||||
}
|
||||
|
||||
private _toggleSidebar(ev: CustomEvent) {
|
||||
if (ev.detail.action !== "tap") {
|
||||
return;
|
||||
|
@@ -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))
|
||||
|
@@ -27,7 +27,7 @@ export class HaTimeInput extends LitElement {
|
||||
const parts = this.value?.split(":") || [];
|
||||
let hours = parts[0];
|
||||
const numberHours = Number(parts[0]);
|
||||
if (numberHours && useAMPM && numberHours > 12) {
|
||||
if (numberHours && useAMPM && numberHours > 12 && numberHours < 24) {
|
||||
hours = String(numberHours - 12).padStart(2, "0");
|
||||
}
|
||||
if (useAMPM && numberHours === 0) {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user