diff --git a/build-scripts/gulp/compress.js b/build-scripts/gulp/compress.js index e1fff793a6..81e1c87abe 100644 --- a/build-scripts/gulp/compress.js +++ b/build-scripts/gulp/compress.js @@ -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 + ) ); diff --git a/hassio/src/dialogs/backup/dialog-hassio-backup.ts b/hassio/src/dialogs/backup/dialog-hassio-backup.ts index 91b6643094..60e7cc2534 100644 --- a/hassio/src/dialogs/backup/dialog-hassio-backup.ts +++ b/hassio/src/dialogs/backup/dialog-hassio-backup.ts @@ -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) ); diff --git a/package.json b/package.json index bb9054c920..8e08c3ecce 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pyproject.toml b/pyproject.toml index acf2d514e8..43d693b7b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/src/common/array/ensure-array.ts b/src/common/array/ensure-array.ts index 360024f6ab..dadfa73235 100644 --- a/src/common/array/ensure-array.ts +++ b/src/common/array/ensure-array.ts @@ -1,14 +1,19 @@ -type NonUndefined = T extends undefined ? never : T; +type NonNullUndefined = 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(value: T | T[]): NonUndefined[]; -export function ensureArray(value: T | readonly T[]): NonUndefined[]; +export function ensureArray(value: null): null; +export function ensureArray(value: T | T[]): NonNullUndefined[]; +export function ensureArray(value: T | readonly T[]): NonNullUndefined[]; export function ensureArray(value) { - if (value === undefined || Array.isArray(value)) { + if (value === undefined || value === null || Array.isArray(value)) { return value; } return [value]; diff --git a/src/common/navigate.ts b/src/common/navigate.ts index a134cf57f1..da636bf58b 100644 --- a/src/common/navigate.ts +++ b/src/common/navigate.ts @@ -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((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); }); }); } diff --git a/src/common/util/copy-clipboard.ts b/src/common/util/copy-clipboard.ts index 1708858c85..c50ad02418 100644 --- a/src/common/util/copy-clipboard.ts +++ b/src/common/util/copy-clipboard.ts @@ -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); }; diff --git a/src/common/util/promise-timeout.ts b/src/common/util/promise-timeout.ts index 43b3359026..c38acce11a 100644 --- a/src/common/util/promise-timeout.ts +++ b/src/common/util/promise-timeout.ts @@ -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) => { const timeout = new Promise((_resolve, reject) => { setTimeout(() => { - reject(`Timed out in ${ms} ms.`); + reject(new TimeoutError(ms)); }, ms); }); diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 22060e0b42..017a5dab07 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -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")} + ${this._isZoomed && this.chartType !== "timeline" + ? html`` + : nothing} ${this._tooltip ? html`
{ 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); + } `; } } diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index 4278562171..af1f1f358b 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -515,7 +515,7 @@ export class HaDataTable extends LitElement { return html`
${row.content}
`; } if (row.empty) { - return html`
`; + return html`
`; } return html`
diff --git a/src/components/ha-service-control.ts b/src/components/ha-service-control.ts index 4bb9449323..5dd4222b23 100644 --- a/src/components/ha-service-control.ts +++ b/src/components/ha-service-control.ts @@ -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 }) diff --git a/src/components/media-player/dialog-media-manage.ts b/src/components/media-player/dialog-media-manage.ts index dde5bcc547..543adb3aa5 100644 --- a/src/components/media-player/dialog-media-manage.ts +++ b/src/components/media-player/dialog-media-manage.ts @@ -117,7 +117,7 @@ class DialogMediaManage extends LitElement { : html` ${this.hass.localize( "ui.components.media-browser.file_management.tip_storage_panel" - )} - `, + )}`, } )} ` diff --git a/src/components/trace/hat-graph-node.ts b/src/components/trace/hat-graph-node.ts index de9e765607..994f1dc68a 100644 --- a/src/components/trace/hat-graph-node.ts +++ b/src/components/trace/hat-graph-node.ts @@ -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]) { diff --git a/src/components/trace/hat-script-graph.ts b/src/components/trace/hat-script-graph.ts index e0e9c1f7d1..1bc7f057c1 100644 --- a/src/components/trace/hat-script-graph.ts +++ b/src/components/trace/hat-script-graph.ts @@ -91,7 +91,7 @@ export class HatScriptGraph extends LitElement { } return html`
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( diff --git a/src/data/hassio/backup.ts b/src/data/hassio/backup.ts index 8a88e1a5b9..0257128bf7 100644 --- a/src/data/hassio/backup.ts +++ b/src/data/hassio/backup.ts @@ -46,6 +46,7 @@ export interface HassioFullBackupCreateParams { name: string; password?: string; confirm_password?: string; + background?: boolean; } export interface HassioPartialBackupCreateParams extends HassioFullBackupCreateParams { diff --git a/src/data/lovelace/config/view.ts b/src/data/lovelace/config/view.ts index 89a96b1a54..915c5545cb 100644 --- a/src/data/lovelace/config/view.ts +++ b/src/data/lovelace/config/view.ts @@ -9,7 +9,7 @@ export interface ShowViewConfig { export interface LovelaceViewBackgroundConfig { image?: string; - transparency?: number; + opacity?: number; size?: "auto" | "cover" | "contain"; alignment?: | "top left" diff --git a/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts b/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts index d6334169e5..85f30279d3 100644 --- a/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts +++ b/src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts @@ -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 { ${this._error ? html`
${this._error}
` : ""} diff --git a/src/dialogs/make-dialog-manager.ts b/src/dialogs/make-dialog-manager.ts index 5dd52af166..6a780e0189 100644 --- a/src/dialogs/make-dialog-manager.ts +++ b/src/dialogs/make-dialog-manager.ts @@ -225,6 +225,7 @@ export const makeDialogManager = ( }; const _handleClosedFocus = async (ev: HASSDomEvent) => { + if (!LOADED[ev.detail.dialog]) return; const closedFocusTargets = LOADED[ev.detail.dialog].closedFocusTargets; delete LOADED[ev.detail.dialog].closedFocusTargets; if (!closedFocusTargets) return; diff --git a/src/dialogs/more-info/controls/more-info-script.ts b/src/dialogs/more-info/controls/more-info-script.ts index cacaeeca3d..aba8253a0e 100644 --- a/src/dialogs/more-info/controls/more-info-script.ts +++ b/src/dialogs/more-info/controls/more-info-script.ts @@ -99,6 +99,7 @@ class MoreInfoScript extends LitElement { ${this.hass.localize("ui.card.script.run_script")}
navigate(`/config/devices/device/${device.id}`), diff --git a/src/layouts/hass-tabs-subpage.ts b/src/layouts/hass-tabs-subpage.ts index b69610b5e3..ca930a01b5 100644 --- a/src/layouts/hass-tabs-subpage.ts +++ b/src/layouts/hass-tabs-subpage.ts @@ -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( diff --git a/src/panels/config/automation/action/ha-automation-action-row.ts b/src/panels/config/automation/action/ha-automation-action-row.ts index ebccfa306e..c36f1be56f 100644 --- a/src/panels/config/automation/action/ha-automation-action-row.ts +++ b/src/panels/config/automation/action/ha-automation-action-row.ts @@ -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] { diff --git a/src/panels/config/automation/automation-rename-dialog/dialog-automation-rename.ts b/src/panels/config/automation/automation-rename-dialog/dialog-automation-rename.ts index df0b7aa1d0..f9a829d618 100644 --- a/src/panels/config/automation/automation-rename-dialog/dialog-automation-rename.ts +++ b/src/panels/config/automation/automation-rename-dialog/dialog-automation-rename.ts @@ -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, diff --git a/src/panels/config/automation/condition/ha-automation-condition-row.ts b/src/panels/config/automation/condition/ha-automation-condition-row.ts index 8df41296e8..ded7354eca 100644 --- a/src/panels/config/automation/condition/ha-automation-condition-row.ts +++ b/src/panels/config/automation/condition/ha-automation-condition-row.ts @@ -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; diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index 26fe6cc0f6..05df54facf 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -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 { diff --git a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts index 13efcbf7e7..43c8758e61 100644 --- a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts +++ b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts @@ -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; diff --git a/src/panels/config/backup/components/config/ha-backup-config-agents.ts b/src/panels/config/backup/components/config/ha-backup-config-agents.ts index 6a15f55c94..3a1706f3d4 100644 --- a/src/panels/config/backup/components/config/ha-backup-config-agents.ts +++ b/src/panels/config/backup/components/config/ha-backup-config-agents.ts @@ -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` ${isLocalAgent(agentId) @@ -107,7 +114,9 @@ class HaBackupConfigAgents extends LitElement { @@ -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; } diff --git a/src/panels/config/backup/components/config/ha-backup-config-data.ts b/src/panels/config/backup/components/config/ha-backup-config-data.ts index bae085447a..5c3dce968b 100644 --- a/src/panels/config/backup/components/config/ha-backup-config-data.ts +++ b/src/panels/config/backup/components/config/ha-backup-config-data.ts @@ -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} > @@ -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; diff --git a/src/panels/config/backup/components/config/ha-backup-config-encryption-key.ts b/src/panels/config/backup/components/config/ha-backup-config-encryption-key.ts index 0298a6c5bf..4254215d55 100644 --- a/src/panels/config/backup/components/config/ha-backup-config-encryption-key.ts +++ b/src/panels/config/backup/components/config/ha-backup-config-encryption-key.ts @@ -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 - + + Show my encryption key + + Please keep your encryption key private. + + Show + Change encryption key @@ -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, diff --git a/src/panels/config/backup/components/config/ha-backup-config-schedule.ts b/src/panels/config/backup/components/config/ha-backup-config-schedule.ts index f7c9e2e50d..0a48fc9a0e 100644 --- a/src/panels/config/backup/components/config/ha-backup-config-schedule.ts +++ b/src/panels/config/backup/components/config/ha-backup-config-schedule.ts @@ -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; diff --git a/src/panels/config/backup/components/overview/ha-backup-overview-onboarding.ts b/src/panels/config/backup/components/overview/ha-backup-overview-onboarding.ts index ebce3ea554..a094cad22d 100644 --- a/src/panels/config/backup/components/overview/ha-backup-overview-onboarding.ts +++ b/src/panels/config/backup/components/overview/ha-backup-overview-onboarding.ts @@ -37,10 +37,7 @@ class HaBackupOverviewBackups extends LitElement {

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.

diff --git a/src/panels/config/backup/components/overview/ha-backup-overview-settings.ts b/src/panels/config/backup/components/overview/ha-backup-overview-settings.ts index 5936937b19..3b3f56cdfb 100644 --- a/src/panels/config/backup/components/overview/ha-backup-overview-settings.ts +++ b/src/panels/config/backup/components/overview/ha-backup-overview-settings.ts @@ -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` -
Automatic backups
+
Backup settings
- Schedule and number of backups to keep + Automatic backup schedule and retention
@@ -174,7 +174,7 @@ class HaBackupBackupsSummary extends LitElement {
- Configure automatic backups + Configure backup settings
diff --git a/src/panels/config/backup/components/overview/ha-backup-overview-summary.ts b/src/panels/config/backup/components/overview/ha-backup-overview-summary.ts index 1d63ac875e..e6e4d78195 100644 --- a/src/panels/config/backup/components/overview/ha-backup-overview-summary.ts +++ b/src/panels/config/backup/components/overview/ha-backup-overview-summary.ts @@ -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` + + + + ${nextBackupDescription} + + `; } - 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 = diff --git a/src/panels/config/backup/dialogs/dialog-backup-onboarding.ts b/src/panels/config/backup/dialogs/dialog-backup-onboarding.ts index 7719ebd066..ab66157e46 100644 --- a/src/panels/config/backup/dialogs/dialog-backup-onboarding.ts +++ b/src/panels/config/backup/dialogs/dialog-backup-onboarding.ts @@ -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` @@ -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"), }); diff --git a/src/panels/config/backup/dialogs/dialog-change-backup-encryption-key.ts b/src/panels/config/backup/dialogs/dialog-change-backup-encryption-key.ts index 1b500b34f9..c95fc077c1 100644 --- a/src/panels/config/backup/dialogs/dialog-change-backup-encryption-key.ts +++ b/src/panels/config/backup/dialogs/dialog-change-backup-encryption-key.ts @@ -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` @@ -166,10 +168,22 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog { case "new": return html`

- 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. +

+
+

${this._newEncryptionKey}

+ +
+ `; + case "done": + return html`

+ 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.

${this._newEncryptionKey}

@@ -189,24 +203,16 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog { Download - - `; - case "done": - return html` -
- Casita Home Assistant logo -

Encryption key changed

-
- `; + `; } 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; - } `, ]; } diff --git a/src/panels/config/backup/dialogs/dialog-generate-backup.ts b/src/panels/config/backup/dialogs/dialog-generate-backup.ts index 95ddb89809..b8c64c1255 100644 --- a/src/panels/config/backup/dialogs/dialog-generate-backup.ts +++ b/src/panels/config/backup/dialogs/dialog-generate-backup.ts @@ -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` Create backup ` - : html`Next`} + : html`Next`}
`; } + 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; diff --git a/src/panels/config/backup/dialogs/dialog-restore-backup.ts b/src/panels/config/backup/dialogs/dialog-restore-backup.ts index 18a4b68a08..ecf6a6f156 100644 --- a/src/panels/config/backup/dialogs/dialog-restore-backup.ts +++ b/src/panels/config/backup/dialogs/dialog-restore-backup.ts @@ -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; @@ -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`

- ${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."} -

+ 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.
+ ${this._params!.selectedData.homeassistant_included + ? html`After restoring the backup, your new backups will be + encrypted with the encryption key that was present during + the time of this backup.` + : nothing}` + : "The backup is encrypted. Provide the encryption key to decrypt the backup."} + `; } @@ -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; + } `, ]; } diff --git a/src/panels/config/backup/dialogs/dialog-show-backup-encryption-key.ts b/src/panels/config/backup/dialogs/dialog-show-backup-encryption-key.ts new file mode 100644 index 0000000000..5861ad35a7 --- /dev/null +++ b/src/panels/config/backup/dialogs/dialog-show-backup-encryption-key.ts @@ -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` + + + + Encryption key + +
+

+ Make sure you save the encryption key in a secure place so always + have access to your backups. +

+
+

${this._params?.currentKey}

+ +
+ + + Download emergency kit + + We recommend saving this encryption key file somewhere secure. + + + + Download + + + +
+
+ Close +
+
+ `; + } + + 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; + } +} diff --git a/src/panels/config/backup/dialogs/dialog-upload-backup.ts b/src/panels/config/backup/dialogs/dialog-upload-backup.ts index a9b3e9262c..e1258a0f51 100644 --- a/src/panels/config/backup/dialogs/dialog-upload-backup.ts +++ b/src/panels/config/backup/dialogs/dialog-upload-backup.ts @@ -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 Upload backup
+ ${this._error + ? html`${this._error}` + : nothing} - ${this._error - ? html`${this._error}` - : nothing}
Cancel @@ -161,6 +160,10 @@ export class DialogUploadBackup max-width: 500px; max-height: 100%; } + ha-alert { + display: block; + margin-bottom: 16px; + } `, ]; } diff --git a/src/panels/config/backup/dialogs/show-dialog-backup_onboarding.ts b/src/panels/config/backup/dialogs/show-dialog-backup_onboarding.ts index c411d6f0d7..0602077fc1 100644 --- a/src/panels/config/backup/dialogs/show-dialog-backup_onboarding.ts +++ b/src/panels/config/backup/dialogs/show-dialog-backup_onboarding.ts @@ -5,6 +5,7 @@ export interface BackupOnboardingDialogParams { submit?: (value: boolean) => void; cancel?: () => void; cloudStatus?: CloudStatus; + skipWelcome?: boolean; } const loadDialog = () => import("./dialog-backup-onboarding"); diff --git a/src/panels/config/backup/dialogs/show-dialog-show-backup-encryption-key.ts b/src/panels/config/backup/dialogs/show-dialog-show-backup-encryption-key.ts new file mode 100644 index 0000000000..e69d7ae774 --- /dev/null +++ b/src/panels/config/backup/dialogs/show-dialog-show-backup-encryption-key.ts @@ -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, + }); diff --git a/src/panels/config/backup/ha-config-backup-backups.ts b/src/panels/config/backup/ha-config-backup-backups.ts index f6063ba21c..18d65557f0 100644 --- a/src/panels/config/backup/ha-config-backup-backups.ts +++ b/src/panels/config/backup/ha-config-backup-backups.ts @@ -301,10 +301,10 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { return html` Created + + + ${this._backup.protected + ? "Encrypted AES-128" + : "Not encrypted"} + + Protected +
diff --git a/src/panels/config/backup/ha-config-backup-overview.ts b/src/panels/config/backup/ha-config-backup-overview.ts index 0a132b799f..26e86a2105 100644 --- a/src/panels/config/backup/ha-config-backup-overview.ts +++ b/src/panels/config/backup/ha-config-backup-overview.ts @@ -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` -
- - - ${isHassio - ? html` - - Change local location - ` - : nothing} - - - Upload backup - - -
+ + + + + Upload backup + +
${backupInProgress ? html` @@ -188,22 +160,24 @@ class HaConfigBackupOverview extends LitElement { > ` - : html` - - - `} + : this.config + ? html` + + + ` + : nothing} - ${!this._needsOnboarding + ${!this._needsOnboarding && this.config ? html` + ${isComponentLoaded(this.hass, "hassio") + ? html` + + + + + Change default action location + + + ` + : nothing} +
Automatic backups
@@ -134,6 +164,14 @@ class HaConfigBackupSettings extends LitElement { .cloudStatus=${this.cloudStatus} @value-changed=${this._agentsConfigChanged} > + ${!this._config.create_backup.agent_ids.length + ? html`You have to select at least one location to create a + backup.
` + : nothing}
@@ -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 = { diff --git a/src/panels/config/backup/ha-config-backup.ts b/src/panels/config/backup/ha-config-backup.ts index bf8be4d454..7f4a761a48 100644 --- a/src/panels/config/backup/ha-config-backup.ts +++ b/src/panels/config/backup/ha-config-backup.ts @@ -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; diff --git a/src/panels/config/helpers/dialog-helper-detail.ts b/src/panels/config/helpers/dialog-helper-detail.ts index 7bce02c8a3..bcbcfd74a8 100644 --- a/src/panels/config/helpers/dialog-helper-detail.ts +++ b/src/panels/config/helpers/dialog-helper-detail.ts @@ -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() { diff --git a/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts b/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts index 0bf5a9baa5..436ea07d5c 100644 --- a/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts +++ b/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts @@ -177,7 +177,7 @@ export class HaConfigLovelaceRescources extends LitElement { .filter=${this._filter} @search-changed=${this._handleSearchChange} @row-click=${this._editResource} - hasFab + has-fab clickable > { 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() - ), }; } }); diff --git a/src/panels/config/person/dialog-person-detail.ts b/src/panels/config/person/dialog-person-detail.ts index 90dc3eb8dd..a8c9d2638f 100644 --- a/src/panels/config/person/dialog-person-detail.ts +++ b/src/panels/config/person/dialog-person-detail.ts @@ -180,7 +180,7 @@ class DialogPersonDetail extends LitElement implements HassDialog { ${this._renderUserFields()} - ${!this._deviceTrackersAvailable(this.hass) + ${this._deviceTrackersAvailable(this.hass) ? html`

${this.hass.localize( diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index c1d9bc39ca..8f7a639a32 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -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 { diff --git a/src/panels/config/script/ha-script-field-row.ts b/src/panels/config/script/ha-script-field-row.ts index e652b1f8cb..f214216c84 100644 --- a/src/panels/config/script/ha-script-field-row.ts +++ b/src/panels/config/script/ha-script-field-row.ts @@ -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] { diff --git a/src/panels/lovelace/card-features/hui-update-actions-card-feature.ts b/src/panels/lovelace/card-features/hui-update-actions-card-feature.ts index aa0ec42d3d..1956df515a 100644 --- a/src/panels/lovelace/card-features/hui-update-actions-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-update-actions-card-feature.ts @@ -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); diff --git a/src/panels/lovelace/cards/hui-history-graph-card.ts b/src/panels/lovelace/cards/hui-history-graph-card.ts index 490180ac91..95d6004b13 100644 --- a/src/panels/lovelace/cards/hui-history-graph-card.ts +++ b/src/panels/lovelace/cards/hui-history-graph-card.ts @@ -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"); diff --git a/src/panels/lovelace/cards/hui-statistics-graph-card.ts b/src/panels/lovelace/cards/hui-statistics-graph-card.ts index aafd5b22f1..4e703dafb4 100644 --- a/src/panels/lovelace/cards/hui-statistics-graph-card.ts +++ b/src/panels/lovelace/cards/hui-statistics-graph-card.ts @@ -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"); diff --git a/src/panels/lovelace/editor/config-elements/hui-update-actions-card-feature-editor.ts b/src/panels/lovelace/editor/config-elements/hui-update-actions-card-feature-editor.ts index 201fd343bb..9497c6ae90 100644 --- a/src/panels/lovelace/editor/config-elements/hui-update-actions-card-feature-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-update-actions-card-feature-editor.ts @@ -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, diff --git a/src/panels/lovelace/editor/view-editor/hui-view-background-editor.ts b/src/panels/lovelace/editor/view-editor/hui-view-background-editor.ts index ec1a102a8d..fe5624f2a7 100644 --- a/src/panels/lovelace/editor/view-editor/hui-view-background-editor.ts +++ b/src/panels/lovelace/editor/view-editor/hui-view-background-editor.ts @@ -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( diff --git a/src/panels/lovelace/views/hui-view-background.ts b/src/panels/lovelace/views/hui-view-background.ts index fbb5e933c9..5a065d2650 100644 --- a/src/panels/lovelace/views/hui-view-background.ts +++ b/src/panels/lovelace/views/hui-view-background.ts @@ -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; diff --git a/src/panels/my/ha-panel-my.ts b/src/panels/my/ha-panel-my.ts index cab1cfbafb..c2ae6a8e72 100644 --- a/src/panels/my/ha-panel-my.ts +++ b/src/panels/my/ha-panel-my.ts @@ -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) { diff --git a/src/state/quick-bar-mixin.ts b/src/state/quick-bar-mixin.ts index 30a96e7e93..daea444079 100644 --- a/src/state/quick-bar-mixin.ts +++ b/src/state/quick-bar-mixin.ts @@ -150,9 +150,7 @@ export default >(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) { diff --git a/src/translations/en.json b/src/translations/en.json index 0b1477cba9..688df34b20 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -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": { diff --git a/yarn.lock b/yarn.lock index 385d2ed0c4..e6e34683df 100644 --- a/yarn.lock +++ b/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"