mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
Merge branch 'rc'
This commit is contained in:
commit
7aa2136c21
@ -3,6 +3,7 @@
|
||||
import { constants } from "node:zlib";
|
||||
import gulp from "gulp";
|
||||
import brotli from "gulp-brotli";
|
||||
import zopfli from "gulp-zopfli-green";
|
||||
import paths from "../paths.cjs";
|
||||
|
||||
const filesGlob = "*.{js,json,css,svg,xml}";
|
||||
@ -12,17 +13,18 @@ const brotliOptions = {
|
||||
[constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY,
|
||||
},
|
||||
};
|
||||
const zopfliOptions = { threshold: 150 };
|
||||
|
||||
const compressModern = (rootDir, modernDir) =>
|
||||
const compressModern = (rootDir, modernDir, compress) =>
|
||||
gulp
|
||||
.src([`${modernDir}/**/${filesGlob}`, `${rootDir}/sw-modern.js`], {
|
||||
base: rootDir,
|
||||
allowEmpty: true,
|
||||
})
|
||||
.pipe(brotli(brotliOptions))
|
||||
.pipe(compress === "zopfli" ? zopfli(zopfliOptions) : brotli(brotliOptions))
|
||||
.pipe(gulp.dest(rootDir));
|
||||
|
||||
const compressOther = (rootDir, modernDir) =>
|
||||
const compressOther = (rootDir, modernDir, compress) =>
|
||||
gulp
|
||||
.src(
|
||||
[
|
||||
@ -33,21 +35,52 @@ const compressOther = (rootDir, modernDir) =>
|
||||
],
|
||||
{ base: rootDir, allowEmpty: true }
|
||||
)
|
||||
.pipe(brotli(brotliOptions))
|
||||
.pipe(compress === "zopfli" ? zopfli(zopfliOptions) : brotli(brotliOptions))
|
||||
.pipe(gulp.dest(rootDir));
|
||||
|
||||
const compressAppModern = () =>
|
||||
compressModern(paths.app_output_root, paths.app_output_latest);
|
||||
const compressHassioModern = () =>
|
||||
compressModern(paths.hassio_output_root, paths.hassio_output_latest);
|
||||
const compressAppModernBrotli = () =>
|
||||
compressModern(paths.app_output_root, paths.app_output_latest, "brotli");
|
||||
const compressAppModernZopfli = () =>
|
||||
compressModern(paths.app_output_root, paths.app_output_latest, "zopfli");
|
||||
|
||||
const compressAppOther = () =>
|
||||
compressOther(paths.app_output_root, paths.app_output_latest);
|
||||
const compressHassioOther = () =>
|
||||
compressOther(paths.hassio_output_root, paths.hassio_output_latest);
|
||||
const compressHassioModernBrotli = () =>
|
||||
compressModern(
|
||||
paths.hassio_output_root,
|
||||
paths.hassio_output_latest,
|
||||
"brotli"
|
||||
);
|
||||
const compressHassioModernZopfli = () =>
|
||||
compressModern(
|
||||
paths.hassio_output_root,
|
||||
paths.hassio_output_latest,
|
||||
"zopfli"
|
||||
);
|
||||
|
||||
gulp.task("compress-app", gulp.parallel(compressAppModern, compressAppOther));
|
||||
const compressAppOtherBrotli = () =>
|
||||
compressOther(paths.app_output_root, paths.app_output_latest, "brotli");
|
||||
const compressAppOtherZopfli = () =>
|
||||
compressOther(paths.app_output_root, paths.app_output_latest, "zopfli");
|
||||
|
||||
const compressHassioOtherBrotli = () =>
|
||||
compressOther(paths.hassio_output_root, paths.hassio_output_latest, "brotli");
|
||||
const compressHassioOtherZopfli = () =>
|
||||
compressOther(paths.hassio_output_root, paths.hassio_output_latest, "zopfli");
|
||||
|
||||
gulp.task(
|
||||
"compress-app",
|
||||
gulp.parallel(
|
||||
compressAppModernBrotli,
|
||||
compressAppOtherBrotli,
|
||||
compressAppModernZopfli,
|
||||
compressAppOtherZopfli
|
||||
)
|
||||
);
|
||||
gulp.task(
|
||||
"compress-hassio",
|
||||
gulp.parallel(compressHassioModern, compressHassioOther)
|
||||
gulp.parallel(
|
||||
compressHassioModernBrotli,
|
||||
compressHassioOtherBrotli,
|
||||
compressHassioModernZopfli,
|
||||
compressHassioOtherZopfli
|
||||
)
|
||||
);
|
||||
|
@ -179,8 +179,8 @@ class HassioBackupDialog
|
||||
}
|
||||
|
||||
private async _restoreClicked() {
|
||||
this._restoringBackup = true;
|
||||
const backupDetails = this._backupContent.backupDetails();
|
||||
this._restoringBackup = true;
|
||||
|
||||
const supervisor = this._dialogParams?.supervisor;
|
||||
if (supervisor !== undefined && supervisor.info.state !== "running") {
|
||||
@ -196,12 +196,12 @@ class HassioBackupDialog
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: this._localize(
|
||||
this._backupContent.backupType === "full"
|
||||
this._backup!.type === "full"
|
||||
? "confirm_restore_full_backup_title"
|
||||
: "confirm_restore_partial_backup_title"
|
||||
),
|
||||
text: this._localize(
|
||||
this._backupContent.backupType === "full"
|
||||
this._backup!.type === "full"
|
||||
? "confirm_restore_full_backup_text"
|
||||
: "confirm_restore_partial_backup_text"
|
||||
),
|
||||
@ -216,9 +216,9 @@ class HassioBackupDialog
|
||||
try {
|
||||
await restoreBackup(
|
||||
this.hass,
|
||||
this._backupContent.backupType,
|
||||
this._backup!.type,
|
||||
this._backup!.slug,
|
||||
backupDetails,
|
||||
{ ...backupDetails, background: this._dialogParams?.onboarding },
|
||||
!!this.hass && atLeastVersion(this.hass.config.version, 2021, 9)
|
||||
);
|
||||
|
||||
|
@ -115,6 +115,7 @@
|
||||
"element-internals-polyfill": "1.3.12",
|
||||
"fuse.js": "7.0.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"gulp-zopfli-green": "6.0.2",
|
||||
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
|
||||
"home-assistant-js-websocket": "9.4.0",
|
||||
"idb-keyval": "6.2.1",
|
||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20241224.0"
|
||||
version = "20250103.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@ -1,14 +1,19 @@
|
||||
type NonUndefined<T> = T extends undefined ? never : T;
|
||||
type NonNullUndefined<T> = T extends undefined
|
||||
? never
|
||||
: T extends null
|
||||
? never
|
||||
: T;
|
||||
|
||||
/**
|
||||
* Ensure that the input is an array or wrap it in an array
|
||||
* @param value - The value to ensure is an array
|
||||
*/
|
||||
export function ensureArray(value: undefined): undefined;
|
||||
export function ensureArray<T>(value: T | T[]): NonUndefined<T>[];
|
||||
export function ensureArray<T>(value: T | readonly T[]): NonUndefined<T>[];
|
||||
export function ensureArray(value: null): null;
|
||||
export function ensureArray<T>(value: T | T[]): NonNullUndefined<T>[];
|
||||
export function ensureArray<T>(value: T | readonly T[]): NonNullUndefined<T>[];
|
||||
export function ensureArray(value) {
|
||||
if (value === undefined || Array.isArray(value)) {
|
||||
if (value === undefined || value === null || Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
return [value];
|
||||
|
@ -14,9 +14,16 @@ export interface NavigateOptions {
|
||||
data?: any;
|
||||
}
|
||||
|
||||
export const navigate = async (path: string, options?: NavigateOptions) => {
|
||||
// max time to wait for dialogs to close before navigating
|
||||
const DIALOG_WAIT_TIMEOUT = 500;
|
||||
|
||||
export const navigate = async (
|
||||
path: string,
|
||||
options?: NavigateOptions,
|
||||
timestamp = Date.now()
|
||||
) => {
|
||||
const { history } = mainWindow;
|
||||
if (history.state?.dialog) {
|
||||
if (history.state?.dialog && Date.now() - timestamp < DIALOG_WAIT_TIMEOUT) {
|
||||
const closed = await closeAllDialogs();
|
||||
if (!closed) {
|
||||
// eslint-disable-next-line no-console
|
||||
@ -26,7 +33,7 @@ export const navigate = async (path: string, options?: NavigateOptions) => {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
// need to wait for history state to be updated in case a dialog was closed
|
||||
setTimeout(() => {
|
||||
navigate(path, options).then(resolve);
|
||||
navigate(path, options, timestamp).then(resolve);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
export const copyToClipboard = async (str) => {
|
||||
export const copyToClipboard = async (str, rootEl?: HTMLElement) => {
|
||||
if (navigator.clipboard) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(str);
|
||||
@ -8,10 +8,12 @@ export const copyToClipboard = async (str) => {
|
||||
}
|
||||
}
|
||||
|
||||
const root = rootEl ?? document.body;
|
||||
|
||||
const el = document.createElement("textarea");
|
||||
el.value = str;
|
||||
document.body.appendChild(el);
|
||||
root.appendChild(el);
|
||||
el.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(el);
|
||||
root.removeChild(el);
|
||||
};
|
||||
|
@ -1,7 +1,25 @@
|
||||
class TimeoutError extends Error {
|
||||
public timeout: number;
|
||||
|
||||
constructor(timeout: number, ...params) {
|
||||
super(...params);
|
||||
|
||||
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, TimeoutError);
|
||||
}
|
||||
|
||||
this.name = "TimeoutError";
|
||||
// Custom debugging information
|
||||
this.timeout = timeout;
|
||||
this.message = `Timed out in ${timeout} ms.`;
|
||||
}
|
||||
}
|
||||
|
||||
export const promiseTimeout = (ms: number, promise: Promise<any> | any) => {
|
||||
const timeout = new Promise((_resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(`Timed out in ${ms} ms.`);
|
||||
reject(new TimeoutError(ms));
|
||||
}, ms);
|
||||
});
|
||||
|
||||
|
@ -10,11 +10,13 @@ import { css, html, nothing, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { mdiRestart } from "@mdi/js";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { clamp } from "../../common/number/clamp";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { isMac } from "../../util/is_mac";
|
||||
import "../ha-icon-button";
|
||||
|
||||
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
|
||||
|
||||
@ -300,6 +302,16 @@ export class HaChartBase extends LitElement {
|
||||
: this.hass.localize("ui.components.history_charts.zoom_hint")}
|
||||
</div>
|
||||
</div>
|
||||
${this._isZoomed && this.chartType !== "timeline"
|
||||
? html`<ha-icon-button
|
||||
class="zoom-reset"
|
||||
.path=${mdiRestart}
|
||||
@click=${this._handleZoomReset}
|
||||
title=${this.hass.localize(
|
||||
"ui.components.history_charts.zoom_reset"
|
||||
)}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
${this._tooltip
|
||||
? html`<div
|
||||
class="chart-tooltip ${classMap({
|
||||
@ -420,7 +432,11 @@ export class HaChartBase extends LitElement {
|
||||
modifierKey,
|
||||
speed: 0.05,
|
||||
},
|
||||
mode: "x",
|
||||
mode:
|
||||
this.chartType !== "timeline" &&
|
||||
(this.options?.scales?.y as any)?.type === "category"
|
||||
? "y"
|
||||
: "x",
|
||||
onZoomComplete: () => {
|
||||
const isZoomed = this.chart?.isZoomedOrPanned() ?? false;
|
||||
if (this._isZoomed && !isZoomed) {
|
||||
@ -541,6 +557,10 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleZoomReset() {
|
||||
this.chart?.resetZoom();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
@ -552,6 +572,9 @@ export class HaChartBase extends LitElement {
|
||||
height: 0;
|
||||
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.chart-container {
|
||||
position: relative;
|
||||
}
|
||||
canvas {
|
||||
max-height: var(--chart-max-height, 400px);
|
||||
}
|
||||
@ -670,6 +693,16 @@ export class HaChartBase extends LitElement {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0 0 32px 32px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.zoom-reset {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 4px;
|
||||
background: var(--card-background-color);
|
||||
border-radius: 4px;
|
||||
--mdc-icon-button-size: 32px;
|
||||
color: var(--primary-color);
|
||||
border: 1px solid var(--divider-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@ -515,7 +515,7 @@ export class HaDataTable extends LitElement {
|
||||
return html`<div class="mdc-data-table__row">${row.content}</div>`;
|
||||
}
|
||||
if (row.empty) {
|
||||
return html`<div class="mdc-data-table__row"></div>`;
|
||||
return html`<div class="mdc-data-table__row empty-row"></div>`;
|
||||
}
|
||||
return html`
|
||||
<div
|
||||
@ -960,6 +960,13 @@ export class HaDataTable extends LitElement {
|
||||
width: var(--table-row-width, 100%);
|
||||
}
|
||||
|
||||
.mdc-data-table__row.empty-row {
|
||||
height: var(
|
||||
--data-table-empty-row-height,
|
||||
var(--data-table-row-height, 52px)
|
||||
);
|
||||
}
|
||||
|
||||
.mdc-data-table__row ~ .mdc-data-table__row {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
|
@ -222,7 +222,9 @@ export class HaDevicePicker extends LitElement {
|
||||
|
||||
return {
|
||||
id: device.id,
|
||||
name: name,
|
||||
name:
|
||||
name ||
|
||||
this.hass.localize("ui.components.device-picker.unnamed_device"),
|
||||
area:
|
||||
device.area_id && areas[device.area_id]
|
||||
? areas[device.area_id].name
|
||||
|
@ -102,10 +102,10 @@ export class HaDialog extends DialogBase {
|
||||
align-items: var(--vertical-align-dialog, center);
|
||||
}
|
||||
.mdc-dialog__title {
|
||||
padding: 12px 12px 0;
|
||||
padding: 24px 24px 0 24px;
|
||||
}
|
||||
.mdc-dialog--scrollable .mdc-dialog__title {
|
||||
padding: 12px;
|
||||
.mdc-dialog__title:has(span) {
|
||||
padding: 12px 12px 0;
|
||||
}
|
||||
.mdc-dialog__actions {
|
||||
padding: 12px 24px 12px 24px;
|
||||
|
@ -62,6 +62,7 @@ export class HaGauge extends LitElement {
|
||||
if (
|
||||
!this._updated ||
|
||||
(!changedProperties.has("value") &&
|
||||
!changedProperties.has("valueText") &&
|
||||
!changedProperties.has("label") &&
|
||||
!changedProperties.has("_segment_label"))
|
||||
) {
|
||||
|
@ -95,7 +95,7 @@ export class HaPictureUpload extends LitElement {
|
||||
<ha-button
|
||||
@click=${this._handleChangeClick}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.picture-upload.change_picture"
|
||||
"ui.components.picture-upload.clear_picture"
|
||||
)}
|
||||
>
|
||||
</ha-button>
|
||||
|
@ -89,7 +89,7 @@ export class HaServiceControl extends LitElement {
|
||||
@property({ attribute: "show-advanced", type: Boolean }) public showAdvanced =
|
||||
false;
|
||||
|
||||
@property({ attribute: false, type: Boolean, reflect: true })
|
||||
@property({ attribute: "hide-picker", type: Boolean, reflect: true })
|
||||
public hidePicker = false;
|
||||
|
||||
@property({ attribute: "hide-description", type: Boolean })
|
||||
|
@ -117,7 +117,7 @@ class DialogMediaManage extends LitElement {
|
||||
: html`
|
||||
<ha-button
|
||||
class="danger"
|
||||
slot="title"
|
||||
slot="navigationIcon"
|
||||
.disabled=${this._deleting}
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.media-browser.file_management.${
|
||||
@ -212,8 +212,8 @@ class DialogMediaManage extends LitElement {
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.file_management.tip_storage_panel"
|
||||
)}
|
||||
</a>`,
|
||||
)}</a
|
||||
>`,
|
||||
}
|
||||
)}
|
||||
</ha-tip>`
|
||||
|
@ -20,8 +20,8 @@ export class HatGraphNode extends LitElement {
|
||||
@property({ attribute: false, reflect: true, type: Boolean }) notEnabled =
|
||||
false;
|
||||
|
||||
@property({ attribute: false, reflect: true, type: Boolean }) graphStart =
|
||||
false;
|
||||
@property({ attribute: "graph-start", reflect: true, type: Boolean })
|
||||
graphStart = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "nofocus" }) noFocus = false;
|
||||
|
||||
@ -112,7 +112,7 @@ export class HatGraphNode extends LitElement {
|
||||
var(--hat-graph-node-size) + var(--hat-graph-spacing) + 1px
|
||||
);
|
||||
}
|
||||
:host([graphStart]) {
|
||||
:host([graph-start]) {
|
||||
height: calc(var(--hat-graph-node-size) + 2px);
|
||||
}
|
||||
:host([track]) {
|
||||
|
@ -91,7 +91,7 @@ export class HatScriptGraph extends LitElement {
|
||||
}
|
||||
return html`
|
||||
<hat-graph-node
|
||||
graphStart
|
||||
graph-start
|
||||
?track=${track}
|
||||
@focus=${this._selectNode(config, path)}
|
||||
?active=${this.selected === path}
|
||||
@ -354,8 +354,8 @@ export class HatScriptGraph extends LitElement {
|
||||
></hat-graph-node>
|
||||
<div
|
||||
style=${`width: ${NODE_SIZE + SPACING}px;`}
|
||||
graphStart
|
||||
graphEnd
|
||||
graph-start
|
||||
graph-end
|
||||
></div>
|
||||
<div ?track=${trackPass}></div>
|
||||
<hat-graph-node
|
||||
|
@ -737,18 +737,22 @@ const tryDescribeTrigger = (
|
||||
? computeStateName(hass.states[trigger.entity_id])
|
||||
: trigger.entity_id;
|
||||
|
||||
let offsetChoice = trigger.offset.startsWith("-") ? "before" : "after";
|
||||
let offset: string | string[] = trigger.offset.startsWith("-")
|
||||
? trigger.offset.substring(1).split(":")
|
||||
: trigger.offset.split(":");
|
||||
const duration = {
|
||||
hours: offset.length > 0 ? +offset[0] : 0,
|
||||
minutes: offset.length > 1 ? +offset[1] : 0,
|
||||
seconds: offset.length > 2 ? +offset[2] : 0,
|
||||
};
|
||||
offset = formatDurationLong(hass.locale, duration);
|
||||
if (offset === "") {
|
||||
offsetChoice = "other";
|
||||
let offsetChoice: string = "other";
|
||||
let offset: string | string[] = "";
|
||||
if (trigger.offset) {
|
||||
offsetChoice = trigger.offset.startsWith("-") ? "before" : "after";
|
||||
offset = trigger.offset.startsWith("-")
|
||||
? trigger.offset.substring(1).split(":")
|
||||
: trigger.offset.split(":");
|
||||
const duration = {
|
||||
hours: offset.length > 0 ? +offset[0] : 0,
|
||||
minutes: offset.length > 1 ? +offset[1] : 0,
|
||||
seconds: offset.length > 2 ? +offset[2] : 0,
|
||||
};
|
||||
offset = formatDurationLong(hass.locale, duration);
|
||||
if (offset === "") {
|
||||
offsetChoice = "other";
|
||||
}
|
||||
}
|
||||
|
||||
return hass.localize(
|
||||
|
@ -46,6 +46,7 @@ export interface HassioFullBackupCreateParams {
|
||||
name: string;
|
||||
password?: string;
|
||||
confirm_password?: string;
|
||||
background?: boolean;
|
||||
}
|
||||
export interface HassioPartialBackupCreateParams
|
||||
extends HassioFullBackupCreateParams {
|
||||
|
@ -9,7 +9,7 @@ export interface ShowViewConfig {
|
||||
|
||||
export interface LovelaceViewBackgroundConfig {
|
||||
image?: string;
|
||||
transparency?: number;
|
||||
opacity?: number;
|
||||
size?: "auto" | "cover" | "contain";
|
||||
alignment?:
|
||||
| "top left"
|
||||
|
@ -3,7 +3,7 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-dialog";
|
||||
import { createCloseHeading } from "../../components/ha-dialog";
|
||||
import "../../components/ha-formfield";
|
||||
import "../../components/ha-switch";
|
||||
import type { HaSwitch } from "../../components/ha-switch";
|
||||
@ -52,14 +52,14 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${this.hass.localize(
|
||||
"ui.dialogs.config_entry_system_options.title",
|
||||
{
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize("ui.dialogs.config_entry_system_options.title", {
|
||||
integration:
|
||||
this.hass.localize(
|
||||
`component.${this._params.entry.domain}.title`
|
||||
) || this._params.entry.domain,
|
||||
}
|
||||
})
|
||||
)}
|
||||
>
|
||||
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||
|
@ -225,6 +225,7 @@ export const makeDialogManager = (
|
||||
};
|
||||
|
||||
const _handleClosedFocus = async (ev: HASSDomEvent<DialogClosedParams>) => {
|
||||
if (!LOADED[ev.detail.dialog]) return;
|
||||
const closedFocusTargets = LOADED[ev.detail.dialog].closedFocusTargets;
|
||||
delete LOADED[ev.detail.dialog].closedFocusTargets;
|
||||
if (!closedFocusTargets) return;
|
||||
|
@ -99,6 +99,7 @@ class MoreInfoScript extends LitElement {
|
||||
${this.hass.localize("ui.card.script.run_script")}
|
||||
</div>
|
||||
<ha-service-control
|
||||
hide-picker
|
||||
hide-description
|
||||
.hass=${this.hass}
|
||||
.value=${this._scriptData}
|
||||
|
@ -530,7 +530,9 @@ export class QuickBar extends LitElement {
|
||||
? this.hass.areas[device.area_id]
|
||||
: undefined;
|
||||
const deviceItem = {
|
||||
primaryText: computeDeviceName(device, this.hass),
|
||||
primaryText:
|
||||
computeDeviceName(device, this.hass) ||
|
||||
this.hass.localize("ui.components.device-picker.unnamed_device"),
|
||||
deviceId: device.id,
|
||||
area: area?.name,
|
||||
action: () => navigate(`/config/devices/device/${device.id}`),
|
||||
|
@ -322,6 +322,13 @@ class HassTabsSubpage extends LitElement {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
:host([narrow]) .content {
|
||||
height: calc(100% - var(--header-height));
|
||||
height: calc(
|
||||
100% - var(--header-height) - env(safe-area-inset-bottom)
|
||||
);
|
||||
}
|
||||
|
||||
:host([narrow]) .content.tabs {
|
||||
height: calc(100% - 2 * var(--header-height));
|
||||
height: calc(
|
||||
|
@ -614,9 +614,6 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
ha-icon-button {
|
||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||
}
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
@ -649,6 +646,8 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
.disabled-bar {
|
||||
background: var(--divider-color, #e0e0e0);
|
||||
text-align: center;
|
||||
border-top-right-radius: var(--ha-card-border-radius, 12px);
|
||||
border-top-left-radius: var(--ha-card-border-radius, 12px);
|
||||
}
|
||||
|
||||
mwc-list-item[disabled] {
|
||||
|
@ -328,8 +328,7 @@ class DialogAutomationRename extends LitElement implements HassDialog {
|
||||
ha-icon-picker,
|
||||
ha-category-picker,
|
||||
ha-labels-picker,
|
||||
ha-area-picker,
|
||||
ha-chip-set {
|
||||
ha-area-picker {
|
||||
display: block;
|
||||
}
|
||||
ha-icon-picker,
|
||||
|
@ -504,9 +504,6 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
ha-button-menu {
|
||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||
}
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
@ -539,6 +536,8 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
.disabled-bar {
|
||||
background: var(--divider-color, #e0e0e0);
|
||||
text-align: center;
|
||||
border-top-right-radius: var(--ha-card-border-radius, 12px);
|
||||
border-top-left-radius: var(--ha-card-border-radius, 12px);
|
||||
}
|
||||
ha-list-item[disabled] {
|
||||
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
|
||||
@ -560,6 +559,8 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s;
|
||||
text-align: center;
|
||||
border-top-right-radius: var(--ha-card-border-radius, 12px);
|
||||
border-top-left-radius: var(--ha-card-border-radius, 12px);
|
||||
}
|
||||
.testing.active {
|
||||
max-height: 100px;
|
||||
|
@ -27,6 +27,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import { afterNextRender } from "../../../common/util/render-status";
|
||||
import { promiseTimeout } from "../../../common/util/promise-timeout";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-icon";
|
||||
@ -944,8 +945,37 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
|
||||
// wait for automation to appear in entity registry when creating a new automation
|
||||
if (entityRegPromise) {
|
||||
const automation = await entityRegPromise;
|
||||
entityId = automation.entity_id;
|
||||
try {
|
||||
const automation = await promiseTimeout(2000, entityRegPromise);
|
||||
entityId = automation.entity_id;
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.name === "TimeoutError") {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.new_automation_setup_failed_title",
|
||||
{
|
||||
type: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.type_automation"
|
||||
),
|
||||
}
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.new_automation_setup_failed_text",
|
||||
{
|
||||
type: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.type_automation"
|
||||
),
|
||||
types: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.type_automation_plural"
|
||||
),
|
||||
}
|
||||
),
|
||||
warning: true,
|
||||
});
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (entityId) {
|
||||
@ -965,9 +995,9 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
navigate(`/config/automation/edit/${id}`, { replace: true });
|
||||
}
|
||||
} catch (errors: any) {
|
||||
this._errors = errors.body.message || errors.error || errors.body;
|
||||
this._errors = errors.body?.message || errors.error || errors.body;
|
||||
showToast(this, {
|
||||
message: errors.body.message || errors.error || errors.body,
|
||||
message: errors.body?.message || errors.error || errors.body,
|
||||
});
|
||||
throw errors;
|
||||
} finally {
|
||||
|
@ -651,9 +651,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
ha-button-menu {
|
||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||
}
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
@ -686,6 +683,8 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
.disabled-bar {
|
||||
background: var(--divider-color, #e0e0e0);
|
||||
text-align: center;
|
||||
border-top-right-radius: var(--ha-card-border-radius, 12px);
|
||||
border-top-left-radius: var(--ha-card-border-radius, 12px);
|
||||
}
|
||||
.triggered {
|
||||
cursor: pointer;
|
||||
@ -702,6 +701,8 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s;
|
||||
text-align: center;
|
||||
border-top-right-radius: var(--ha-card-border-radius, 12px);
|
||||
border-top-left-radius: var(--ha-card-border-radius, 12px);
|
||||
}
|
||||
.triggered.active {
|
||||
max-height: 100px;
|
||||
|
@ -51,6 +51,9 @@ class HaBackupConfigAgents extends LitElement {
|
||||
|
||||
private _description(agentId: string) {
|
||||
if (agentId === CLOUD_AGENT) {
|
||||
if (this.cloudStatus.logged_in && !this.cloudStatus.active_subscription) {
|
||||
return "You currently do not have an active Home Assistant Cloud subscription.";
|
||||
}
|
||||
return "Note: It stores only one backup with a maximum size of 5 GB, regardless of your settings.";
|
||||
}
|
||||
if (isNetworkMountAgent(agentId)) {
|
||||
@ -72,6 +75,10 @@ class HaBackupConfigAgents extends LitElement {
|
||||
this._agentIds
|
||||
);
|
||||
const description = this._description(agentId);
|
||||
const noCloudSubscription =
|
||||
agentId === CLOUD_AGENT &&
|
||||
this.cloudStatus.logged_in &&
|
||||
!this.cloudStatus.active_subscription;
|
||||
return html`
|
||||
<ha-md-list-item>
|
||||
${isLocalAgent(agentId)
|
||||
@ -107,7 +114,9 @@ class HaBackupConfigAgents extends LitElement {
|
||||
<ha-switch
|
||||
slot="end"
|
||||
id=${agentId}
|
||||
.checked=${this._value.includes(agentId)}
|
||||
.checked=${!noCloudSubscription &&
|
||||
this._value.includes(agentId)}
|
||||
.disabled=${noCloudSubscription}
|
||||
@change=${this._agentToggled}
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
@ -133,7 +142,11 @@ class HaBackupConfigAgents extends LitElement {
|
||||
// Ensure we don't have duplicates, agents exist in the list and cloud is logged in
|
||||
this.value = [...new Set(this.value)]
|
||||
.filter((agent) => this._agentIds.some((id) => id === agent))
|
||||
.filter((id) => id !== CLOUD_AGENT || this.cloudStatus.logged_in);
|
||||
.filter(
|
||||
(id) =>
|
||||
id !== CLOUD_AGENT ||
|
||||
(this.cloudStatus.logged_in && this.cloudStatus.active_subscription)
|
||||
);
|
||||
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
@ -144,9 +157,6 @@ class HaBackupConfigAgents extends LitElement {
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
}
|
||||
ha-md-list-item {
|
||||
--md-item-overflow: visible;
|
||||
}
|
||||
ha-md-list-item img {
|
||||
width: 48px;
|
||||
}
|
||||
|
@ -138,7 +138,8 @@ class HaBackupConfigData extends LitElement {
|
||||
const include_addons = data.addons_mode === "custom" ? data.addons : [];
|
||||
|
||||
this.value = {
|
||||
include_homeassistant: data.homeassistant || this.forceHomeAssistant,
|
||||
include_homeassistant:
|
||||
data.homeassistant || data.database || this.forceHomeAssistant,
|
||||
include_addons: include_addons.length ? include_addons : undefined,
|
||||
include_all_addons: data.addons_mode === "all",
|
||||
include_database: data.database,
|
||||
@ -168,7 +169,7 @@ class HaBackupConfigData extends LitElement {
|
||||
slot="end"
|
||||
@change=${this._switchChanged}
|
||||
.checked=${data.homeassistant}
|
||||
.disabled=${this.forceHomeAssistant}
|
||||
.disabled=${this.forceHomeAssistant || data.database}
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
|
||||
@ -296,7 +297,6 @@ class HaBackupConfigData extends LitElement {
|
||||
...data,
|
||||
[target.id]: target.checked,
|
||||
});
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
|
||||
private _selectChanged(ev: Event) {
|
||||
@ -309,7 +309,6 @@ class HaBackupConfigData extends LitElement {
|
||||
if (target.id === "addons_mode") {
|
||||
this._showAddons = target.value === "custom";
|
||||
}
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
|
||||
private _addonsChanged(ev: CustomEvent) {
|
||||
@ -320,7 +319,6 @@ class HaBackupConfigData extends LitElement {
|
||||
...data,
|
||||
addons,
|
||||
});
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
@ -332,9 +330,6 @@ class HaBackupConfigData extends LitElement {
|
||||
ha-md-select {
|
||||
min-width: 210px;
|
||||
}
|
||||
ha-md-list-item {
|
||||
--md-item-overflow: visible;
|
||||
}
|
||||
@media all and (max-width: 450px) {
|
||||
ha-md-select {
|
||||
min-width: 160px;
|
||||
|
@ -9,6 +9,7 @@ import { showChangeBackupEncryptionKeyDialog } from "../../dialogs/show-dialog-c
|
||||
import { showSetBackupEncryptionKeyDialog } from "../../dialogs/show-dialog-set-backup-encryption-key";
|
||||
|
||||
import { downloadEmergencyKit } from "../../../../../data/backup";
|
||||
import { showShowBackupEncryptionKeyDialog } from "../../dialogs/show-dialog-show-backup-encryption-key";
|
||||
|
||||
@customElement("ha-backup-config-encryption-key")
|
||||
class HaBackupConfigEncryptionKey extends LitElement {
|
||||
@ -34,7 +35,13 @@ class HaBackupConfigEncryptionKey extends LitElement {
|
||||
Download
|
||||
</ha-button>
|
||||
</ha-md-list-item>
|
||||
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">Show my encryption key</span>
|
||||
<span slot="supporting-text">
|
||||
Please keep your encryption key private.
|
||||
</span>
|
||||
<ha-button slot="end" @click=${this._show}>Show</ha-button>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">Change encryption key</span>
|
||||
<span slot="supporting-text">
|
||||
@ -68,6 +75,10 @@ class HaBackupConfigEncryptionKey extends LitElement {
|
||||
downloadEmergencyKit(this.hass, this._value);
|
||||
}
|
||||
|
||||
private _show() {
|
||||
showShowBackupEncryptionKeyDialog(this, { currentKey: this._value });
|
||||
}
|
||||
|
||||
private _change() {
|
||||
showChangeBackupEncryptionKeyDialog(this, {
|
||||
currentKey: this._value,
|
||||
|
@ -323,9 +323,6 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
ha-md-select {
|
||||
min-width: 210px;
|
||||
}
|
||||
ha-md-list-item {
|
||||
--md-item-overflow: visible;
|
||||
}
|
||||
@media all and (max-width: 450px) {
|
||||
ha-md-select {
|
||||
min-width: 160px;
|
||||
|
@ -37,10 +37,7 @@ class HaBackupOverviewBackups extends LitElement {
|
||||
<p>
|
||||
Backups are essential for a reliable smart home. They help protect
|
||||
the work you've put into setting up your smart home, and if the
|
||||
worst happens, you can get back up and running quickly. It is
|
||||
recommended that you create a backup every day. You should keep
|
||||
three backups in at least two different locations, one of which
|
||||
should be off-site.
|
||||
worst happens, you can get back up and running quickly.
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
|
@ -34,7 +34,7 @@ class HaBackupBackupsSummary extends LitElement {
|
||||
const { state: schedule } = config.schedule;
|
||||
|
||||
if (schedule === BackupScheduleState.NEVER) {
|
||||
return "Automatic backups are disabled";
|
||||
return "Automatic backups are not scheduled";
|
||||
}
|
||||
|
||||
let copiesText = "and keep all backups";
|
||||
@ -116,7 +116,7 @@ class HaBackupBackupsSummary extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-card class="my-backups">
|
||||
<div class="card-header">Automatic backups</div>
|
||||
<div class="card-header">Backup settings</div>
|
||||
<div class="card-content">
|
||||
<ha-md-list>
|
||||
<ha-md-list-item
|
||||
@ -128,7 +128,7 @@ class HaBackupBackupsSummary extends LitElement {
|
||||
${this._scheduleDescription(this.config)}
|
||||
</div>
|
||||
<div slot="supporting-text">
|
||||
Schedule and number of backups to keep
|
||||
Automatic backup schedule and retention
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
@ -174,7 +174,7 @@ class HaBackupBackupsSummary extends LitElement {
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button @click=${this._configure}>
|
||||
Configure automatic backups
|
||||
Configure backup settings
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
|
@ -84,22 +84,22 @@ class HaBackupOverviewBackups extends LitElement {
|
||||
|
||||
const lastSuccessfulBackup = this._lastSuccessfulBackup(this.backups);
|
||||
|
||||
const lastSuccessfulBackupDate = lastSuccessfulBackup
|
||||
? new Date(lastSuccessfulBackup.date)
|
||||
: new Date(0);
|
||||
|
||||
const lastAttempt = this.config.last_attempted_automatic_backup
|
||||
? new Date(this.config.last_attempted_automatic_backup)
|
||||
: undefined;
|
||||
|
||||
const lastCompletedBackupDate = this.config.last_completed_automatic_backup
|
||||
? new Date(this.config.last_completed_automatic_backup)
|
||||
: undefined;
|
||||
|
||||
const now = new Date();
|
||||
|
||||
const lastBackupDescription = lastSuccessfulBackup
|
||||
? `Last successful backup ${relativeTime(lastSuccessfulBackupDate, this.hass.locale, now, true)} and stored to ${lastSuccessfulBackup.agent_ids?.length} locations.`
|
||||
? `Last successful backup ${relativeTime(new Date(lastSuccessfulBackup.date), this.hass.locale, now, true)} and stored in ${lastSuccessfulBackup.agent_ids?.length} locations.`
|
||||
: "You have no successful backups.";
|
||||
|
||||
if (lastAttempt && lastAttempt > lastSuccessfulBackupDate) {
|
||||
const lastAttemptDescription = `The last automatic backup trigged ${relativeTime(lastAttempt, this.hass.locale, now, true)} wasn't successful.`;
|
||||
if (lastAttempt && lastAttempt > (lastCompletedBackupDate || 0)) {
|
||||
const lastAttemptDescription = `The last automatic backup triggered ${relativeTime(lastAttempt, this.hass.locale, now, true)} wasn't successful.`;
|
||||
return html`
|
||||
<ha-backup-summary-card
|
||||
heading="Last automatic backup failed"
|
||||
@ -119,6 +119,10 @@ class HaBackupOverviewBackups extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
const nextBackupDescription = this._nextBackupDescription(
|
||||
this.config.schedule.state
|
||||
);
|
||||
|
||||
if (!lastSuccessfulBackup) {
|
||||
return html`
|
||||
<ha-backup-summary-card
|
||||
@ -126,18 +130,20 @@ class HaBackupOverviewBackups extends LitElement {
|
||||
description="You have no automatic backups yet."
|
||||
status="warning"
|
||||
>
|
||||
<ha-md-list>
|
||||
<ha-md-list-item>
|
||||
<ha-svg-icon slot="start" .path=${mdiCalendar}></ha-svg-icon>
|
||||
<span slot="headline">${nextBackupDescription}</span>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
</ha-backup-summary-card>
|
||||
`;
|
||||
}
|
||||
|
||||
const nextBackupDescription = this._nextBackupDescription(
|
||||
this.config.schedule.state
|
||||
);
|
||||
|
||||
const numberOfDays = differenceInDays(
|
||||
// Subtract a few hours to avoid showing as overdue if it's just a few hours (e.g. daylight saving)
|
||||
addHours(now, -OVERDUE_MARGIN_HOURS),
|
||||
lastSuccessfulBackupDate
|
||||
new Date(lastSuccessfulBackup.date)
|
||||
);
|
||||
|
||||
const isOverdue =
|
||||
|
@ -90,7 +90,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
|
||||
|
||||
public showDialog(params: BackupOnboardingDialogParams): void {
|
||||
this._params = params;
|
||||
this._step = STEPS[0];
|
||||
this._step = this._firstStep;
|
||||
this._config = RECOMMENDED_CONFIG;
|
||||
|
||||
const agents: string[] = [];
|
||||
@ -129,6 +129,10 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
|
||||
this._params = undefined;
|
||||
}
|
||||
|
||||
private get _firstStep(): Step {
|
||||
return this._params?.skipWelcome ? STEPS[1] : STEPS[0];
|
||||
}
|
||||
|
||||
private async _done() {
|
||||
if (!this._config) {
|
||||
return;
|
||||
@ -187,7 +191,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
|
||||
}
|
||||
|
||||
const isLastStep = this._step === STEPS[STEPS.length - 1];
|
||||
const isFirstStep = this._step === STEPS[0];
|
||||
const isFirstStep = this._step === this._firstStep;
|
||||
|
||||
return html`
|
||||
<ha-md-dialog disable-cancel-action open @closed=${this.closeDialog}>
|
||||
@ -396,7 +400,10 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
|
||||
}
|
||||
|
||||
private async _copyKeyToClipboard() {
|
||||
await copyToClipboard(this._config!.create_backup.password!);
|
||||
await copyToClipboard(
|
||||
this._config!.create_backup.password!,
|
||||
this.renderRoot.querySelector("div")!
|
||||
);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||
});
|
||||
|
@ -92,7 +92,9 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
|
||||
? "Save current encryption key"
|
||||
: this._step === "new"
|
||||
? "New encryption key"
|
||||
: "";
|
||||
: this._step === "done"
|
||||
? "Save new encryption key"
|
||||
: "";
|
||||
|
||||
return html`
|
||||
<ha-md-dialog disable-cancel-action open @closed=${this.closeDialog}>
|
||||
@ -166,10 +168,22 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
|
||||
case "new":
|
||||
return html`
|
||||
<p>
|
||||
Keep this encryption key in a safe place, as you will need it to
|
||||
access your backup, allowing it to be restored. Either record the
|
||||
All next backups will use the new encryption key. Encryption keeps
|
||||
your backups private and secure.
|
||||
</p>
|
||||
<div class="encryption-key">
|
||||
<p>${this._newEncryptionKey}</p>
|
||||
<ha-icon-button
|
||||
.path=${mdiContentCopy}
|
||||
@click=${this._copyKeyToClipboard}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
`;
|
||||
case "done":
|
||||
return html`<p>
|
||||
Keep this new encryption key in a safe place, as you will need it to
|
||||
access your backups, allowing it to be restored. Either record the
|
||||
characters below or download them as an emergency kit file.
|
||||
Encryption keeps your backups private and secure.
|
||||
</p>
|
||||
<div class="encryption-key">
|
||||
<p>${this._newEncryptionKey}</p>
|
||||
@ -189,24 +203,16 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
|
||||
Download
|
||||
</ha-button>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
`;
|
||||
case "done":
|
||||
return html`
|
||||
<div class="done">
|
||||
<img
|
||||
src="/static/images/voice-assistant/hi.png"
|
||||
alt="Casita Home Assistant logo"
|
||||
/>
|
||||
<p>Encryption key changed</p>
|
||||
</div>
|
||||
`;
|
||||
</ha-md-list>`;
|
||||
}
|
||||
return nothing;
|
||||
}
|
||||
|
||||
private async _copyKeyToClipboard() {
|
||||
await copyToClipboard(this._newEncryptionKey);
|
||||
await copyToClipboard(
|
||||
this._newEncryptionKey,
|
||||
this.renderRoot.querySelector("div")!
|
||||
);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||
});
|
||||
@ -216,7 +222,10 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
|
||||
if (!this._params?.currentKey) {
|
||||
return;
|
||||
}
|
||||
await copyToClipboard(this._params.currentKey);
|
||||
await copyToClipboard(
|
||||
this._params.currentKey,
|
||||
this.renderRoot.querySelector("div")!
|
||||
);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||
});
|
||||
@ -297,13 +306,6 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
.done {
|
||||
text-align: center;
|
||||
font-size: 22px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 28px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@ -100,7 +100,10 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
|
||||
this._agentIds = agents
|
||||
.map((agent) => agent.agent_id)
|
||||
.filter(
|
||||
(id) => id !== CLOUD_AGENT || this._params?.cloudStatus?.logged_in
|
||||
(id) =>
|
||||
id !== CLOUD_AGENT ||
|
||||
(this._params?.cloudStatus?.logged_in &&
|
||||
this._params?.cloudStatus?.active_subscription)
|
||||
)
|
||||
.sort(compareAgents);
|
||||
}
|
||||
@ -200,17 +203,36 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
|
||||
? html`
|
||||
<ha-button
|
||||
@click=${this._submit}
|
||||
.disabled=${!selectedAgents.length}
|
||||
.disabled=${this._formData.agents_mode === "custom" &&
|
||||
!selectedAgents.length}
|
||||
>
|
||||
Create backup
|
||||
</ha-button>
|
||||
`
|
||||
: html`<ha-button @click=${this._nextStep}>Next</ha-button>`}
|
||||
: html`<ha-button
|
||||
@click=${this._nextStep}
|
||||
.disabled=${this._step === "data" && this._noDataSelected}
|
||||
>Next</ha-button
|
||||
>`}
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private get _noDataSelected() {
|
||||
const hassio = isComponentLoaded(this.hass, "hassio");
|
||||
if (
|
||||
this._formData?.data.include_homeassistant ||
|
||||
this._formData?.data.include_database ||
|
||||
(hassio && this._formData?.data.include_folders?.length) ||
|
||||
(hassio && this._formData?.data.include_all_addons) ||
|
||||
(hassio && this._formData?.data.include_addons?.length)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _renderData() {
|
||||
if (!this._formData) {
|
||||
return nothing;
|
||||
|
@ -23,7 +23,10 @@ import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { RestoreBackupDialogParams } from "./show-dialog-restore-backup";
|
||||
import type { RestoreBackupStage } from "../../../../data/backup_manager";
|
||||
import type {
|
||||
RestoreBackupStage,
|
||||
RestoreBackupState,
|
||||
} from "../../../../data/backup_manager";
|
||||
import { subscribeBackupEvents } from "../../../../data/backup_manager";
|
||||
|
||||
type FormData = {
|
||||
@ -52,8 +55,12 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
||||
|
||||
@state() private _userPassword?: string;
|
||||
|
||||
@state() private _usedUserInput = false;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _state?: RestoreBackupState;
|
||||
|
||||
@state() private _stage?: RestoreBackupStage | null;
|
||||
|
||||
@state() private _unsub?: Promise<UnsubscribeFunc>;
|
||||
@ -64,6 +71,11 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
||||
this._params = params;
|
||||
|
||||
this._formData = INITIAL_DATA;
|
||||
this._userPassword = undefined;
|
||||
this._usedUserInput = false;
|
||||
this._error = undefined;
|
||||
this._state = undefined;
|
||||
this._stage = undefined;
|
||||
if (this._params.backup.protected) {
|
||||
this._backupEncryptionKey = await this._fetchEncryptionKey();
|
||||
if (!this._backupEncryptionKey) {
|
||||
@ -85,7 +97,9 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
||||
this._params = undefined;
|
||||
this._backupEncryptionKey = undefined;
|
||||
this._userPassword = undefined;
|
||||
this._usedUserInput = false;
|
||||
this._error = undefined;
|
||||
this._state = undefined;
|
||||
this._stage = undefined;
|
||||
this._step = undefined;
|
||||
this._unsubscribe();
|
||||
@ -149,15 +163,24 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
||||
}
|
||||
|
||||
private _renderEncryption() {
|
||||
return html`<p>
|
||||
${this._userPassword
|
||||
? "The provided encryption key was incorrect, please try again."
|
||||
: this._backupEncryptionKey
|
||||
? "The backup is encrypted with a different key or password than that is saved on this system. Please enter the key for this backup."
|
||||
: "The backup is encrypted. Provide the encryption key to decrypt the backup."}
|
||||
</p>
|
||||
return html`${this._usedUserInput
|
||||
? "The provided encryption key was incorrect, please try again."
|
||||
: this._backupEncryptionKey
|
||||
? html`The Backup is encrypted with a different encryption key than
|
||||
that is saved on this system. Please enter the encryption key for
|
||||
this backup.<br />
|
||||
${this._params!.selectedData.homeassistant_included
|
||||
? html`<ha-alert alert-type="warning"
|
||||
>After restoring the backup, your new backups will be
|
||||
encrypted with the encryption key that was present during
|
||||
the time of this backup.</ha-alert
|
||||
>`
|
||||
: nothing}`
|
||||
: "The backup is encrypted. Provide the encryption key to decrypt the backup."}
|
||||
|
||||
<ha-password-field
|
||||
@change=${this._passwordChanged}
|
||||
@input=${this._passwordChanged}
|
||||
label="Encryption key"
|
||||
.value=${this._userPassword || ""}
|
||||
></ha-password-field>`;
|
||||
}
|
||||
@ -186,16 +209,22 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
||||
|
||||
private async _restoreBackup() {
|
||||
this._unsubscribe();
|
||||
this._state = undefined;
|
||||
this._stage = undefined;
|
||||
this._error = undefined;
|
||||
try {
|
||||
this._step = "progress";
|
||||
window.addEventListener("connection-status", this._connectionStatus);
|
||||
this._subscribeBackupEvents();
|
||||
await this._doRestoreBackup(
|
||||
this._userPassword || this._backupEncryptionKey
|
||||
);
|
||||
} catch (e: any) {
|
||||
this._unsubscribe();
|
||||
await this._unsubscribe();
|
||||
if (e.code === "password_incorrect") {
|
||||
this._error = undefined;
|
||||
if (this._userPassword) {
|
||||
this._usedUserInput = true;
|
||||
}
|
||||
this._step = "encryption";
|
||||
} else {
|
||||
this._error = e.message;
|
||||
@ -203,17 +232,15 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
||||
}
|
||||
}
|
||||
|
||||
private _connectionStatus = (ev) => {
|
||||
if (ev.detail === "connected") {
|
||||
this.closeDialog();
|
||||
}
|
||||
};
|
||||
|
||||
private _subscribeBackupEvents() {
|
||||
this._unsub = subscribeBackupEvents(this.hass!, (event) => {
|
||||
if (event.manager_state === "idle" && this._state === "in_progress") {
|
||||
this.closeDialog();
|
||||
}
|
||||
if (event.manager_state !== "restore_backup") {
|
||||
return;
|
||||
}
|
||||
this._state = event.state;
|
||||
if (event.state === "completed") {
|
||||
this.closeDialog();
|
||||
}
|
||||
@ -227,11 +254,12 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
||||
}
|
||||
|
||||
private _unsubscribe() {
|
||||
window.removeEventListener("connection-status", this._connectionStatus);
|
||||
if (this._unsub) {
|
||||
this._unsub.then((unsub) => unsub());
|
||||
const prom = this._unsub.then((unsub) => unsub());
|
||||
this._unsub = undefined;
|
||||
return prom;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _restoreState() {
|
||||
@ -306,6 +334,14 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
|
||||
ha-circular-progress {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
ha-alert[alert-type="warning"] {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
}
|
||||
ha-password-field {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@ -0,0 +1,174 @@
|
||||
import { mdiClose, mdiContentCopy, mdiDownload } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-icon-button-prev";
|
||||
import "../../../../components/ha-md-dialog";
|
||||
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-password-field";
|
||||
import { downloadEmergencyKit } from "../../../../data/backup";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import type { ShowBackupEncryptionKeyDialogParams } from "./show-dialog-show-backup-encryption-key";
|
||||
|
||||
@customElement("ha-dialog-show-backup-encryption-key")
|
||||
class DialogShowBackupEncryptionKey extends LitElement implements HassDialog {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: ShowBackupEncryptionKeyDialogParams;
|
||||
|
||||
@query("ha-md-dialog") private _dialog!: HaMdDialog;
|
||||
|
||||
public showDialog(params: ShowBackupEncryptionKeyDialogParams): void {
|
||||
this._params = params;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
private _closeDialog() {
|
||||
this._dialog.close();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-md-dialog disable-cancel-action open @closed=${this.closeDialog}>
|
||||
<ha-dialog-header slot="headline">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this.hass.localize("ui.dialogs.generic.close")}
|
||||
.path=${mdiClose}
|
||||
@click=${this._closeDialog}
|
||||
></ha-icon-button>
|
||||
<span slot="title">Encryption key</span>
|
||||
</ha-dialog-header>
|
||||
<div slot="content">
|
||||
<p>
|
||||
Make sure you save the encryption key in a secure place so always
|
||||
have access to your backups.
|
||||
</p>
|
||||
<div class="encryption-key">
|
||||
<p>${this._params?.currentKey}</p>
|
||||
<ha-icon-button
|
||||
.path=${mdiContentCopy}
|
||||
@click=${this._copyKeyToClipboard}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<ha-md-list>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">Download emergency kit</span>
|
||||
<span slot="supporting-text">
|
||||
We recommend saving this encryption key file somewhere secure.
|
||||
</span>
|
||||
<ha-button slot="end" @click=${this._download}>
|
||||
<ha-svg-icon .path=${mdiDownload} slot="icon"></ha-svg-icon>
|
||||
Download
|
||||
</ha-button>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
</div>
|
||||
<div slot="actions">
|
||||
<ha-button @click=${this._closeDialog}>Close</ha-button>
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _copyKeyToClipboard() {
|
||||
if (!this._params?.currentKey) {
|
||||
return;
|
||||
}
|
||||
await copyToClipboard(
|
||||
this._params?.currentKey,
|
||||
this.renderRoot.querySelector("div")!
|
||||
);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||
});
|
||||
}
|
||||
|
||||
private _download() {
|
||||
if (!this._params?.currentKey) {
|
||||
return;
|
||||
}
|
||||
downloadEmergencyKit(this.hass, this._params.currentKey, "old");
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-md-dialog {
|
||||
width: 90vw;
|
||||
max-width: 560px;
|
||||
--dialog-content-padding: 8px 24px;
|
||||
}
|
||||
ha-md-list {
|
||||
background: none;
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
}
|
||||
ha-button.danger {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
.encryption-key {
|
||||
border: 1px solid var(--divider-color);
|
||||
background-color: var(--primary-background-color);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
.encryption-key p {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
font-family: "Roboto Mono", "Consolas", "Menlo", monospace;
|
||||
font-size: 20px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 28px;
|
||||
text-align: center;
|
||||
}
|
||||
.encryption-key ha-icon-button {
|
||||
flex: none;
|
||||
margin: -16px;
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
ha-md-dialog {
|
||||
max-width: none;
|
||||
}
|
||||
div[slot="content"] {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-dialog-show-backup-encryption-key": DialogShowBackupEncryptionKey;
|
||||
}
|
||||
}
|
@ -20,7 +20,6 @@ import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showAlertDialog } from "../../../lovelace/custom-card-helpers";
|
||||
import "../components/ha-backup-agents-picker";
|
||||
import type { UploadBackupDialogParams } from "./show-dialog-upload-backup";
|
||||
|
||||
const SUPPORTED_FORMAT = "application/x-tar";
|
||||
@ -90,6 +89,9 @@ export class DialogUploadBackup
|
||||
<span slot="title">Upload backup</span>
|
||||
</ha-dialog-header>
|
||||
<div slot="content">
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
<ha-file-upload
|
||||
.hass=${this.hass}
|
||||
.uploading=${this._uploading}
|
||||
@ -99,9 +101,6 @@ export class DialogUploadBackup
|
||||
supports="Supports .tar files"
|
||||
@file-picked=${this._filePicked}
|
||||
></ha-file-upload>
|
||||
${this._error
|
||||
? html`<ha-alert alertType="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
</div>
|
||||
<div slot="actions">
|
||||
<ha-button @click=${this.closeDialog}>Cancel</ha-button>
|
||||
@ -161,6 +160,10 @@ export class DialogUploadBackup
|
||||
max-width: 500px;
|
||||
max-height: 100%;
|
||||
}
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ export interface BackupOnboardingDialogParams {
|
||||
submit?: (value: boolean) => void;
|
||||
cancel?: () => void;
|
||||
cloudStatus?: CloudStatus;
|
||||
skipWelcome?: boolean;
|
||||
}
|
||||
|
||||
const loadDialog = () => import("./dialog-backup-onboarding");
|
||||
|
@ -0,0 +1,17 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
|
||||
export interface ShowBackupEncryptionKeyDialogParams {
|
||||
currentKey: string;
|
||||
}
|
||||
|
||||
const loadDialog = () => import("./dialog-show-backup-encryption-key");
|
||||
|
||||
export const showShowBackupEncryptionKeyDialog = (
|
||||
element: HTMLElement,
|
||||
params?: ShowBackupEncryptionKeyDialogParams
|
||||
) =>
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "ha-dialog-show-backup-encryption-key",
|
||||
dialogImport: loadDialog,
|
||||
dialogParams: params,
|
||||
});
|
@ -301,10 +301,10 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
hasFab
|
||||
has-fab
|
||||
.tabs=${[
|
||||
{
|
||||
translationKey: "ui.panel.config.backup.caption",
|
||||
name: "My backups",
|
||||
path: `/config/backup/list`,
|
||||
},
|
||||
]}
|
||||
|
@ -143,6 +143,14 @@ class HaConfigBackupDetails extends LitElement {
|
||||
)}
|
||||
<span slot="supporting-text">Created</span>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${this._backup.protected
|
||||
? "Encrypted AES-128"
|
||||
: "Not encrypted"}
|
||||
</span>
|
||||
<span slot="supporting-text">Protected</span>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
</div>
|
||||
</ha-card>
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { mdiDotsVertical, mdiHarddisk, mdiPlus, mdiUpload } from "@mdi/js";
|
||||
import { mdiDotsVertical, mdiPlus, mdiUpload } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
|
||||
import "../../../components/ha-button";
|
||||
@ -33,7 +32,6 @@ import "./components/overview/ha-backup-overview-settings";
|
||||
import "./components/overview/ha-backup-overview-summary";
|
||||
import { showBackupOnboardingDialog } from "./dialogs/show-dialog-backup_onboarding";
|
||||
import { showGenerateBackupDialog } from "./dialogs/show-dialog-generate-backup";
|
||||
import { showLocalBackupLocationDialog } from "./dialogs/show-dialog-local-backup-location";
|
||||
import { showNewBackupDialog } from "./dialogs/show-dialog-new-backup";
|
||||
import { showUploadBackupDialog } from "./dialogs/show-dialog-upload-backup";
|
||||
|
||||
@ -63,22 +61,15 @@ class HaConfigBackupOverview extends LitElement {
|
||||
await showUploadBackupDialog(this, {});
|
||||
}
|
||||
|
||||
private async _changeLocalLocation(ev) {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
|
||||
showLocalBackupLocationDialog(this, {});
|
||||
}
|
||||
|
||||
private _handleOnboardingButtonClick(ev) {
|
||||
ev.stopPropagation();
|
||||
this._setupAutomaticBackup();
|
||||
this._setupAutomaticBackup(true);
|
||||
}
|
||||
|
||||
private async _setupAutomaticBackup() {
|
||||
private async _setupAutomaticBackup(skipWelcome = false) {
|
||||
const success = await showBackupOnboardingDialog(this, {
|
||||
cloudStatus: this.cloudStatus,
|
||||
skipWelcome,
|
||||
});
|
||||
if (!success) {
|
||||
return;
|
||||
@ -127,15 +118,13 @@ class HaConfigBackupOverview extends LitElement {
|
||||
}
|
||||
|
||||
private get _needsOnboarding() {
|
||||
return !this.config?.create_backup.password;
|
||||
return this.config && !this.config.create_backup.password;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const backupInProgress =
|
||||
"state" in this.manager && this.manager.state === "in_progress";
|
||||
|
||||
const isHassio = isComponentLoaded(this.hass, "hassio");
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
back-path="/config/system"
|
||||
@ -143,34 +132,17 @@ class HaConfigBackupOverview extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.header=${"Backup"}
|
||||
>
|
||||
<div slot="toolbar-icon">
|
||||
<ha-button-menu>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
${isHassio
|
||||
? html`<ha-list-item
|
||||
graphic="icon"
|
||||
@request-selected=${this._changeLocalLocation}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiHarddisk}
|
||||
></ha-svg-icon>
|
||||
Change local location
|
||||
</ha-list-item>`
|
||||
: nothing}
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
@request-selected=${this._uploadBackup}
|
||||
>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiUpload}></ha-svg-icon>
|
||||
Upload backup
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
<ha-button-menu slot="toolbar-icon">
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-list-item graphic="icon" @request-selected=${this._uploadBackup}>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiUpload}></ha-svg-icon>
|
||||
Upload backup
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
<div class="content">
|
||||
${backupInProgress
|
||||
? html`
|
||||
@ -188,22 +160,24 @@ class HaConfigBackupOverview extends LitElement {
|
||||
>
|
||||
</ha-backup-overview-onboarding>
|
||||
`
|
||||
: html`
|
||||
<ha-backup-overview-summary
|
||||
.hass=${this.hass}
|
||||
.backups=${this.backups}
|
||||
.config=${this.config}
|
||||
.fetching=${this.fetching}
|
||||
>
|
||||
</ha-backup-overview-summary>
|
||||
`}
|
||||
: this.config
|
||||
? html`
|
||||
<ha-backup-overview-summary
|
||||
.hass=${this.hass}
|
||||
.backups=${this.backups}
|
||||
.config=${this.config}
|
||||
.fetching=${this.fetching}
|
||||
>
|
||||
</ha-backup-overview-summary>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-backup-overview-backups
|
||||
.hass=${this.hass}
|
||||
.backups=${this.backups}
|
||||
></ha-backup-overview-backups>
|
||||
|
||||
${!this._needsOnboarding
|
||||
${!this._needsOnboarding && this.config
|
||||
? html`
|
||||
<ha-backup-overview-settings
|
||||
.hass=${this.hass}
|
||||
|
@ -1,14 +1,21 @@
|
||||
import { mdiDotsVertical, mdiHarddisk } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import { nextRender } from "../../../common/util/render-status";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-password-field";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { BackupConfig } from "../../../data/backup";
|
||||
import { updateBackupConfig } from "../../../data/backup";
|
||||
import type { CloudStatus } from "../../../data/cloud";
|
||||
@ -20,6 +27,7 @@ import type { BackupConfigData } from "./components/config/ha-backup-config-data
|
||||
import "./components/config/ha-backup-config-encryption-key";
|
||||
import "./components/config/ha-backup-config-schedule";
|
||||
import type { BackupConfigSchedule } from "./components/config/ha-backup-config-schedule";
|
||||
import { showLocalBackupLocationDialog } from "./dialogs/show-dialog-local-backup-location";
|
||||
|
||||
@customElement("ha-config-backup-settings")
|
||||
class HaConfigBackupSettings extends LitElement {
|
||||
@ -91,8 +99,30 @@ class HaConfigBackupSettings extends LitElement {
|
||||
back-path="/config/backup"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${"Automatic backups"}
|
||||
.header=${"Backup settings"}
|
||||
>
|
||||
${isComponentLoaded(this.hass, "hassio")
|
||||
? html`
|
||||
<ha-button-menu slot="toolbar-icon">
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
@request-selected=${this._changeLocalLocation}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiHarddisk}
|
||||
></ha-svg-icon>
|
||||
Change default action location
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<div class="content">
|
||||
<ha-card id="schedule">
|
||||
<div class="card-header">Automatic backups</div>
|
||||
@ -134,6 +164,14 @@ class HaConfigBackupSettings extends LitElement {
|
||||
.cloudStatus=${this.cloudStatus}
|
||||
@value-changed=${this._agentsConfigChanged}
|
||||
></ha-backup-config-agents>
|
||||
${!this._config.create_backup.agent_ids.length
|
||||
? html`<ha-alert
|
||||
alert-type="warning"
|
||||
title="No location selected"
|
||||
>You have to select at least one location to create a
|
||||
backup.</ha-alert
|
||||
><br />`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card>
|
||||
@ -157,6 +195,14 @@ class HaConfigBackupSettings extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _changeLocalLocation(ev) {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
|
||||
showLocalBackupLocationDialog(this, {});
|
||||
}
|
||||
|
||||
private _scheduleConfigChanged(ev) {
|
||||
const value = ev.detail.value as BackupConfigSchedule;
|
||||
this._config = {
|
||||
|
@ -33,7 +33,7 @@ declare global {
|
||||
class HaConfigBackup extends SubscribeMixin(HassRouterPage) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public cloudStatus!: CloudStatus;
|
||||
@property({ attribute: false }) public cloudStatus?: CloudStatus;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
|
@ -32,6 +32,7 @@ import { brandsUrl } from "../../../util/brands-url";
|
||||
import type { Helper, HelperDomain } from "./const";
|
||||
import { isHelperDomain } from "./const";
|
||||
import type { ShowDialogHelperDetailParams } from "./show-dialog-helper-detail";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
type HelperCreators = {
|
||||
[domain in HelperDomain]: {
|
||||
@ -129,6 +130,7 @@ export class DialogHelperDetail extends LitElement {
|
||||
this._error = undefined;
|
||||
this._domain = undefined;
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
|
@ -177,7 +177,7 @@ export class HaConfigLovelaceRescources extends LitElement {
|
||||
.filter=${this._filter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
@row-click=${this._editResource}
|
||||
hasFab
|
||||
has-fab
|
||||
clickable
|
||||
>
|
||||
<ha-fab
|
||||
|
@ -525,6 +525,9 @@ export class HassioNetwork extends LitElement {
|
||||
IP_VERSIONS.forEach((version) => {
|
||||
interfaceOptions[version] = {
|
||||
method: this._interface![version]?.method || "auto",
|
||||
nameservers: this._interface![version]?.nameservers?.filter(
|
||||
(ns: string) => ns.trim()
|
||||
),
|
||||
};
|
||||
if (this._interface![version]?.method === "static") {
|
||||
interfaceOptions[version] = {
|
||||
@ -533,9 +536,6 @@ export class HassioNetwork extends LitElement {
|
||||
(address: string) => address.trim()
|
||||
),
|
||||
gateway: this._interface![version]?.gateway,
|
||||
nameservers: this._interface![version]?.nameservers?.filter(
|
||||
(ns: string) => ns.trim()
|
||||
),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -180,7 +180,7 @@ class DialogPersonDetail extends LitElement implements HassDialog {
|
||||
</ha-settings-row>
|
||||
|
||||
${this._renderUserFields()}
|
||||
${!this._deviceTrackersAvailable(this.hass)
|
||||
${this._deviceTrackersAvailable(this.hass)
|
||||
? html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
|
@ -26,6 +26,7 @@ import { navigate } from "../../../common/navigate";
|
||||
import { slugify } from "../../../common/string/slugify";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import { afterNextRender } from "../../../common/util/render-status";
|
||||
import { promiseTimeout } from "../../../common/util/promise-timeout";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-fab";
|
||||
|
||||
@ -915,17 +916,49 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
|
||||
// wait for new script to appear in entity registry
|
||||
if (entityRegPromise) {
|
||||
const script = await entityRegPromise;
|
||||
entityId = script.entity_id;
|
||||
try {
|
||||
const script = await promiseTimeout(2000, entityRegPromise);
|
||||
entityId = script.entity_id;
|
||||
} catch (e) {
|
||||
entityId = undefined;
|
||||
if (e instanceof Error && e.name === "TimeoutError") {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.new_automation_setup_failed_title",
|
||||
{
|
||||
type: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.type_script"
|
||||
),
|
||||
}
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.new_automation_setup_failed_text",
|
||||
{
|
||||
type: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.type_script"
|
||||
),
|
||||
types: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.type_script_plural"
|
||||
),
|
||||
}
|
||||
),
|
||||
warning: true,
|
||||
});
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await updateEntityRegistryEntry(this.hass, entityId!, {
|
||||
categories: {
|
||||
script: this._entityRegistryUpdate.category || null,
|
||||
},
|
||||
labels: this._entityRegistryUpdate.labels || [],
|
||||
area_id: this._entityRegistryUpdate.area || null,
|
||||
});
|
||||
if (entityId) {
|
||||
await updateEntityRegistryEntry(this.hass, entityId, {
|
||||
categories: {
|
||||
script: this._entityRegistryUpdate.category || null,
|
||||
},
|
||||
labels: this._entityRegistryUpdate.labels || [],
|
||||
area_id: this._entityRegistryUpdate.area || null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this._dirty = false;
|
||||
@ -934,9 +967,9 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
navigate(`/config/script/edit/${id}`, { replace: true });
|
||||
}
|
||||
} catch (errors: any) {
|
||||
this._errors = errors.body.message || errors.error || errors.body;
|
||||
this._errors = errors.body?.message || errors.error || errors.body;
|
||||
showToast(this, {
|
||||
message: errors.body.message || errors.error || errors.body,
|
||||
message: errors.body?.message || errors.error || errors.body,
|
||||
});
|
||||
throw errors;
|
||||
} finally {
|
||||
|
@ -295,9 +295,6 @@ export default class HaScriptFieldRow extends LitElement {
|
||||
ha-icon-button {
|
||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||
}
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
@ -330,6 +327,8 @@ export default class HaScriptFieldRow extends LitElement {
|
||||
.disabled-bar {
|
||||
background: var(--divider-color, #e0e0e0);
|
||||
text-align: center;
|
||||
border-top-right-radius: var(--ha-card-border-radius, 12px);
|
||||
border-top-left-radius: var(--ha-card-border-radius, 12px);
|
||||
}
|
||||
|
||||
ha-list-item[disabled] {
|
||||
|
@ -16,7 +16,7 @@ import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
|
||||
import { cardFeatureStyles } from "./common/card-feature-styles";
|
||||
import type { UpdateActionsCardFeatureConfig } from "./types";
|
||||
|
||||
export const DEFAULT_UPDATE_BACKUP_OPTION = "ask";
|
||||
export const DEFAULT_UPDATE_BACKUP_OPTION = "no";
|
||||
|
||||
export const supportsUpdateActionsCardFeature = (stateObj: HassEntity) => {
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
|
@ -16,7 +16,7 @@ import { getSensorNumericDeviceClasses } from "../../../data/sensor";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { hasConfigOrEntitiesChanged } from "../common/has-changed";
|
||||
import { processConfigEntities } from "../common/process-config-entities";
|
||||
import type { LovelaceCard } from "../types";
|
||||
import type { LovelaceCard, LovelaceGridOptions } from "../types";
|
||||
import type { HistoryGraphCardConfig } from "./types";
|
||||
import { createSearchParam } from "../../../common/url/search-params";
|
||||
|
||||
@ -56,6 +56,14 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
|
||||
return this._config?.title ? 2 : 0 + 2 * (this._entityIds?.length || 1);
|
||||
}
|
||||
|
||||
getGridOptions(): LovelaceGridOptions {
|
||||
return {
|
||||
columns: 12,
|
||||
min_columns: 6,
|
||||
min_rows: (this._config?.entities?.length || 1) * 2,
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: HistoryGraphCardConfig): void {
|
||||
if (!config.entities || !Array.isArray(config.entities)) {
|
||||
throw new Error("Entities need to be an array");
|
||||
|
@ -18,7 +18,7 @@ import type { HomeAssistant } from "../../../types";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { hasConfigOrEntitiesChanged } from "../common/has-changed";
|
||||
import { processConfigEntities } from "../common/process-config-entities";
|
||||
import type { LovelaceCard } from "../types";
|
||||
import type { LovelaceCard, LovelaceGridOptions } from "../types";
|
||||
import type { StatisticsGraphCardConfig } from "./types";
|
||||
|
||||
export const DEFAULT_DAYS_TO_SHOW = 30;
|
||||
@ -93,6 +93,14 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
|
||||
);
|
||||
}
|
||||
|
||||
getGridOptions(): LovelaceGridOptions {
|
||||
return {
|
||||
columns: 12,
|
||||
min_columns: 9,
|
||||
min_rows: 4,
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: StatisticsGraphCardConfig): void {
|
||||
if (!config.entities || !Array.isArray(config.entities)) {
|
||||
throw new Error("Entities need to be an array");
|
||||
|
@ -38,7 +38,7 @@ export class HuiUpdateActionsCardFeatureEditor
|
||||
disabled: !supportsBackup,
|
||||
selector: {
|
||||
select: {
|
||||
default: "yes",
|
||||
default: "no",
|
||||
mode: "dropdown",
|
||||
options: ["ask", "yes", "no"].map((option) => ({
|
||||
value: option,
|
||||
|
@ -37,7 +37,7 @@ export class HuiViewBackgroundEditor extends LitElement {
|
||||
type: "expandable" as const,
|
||||
schema: [
|
||||
{
|
||||
name: "transparency",
|
||||
name: "opacity",
|
||||
selector: {
|
||||
number: { min: 1, max: 100, mode: "slider" },
|
||||
},
|
||||
@ -117,7 +117,7 @@ export class HuiViewBackgroundEditor extends LitElement {
|
||||
|
||||
if (!background) {
|
||||
background = {
|
||||
transparency: 33,
|
||||
opacity: 33,
|
||||
alignment: "center",
|
||||
size: "cover",
|
||||
repeat: "repeat",
|
||||
@ -125,7 +125,7 @@ export class HuiViewBackgroundEditor extends LitElement {
|
||||
};
|
||||
} else {
|
||||
background = {
|
||||
transparency: 100,
|
||||
opacity: 100,
|
||||
alignment: "center",
|
||||
size: "cover",
|
||||
repeat: "no-repeat",
|
||||
@ -162,9 +162,9 @@ export class HuiViewBackgroundEditor extends LitElement {
|
||||
return this.hass.localize(
|
||||
"ui.panel.lovelace.editor.edit_view.background.image"
|
||||
);
|
||||
case "transparency":
|
||||
case "opacity":
|
||||
return this.hass.localize(
|
||||
"ui.panel.lovelace.editor.edit_view.background.transparency"
|
||||
"ui.panel.lovelace.editor.edit_view.background.opacity"
|
||||
);
|
||||
case "alignment":
|
||||
return this.hass.localize(
|
||||
|
@ -67,8 +67,8 @@ export class HUIViewBackground extends LitElement {
|
||||
background?: string | LovelaceViewBackgroundConfig
|
||||
) {
|
||||
if (typeof background === "object" && background.image) {
|
||||
if (background.transparency) {
|
||||
return `${background.transparency}%`;
|
||||
if (background.opacity) {
|
||||
return `${background.opacity}%`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
@ -16,7 +16,7 @@ import "../../layouts/hass-error-screen";
|
||||
import type { HomeAssistant, Route } from "../../types";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
|
||||
export const getMyRedirects = (hasSupervisor: boolean): Redirects => ({
|
||||
export const getMyRedirects = (): Redirects => ({
|
||||
application_credentials: {
|
||||
redirect: "/config/application_credentials",
|
||||
},
|
||||
@ -244,16 +244,24 @@ export const getMyRedirects = (hasSupervisor: boolean): Redirects => ({
|
||||
redirect: "/media-browser",
|
||||
},
|
||||
backup: {
|
||||
component: hasSupervisor ? "hassio" : "backup",
|
||||
redirect: hasSupervisor ? "/hassio/backups" : "/config/backup",
|
||||
component: "backup",
|
||||
redirect: "/config/backup",
|
||||
},
|
||||
backup_list: {
|
||||
component: "backup",
|
||||
redirect: "/config/backup/backups",
|
||||
},
|
||||
backup_config: {
|
||||
component: "backup",
|
||||
redirect: "/config/backup/settings",
|
||||
},
|
||||
supervisor_snapshots: {
|
||||
component: hasSupervisor ? "hassio" : "backup",
|
||||
redirect: hasSupervisor ? "/hassio/backups" : "/config/backup",
|
||||
component: "backup",
|
||||
redirect: "/config/backup",
|
||||
},
|
||||
supervisor_backups: {
|
||||
component: hasSupervisor ? "hassio" : "backup",
|
||||
redirect: hasSupervisor ? "/hassio/backups" : "/config/backup",
|
||||
component: "backup",
|
||||
redirect: "/config/backup",
|
||||
},
|
||||
supervisor_system: {
|
||||
// Moved from Supervisor panel in 2022.5
|
||||
@ -278,10 +286,8 @@ export const getMyRedirects = (hasSupervisor: boolean): Redirects => ({
|
||||
},
|
||||
});
|
||||
|
||||
const getRedirect = (
|
||||
path: string,
|
||||
hasSupervisor: boolean
|
||||
): Redirect | undefined => getMyRedirects(hasSupervisor)?.[path];
|
||||
const getRedirect = (path: string): Redirect | undefined =>
|
||||
getMyRedirects()?.[path];
|
||||
|
||||
export type ParamType = "url" | "string" | "string?";
|
||||
|
||||
@ -314,7 +320,7 @@ class HaPanelMy extends LitElement {
|
||||
const path = this.route.path.substring(1);
|
||||
const hasSupervisor = isComponentLoaded(this.hass, "hassio");
|
||||
|
||||
this._redirect = getRedirect(path, hasSupervisor);
|
||||
this._redirect = getRedirect(path);
|
||||
|
||||
if (path.startsWith("supervisor") && this._redirect === undefined) {
|
||||
if (!hasSupervisor) {
|
||||
|
@ -150,9 +150,7 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
|
||||
|
||||
const myPanel = await import("../panels/my/ha-panel-my");
|
||||
|
||||
for (const [slug, redirect] of Object.entries(
|
||||
myPanel.getMyRedirects(isHassio)
|
||||
)) {
|
||||
for (const [slug, redirect] of Object.entries(myPanel.getMyRedirects())) {
|
||||
if (targetPath.startsWith(redirect.redirect)) {
|
||||
myParams.append("redirect", slug);
|
||||
if (redirect.params) {
|
||||
|
@ -651,6 +651,7 @@
|
||||
"no_devices": "You don't have any devices",
|
||||
"no_match": "No matching devices found",
|
||||
"device": "Device",
|
||||
"unnamed_device": "Unnamed device",
|
||||
"no_area": "No area"
|
||||
},
|
||||
"category-picker": {
|
||||
@ -755,7 +756,7 @@
|
||||
},
|
||||
"picture-upload": {
|
||||
"label": "Add picture",
|
||||
"change_picture": "Change picture",
|
||||
"clear_picture": "Clear picture",
|
||||
"current_image_alt": "Current picture",
|
||||
"supported_formats": "Supports JPEG, PNG, or GIF image.",
|
||||
"unsupported_format": "Unsupported format, please choose a JPEG, PNG, or GIF image.",
|
||||
@ -830,7 +831,8 @@
|
||||
"source_history": "Source: History",
|
||||
"source_stats": "Source: Long term statistics",
|
||||
"zoom_hint": "Use ctrl + scroll to zoom in/out",
|
||||
"zoom_hint_mac": "Use ⌘ + scroll to zoom in/out"
|
||||
"zoom_hint_mac": "Use ⌘ + scroll to zoom in/out",
|
||||
"zoom_reset": "Reset zoom"
|
||||
},
|
||||
"map": {
|
||||
"error": "Unable to load map"
|
||||
@ -2213,7 +2215,7 @@
|
||||
},
|
||||
"dialogs": {
|
||||
"local_backup_location": {
|
||||
"title": "Change local backup location",
|
||||
"title": "Change default local backup location",
|
||||
"description": "Change the default location where local backups are stored on your Home Assistant instance.",
|
||||
"note": "This location will be used when you create a backup using the supervisor actions in an automation for example.",
|
||||
"options": {
|
||||
@ -3063,6 +3065,12 @@
|
||||
"unknown_entity": "unknown entity",
|
||||
"edit_unknown_device": "Editor not available for unknown device",
|
||||
"switch_ui_yaml_error": "There are currently YAML errors in the automation, and it cannot be parsed. Switching to UI mode may cause pending changes to be lost. Press cancel to correct any errors before proceeding to prevent loss of pending changes, or continue if you are sure.",
|
||||
"type_automation": "automation",
|
||||
"type_script": "script",
|
||||
"type_automation_plural": "[%key:ui::panel::config::blueprint::overview::types_plural::automation%]",
|
||||
"type_script_plural": "[%key:ui::panel::config::blueprint::overview::types_plural::script%]",
|
||||
"new_automation_setup_failed_title": "New {type} setup failed",
|
||||
"new_automation_setup_failed_text": "Your new {type} has saved, but waiting for it to setup has timed out. This could be due to errors parsing your configuration.yaml, please check the configuration in developer tools. Your {type} will not be visible until this is corrected, and {types} are reloaded. Changes to area, category, or labels were not saved and must be reapplied.",
|
||||
"triggers": {
|
||||
"name": "Triggers",
|
||||
"header": "When",
|
||||
@ -5955,7 +5963,7 @@
|
||||
"bottom right": "Bottom right"
|
||||
}
|
||||
},
|
||||
"transparency": "Background transparency",
|
||||
"opacity": "Background opacity",
|
||||
"repeat": {
|
||||
"name": "Background repeat",
|
||||
"options": {
|
||||
|
69
yarn.lock
69
yarn.lock
@ -1801,6 +1801,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@gfx/zopfli@npm:^1.0.15":
|
||||
version: 1.0.15
|
||||
resolution: "@gfx/zopfli@npm:1.0.15"
|
||||
dependencies:
|
||||
base64-js: "npm:^1.3.0"
|
||||
checksum: 10/2721ad8c0cbbdac7d5ca9e01ad05f232b4e3cdcecf88f9b0ef9a2bdc7d05e1ca54ea68905430cf36338ef1077acec178aa7b258c67baa7a4c2b6d74067605723
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@gulpjs/messages@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "@gulpjs/messages@npm:1.1.0"
|
||||
@ -5684,6 +5693,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"any-promise@npm:^1.1.0":
|
||||
version: 1.3.0
|
||||
resolution: "any-promise@npm:1.3.0"
|
||||
checksum: 10/6737469ba353b5becf29e4dc3680736b9caa06d300bda6548812a8fee63ae7d336d756f88572fa6b5219aed36698d808fa55f62af3e7e6845c7a1dc77d240edb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"anymatch@npm:^3.1.3, anymatch@npm:~3.1.2":
|
||||
version: 3.1.3
|
||||
resolution: "anymatch@npm:3.1.3"
|
||||
@ -6030,7 +6046,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"base64-js@npm:^1.3.1":
|
||||
"base64-js@npm:^1.3.0, base64-js@npm:^1.3.1":
|
||||
version: 1.5.1
|
||||
resolution: "base64-js@npm:1.5.1"
|
||||
checksum: 10/669632eb3745404c2f822a18fc3a0122d2f9a7a13f7fb8b5823ee19d1d2ff9ee5b52c53367176ea4ad093c332fd5ab4bd0ebae5a8e27917a4105a4cfc86b1005
|
||||
@ -6237,7 +6253,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bytes@npm:3.1.2":
|
||||
"bytes@npm:3.1.2, bytes@npm:^3.1.2":
|
||||
version: 3.1.2
|
||||
resolution: "bytes@npm:3.1.2"
|
||||
checksum: 10/a10abf2ba70c784471d6b4f58778c0beeb2b5d405148e66affa91f23a9f13d07603d0a0354667310ae1d6dc141474ffd44e2a074be0f6e2254edb8fc21445388
|
||||
@ -7070,7 +7086,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"defaults@npm:^1.0.3":
|
||||
"defaults@npm:^1.0.3, defaults@npm:^1.0.4":
|
||||
version: 1.0.4
|
||||
resolution: "defaults@npm:1.0.4"
|
||||
dependencies:
|
||||
@ -8180,7 +8196,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fancy-log@npm:2.0.0":
|
||||
"fancy-log@npm:2.0.0, fancy-log@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "fancy-log@npm:2.0.0"
|
||||
dependencies:
|
||||
@ -8973,6 +8989,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"gulp-zopfli-green@npm:6.0.2":
|
||||
version: 6.0.2
|
||||
resolution: "gulp-zopfli-green@npm:6.0.2"
|
||||
dependencies:
|
||||
"@gfx/zopfli": "npm:^1.0.15"
|
||||
bytes: "npm:^3.1.2"
|
||||
defaults: "npm:^1.0.4"
|
||||
fancy-log: "npm:^2.0.0"
|
||||
plugin-error: "npm:^2.0.1"
|
||||
stream-to-array: "npm:^2.3.0"
|
||||
through2: "npm:^4.0.2"
|
||||
checksum: 10/52e899dfb86777ff8f97a23af99c59e203ea485fbf04d0a8f4f1cfbd4d4c496808a3593ae8dac16584fc4b4d81cf127b2eda5355a61bcc213875c95cc86d41da
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"gulp@npm:5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "gulp@npm:5.0.0"
|
||||
@ -9254,6 +9285,7 @@ __metadata:
|
||||
gulp-brotli: "npm:3.0.0"
|
||||
gulp-json-transform: "npm:0.5.0"
|
||||
gulp-rename: "npm:2.0.0"
|
||||
gulp-zopfli-green: "npm:6.0.2"
|
||||
hls.js: "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch"
|
||||
home-assistant-js-websocket: "npm:9.4.0"
|
||||
html-minifier-terser: "npm:7.2.0"
|
||||
@ -12161,6 +12193,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"plugin-error@npm:^2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "plugin-error@npm:2.0.1"
|
||||
dependencies:
|
||||
ansi-colors: "npm:^1.0.1"
|
||||
checksum: 10/9a4f91461cd24cce401112098969991d7aa6b4c94f78e0381234280c07da779570a8b21ab143292b534ec0117c09705a67e5d756c1c303d4706fdd7f861bf5bc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pngjs@npm:^3.0.0, pngjs@npm:^3.3.3":
|
||||
version: 3.4.0
|
||||
resolution: "pngjs@npm:3.4.0"
|
||||
@ -12383,7 +12424,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"readable-stream@npm:2 || 3, readable-stream@npm:^3.0.6, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0":
|
||||
"readable-stream@npm:2 || 3, readable-stream@npm:3, readable-stream@npm:^3.0.6, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0":
|
||||
version: 3.6.2
|
||||
resolution: "readable-stream@npm:3.6.2"
|
||||
dependencies:
|
||||
@ -13566,6 +13607,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"stream-to-array@npm:^2.3.0":
|
||||
version: 2.3.0
|
||||
resolution: "stream-to-array@npm:2.3.0"
|
||||
dependencies:
|
||||
any-promise: "npm:^1.1.0"
|
||||
checksum: 10/7feaf63b38399b850615e6ffcaa951e96e4c8f46745dbce4b553a94c5dc43966933813747014935a3ff97793e7f30a65270bde19f82b2932871a1879229a77cf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"streamx@npm:^2.12.0, streamx@npm:^2.12.5, streamx@npm:^2.13.2, streamx@npm:^2.14.0":
|
||||
version: 2.21.1
|
||||
resolution: "streamx@npm:2.21.1"
|
||||
@ -13993,6 +14043,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"through2@npm:^4.0.2":
|
||||
version: 4.0.2
|
||||
resolution: "through2@npm:4.0.2"
|
||||
dependencies:
|
||||
readable-stream: "npm:3"
|
||||
checksum: 10/72c246233d9a989bbebeb6b698ef0b7b9064cb1c47930f79b25d87b6c867e075432811f69b7b2ac8da00ca308191c507bdab913944be8019ac43b036ce88f6ba
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"thunky@npm:^1.0.2":
|
||||
version: 1.1.0
|
||||
resolution: "thunky@npm:1.1.0"
|
||||
|
Loading…
x
Reference in New Issue
Block a user