Merge pull request #7168 from home-assistant/dev
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 186 KiB After Width: | Height: | Size: 186 KiB |
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
@ -212,13 +212,8 @@
|
||||
Chromecast is a technology developed by Google, and is available on:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Google Chrome (all platforms except on iOS)</li>
|
||||
<li>
|
||||
Microsoft Edge (all platforms,
|
||||
<a href="https://www.microsoftedgeinsider.com" target="_blank"
|
||||
>dev and canary builds only</a
|
||||
>)
|
||||
</li>
|
||||
<li>Google Chrome (all platforms except iOS)</li>
|
||||
<li>Microsoft Edge (all platforms)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
@ -6,13 +6,58 @@ import { castContext } from "./cast_context";
|
||||
import { HcMain } from "./layout/hc-main";
|
||||
import { ReceivedMessage } from "./types";
|
||||
|
||||
const controller = new HcMain();
|
||||
document.body.append(controller);
|
||||
const lovelaceController = new HcMain();
|
||||
document.body.append(lovelaceController);
|
||||
|
||||
const mediaPlayer = document.createElement("cast-media-player");
|
||||
mediaPlayer.style.display = "none";
|
||||
document.body.append(mediaPlayer);
|
||||
const playerStylesAdded = false;
|
||||
|
||||
let controls: HTMLElement | null;
|
||||
|
||||
const setTouchControlsVisibility = (visible: boolean) => {
|
||||
if (!castContext.getDeviceCapabilities().touch_input_supported) {
|
||||
return;
|
||||
}
|
||||
controls =
|
||||
controls ||
|
||||
(document.body.querySelector("touch-controls") as HTMLElement | null);
|
||||
if (controls) {
|
||||
controls.style.display = visible ? "initial" : "none";
|
||||
}
|
||||
};
|
||||
|
||||
const showLovelaceController = () => {
|
||||
mediaPlayer.style.display = "none";
|
||||
lovelaceController.style.display = "initial";
|
||||
document.body.setAttribute("style", "overflow-y: auto !important");
|
||||
setTouchControlsVisibility(false);
|
||||
};
|
||||
|
||||
const showMediaPlayer = () => {
|
||||
lovelaceController.style.display = "none";
|
||||
mediaPlayer.style.display = "initial";
|
||||
document.body.removeAttribute("style");
|
||||
setTouchControlsVisibility(true);
|
||||
if (!playerStylesAdded) {
|
||||
const style = document.createElement("style");
|
||||
style.innerHTML = `
|
||||
body {
|
||||
--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;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
};
|
||||
|
||||
const options = new cast.framework.CastReceiverOptions();
|
||||
options.disableIdleTimeout = true;
|
||||
options.customNamespaces = {
|
||||
// @ts-ignore
|
||||
[CAST_NS]: cast.framework.system.MessageType.JSON,
|
||||
};
|
||||
|
||||
@ -30,13 +75,61 @@ options.uiConfig = new cast.framework.ui.UiConfig();
|
||||
// @ts-ignore
|
||||
options.uiConfig.touchScreenOptimizedApp = true;
|
||||
|
||||
castContext.setInactivityTimeout(86400); // 1 day
|
||||
|
||||
castContext.addCustomMessageListener(
|
||||
CAST_NS,
|
||||
// @ts-ignore
|
||||
(ev: ReceivedMessage<HassMessage>) => {
|
||||
// We received a show Lovelace command, stop media from playing, hide media player and show Lovelace controller
|
||||
if (
|
||||
playerManager.getPlayerState() !==
|
||||
cast.framework.messages.PlayerState.IDLE
|
||||
) {
|
||||
playerManager.stop();
|
||||
} else {
|
||||
showLovelaceController();
|
||||
}
|
||||
const msg = ev.data;
|
||||
msg.senderId = ev.senderId;
|
||||
controller.processIncomingMessage(msg);
|
||||
lovelaceController.processIncomingMessage(msg);
|
||||
}
|
||||
);
|
||||
|
||||
const playerManager = castContext.getPlayerManager();
|
||||
|
||||
playerManager.setMessageInterceptor(
|
||||
cast.framework.messages.MessageType.LOAD,
|
||||
(loadRequestData) => {
|
||||
// We received a play media command, hide Lovelace and show media player
|
||||
showMediaPlayer();
|
||||
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;
|
||||
}
|
||||
);
|
||||
|
||||
playerManager.addEventListener(
|
||||
cast.framework.events.EventType.MEDIA_STATUS,
|
||||
(event) => {
|
||||
if (
|
||||
event.mediaStatus?.playerState ===
|
||||
cast.framework.messages.PlayerState.IDLE &&
|
||||
event.mediaStatus?.idleReason &&
|
||||
event.mediaStatus?.idleReason !==
|
||||
cast.framework.messages.IdleReason.INTERRUPTED
|
||||
) {
|
||||
// media finished or stopped, return to default Lovelace
|
||||
showLovelaceController();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -9,7 +9,6 @@ import {
|
||||
} from "lit-element";
|
||||
import { LovelaceConfig } from "../../../../src/data/lovelace";
|
||||
import { Lovelace } from "../../../../src/panels/lovelace/types";
|
||||
import "../../../../src/panels/lovelace/views/hui-panel-view";
|
||||
import "../../../../src/panels/lovelace/views/hui-view";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import "./hc-launch-screen";
|
||||
@ -45,22 +44,14 @@ class HcLovelace extends LitElement {
|
||||
deleteConfig: async () => undefined,
|
||||
setEditMode: () => undefined,
|
||||
};
|
||||
return this.lovelaceConfig.views[index].panel
|
||||
? html`
|
||||
<hui-panel-view
|
||||
.hass=${this.hass}
|
||||
.lovelace=${lovelace}
|
||||
.config=${this.lovelaceConfig.views[index]}
|
||||
></hui-panel-view>
|
||||
`
|
||||
: html`
|
||||
<hui-view
|
||||
.hass=${this.hass}
|
||||
.lovelace=${lovelace}
|
||||
.index=${index}
|
||||
columns="2"
|
||||
></hui-view>
|
||||
`;
|
||||
return html`
|
||||
<hui-view
|
||||
.hass=${this.hass}
|
||||
.lovelace=${lovelace}
|
||||
.index=${index}
|
||||
columns="2"
|
||||
></hui-view>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps) {
|
||||
|
@ -216,9 +216,7 @@ export class HcMain extends HassElement {
|
||||
}
|
||||
this._showDemo = false;
|
||||
this._lovelacePath = msg.viewPath;
|
||||
if (castContext.getDeviceCapabilities().touch_input_supported) {
|
||||
this._breakFree();
|
||||
}
|
||||
|
||||
this._sendStatus();
|
||||
}
|
||||
|
||||
@ -241,9 +239,6 @@ export class HcMain extends HassElement {
|
||||
this._showDemo = true;
|
||||
this._lovelacePath = "overview";
|
||||
this._sendStatus();
|
||||
if (castContext.getDeviceCapabilities().touch_input_supported) {
|
||||
this._breakFree();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -264,14 +259,6 @@ export class HcMain extends HassElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _breakFree() {
|
||||
const controls = document.body.querySelector("touch-controls");
|
||||
if (controls) {
|
||||
controls.remove();
|
||||
}
|
||||
document.body.setAttribute("style", "overflow-y: auto !important");
|
||||
}
|
||||
|
||||
private sendMessage(senderId: string, response: any) {
|
||||
castContext.sendCustomMessage(CAST_NS, senderId, response);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 8.7 KiB |
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 8.7 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 767 B After Width: | Height: | Size: 532 B |
Before Width: | Height: | Size: 803 B After Width: | Height: | Size: 535 B |
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 94 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 375 B After Width: | Height: | Size: 184 B |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 110 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 110 KiB |
@ -1,7 +1,7 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { property, PropertyValues, UpdatingElement } from "lit-element";
|
||||
import dynamicContentUpdater from "../../../src/common/dom/dynamic_content_updater";
|
||||
import { stateMoreInfoType } from "../../../src/common/entity/state_more_info_type";
|
||||
import { stateMoreInfoType } from "../../../src/dialogs/more-info/state_more_info_control";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-alarm_control_panel";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-automation";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-camera";
|
||||
|
@ -175,7 +175,7 @@ class HassioAddonConfig extends LitElement {
|
||||
}
|
||||
iron-autogrow-textarea {
|
||||
width: 100%;
|
||||
font-family: monospace;
|
||||
font-family: var(--code-font-family, monospace);
|
||||
}
|
||||
.syntaxerror {
|
||||
color: var(--error-color);
|
||||
|
81
hassio/src/components/hassio-upload-snapshot.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import "../../../src/components/ha-file-upload";
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
import { mdiFolderUpload } from "@mdi/js";
|
||||
import "@polymer/iron-input/iron-input";
|
||||
import "@polymer/paper-input/paper-input-container";
|
||||
import {
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/ha-circular-progress";
|
||||
import "../../../src/components/ha-svg-icon";
|
||||
import {
|
||||
HassioSnapshot,
|
||||
uploadSnapshot,
|
||||
} from "../../../src/data/hassio/snapshot";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"snapshot-uploaded": { snapshot: HassioSnapshot };
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("hassio-upload-snapshot")
|
||||
export class HassioUploadSnapshot extends LitElement {
|
||||
public hass!: HomeAssistant;
|
||||
|
||||
@internalProperty() public value: string | null = null;
|
||||
|
||||
@internalProperty() private _uploading = false;
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<ha-file-upload
|
||||
.uploading=${this._uploading}
|
||||
.icon=${mdiFolderUpload}
|
||||
accept="application/x-tar"
|
||||
label="Upload snapshot"
|
||||
@file-picked=${this._uploadFile}
|
||||
auto-open-file-dialog
|
||||
></ha-file-upload>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _uploadFile(ev) {
|
||||
const file = ev.detail.files[0];
|
||||
|
||||
if (!["application/x-tar"].includes(file.type)) {
|
||||
showAlertDialog(this, {
|
||||
title: "Unsupported file format",
|
||||
text: "Please choose a Home Assistant snapshot file (.tar)",
|
||||
confirmText: "ok",
|
||||
});
|
||||
return;
|
||||
}
|
||||
this._uploading = true;
|
||||
try {
|
||||
const snapshot = await uploadSnapshot(this.hass, file);
|
||||
fireEvent(this, "snapshot-uploaded", { snapshot: snapshot.data });
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Upload failed",
|
||||
text: err.toString(),
|
||||
confirmText: "ok",
|
||||
});
|
||||
} finally {
|
||||
this._uploading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-upload-snapshot": HassioUploadSnapshot;
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ import {
|
||||
} from "lit-element";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { navigate } from "../../../src/common/navigate";
|
||||
import { compare } from "../../../src/common/string/compare";
|
||||
import "../../../src/components/ha-card";
|
||||
import { HassioAddonInfo } from "../../../src/data/hassio/addon";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
@ -21,25 +22,27 @@ import { hassioStyle } from "../resources/hassio-style";
|
||||
class HassioAddons extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public addons?: HassioAddonInfo[];
|
||||
@property({ attribute: false }) public addons?: HassioAddonInfo[];
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="content">
|
||||
<h1>Add-ons</h1>
|
||||
<div class="card-group">
|
||||
${!this.addons
|
||||
${!this.addons?.length
|
||||
? html`
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
You don't have any add-ons installed yet. Head over to
|
||||
<a href="#" @click=${this._openStore}>the add-on store</a>
|
||||
<button class="link" @click=${this._openStore}>
|
||||
the add-on store
|
||||
</button>
|
||||
to get started!
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
: this.addons
|
||||
.sort((a, b) => (a.name > b.name ? 1 : -1))
|
||||
.sort((a, b) => compare(a.name, b.name))
|
||||
.map(
|
||||
(addon) => html`
|
||||
<ha-card .addon=${addon} @click=${this._addonTapped}>
|
||||
|
107
hassio/src/dialogs/snapshot/dialog-hassio-snapshot-upload.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/ha-header-bar";
|
||||
import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import "../../components/hassio-upload-snapshot";
|
||||
import { HassioSnapshotUploadDialogParams } from "./show-dialog-snapshot-upload";
|
||||
|
||||
@customElement("dialog-hassio-snapshot-upload")
|
||||
export class DialogHassioSnapshotUpload extends LitElement
|
||||
implements HassDialog {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@internalProperty() private _params?: HassioSnapshotUploadDialogParams;
|
||||
|
||||
public async showDialog(
|
||||
params: HassioSnapshotUploadDialogParams
|
||||
): Promise<void> {
|
||||
this._params = params;
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
if (this._params && !this._params.onboarding) {
|
||||
if (this._params.reloadSnapshot) {
|
||||
this._params.reloadSnapshot();
|
||||
}
|
||||
}
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._params) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
hideActions
|
||||
.heading=${true}
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
<div slot="heading">
|
||||
<ha-header-bar>
|
||||
<span slot="title">
|
||||
Upload snapshot
|
||||
</span>
|
||||
<mwc-icon-button slot="actionItems" dialogAction="cancel">
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</ha-header-bar>
|
||||
</div>
|
||||
<hassio-upload-snapshot
|
||||
@snapshot-uploaded=${this._snapshotUploaded}
|
||||
.hass=${this.hass}
|
||||
></hassio-upload-snapshot>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _snapshotUploaded(ev) {
|
||||
const snapshot = ev.detail.snapshot;
|
||||
this._params?.showSnapshot(snapshot.slug);
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-header-bar {
|
||||
--mdc-theme-on-primary: var(--primary-text-color);
|
||||
--mdc-theme-primary: var(--mdc-theme-surface);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* overrule the ha-style-dialog max-height on small screens */
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
ha-header-bar {
|
||||
--mdc-theme-primary: var(--app-header-background-color);
|
||||
--mdc-theme-on-primary: var(--app-header-text-color, white);
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-hassio-snapshot-upload": DialogHassioSnapshotUpload;
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import "@material/mwc-button";
|
||||
import { mdiDelete, mdiDownload, mdiHistory } from "@mdi/js";
|
||||
import { mdiClose, mdiDelete, mdiDownload, mdiHistory } from "@mdi/js";
|
||||
import { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import {
|
||||
@ -12,7 +12,8 @@ import {
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "../../../../src/components/ha-header-bar";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import { getSignedPath } from "../../../../src/data/auth";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
@ -22,7 +23,7 @@ import {
|
||||
} from "../../../../src/data/hassio/snapshot";
|
||||
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
|
||||
import { PolymerChangedEvent } from "../../../../src/polymer-types";
|
||||
import { haStyleDialog } from "../../../../src/resources/styles";
|
||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import { HassioSnapshotDialogParams } from "./show-dialog-hassio-snapshot";
|
||||
|
||||
@ -75,6 +76,8 @@ class HassioSnapshotDialog extends LitElement {
|
||||
|
||||
@internalProperty() private _error?: string;
|
||||
|
||||
@internalProperty() private _onboarding = false;
|
||||
|
||||
@internalProperty() private _snapshot?: HassioSnapshotDetail;
|
||||
|
||||
@internalProperty() private _folders!: FolderItem[];
|
||||
@ -90,13 +93,14 @@ class HassioSnapshotDialog extends LitElement {
|
||||
public async showDialog(params: HassioSnapshotDialogParams) {
|
||||
this._snapshot = await fetchHassioSnapshotInfo(this.hass, params.slug);
|
||||
this._folders = _computeFolders(
|
||||
this._snapshot.folders
|
||||
this._snapshot?.folders
|
||||
).sort((a: FolderItem, b: FolderItem) => (a.name > b.name ? 1 : -1));
|
||||
this._addons = _computeAddons(
|
||||
this._snapshot.addons
|
||||
this._snapshot?.addons
|
||||
).sort((a: AddonItem, b: AddonItem) => (a.name > b.name ? 1 : -1));
|
||||
|
||||
this._dialogParams = params;
|
||||
this._onboarding = params.onboarding ?? false;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@ -104,12 +108,17 @@ class HassioSnapshotDialog extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
stacked
|
||||
@closing=${this._closeDialog}
|
||||
.heading=${createCloseHeading(this.hass, this._computeName)}
|
||||
>
|
||||
<ha-dialog open stacked @closing=${this._closeDialog} .heading=${true}>
|
||||
<div slot="heading">
|
||||
<ha-header-bar>
|
||||
<span slot="title">
|
||||
${this._computeName}
|
||||
</span>
|
||||
<mwc-icon-button slot="actionItems" dialogAction="cancel">
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</ha-header-bar>
|
||||
</div>
|
||||
<div class="details">
|
||||
${this._snapshot.type === "full"
|
||||
? "Full snapshot"
|
||||
@ -182,11 +191,15 @@ class HassioSnapshotDialog extends LitElement {
|
||||
${this._error ? html` <p class="error">Error: ${this._error}</p> ` : ""}
|
||||
|
||||
<div>Actions:</div>
|
||||
|
||||
<mwc-button @click=${this._downloadClicked} slot="primaryAction">
|
||||
<ha-svg-icon path=${mdiDownload} class="icon"></ha-svg-icon>
|
||||
Download Snapshot
|
||||
</mwc-button>
|
||||
${!this._onboarding
|
||||
? html`<mwc-button
|
||||
@click=${this._downloadClicked}
|
||||
slot="primaryAction"
|
||||
>
|
||||
<ha-svg-icon path=${mdiDownload} class="icon"></ha-svg-icon>
|
||||
Download Snapshot
|
||||
</mwc-button>`
|
||||
: ""}
|
||||
|
||||
<mwc-button
|
||||
@click=${this._partialRestoreClicked}
|
||||
@ -206,16 +219,22 @@ class HassioSnapshotDialog extends LitElement {
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
<mwc-button @click=${this._deleteClicked} slot="secondaryAction">
|
||||
<ha-svg-icon path=${mdiDelete} class="icon warning"></ha-svg-icon>
|
||||
<span class="warning">Delete Snapshot</span>
|
||||
</mwc-button>
|
||||
${!this._onboarding
|
||||
? html`<mwc-button
|
||||
@click=${this._deleteClicked}
|
||||
slot="secondaryAction"
|
||||
>
|
||||
<ha-svg-icon path=${mdiDelete} class="icon warning"></ha-svg-icon>
|
||||
<span class="warning">Delete Snapshot</span>
|
||||
</mwc-button>`
|
||||
: ""}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
paper-checkbox {
|
||||
@ -242,6 +261,18 @@ class HassioSnapshotDialog extends LitElement {
|
||||
.no-margin-top {
|
||||
margin-top: 0;
|
||||
}
|
||||
ha-header-bar {
|
||||
--mdc-theme-on-primary: var(--primary-text-color);
|
||||
--mdc-theme-primary: var(--mdc-theme-surface);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* overrule the ha-style-dialog max-height on small screens */
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
ha-header-bar {
|
||||
--mdc-theme-primary: var(--app-header-background-color);
|
||||
--mdc-theme-on-primary: var(--app-header-text-color, white);
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
@ -272,6 +303,8 @@ class HassioSnapshotDialog extends LitElement {
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: "Are you sure you want partially to restore this snapshot?",
|
||||
confirmText: "restore",
|
||||
dismissText: "cancel",
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
@ -300,22 +333,31 @@ class HassioSnapshotDialog extends LitElement {
|
||||
data.password = this._snapshotPassword;
|
||||
}
|
||||
|
||||
this.hass
|
||||
.callApi(
|
||||
"POST",
|
||||
if (!this._onboarding) {
|
||||
this.hass
|
||||
.callApi(
|
||||
"POST",
|
||||
|
||||
`hassio/snapshots/${this._snapshot!.slug}/restore/partial`,
|
||||
data
|
||||
)
|
||||
.then(
|
||||
() => {
|
||||
alert("Snapshot restored!");
|
||||
this._closeDialog();
|
||||
},
|
||||
(error) => {
|
||||
this._error = error.body.message;
|
||||
}
|
||||
);
|
||||
`hassio/snapshots/${this._snapshot!.slug}/restore/partial`,
|
||||
data
|
||||
)
|
||||
.then(
|
||||
() => {
|
||||
alert("Snapshot restored!");
|
||||
this._closeDialog();
|
||||
},
|
||||
(error) => {
|
||||
this._error = error.body.message;
|
||||
}
|
||||
);
|
||||
} else {
|
||||
fireEvent(this, "restoring");
|
||||
fetch(`/api/hassio/snapshots/${this._snapshot!.slug}/restore/partial`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
this._closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
private async _fullRestoreClicked() {
|
||||
@ -323,6 +365,8 @@ class HassioSnapshotDialog extends LitElement {
|
||||
!(await showConfirmationDialog(this, {
|
||||
title:
|
||||
"Are you sure you want to wipe your system and restore this snapshot?",
|
||||
confirmText: "restore",
|
||||
dismissText: "cancel",
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
@ -331,28 +375,38 @@ class HassioSnapshotDialog extends LitElement {
|
||||
const data = this._snapshot!.protected
|
||||
? { password: this._snapshotPassword }
|
||||
: undefined;
|
||||
|
||||
this.hass
|
||||
.callApi(
|
||||
"POST",
|
||||
`hassio/snapshots/${this._snapshot!.slug}/restore/full`,
|
||||
data
|
||||
)
|
||||
.then(
|
||||
() => {
|
||||
alert("Snapshot restored!");
|
||||
this._closeDialog();
|
||||
},
|
||||
(error) => {
|
||||
this._error = error.body.message;
|
||||
}
|
||||
);
|
||||
if (!this._onboarding) {
|
||||
this.hass
|
||||
.callApi(
|
||||
"POST",
|
||||
`hassio/snapshots/${this._snapshot!.slug}/restore/full`,
|
||||
data
|
||||
)
|
||||
.then(
|
||||
() => {
|
||||
alert("Snapshot restored!");
|
||||
this._closeDialog();
|
||||
},
|
||||
(error) => {
|
||||
this._error = error.body.message;
|
||||
}
|
||||
);
|
||||
} else {
|
||||
fireEvent(this, "restoring");
|
||||
fetch(`/api/hassio/snapshots/${this._snapshot!.slug}/restore/full`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
this._closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
private async _deleteClicked() {
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: "Are you sure you want to delete this snapshot?",
|
||||
confirmText: "delete",
|
||||
dismissText: "cancel",
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
@ -363,7 +417,9 @@ class HassioSnapshotDialog extends LitElement {
|
||||
.callApi("POST", `hassio/snapshots/${this._snapshot!.slug}/remove`)
|
||||
.then(
|
||||
() => {
|
||||
this._dialogParams!.onDelete();
|
||||
if (this._dialogParams!.onDelete) {
|
||||
this._dialogParams!.onDelete();
|
||||
}
|
||||
this._closeDialog();
|
||||
},
|
||||
(error) => {
|
||||
|
@ -2,7 +2,8 @@ import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
|
||||
export interface HassioSnapshotDialogParams {
|
||||
slug: string;
|
||||
onDelete: () => void;
|
||||
onDelete?: () => void;
|
||||
onboarding?: boolean;
|
||||
}
|
||||
|
||||
export const showHassioSnapshotDialog = (
|
||||
|
22
hassio/src/dialogs/snapshot/show-dialog-snapshot-upload.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "./dialog-hassio-snapshot-upload";
|
||||
|
||||
export interface HassioSnapshotUploadDialogParams {
|
||||
showSnapshot: (slug: string) => void;
|
||||
reloadSnapshot?: () => Promise<void>;
|
||||
onboarding?: boolean;
|
||||
}
|
||||
|
||||
export const showSnapshotUploadDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: HassioSnapshotUploadDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-snapshot-upload",
|
||||
dialogImport: () =>
|
||||
import(
|
||||
/* webpackChunkName: "dialog-hassio-snapshot-upload" */ "./dialog-hassio-snapshot-upload"
|
||||
),
|
||||
dialogParams,
|
||||
});
|
||||
};
|
@ -1,6 +1,12 @@
|
||||
import "@material/mwc-button";
|
||||
import "@material/mwc-icon-button";
|
||||
import { mdiPackageVariant, mdiPackageVariantClosed, mdiReload } from "@mdi/js";
|
||||
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import {
|
||||
mdiDotsVertical,
|
||||
mdiPackageVariant,
|
||||
mdiPackageVariantClosed,
|
||||
} from "@mdi/js";
|
||||
import "@polymer/paper-checkbox/paper-checkbox";
|
||||
import type { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
@ -19,8 +25,10 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
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-button-menu";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-svg-icon";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
@ -39,7 +47,9 @@ import { PolymerChangedEvent } from "../../../src/polymer-types";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../src/types";
|
||||
import "../components/hassio-card-content";
|
||||
import "../components/hassio-upload-snapshot";
|
||||
import { showHassioSnapshotDialog } from "../dialogs/snapshot/show-dialog-hassio-snapshot";
|
||||
import { showSnapshotUploadDialog } from "../dialogs/snapshot/show-dialog-snapshot-upload";
|
||||
import { supervisorTabs } from "../hassio-tabs";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
@ -101,14 +111,23 @@ class HassioSnapshots extends LitElement {
|
||||
.tabs=${supervisorTabs}
|
||||
>
|
||||
<span slot="header">Snapshots</span>
|
||||
|
||||
<mwc-icon-button
|
||||
<ha-button-menu
|
||||
corner="BOTTOM_START"
|
||||
slot="toolbar-icon"
|
||||
aria-label="Reload snapshots"
|
||||
@click=${this.refreshData}
|
||||
@action=${this._handleAction}
|
||||
>
|
||||
<ha-svg-icon path=${mdiReload}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
<mwc-icon-button slot="trigger" alt="menu">
|
||||
<ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
<mwc-list-item>
|
||||
Reload
|
||||
</mwc-list-item>
|
||||
${atLeastVersion(this.hass.config.version, 0, 116)
|
||||
? html`<mwc-list-item>
|
||||
Upload snapshot
|
||||
</mwc-list-item>`
|
||||
: ""}
|
||||
</ha-button-menu>
|
||||
|
||||
<div class="content">
|
||||
<h1>
|
||||
@ -257,6 +276,17 @@ class HassioSnapshots extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this.refreshData();
|
||||
break;
|
||||
case 1:
|
||||
this._showUploadSnapshotDialog();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleTextValueChanged(ev: PolymerChangedEvent<string>) {
|
||||
const input = ev.currentTarget as PaperInputElement;
|
||||
this[`_${input.name}`] = ev.detail.value;
|
||||
@ -362,6 +392,17 @@ class HassioSnapshots extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _showUploadSnapshotDialog() {
|
||||
showSnapshotUploadDialog(this, {
|
||||
showSnapshot: (slug: string) =>
|
||||
showHassioSnapshotDialog(this, {
|
||||
slug,
|
||||
onDelete: () => this._updateSnapshots(),
|
||||
}),
|
||||
reloadSnapshot: () => this.refreshData(),
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyle,
|
||||
|
168
hassio/src/system/hassio-system-metrics.ts
Normal file
@ -0,0 +1,168 @@
|
||||
import "@material/mwc-button";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-bar";
|
||||
import "../../../src/components/ha-button-menu";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-settings-row";
|
||||
import { fetchHassioStats, HassioStats } from "../../../src/data/hassio/common";
|
||||
import { HassioHostInfo } from "../../../src/data/hassio/host";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import {
|
||||
getValueInPercentage,
|
||||
roundWithOneDecimal,
|
||||
} from "../../../src/util/calculate";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
@customElement("hassio-system-metrics")
|
||||
class HassioSystemMetrics extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public hostInfo!: HassioHostInfo;
|
||||
|
||||
@internalProperty() private _supervisorMetrics?: HassioStats;
|
||||
|
||||
@internalProperty() private _coreMetrics?: HassioStats;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
const usedSpace = this._getUsedSpace(this.hostInfo);
|
||||
const metrics = [
|
||||
{
|
||||
description: "Core CPU usage",
|
||||
value: this._coreMetrics?.cpu_percent,
|
||||
},
|
||||
{
|
||||
description: "Core RAM usage",
|
||||
value: this._coreMetrics?.memory_percent,
|
||||
},
|
||||
{
|
||||
description: "Supervisor CPU usage",
|
||||
value: this._supervisorMetrics?.cpu_percent,
|
||||
},
|
||||
{
|
||||
description: "Supervisor RAM usage",
|
||||
value: this._supervisorMetrics?.memory_percent,
|
||||
},
|
||||
{
|
||||
description: "Used space",
|
||||
value: usedSpace,
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<ha-card header="System metrics">
|
||||
<div class="card-content">
|
||||
${metrics.map((metric) =>
|
||||
this._renderMetric(metric.description, metric.value ?? 0)
|
||||
)}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._loadData();
|
||||
}
|
||||
|
||||
private _renderMetric(description: string, value: number): TemplateResult {
|
||||
const roundedValue = roundWithOneDecimal(value);
|
||||
return html`<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${description}
|
||||
</span>
|
||||
<div slot="description">
|
||||
<span class="value">
|
||||
${roundedValue}%
|
||||
</span>
|
||||
<ha-bar
|
||||
class="${classMap({
|
||||
"target-warning": roundedValue > 50,
|
||||
"target-critical": roundedValue > 85,
|
||||
})}"
|
||||
.value=${value}
|
||||
></ha-bar>
|
||||
</div>
|
||||
</ha-settings-row>`;
|
||||
}
|
||||
|
||||
private _getUsedSpace = memoizeOne((hostInfo: HassioHostInfo) =>
|
||||
roundWithOneDecimal(
|
||||
getValueInPercentage(hostInfo.disk_used, 0, hostInfo.disk_total)
|
||||
)
|
||||
);
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
const [supervisor, core] = await Promise.all([
|
||||
fetchHassioStats(this.hass, "supervisor"),
|
||||
fetchHassioStats(this.hass, "core"),
|
||||
]);
|
||||
this._supervisorMetrics = supervisor;
|
||||
this._coreMetrics = core;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
hassioStyle,
|
||||
css`
|
||||
ha-card {
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
}
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
height: 54px;
|
||||
width: 100%;
|
||||
}
|
||||
ha-settings-row > div[slot="description"] {
|
||||
white-space: normal;
|
||||
color: var(--secondary-text-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
ha-bar {
|
||||
--ha-bar-primary-color: var(
|
||||
--hassio-bar-ok-color,
|
||||
var(--success-color)
|
||||
);
|
||||
}
|
||||
.target-warning {
|
||||
--ha-bar-primary-color: var(
|
||||
--hassio-bar-warning-color,
|
||||
var(--warning-color)
|
||||
);
|
||||
}
|
||||
.target-critical {
|
||||
--ha-bar-primary-color: var(
|
||||
--hassio-bar-critical-color,
|
||||
var(--error-color)
|
||||
);
|
||||
}
|
||||
.value {
|
||||
width: 42px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-system-metrics": HassioSystemMetrics;
|
||||
}
|
||||
}
|
@ -23,6 +23,7 @@ import { hassioStyle } from "../resources/hassio-style";
|
||||
import "./hassio-host-info";
|
||||
import "./hassio-supervisor-info";
|
||||
import "./hassio-supervisor-log";
|
||||
import "./hassio-system-metrics";
|
||||
|
||||
@customElement("hassio-system")
|
||||
class HassioSystem extends LitElement {
|
||||
@ -64,6 +65,10 @@ class HassioSystem extends LitElement {
|
||||
.hostInfo=${this.hostInfo}
|
||||
.hassOsInfo=${this.hassOsInfo}
|
||||
></hassio-host-info>
|
||||
<hassio-system-metrics
|
||||
.hass=${this.hass}
|
||||
.hostInfo=${this.hostInfo}
|
||||
></hassio-system-metrics>
|
||||
</div>
|
||||
<hassio-supervisor-log .hass=${this.hass}></hassio-supervisor-log>
|
||||
</div>
|
||||
|
@ -44,8 +44,8 @@
|
||||
"@material/mwc-tab": "^0.18.0",
|
||||
"@material/mwc-tab-bar": "^0.18.0",
|
||||
"@material/top-app-bar": "=8.0.0-canary.096a7a066.0",
|
||||
"@mdi/js": "5.5.55",
|
||||
"@mdi/svg": "5.5.55",
|
||||
"@mdi/js": "5.6.55",
|
||||
"@mdi/svg": "5.6.55",
|
||||
"@polymer/app-layout": "^3.0.2",
|
||||
"@polymer/app-route": "^3.0.2",
|
||||
"@polymer/app-storage": "^3.0.2",
|
||||
@ -143,7 +143,7 @@
|
||||
"@rollup/plugin-node-resolve": "^7.1.3",
|
||||
"@rollup/plugin-replace": "^2.3.2",
|
||||
"@types/chai": "^4.1.7",
|
||||
"@types/chromecast-caf-receiver": "^3.0.12",
|
||||
"@types/chromecast-caf-receiver": "^5.0.11",
|
||||
"@types/codemirror": "^0.0.97",
|
||||
"@types/hls.js": "^0.12.3",
|
||||
"@types/js-yaml": "^3.12.1",
|
||||
|
@ -24,7 +24,6 @@
|
||||
"filesToIgnore": [
|
||||
"**/*.html",
|
||||
"**/src/panels/config/js/**/*.js",
|
||||
"**/ha-paper-slider.js",
|
||||
"**/ha-iconset-svg.js",
|
||||
"**/ha-script-editor.js",
|
||||
"**/ha-automation-editor.js",
|
||||
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 263 B After Width: | Height: | Size: 258 B |
2
setup.py
@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20200918.2",
|
||||
version="20200930.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||
author="The Home Assistant Authors",
|
||||
|
@ -7,6 +7,66 @@
|
||||
/** Icon to use when no icon specified for domain. */
|
||||
export const DEFAULT_DOMAIN_ICON = "hass:bookmark";
|
||||
|
||||
/** Icons for each domain */
|
||||
export const FIXED_DOMAIN_ICONS = {
|
||||
alert: "hass:alert",
|
||||
alexa: "hass:amazon-alexa",
|
||||
air_quality: "hass:air-filter",
|
||||
automation: "hass:robot",
|
||||
calendar: "hass:calendar",
|
||||
camera: "hass:video",
|
||||
climate: "hass:thermostat",
|
||||
configurator: "hass:cog",
|
||||
conversation: "hass:text-to-speech",
|
||||
counter: "hass:counter",
|
||||
device_tracker: "hass:account",
|
||||
fan: "hass:fan",
|
||||
google_assistant: "hass:google-assistant",
|
||||
group: "hass:google-circles-communities",
|
||||
homeassistant: "hass:home-assistant",
|
||||
homekit: "hass:home-automation",
|
||||
image_processing: "hass:image-filter-frames",
|
||||
input_boolean: "hass:toggle-switch-outline",
|
||||
input_datetime: "hass:calendar-clock",
|
||||
input_number: "hass:ray-vertex",
|
||||
input_select: "hass:format-list-bulleted",
|
||||
input_text: "hass:form-textbox",
|
||||
light: "hass:lightbulb",
|
||||
mailbox: "hass:mailbox",
|
||||
notify: "hass:comment-alert",
|
||||
persistent_notification: "hass:bell",
|
||||
person: "hass:account",
|
||||
plant: "hass:flower",
|
||||
proximity: "hass:apple-safari",
|
||||
remote: "hass:remote",
|
||||
scene: "hass:palette",
|
||||
script: "hass:script-text",
|
||||
sensor: "hass:eye",
|
||||
simple_alarm: "hass:bell",
|
||||
sun: "hass:white-balance-sunny",
|
||||
switch: "hass:flash",
|
||||
timer: "hass:timer-outline",
|
||||
updater: "hass:cloud-upload",
|
||||
vacuum: "hass:robot-vacuum",
|
||||
water_heater: "hass:thermometer",
|
||||
weather: "hass:weather-cloudy",
|
||||
zone: "hass:map-marker-radius",
|
||||
};
|
||||
|
||||
export const FIXED_DEVICE_CLASS_ICONS = {
|
||||
current: "hass:current-ac",
|
||||
energy: "hass:flash",
|
||||
humidity: "hass:water-percent",
|
||||
illuminance: "hass:brightness-5",
|
||||
temperature: "hass:thermometer",
|
||||
pressure: "hass:gauge",
|
||||
power: "hass:flash",
|
||||
power_factor: "hass:angle-acute",
|
||||
signal_strength: "hass:wifi",
|
||||
timestamp: "hass:clock",
|
||||
voltage: "hass:sine-wave",
|
||||
};
|
||||
|
||||
/** Domains that have a state card. */
|
||||
export const DOMAINS_WITH_CARD = [
|
||||
"climate",
|
||||
@ -63,6 +123,10 @@ export const DOMAINS_MORE_INFO_NO_HISTORY = ["camera", "configurator", "scene"];
|
||||
/** States that we consider "off". */
|
||||
export const STATES_OFF = ["closed", "locked", "off"];
|
||||
|
||||
/** Binary States */
|
||||
export const BINARY_STATE_ON = "on";
|
||||
export const BINARY_STATE_OFF = "off";
|
||||
|
||||
/** Domains where we allow toggle in Lovelace. */
|
||||
export const DOMAINS_TOGGLE = new Set([
|
||||
"fan",
|
||||
|
@ -2,9 +2,9 @@ import { HassEntity } from "home-assistant-js-websocket";
|
||||
|
||||
/** Return an icon representing a binary sensor state. */
|
||||
|
||||
export const binarySensorIcon = (state: HassEntity) => {
|
||||
const is_off = state.state && state.state === "off";
|
||||
switch (state.attributes.device_class) {
|
||||
export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => {
|
||||
const is_off = state === "off";
|
||||
switch (stateObj?.attributes.device_class) {
|
||||
case "battery":
|
||||
return is_off ? "hass:battery" : "hass:battery-outline";
|
||||
case "battery_charging":
|
||||
@ -17,8 +17,9 @@ export const binarySensorIcon = (state: HassEntity) => {
|
||||
return is_off ? "hass:door-closed" : "hass:door-open";
|
||||
case "garage_door":
|
||||
return is_off ? "hass:garage" : "hass:garage-open";
|
||||
case "gas":
|
||||
case "power":
|
||||
return is_off ? "hass:power-off" : "hass:power-on";
|
||||
case "gas":
|
||||
case "problem":
|
||||
case "safety":
|
||||
case "smoke":
|
||||
|
@ -9,14 +9,17 @@ import { computeStateDomain } from "./compute_state_domain";
|
||||
export const computeStateDisplay = (
|
||||
localize: LocalizeFunc,
|
||||
stateObj: HassEntity,
|
||||
language: string
|
||||
language: string,
|
||||
state?: string
|
||||
): string => {
|
||||
if (stateObj.state === UNKNOWN || stateObj.state === UNAVAILABLE) {
|
||||
return localize(`state.default.${stateObj.state}`);
|
||||
const compareState = state !== undefined ? state : stateObj.state;
|
||||
|
||||
if (compareState === UNKNOWN || compareState === UNAVAILABLE) {
|
||||
return localize(`state.default.${compareState}`);
|
||||
}
|
||||
|
||||
if (stateObj.attributes.unit_of_measurement) {
|
||||
return `${stateObj.state} ${stateObj.attributes.unit_of_measurement}`;
|
||||
return `${compareState} ${stateObj.attributes.unit_of_measurement}`;
|
||||
}
|
||||
|
||||
const domain = computeStateDomain(stateObj);
|
||||
@ -56,7 +59,7 @@ export const computeStateDisplay = (
|
||||
}
|
||||
|
||||
if (domain === "humidifier") {
|
||||
if (stateObj.state === "on" && stateObj.attributes.humidity) {
|
||||
if (compareState === "on" && stateObj.attributes.humidity) {
|
||||
return `${stateObj.attributes.humidity}%`;
|
||||
}
|
||||
}
|
||||
@ -65,11 +68,11 @@ export const computeStateDisplay = (
|
||||
// Return device class translation
|
||||
(stateObj.attributes.device_class &&
|
||||
localize(
|
||||
`component.${domain}.state.${stateObj.attributes.device_class}.${stateObj.state}`
|
||||
`component.${domain}.state.${stateObj.attributes.device_class}.${compareState}`
|
||||
)) ||
|
||||
// Return default translation
|
||||
localize(`component.${domain}.state._.${stateObj.state}`) ||
|
||||
localize(`component.${domain}.state._.${compareState}`) ||
|
||||
// We don't know! Return the raw state.
|
||||
stateObj.state
|
||||
compareState
|
||||
);
|
||||
};
|
||||
|
@ -1,13 +1,12 @@
|
||||
/** Return an icon representing a cover state. */
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { domainIcon } from "./domain_icon";
|
||||
|
||||
export const coverIcon = (state: HassEntity): string => {
|
||||
const open = state.state !== "closed";
|
||||
export const coverIcon = (state?: string, stateObj?: HassEntity): string => {
|
||||
const open = state !== "closed";
|
||||
|
||||
switch (state.attributes.device_class) {
|
||||
switch (stateObj?.attributes.device_class) {
|
||||
case "garage":
|
||||
switch (state.state) {
|
||||
switch (state) {
|
||||
case "opening":
|
||||
return "hass:arrow-up-box";
|
||||
case "closing":
|
||||
@ -18,7 +17,7 @@ export const coverIcon = (state: HassEntity): string => {
|
||||
return "hass:garage-open";
|
||||
}
|
||||
case "gate":
|
||||
switch (state.state) {
|
||||
switch (state) {
|
||||
case "opening":
|
||||
case "closing":
|
||||
return "hass:gate-arrow-right";
|
||||
@ -32,7 +31,7 @@ export const coverIcon = (state: HassEntity): string => {
|
||||
case "damper":
|
||||
return open ? "hass:circle" : "hass:circle-slice-8";
|
||||
case "shutter":
|
||||
switch (state.state) {
|
||||
switch (state) {
|
||||
case "opening":
|
||||
return "hass:arrow-up-box";
|
||||
case "closing":
|
||||
@ -44,7 +43,7 @@ export const coverIcon = (state: HassEntity): string => {
|
||||
}
|
||||
case "blind":
|
||||
case "curtain":
|
||||
switch (state.state) {
|
||||
switch (state) {
|
||||
case "opening":
|
||||
return "hass:arrow-up-box";
|
||||
case "closing":
|
||||
@ -55,7 +54,7 @@ export const coverIcon = (state: HassEntity): string => {
|
||||
return "hass:blinds-open";
|
||||
}
|
||||
case "window":
|
||||
switch (state.state) {
|
||||
switch (state) {
|
||||
case "opening":
|
||||
return "hass:arrow-up-box";
|
||||
case "closing":
|
||||
@ -65,7 +64,16 @@ export const coverIcon = (state: HassEntity): string => {
|
||||
default:
|
||||
return "hass:window-open";
|
||||
}
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
case "opening":
|
||||
return "hass:arrow-up-box";
|
||||
case "closing":
|
||||
return "hass:arrow-down-box";
|
||||
case "closed":
|
||||
return "hass:window-closed";
|
||||
default:
|
||||
return domainIcon("cover", state.state);
|
||||
return "hass:window-open";
|
||||
}
|
||||
};
|
||||
|
@ -1,64 +1,24 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
/**
|
||||
* Return the icon to be used for a domain.
|
||||
*
|
||||
* Optionally pass in a state to influence the domain icon.
|
||||
*/
|
||||
import { DEFAULT_DOMAIN_ICON } from "../const";
|
||||
import { DEFAULT_DOMAIN_ICON, FIXED_DOMAIN_ICONS } from "../const";
|
||||
import { binarySensorIcon } from "./binary_sensor_icon";
|
||||
import { coverIcon } from "./cover_icon";
|
||||
import { sensorIcon } from "./sensor_icon";
|
||||
|
||||
const fixedIcons = {
|
||||
alert: "hass:alert",
|
||||
alexa: "hass:amazon-alexa",
|
||||
air_quality: "hass:air-filter",
|
||||
automation: "hass:robot",
|
||||
calendar: "hass:calendar",
|
||||
camera: "hass:video",
|
||||
climate: "hass:thermostat",
|
||||
configurator: "hass:cog",
|
||||
conversation: "hass:text-to-speech",
|
||||
counter: "hass:counter",
|
||||
device_tracker: "hass:account",
|
||||
fan: "hass:fan",
|
||||
google_assistant: "hass:google-assistant",
|
||||
group: "hass:google-circles-communities",
|
||||
homeassistant: "hass:home-assistant",
|
||||
homekit: "hass:home-automation",
|
||||
humidifier: "hass:air-humidifier",
|
||||
image_processing: "hass:image-filter-frames",
|
||||
input_boolean: "hass:toggle-switch-outline",
|
||||
input_datetime: "hass:calendar-clock",
|
||||
input_number: "hass:ray-vertex",
|
||||
input_select: "hass:format-list-bulleted",
|
||||
input_text: "hass:form-textbox",
|
||||
light: "hass:lightbulb",
|
||||
mailbox: "hass:mailbox",
|
||||
notify: "hass:comment-alert",
|
||||
persistent_notification: "hass:bell",
|
||||
person: "hass:account",
|
||||
plant: "hass:flower",
|
||||
proximity: "hass:apple-safari",
|
||||
remote: "hass:remote",
|
||||
scene: "hass:palette",
|
||||
script: "hass:script-text",
|
||||
sensor: "hass:eye",
|
||||
simple_alarm: "hass:bell",
|
||||
sun: "hass:white-balance-sunny",
|
||||
switch: "hass:flash",
|
||||
timer: "hass:timer-outline",
|
||||
updater: "hass:cloud-upload",
|
||||
vacuum: "hass:robot-vacuum",
|
||||
water_heater: "hass:thermometer",
|
||||
weather: "hass:weather-cloudy",
|
||||
zone: "hass:map-marker-radius",
|
||||
};
|
||||
|
||||
export const domainIcon = (domain: string, state?: string): string => {
|
||||
if (domain in fixedIcons) {
|
||||
return fixedIcons[domain];
|
||||
}
|
||||
export const domainIcon = (
|
||||
domain: string,
|
||||
stateObj?: HassEntity,
|
||||
state?: string
|
||||
): string => {
|
||||
const compareState = state !== undefined ? state : stateObj?.state;
|
||||
|
||||
switch (domain) {
|
||||
case "alarm_control_panel":
|
||||
switch (state) {
|
||||
switch (compareState) {
|
||||
case "armed_home":
|
||||
return "hass:bell-plus";
|
||||
case "armed_night":
|
||||
@ -72,30 +32,24 @@ export const domainIcon = (domain: string, state?: string): string => {
|
||||
}
|
||||
|
||||
case "binary_sensor":
|
||||
return state && state === "off"
|
||||
? "hass:radiobox-blank"
|
||||
: "hass:checkbox-marked-circle";
|
||||
return binarySensorIcon(compareState, stateObj);
|
||||
|
||||
case "cover":
|
||||
switch (state) {
|
||||
case "opening":
|
||||
return "hass:arrow-up-box";
|
||||
case "closing":
|
||||
return "hass:arrow-down-box";
|
||||
case "closed":
|
||||
return "hass:window-closed";
|
||||
default:
|
||||
return "hass:window-open";
|
||||
}
|
||||
return coverIcon(compareState, stateObj);
|
||||
|
||||
case "humidifier":
|
||||
return state && state === "off"
|
||||
? "hass:air-humidifier-off"
|
||||
: "hass:air-humidifier";
|
||||
|
||||
case "lock":
|
||||
return state && state === "unlocked" ? "hass:lock-open" : "hass:lock";
|
||||
return compareState === "unlocked" ? "hass:lock-open" : "hass:lock";
|
||||
|
||||
case "media_player":
|
||||
return state && state === "playing" ? "hass:cast-connected" : "hass:cast";
|
||||
return compareState === "playing" ? "hass:cast-connected" : "hass:cast";
|
||||
|
||||
case "zwave":
|
||||
switch (state) {
|
||||
switch (compareState) {
|
||||
case "dead":
|
||||
return "hass:emoticon-dead";
|
||||
case "sleeping":
|
||||
@ -106,11 +60,32 @@ export const domainIcon = (domain: string, state?: string): string => {
|
||||
return "hass:z-wave";
|
||||
}
|
||||
|
||||
default:
|
||||
// eslint-disable-next-line
|
||||
console.warn(
|
||||
"Unable to find icon for domain " + domain + " (" + state + ")"
|
||||
);
|
||||
return DEFAULT_DOMAIN_ICON;
|
||||
case "sensor": {
|
||||
const icon = sensorIcon(stateObj);
|
||||
if (icon) {
|
||||
return icon;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "input_datetime":
|
||||
if (!stateObj?.attributes.has_date) {
|
||||
return "hass:clock";
|
||||
}
|
||||
if (!stateObj.attributes.has_time) {
|
||||
return "hass:calendar";
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (domain in FIXED_DOMAIN_ICONS) {
|
||||
return FIXED_DOMAIN_ICONS[domain];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
console.warn(
|
||||
"Unable to find icon for domain " + domain + " (" + stateObj + ")"
|
||||
);
|
||||
return DEFAULT_DOMAIN_ICON;
|
||||
};
|
||||
|
@ -1,13 +0,0 @@
|
||||
/** Return an icon representing an input datetime state. */
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { domainIcon } from "./domain_icon";
|
||||
|
||||
export const inputDateTimeIcon = (state: HassEntity): string => {
|
||||
if (!state.attributes.has_date) {
|
||||
return "hass:clock";
|
||||
}
|
||||
if (!state.attributes.has_time) {
|
||||
return "hass:calendar";
|
||||
}
|
||||
return domainIcon("input_datetime");
|
||||
};
|
@ -1,35 +1,23 @@
|
||||
/** Return an icon representing a sensor state. */
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { UNIT_C, UNIT_F } from "../const";
|
||||
import { domainIcon } from "./domain_icon";
|
||||
import { FIXED_DEVICE_CLASS_ICONS, UNIT_C, UNIT_F } from "../const";
|
||||
import { batteryIcon } from "./battery_icon";
|
||||
|
||||
const fixedDeviceClassIcons = {
|
||||
current: "hass:current-ac",
|
||||
energy: "hass:flash",
|
||||
humidity: "hass:water-percent",
|
||||
illuminance: "hass:brightness-5",
|
||||
temperature: "hass:thermometer",
|
||||
pressure: "hass:gauge",
|
||||
power: "hass:flash",
|
||||
power_factor: "hass:angle-acute",
|
||||
signal_strength: "hass:wifi",
|
||||
voltage: "hass:sine-wave",
|
||||
};
|
||||
export const sensorIcon = (stateObj?: HassEntity): string | undefined => {
|
||||
const dclass = stateObj?.attributes.device_class;
|
||||
|
||||
export const sensorIcon = (state: HassEntity) => {
|
||||
const dclass = state.attributes.device_class;
|
||||
|
||||
if (dclass && dclass in fixedDeviceClassIcons) {
|
||||
return fixedDeviceClassIcons[dclass];
|
||||
if (dclass && dclass in FIXED_DEVICE_CLASS_ICONS) {
|
||||
return FIXED_DEVICE_CLASS_ICONS[dclass];
|
||||
}
|
||||
|
||||
if (dclass === "battery") {
|
||||
return batteryIcon(state);
|
||||
return stateObj ? batteryIcon(stateObj) : "hass:battery";
|
||||
}
|
||||
|
||||
const unit = state.attributes.unit_of_measurement;
|
||||
const unit = stateObj?.attributes.unit_of_measurement;
|
||||
if (unit === UNIT_C || unit === UNIT_F) {
|
||||
return "hass:thermometer";
|
||||
}
|
||||
return domainIcon("sensor");
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
@ -1,19 +1,8 @@
|
||||
/** Return an icon representing a state. */
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { DEFAULT_DOMAIN_ICON } from "../const";
|
||||
import { binarySensorIcon } from "./binary_sensor_icon";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
import { coverIcon } from "./cover_icon";
|
||||
import { domainIcon } from "./domain_icon";
|
||||
import { inputDateTimeIcon } from "./input_dateteime_icon";
|
||||
import { sensorIcon } from "./sensor_icon";
|
||||
|
||||
const domainIcons = {
|
||||
binary_sensor: binarySensorIcon,
|
||||
cover: coverIcon,
|
||||
sensor: sensorIcon,
|
||||
input_datetime: inputDateTimeIcon,
|
||||
};
|
||||
|
||||
export const stateIcon = (state: HassEntity) => {
|
||||
if (!state) {
|
||||
@ -23,10 +12,5 @@ export const stateIcon = (state: HassEntity) => {
|
||||
return state.attributes.icon;
|
||||
}
|
||||
|
||||
const domain = computeDomain(state.entity_id);
|
||||
|
||||
if (domain in domainIcons) {
|
||||
return domainIcons[domain](state);
|
||||
}
|
||||
return domainIcon(domain, state.state);
|
||||
return domainIcon(computeDomain(state.entity_id), state);
|
||||
};
|
||||
|
@ -1,15 +0,0 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { DOMAINS_HIDE_MORE_INFO, DOMAINS_WITH_MORE_INFO } from "../const";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
|
||||
export const stateMoreInfoType = (stateObj: HassEntity) => {
|
||||
const domain = computeStateDomain(stateObj);
|
||||
|
||||
if (DOMAINS_WITH_MORE_INFO.includes(domain)) {
|
||||
return domain;
|
||||
}
|
||||
if (DOMAINS_HIDE_MORE_INFO.includes(domain)) {
|
||||
return "hidden";
|
||||
}
|
||||
return "default";
|
||||
};
|
@ -147,7 +147,7 @@ export class HaStateLabelBadge extends LitElement {
|
||||
return "hass:alert-circle";
|
||||
}
|
||||
// state == 'disarmed'
|
||||
return domainIcon(domain, state.state);
|
||||
return domainIcon(domain, state);
|
||||
case "binary_sensor":
|
||||
case "device_tracker":
|
||||
case "updater":
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
/* eslint-plugin-disable lit */
|
||||
import LocalizeMixin from "../../mixins/localize-mixin";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import "../ha-relative-time";
|
||||
import "./state-badge";
|
||||
|
||||
class StateInfo extends PolymerElement {
|
||||
class StateInfo extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
${this.styleTemplate} ${this.stateBadgeTemplate} ${this.infoTemplate}
|
||||
@ -71,13 +73,20 @@ class StateInfo extends PolymerElement {
|
||||
<div class="name" in-dialog$="[[inDialog]]">
|
||||
[[computeStateName(stateObj)]]
|
||||
</div>
|
||||
|
||||
<template is="dom-if" if="[[inDialog]]">
|
||||
<div class="time-ago">
|
||||
<ha-relative-time
|
||||
id="last_changed"
|
||||
hass="[[hass]]"
|
||||
datetime="[[stateObj.last_changed]]"
|
||||
></ha-relative-time>
|
||||
<paper-tooltip animation-delay="0" for="last_changed">
|
||||
[[localize('ui.dialogs.more_info_control.last_updated')]]:
|
||||
<ha-relative-time
|
||||
hass="[[hass]]"
|
||||
datetime="[[stateObj.last_updated]]"
|
||||
></ha-relative-time>
|
||||
</paper-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!inDialog]]">
|
||||
|
@ -36,7 +36,7 @@ export class HaCard extends LitElement {
|
||||
|
||||
:host([outlined]) {
|
||||
box-shadow: none;
|
||||
border-width: 1px;
|
||||
border-width: var(--ha-card-border-width, 1px);
|
||||
border-style: solid;
|
||||
border-color: var(
|
||||
--ha-card-border-color,
|
||||
|
184
src/components/ha-file-upload.ts
Normal file
@ -0,0 +1,184 @@
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import "@polymer/iron-input/iron-input";
|
||||
import "@polymer/paper-input/paper-input-container";
|
||||
import {
|
||||
css,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-circular-progress";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"file-picked": { files: FileList };
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("ha-file-upload")
|
||||
export class HaFileUpload extends LitElement {
|
||||
@property() public accept!: string;
|
||||
|
||||
@property() public icon!: string;
|
||||
|
||||
@property() public label!: string;
|
||||
|
||||
@property() public value: string | TemplateResult | null = null;
|
||||
|
||||
@property({ type: Boolean }) private uploading = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "auto-open-file-dialog" })
|
||||
private autoOpenFileDialog = false;
|
||||
|
||||
@internalProperty() private _drag = false;
|
||||
|
||||
@query("#input") private _input?: HTMLInputElement;
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (this.autoOpenFileDialog) {
|
||||
this._input?.click();
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
if (changedProperties.has("_drag") && !this.uploading) {
|
||||
(this.shadowRoot!.querySelector(
|
||||
"paper-input-container"
|
||||
) as any)._setFocused(this._drag);
|
||||
}
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
${this.uploading
|
||||
? html`<ha-circular-progress
|
||||
alt="Uploading"
|
||||
size="large"
|
||||
active
|
||||
></ha-circular-progress>`
|
||||
: html`
|
||||
<label for="input">
|
||||
<paper-input-container
|
||||
.alwaysFloatLabel=${Boolean(this.value)}
|
||||
@drop=${this._handleDrop}
|
||||
@dragenter=${this._handleDragStart}
|
||||
@dragover=${this._handleDragStart}
|
||||
@dragleave=${this._handleDragEnd}
|
||||
@dragend=${this._handleDragEnd}
|
||||
class=${classMap({
|
||||
dragged: this._drag,
|
||||
})}
|
||||
>
|
||||
<label for="input" slot="label">
|
||||
${this.label}
|
||||
</label>
|
||||
<iron-input slot="input">
|
||||
<input
|
||||
id="input"
|
||||
type="file"
|
||||
class="file"
|
||||
accept=${this.accept}
|
||||
@change=${this._handleFilePicked}
|
||||
/>
|
||||
${this.value}
|
||||
</iron-input>
|
||||
${this.value
|
||||
? html`<mwc-icon-button
|
||||
slot="suffix"
|
||||
@click=${this._clearValue}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
</mwc-icon-button>`
|
||||
: html`<mwc-icon-button slot="suffix">
|
||||
<ha-svg-icon .path=${this.icon}></ha-svg-icon>
|
||||
</mwc-icon-button>`}
|
||||
</paper-input-container>
|
||||
</label>
|
||||
`}
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleDrop(ev: DragEvent) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
if (ev.dataTransfer?.files) {
|
||||
fireEvent(this, "file-picked", { files: ev.dataTransfer.files });
|
||||
}
|
||||
this._drag = false;
|
||||
}
|
||||
|
||||
private _handleDragStart(ev: DragEvent) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this._drag = true;
|
||||
}
|
||||
|
||||
private _handleDragEnd(ev: DragEvent) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this._drag = false;
|
||||
}
|
||||
|
||||
private _handleFilePicked(ev) {
|
||||
fireEvent(this, "file-picked", { files: ev.target.files });
|
||||
}
|
||||
|
||||
private _clearValue(ev: Event) {
|
||||
ev.preventDefault();
|
||||
this.value = null;
|
||||
fireEvent(this, "change");
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
paper-input-container {
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
margin: 0 -8px;
|
||||
}
|
||||
paper-input-container.dragged:before {
|
||||
position: var(--layout-fit_-_position);
|
||||
top: var(--layout-fit_-_top);
|
||||
right: var(--layout-fit_-_right);
|
||||
bottom: var(--layout-fit_-_bottom);
|
||||
left: var(--layout-fit_-_left);
|
||||
background: currentColor;
|
||||
content: "";
|
||||
opacity: var(--dark-divider-opacity);
|
||||
pointer-events: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
input.file {
|
||||
display: none;
|
||||
}
|
||||
img {
|
||||
max-width: 125px;
|
||||
max-height: 125px;
|
||||
}
|
||||
mwc-icon-button {
|
||||
--mdc-icon-button-size: 24px;
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
ha-circular-progress {
|
||||
display: block;
|
||||
text-align-last: center;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-file-upload": HaFileUpload;
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@ import {
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { HaCheckbox } from "../ha-checkbox";
|
||||
import "../ha-paper-slider";
|
||||
import "../ha-slider";
|
||||
import {
|
||||
HaFormElement,
|
||||
HaFormIntegerData,
|
||||
@ -31,7 +31,7 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
|
||||
@property() public suffix?: string;
|
||||
|
||||
@query("paper-input ha-paper-slider") private _input?: HTMLElement;
|
||||
@query("paper-input ha-slider") private _input?: HTMLElement;
|
||||
|
||||
public focus() {
|
||||
if (this._input) {
|
||||
@ -53,7 +53,7 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
></ha-checkbox>
|
||||
`
|
||||
: ""}
|
||||
<ha-paper-slider
|
||||
<ha-slider
|
||||
pin
|
||||
editable
|
||||
.value=${this._value}
|
||||
@ -63,7 +63,7 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
this.schema.optional &&
|
||||
this.schema.default === undefined}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-paper-slider>
|
||||
></ha-slider>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
@ -112,7 +112,7 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
ha-paper-slider {
|
||||
ha-slider {
|
||||
width: 100%;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
45
src/components/ha-help-tooltip.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { mdiHelpCircle } from "@mdi/js";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import {
|
||||
css,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-help-tooltip")
|
||||
export class HaHelpTooltip extends LitElement {
|
||||
@property() public label!: string;
|
||||
|
||||
@property() public position = "top";
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon>
|
||||
<paper-tooltip
|
||||
offset="4"
|
||||
.position=${this.position}
|
||||
.fitToVisibleBounds=${true}
|
||||
>${this.label}</paper-tooltip
|
||||
>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-svg-icon {
|
||||
--mdc-icon-size: var(--ha-help-tooltip-size, 14px);
|
||||
color: var(--ha-help-tooltip-color, var(--disabled-text-color));
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-help-tooltip": HaHelpTooltip;
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "./ha-icon";
|
||||
import "./ha-paper-slider";
|
||||
import "./ha-slider";
|
||||
|
||||
class HaLabeledSlider extends PolymerElement {
|
||||
static get template() {
|
||||
@ -27,7 +27,7 @@ class HaLabeledSlider extends PolymerElement {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
ha-paper-slider {
|
||||
ha-slider {
|
||||
flex-grow: 1;
|
||||
background-image: var(--ha-slider-background);
|
||||
}
|
||||
@ -37,7 +37,7 @@ class HaLabeledSlider extends PolymerElement {
|
||||
<div class="extra-container"><slot name="extra"></slot></div>
|
||||
<div class="slider-container">
|
||||
<ha-icon icon="[[icon]]" hidden$="[[!icon]]"></ha-icon>
|
||||
<ha-paper-slider
|
||||
<ha-slider
|
||||
min="[[min]]"
|
||||
max="[[max]]"
|
||||
step="[[step]]"
|
||||
@ -45,7 +45,7 @@ class HaLabeledSlider extends PolymerElement {
|
||||
disabled="[[disabled]]"
|
||||
disabled="[[disabled]]"
|
||||
value="{{value}}"
|
||||
></ha-paper-slider>
|
||||
></ha-slider>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
@ -72,6 +72,7 @@ class HaMarkdown extends LitElement {
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
line-height: 1.45;
|
||||
font-family: var(--code-font-family, monospace);
|
||||
}
|
||||
ha-markdown-element h2 {
|
||||
font-size: 1.5em;
|
||||
|
@ -1,80 +0,0 @@
|
||||
import "@polymer/paper-slider/paper-slider";
|
||||
|
||||
/**
|
||||
* @polymer
|
||||
* @customElement
|
||||
* @appliesMixin paper-slider
|
||||
*/
|
||||
const PaperSliderClass = customElements.get("paper-slider");
|
||||
|
||||
class HaPaperSlider extends PaperSliderClass {
|
||||
static get template() {
|
||||
const tpl = document.createElement("template");
|
||||
tpl.innerHTML = PaperSliderClass.template.innerHTML;
|
||||
const styleEl = document.createElement("style");
|
||||
styleEl.innerHTML = `
|
||||
.pin > .slider-knob > .slider-knob-inner {
|
||||
font-size: var(--ha-paper-slider-pin-font-size, 10px);
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.disabled.ring > .slider-knob > .slider-knob-inner {
|
||||
background-color: var(--paper-slider-disabled-knob-color, var(--paper-grey-400));
|
||||
border: 2px solid var(--paper-slider-disabled-knob-color, var(--paper-grey-400));
|
||||
}
|
||||
|
||||
.pin > .slider-knob > .slider-knob-inner::before {
|
||||
top: unset;
|
||||
margin-left: unset;
|
||||
|
||||
bottom: calc(15px + var(--calculated-paper-slider-height)/2);
|
||||
left: 50%;
|
||||
width: 2.2em;
|
||||
height: 2.2em;
|
||||
|
||||
-webkit-transform-origin: left bottom;
|
||||
transform-origin: left bottom;
|
||||
-webkit-transform: rotate(-45deg) scale(0) translate(0);
|
||||
transform: rotate(-45deg) scale(0) translate(0);
|
||||
}
|
||||
|
||||
.pin.expand > .slider-knob > .slider-knob-inner::before {
|
||||
-webkit-transform: rotate(-45deg) scale(1) translate(7px, -7px);
|
||||
transform: rotate(-45deg) scale(1) translate(7px, -7px);
|
||||
}
|
||||
|
||||
.pin > .slider-knob > .slider-knob-inner::after {
|
||||
top: unset;
|
||||
font-size: unset;
|
||||
|
||||
bottom: calc(15px + var(--calculated-paper-slider-height)/2);
|
||||
left: 50%;
|
||||
margin-left: -1.1em;
|
||||
width: 2.2em;
|
||||
height: 2.1em;
|
||||
|
||||
-webkit-transform-origin: center bottom;
|
||||
transform-origin: center bottom;
|
||||
-webkit-transform: scale(0) translate(0);
|
||||
transform: scale(0) translate(0);
|
||||
}
|
||||
|
||||
.pin.expand > .slider-knob > .slider-knob-inner::after {
|
||||
-webkit-transform: scale(1) translate(0, -10px);
|
||||
transform: scale(1) translate(0, -10px);
|
||||
}
|
||||
|
||||
:host([dir="rtl"]) .pin.expand > .slider-knob > .slider-knob-inner::after {
|
||||
-webkit-transform: scale(1) translate(0, -17px) scaleX(-1) !important;
|
||||
transform: scale(1) translate(0, -17px) scaleX(-1) !important;
|
||||
}
|
||||
|
||||
.slider-input {
|
||||
width: 54px;
|
||||
}
|
||||
`;
|
||||
tpl.content.appendChild(styleEl);
|
||||
return tpl;
|
||||
}
|
||||
}
|
||||
customElements.define("ha-paper-slider", HaPaperSlider);
|