diff --git a/package.json b/package.json index 4baf43a9c8ff..bb9054c92069 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "type": "module", "dependencies": { "@babel/runtime": "7.26.0", - "@braintree/sanitize-url": "7.1.0", + "@braintree/sanitize-url": "7.1.1", "@codemirror/autocomplete": "6.18.4", "@codemirror/commands": "6.7.1", "@codemirror/language": "6.10.7", @@ -139,7 +139,7 @@ "tinykeys": "3.0.0", "tsparticles-engine": "2.12.0", "tsparticles-preset-links": "2.12.0", - "ua-parser-js": "1.0.39", + "ua-parser-js": "1.0.40", "vis-data": "7.1.9", "vis-network": "9.1.9", "vue": "2.7.16", diff --git a/pyproject.toml b/pyproject.toml index a8d343ca8c8a..acf2d514e8ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20241223.1" +version = "20241224.0" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md" diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 8bf720f65b97..22060e0b420a 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -61,6 +61,8 @@ export class HaChartBase extends LitElement { @state() private _chartHeight?: number; + @state() private _legendHeight?: number; + @state() private _tooltip?: Tooltip; @state() private _hiddenDatasets: Set = new Set(); @@ -214,10 +216,22 @@ export class HaChartBase extends LitElement { this.chart.update("none"); } + protected updated(changedProperties: PropertyValues): void { + super.updated(changedProperties); + if (changedProperties.has("data") || changedProperties.has("options")) { + if (this.options?.plugins?.legend?.display) { + this._legendHeight = + this.renderRoot.querySelector(".chart-legend")?.clientHeight; + } else { + this._legendHeight = 0; + } + } + } + protected render() { return html` ${this.options?.plugins?.legend?.display === true - ? html`
+ ? html`
    ${this._datasetOrder.map((index) => { const dataset = this.data.datasets[index]; @@ -249,7 +263,7 @@ export class HaChartBase extends LitElement {
` : ""}
${this._tooltip ? html`
${this._tooltip.title}
${this._tooltip.beforeBody - ? html`
+ ? html`
${this._tooltip.beforeBody}
` : ""} @@ -456,6 +470,7 @@ export class HaChartBase extends LitElement { private _handleChartScroll(ev: MouseEvent) { const modifier = isMac ? "metaKey" : "ctrlKey"; + this._tooltip = undefined; if (!ev[modifier] && !this._showZoomHint) { this._showZoomHint = true; setTimeout(() => { @@ -498,15 +513,20 @@ export class HaChartBase extends LitElement { this._tooltip = undefined; return; } + const boundingBox = this.getBoundingClientRect(); this._tooltip = { ...context.tooltip, - top: this.chart!.canvas.offsetTop + context.tooltip.caretY + 12 + "px", + top: + boundingBox.y + + (this._legendHeight || 0) + + context.tooltip.caretY + + 12 + + "px", left: - this.chart!.canvas.offsetLeft + clamp( - context.tooltip.caretX, - 100, - this.clientWidth - 100 - this._paddingYAxisInternal + boundingBox.x + context.tooltip.caretX, + boundingBox.x + 100, + boundingBox.x + boundingBox.width - 100 ) - 100 + "px", @@ -525,16 +545,13 @@ export class HaChartBase extends LitElement { return css` :host { display: block; - position: var(--chart-base-position, relative); + position: relative; } - .animationContainer { + .animation-container { overflow: hidden; 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); } @@ -542,10 +559,10 @@ export class HaChartBase extends LitElement { /* allow scrolling if the chart is not zoomed */ touch-action: pan-y !important; } - .chartLegend { + .chart-legend { text-align: center; } - .chartLegend li { + .chart-legend li { cursor: pointer; display: inline-grid; grid-auto-flow: column; @@ -554,16 +571,16 @@ export class HaChartBase extends LitElement { align-items: center; color: var(--secondary-text-color); } - .chartLegend .hidden { + .chart-legend .hidden { text-decoration: line-through; } - .chartLegend .label { + .chart-legend .label { text-overflow: ellipsis; white-space: nowrap; overflow: hidden; } - .chartLegend .bullet, - .chartTooltip .bullet { + .chart-legend .bullet, + .chart-tooltip .bullet { border-width: 1px; border-style: solid; border-radius: 50%; @@ -577,13 +594,13 @@ export class HaChartBase extends LitElement { margin-inline-start: initial; direction: var(--direction); } - .chartTooltip .bullet { + .chart-tooltip .bullet { align-self: baseline; } - .chartTooltip { + .chart-tooltip { padding: 8px; font-size: 90%; - position: absolute; + position: fixed; background: rgba(80, 80, 80, 0.9); color: white; border-radius: 4px; @@ -596,17 +613,17 @@ export class HaChartBase extends LitElement { box-sizing: border-box; direction: var(--direction); } - .chartLegend ul, - .chartTooltip ul { + .chart-legend ul, + .chart-tooltip ul { display: inline-block; padding: 0 0px; margin: 8px 0 0 0; width: 100%; } - .chartTooltip ul { + .chart-tooltip ul { margin: 0 4px; } - .chartTooltip li { + .chart-tooltip li { display: flex; white-space: pre-line; word-break: break-word; @@ -614,16 +631,16 @@ export class HaChartBase extends LitElement { line-height: 16px; padding: 4px 0; } - .chartTooltip .title { + .chart-tooltip .title { text-align: center; font-weight: 500; word-break: break-word; direction: ltr; } - .chartTooltip .footer { + .chart-tooltip .footer { font-weight: 500; } - .chartTooltip .beforeBody { + .chart-tooltip .before-body { text-align: center; font-weight: 300; word-break: break-all; diff --git a/src/components/media-player/dialog-media-manage.ts b/src/components/media-player/dialog-media-manage.ts index ccdd9d167b19..dde5bcc5478b 100644 --- a/src/components/media-player/dialog-media-manage.ts +++ b/src/components/media-player/dialog-media-manage.ts @@ -210,11 +210,9 @@ class DialogMediaManage extends LitElement { href="/config/storage" @click=${this.closeDialog} > - ${this.hass - .localize( - "ui.components.media-browser.file_management.tip_storage_panel" - ) - .toLowerCase()} + ${this.hass.localize( + "ui.components.media-browser.file_management.tip_storage_panel" + )} `, } )} diff --git a/src/data/backup.ts b/src/data/backup.ts index 247cf8f3be0c..d68d74c5ef77 100644 --- a/src/data/backup.ts +++ b/src/data/backup.ts @@ -1,6 +1,16 @@ +import { setHours, setMinutes } from "date-fns"; +import type { HassConfig } from "home-assistant-js-websocket"; +import memoizeOne from "memoize-one"; +import { formatTime } from "../common/datetime/format_time"; import type { LocalizeFunc } from "../common/translations/localize"; import type { HomeAssistant } from "../types"; import { domainToName } from "./integration"; +import type { FrontendLocaleData } from "./translation"; +import { + formatDateTime, + formatDateTimeNumeric, +} from "../common/datetime/format_date_time"; +import { fileDownload } from "../util/file_download"; export const enum BackupScheduleState { NEVER = "never", @@ -282,3 +292,49 @@ export const generateEncryptionKey = () => { }); return result; }; + +export const generateEmergencyKit = ( + hass: HomeAssistant, + encryptionKey: string +) => + "data:text/plain;charset=utf-8," + + encodeURIComponent(`Home Assistant Backup Emergency Kit + +This emergency kit contains your backup encryption key. You need this key +to be able to restore your Home Assistant backups. + +Date: ${formatDateTime(new Date(), hass.locale, hass.config)} + +Instance: +${hass.config.location_name} + +URL: +${hass.auth.data.hassUrl} + +Encryption key: +${encryptionKey} + +For more information visit: https://www.home-assistant.io/more-info/backup-emergency-kit`); + +export const geneateEmergencyKitFileName = ( + hass: HomeAssistant, + append?: string +) => + `home_assistant_backup_emergency_kit_${append ? `${append}_` : ""}${formatDateTimeNumeric(new Date(), hass.locale, hass.config).replace(",", "").replace(" ", "_")}.txt`; + +export const downloadEmergencyKit = ( + hass: HomeAssistant, + key: string, + appendFileName?: string +) => + fileDownload( + generateEmergencyKit(hass, key), + geneateEmergencyKitFileName(hass, appendFileName) + ); + +export const getFormattedBackupTime = memoizeOne( + (locale: FrontendLocaleData, config: HassConfig) => { + const date = setMinutes(setHours(new Date(), 4), 45); + return formatTime(date, locale, config); + } +); diff --git a/src/dialogs/config-flow/show-dialog-config-flow.ts b/src/dialogs/config-flow/show-dialog-config-flow.ts index 4b256c82964b..dd0aaa438e0b 100644 --- a/src/dialogs/config-flow/show-dialog-config-flow.ts +++ b/src/dialogs/config-flow/show-dialog-config-flow.ts @@ -50,7 +50,7 @@ export const showConfigFlowDialog = ( return description ? html` - + ` : step.reason; }, @@ -71,7 +71,7 @@ export const showConfigFlowDialog = ( ); return description ? html` - + ` : ""; }, @@ -163,7 +163,7 @@ export const showConfigFlowDialog = ( ${description ? html` @@ -184,7 +184,7 @@ export const showConfigFlowDialog = ( ${description ? html` @@ -214,7 +214,7 @@ export const showConfigFlowDialog = ( ); return description ? html` - + ` : ""; }, @@ -234,7 +234,7 @@ export const showConfigFlowDialog = ( ); return description ? html` - + ` : ""; }, diff --git a/src/dialogs/config-flow/show-dialog-options-flow.ts b/src/dialogs/config-flow/show-dialog-options-flow.ts index 52a6b0e72e25..12919ff2e312 100644 --- a/src/dialogs/config-flow/show-dialog-options-flow.ts +++ b/src/dialogs/config-flow/show-dialog-options-flow.ts @@ -61,7 +61,7 @@ export const showOptionsFlowDialog = ( ? html` ` @@ -85,7 +85,7 @@ export const showOptionsFlowDialog = ( return description ? html` @@ -183,7 +183,7 @@ export const showOptionsFlowDialog = ( return description ? html` @@ -207,7 +207,7 @@ export const showOptionsFlowDialog = ( return description ? html` diff --git a/src/dialogs/config-flow/step-flow-abort.ts b/src/dialogs/config-flow/step-flow-abort.ts index 2ceeaae0dc96..49992aa9de44 100644 --- a/src/dialogs/config-flow/step-flow-abort.ts +++ b/src/dialogs/config-flow/step-flow-abort.ts @@ -51,7 +51,6 @@ class StepFlowAbort extends LitElement { } private async _handleMissingCreds() { - this._flowDone(); // Prompt to enter credentials and restart integration setup showAddApplicationCredentialDialog(this.params.dialogParentElement!, { selectedDomain: this.domain, @@ -64,6 +63,7 @@ class StepFlowAbort extends LitElement { }); }, }); + this._flowDone(); } private _flowDone(): void { diff --git a/src/dialogs/more-info/controls/more-info-media_player.ts b/src/dialogs/more-info/controls/more-info-media_player.ts index 93828334d4e7..b2da9c6a9a0a 100644 --- a/src/dialogs/more-info/controls/more-info-media_player.ts +++ b/src/dialogs/more-info/controls/more-info-media_player.ts @@ -213,9 +213,10 @@ class MoreInfoMediaPlayer extends LitElement { ha-icon-button[action="turn_off"], ha-icon-button[action="turn_on"] { - margin-inline-end: auto; margin-right: auto; margin-left: inherit; + margin-inline-start: inherit; + margin-inline-end: auto; } .controls { diff --git a/src/dialogs/more-info/ha-more-info-dialog.ts b/src/dialogs/more-info/ha-more-info-dialog.ts index e73438ac69cc..51d14f1cce77 100644 --- a/src/dialogs/more-info/ha-more-info-dialog.ts +++ b/src/dialogs/more-info/ha-more-info-dialog.ts @@ -545,11 +545,7 @@ export class MoreInfoDialog extends LitElement { /* Set the top top of the dialog to a fixed position, so it doesnt jump when the content changes size */ --vertical-align-dialog: flex-start; --dialog-surface-margin-top: 40px; - /* This is needed for the tooltip of the history charts to be positioned correctly */ - --dialog-surface-position: static; - --dialog-content-position: static; --dialog-content-padding: 0; - --chart-base-position: static; } .content { diff --git a/src/dialogs/quick-bar/ha-quick-bar.ts b/src/dialogs/quick-bar/ha-quick-bar.ts index 20469eb4e56b..4b8c56d7ee3a 100644 --- a/src/dialogs/quick-bar/ha-quick-bar.ts +++ b/src/dialogs/quick-bar/ha-quick-bar.ts @@ -40,6 +40,7 @@ import { loadVirtualizer } from "../../resources/virtualizer"; import type { HomeAssistant } from "../../types"; import { showConfirmationDialog } from "../generic/show-dialog-box"; import { QuickBarMode, type QuickBarParams } from "./show-dialog-quick-bar"; +import { computeDeviceName } from "../../data/device_registry"; interface QuickBarItem extends ScorableTextItem { primaryText: string; @@ -522,12 +523,14 @@ export class QuickBar extends LitElement { } private _generateDeviceItems(): DeviceItem[] { - return Object.keys(this.hass.devices) - .map((deviceId) => { - const device = this.hass.devices[deviceId]; - const area = this.hass.areas[device.area_id!]; + return Object.values(this.hass.devices) + .filter((device) => !device.disabled_by) + .map((device) => { + const area = device.area_id + ? this.hass.areas[device.area_id] + : undefined; const deviceItem = { - primaryText: device.name!, + primaryText: computeDeviceName(device, this.hass), deviceId: device.id, area: area?.name, action: () => navigate(`/config/devices/device/${device.id}`), 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 cf429a318796..df0b7aa1d001 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 @@ -14,6 +14,7 @@ import "../../category/ha-category-picker"; import "../../../../components/ha-expansion-panel"; import "../../../../components/chips/ha-chip-set"; import "../../../../components/chips/ha-assist-chip"; +import "../../../../components/ha-area-picker"; import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; import { haStyle, haStyleDialog } from "../../../../resources/styles"; @@ -57,6 +58,7 @@ class DialogAutomationRename extends LitElement implements HassDialog { ); this._newDescription = params.config.description || ""; this._entryUpdates = params.entityRegistryUpdate || { + area: params.entityRegistryEntry?.area_id || "", labels: params.entityRegistryEntry?.labels || [], category: params.entityRegistryEntry?.categories[params.domain] || "", }; @@ -66,6 +68,7 @@ class DialogAutomationRename extends LitElement implements HassDialog { this._newIcon ? "icon" : "", this._entryUpdates.category ? "category" : "", this._entryUpdates.labels.length > 0 ? "labels" : "", + this._entryUpdates.area ? "area" : "", ]; } @@ -193,6 +196,14 @@ class DialogAutomationRename extends LitElement implements HassDialog { @value-changed=${this._registryEntryChanged} >` : nothing} + ${this._visibleOptionals.includes("area") + ? html` ` + : nothing} ${this._renderOptionalChip( @@ -209,6 +220,12 @@ class DialogAutomationRename extends LitElement implements HassDialog { ) ) : nothing} + ${this._renderOptionalChip( + "area", + this.hass.localize( + "ui.panel.config.automation.editor.dialog.add_area" + ) + )} ${this._renderOptionalChip( "category", this.hass.localize( @@ -311,12 +328,14 @@ class DialogAutomationRename extends LitElement implements HassDialog { ha-icon-picker, ha-category-picker, ha-labels-picker, + ha-area-picker, ha-chip-set { display: block; } ha-icon-picker, ha-category-picker, ha-labels-picker, + ha-area-picker, ha-chip-set { margin-top: 16px; } diff --git a/src/panels/config/automation/automation-rename-dialog/show-dialog-automation-rename.ts b/src/panels/config/automation/automation-rename-dialog/show-dialog-automation-rename.ts index 7ee809c3c08c..4573fc85f243 100644 --- a/src/panels/config/automation/automation-rename-dialog/show-dialog-automation-rename.ts +++ b/src/panels/config/automation/automation-rename-dialog/show-dialog-automation-rename.ts @@ -13,6 +13,7 @@ interface BaseRenameDialogParams { } export interface EntityRegistryUpdate { + area: string; labels: string[]; category: string; } diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index fb5d36368062..26fe6cc0f67c 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -167,10 +167,11 @@ export class HaAutomationEditor extends PreventUnsavedMixin( if ( this._entityRegCreated && this._newAutomationId && - changedProps.has("entityRegistry") + changedProps.has("_entityRegistry") ) { const automation = this._entityRegistry.find( (entity: EntityRegistryEntry) => + entity.platform === "automation" && entity.unique_id === this._newAutomationId ); if (automation) { @@ -927,6 +928,14 @@ export class HaAutomationEditor extends PreventUnsavedMixin( this._saving = true; this._validationErrors = undefined; + let entityRegPromise: Promise | undefined; + if (this._entityRegistryUpdate !== undefined && !this._entityId) { + this._newAutomationId = id; + entityRegPromise = new Promise((resolve) => { + this._entityRegCreated = resolve; + }); + } + try { await saveAutomationConfig(this.hass, id, this._config!); @@ -934,13 +943,8 @@ export class HaAutomationEditor extends PreventUnsavedMixin( let entityId = this._entityId; // wait for automation to appear in entity registry when creating a new automation - if (!entityId) { - this._newAutomationId = id; - const automation = await new Promise( - (resolve) => { - this._entityRegCreated = resolve; - } - ); + if (entityRegPromise) { + const automation = await entityRegPromise; entityId = automation.entity_id; } @@ -950,6 +954,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin( automation: this._entityRegistryUpdate.category || null, }, labels: this._entityRegistryUpdate.labels || [], + area_id: this._entityRegistryUpdate.area || null, }); } } 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 2d0504a9085b..6a15f55c946e 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,7 +51,7 @@ class HaBackupConfigAgents extends LitElement { private _description(agentId: string) { if (agentId === CLOUD_AGENT) { - return "Note: It stores only one backup, regardless of your settings."; + return "Note: It stores only one backup with a maximum size of 5 GB, regardless of your settings."; } if (isNetworkMountAgent(agentId)) { return "Network storage"; 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 b2b947fec571..0298a6c5bfeb 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 @@ -6,9 +6,10 @@ import "../../../../../components/ha-md-list"; import "../../../../../components/ha-md-list-item"; import type { HomeAssistant } from "../../../../../types"; import { showChangeBackupEncryptionKeyDialog } from "../../dialogs/show-dialog-change-backup-encryption-key"; -import { fileDownload } from "../../../../../util/file_download"; import { showSetBackupEncryptionKeyDialog } from "../../dialogs/show-dialog-set-backup-encryption-key"; +import { downloadEmergencyKit } from "../../../../../data/backup"; + @customElement("ha-backup-config-encryption-key") class HaBackupConfigEncryptionKey extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -64,10 +65,7 @@ class HaBackupConfigEncryptionKey extends LitElement { if (!this._value) { return; } - fileDownload( - "data:text/plain;charset=utf-8," + encodeURIComponent(this._value), - "emergency_kit.txt" - ); + downloadEmergencyKit(this.hass, this._value); } private _change() { 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 aef76ddb2a5b..f7c9e2e50d43 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 @@ -3,18 +3,21 @@ import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../../common/dom/fire_event"; +import { clamp } from "../../../../../common/number/clamp"; import type { HaCheckbox } from "../../../../../components/ha-checkbox"; import "../../../../../components/ha-md-list"; import "../../../../../components/ha-md-list-item"; import "../../../../../components/ha-md-select"; -import "../../../../../components/ha-md-textfield"; import type { HaMdSelect } from "../../../../../components/ha-md-select"; import "../../../../../components/ha-md-select-option"; +import "../../../../../components/ha-md-textfield"; import "../../../../../components/ha-switch"; import type { BackupConfig } from "../../../../../data/backup"; -import { BackupScheduleState } from "../../../../../data/backup"; +import { + BackupScheduleState, + getFormattedBackupTime, +} from "../../../../../data/backup"; import type { HomeAssistant } from "../../../../../types"; -import { clamp } from "../../../../../common/number/clamp"; export type BackupConfigSchedule = Pick; @@ -120,13 +123,12 @@ class HaBackupConfigSchedule extends LitElement { protected render() { const data = this._getData(this.value); + const time = getFormattedBackupTime(this.hass.locale, this.hass.config); + return html` Use automatic backups - - How often you want to create a backup. - -
Daily at 04:45
+
Daily at ${time}
-
Monday at 04:45
+
Monday at ${time}
-
Tuesday at 04:45
+
Tuesday at ${time}
-
Wednesday at 04:45
+
Wednesday at ${time}
-
Thursday at 04:45
+
Thursday at ${time}
-
Friday at 04:45
+
Friday at ${time}
-
Saturday at 04:45
+
Saturday at ${time}
-
Sunday at 04:45
+
Sunday at ${time}
Backups to keep - The number of backups that are saved + Based on the maximum number of backups or how many days they + should be kept. + addons.sort((a, b) => + stringCompare(a.name, b.name, this.hass.locale.language) + ) + ); + protected render() { return html`
- ${this.addons.map( + ${this._addons(this.addons).map( (item) => html` { + private _lastSuccessfulBackup = memoizeOne((backups: BackupContent[]) => { const sortedBackups = backups - .filter( - (backup) => - backup.with_automatic_settings && !backup.failed_agent_ids?.length - ) + .filter((backup) => backup.with_automatic_settings) .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); return sortedBackups[0] as BackupContent | undefined; }); private _nextBackupDescription(schedule: BackupScheduleState) { - const newDate = setMinutes(setHours(new Date(), 4), 45); - const time = formatTime(newDate, this.hass.locale, this.hass.config); + const time = getFormattedBackupTime(this.hass.locale, this.hass.config); switch (schedule) { case BackupScheduleState.DAILY: @@ -84,37 +82,27 @@ class HaBackupOverviewBackups extends LitElement { `; } - const lastBackup = this._lastBackup(this.backups); + const lastSuccessfulBackup = this._lastSuccessfulBackup(this.backups); - if (!lastBackup) { - return html` - - - `; - } - - const lastBackupDate = new Date(lastBackup.date); - - const now = new Date(); - - const lastBackupDescription = `Last successful backup ${relativeTime(lastBackupDate, this.hass.locale, now, true)} and stored to ${lastBackup.agent_ids?.length} locations.`; - const nextBackupDescription = this._nextBackupDescription( - this.config.schedule.state - ); + 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; - if (lastAttempt && lastAttempt > lastBackupDate) { + 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.` + : "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.`; return html` @@ -131,10 +119,25 @@ class HaBackupOverviewBackups extends LitElement { `; } + if (!lastSuccessfulBackup) { + return html` + + + `; + } + + 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), - lastBackupDate + lastSuccessfulBackupDate ); const isOverdue = @@ -216,12 +219,13 @@ class HaBackupOverviewBackups extends LitElement { animation-timing-function: linear; animation-duration: 1.2s; border-radius: 4px; - height: 20px; + height: 16px; + margin: 2px 0; background: linear-gradient( to right, - rgb(247, 249, 250) 8%, - rgb(235, 238, 240) 18%, - rgb(247, 249, 250) 33% + var(--card-background-color) 8%, + var(--secondary-background-color) 18%, + var(--card-background-color) 33% ) 0% 0% / 936px 104px; } diff --git a/src/panels/config/backup/dialogs/dialog-backup-onboarding.ts b/src/panels/config/backup/dialogs/dialog-backup-onboarding.ts index 2a8ff15bf4c4..7719ebd066fe 100644 --- a/src/panels/config/backup/dialogs/dialog-backup-onboarding.ts +++ b/src/panels/config/backup/dialogs/dialog-backup-onboarding.ts @@ -24,6 +24,7 @@ import { BackupScheduleState, CLOUD_AGENT, CORE_LOCAL_AGENT, + downloadEmergencyKit, generateEncryptionKey, HASSIO_LOCAL_AGENT, updateBackupConfig, @@ -31,7 +32,6 @@ import { import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; import { haStyle, haStyleDialog } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; -import { fileDownload } from "../../../../util/file_download"; import { showToast } from "../../../../util/toast"; import "../components/config/ha-backup-config-agents"; import "../components/config/ha-backup-config-data"; @@ -101,7 +101,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog { agents.push(CORE_LOCAL_AGENT); } // Enable cloud location if logged in - if (this._params.cloudStatus.logged_in) { + if (this._params.cloudStatus?.logged_in) { agents.push(CLOUD_AGENT); } @@ -327,12 +327,6 @@ class DialogBackupOnboarding extends LitElement implements HassDialog { `; case "setup": return html` -

- 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. Once you make your selection, your first backup - will begin. -

Recommended settings @@ -398,14 +392,11 @@ class DialogBackupOnboarding extends LitElement implements HassDialog { if (!key) { return; } - fileDownload( - "data:text/plain;charset=utf-8," + encodeURIComponent(key), - "emergency_kit.txt" - ); + downloadEmergencyKit(this.hass, key); } - private _copyKeyToClipboard() { - copyToClipboard(this._config!.create_backup.password!); + private async _copyKeyToClipboard() { + await copyToClipboard(this._config!.create_backup.password!); showToast(this, { message: this.hass.localize("ui.common.copied_clipboard"), }); @@ -471,6 +462,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog { width: 90vw; max-width: 560px; --dialog-content-padding: 8px 24px; + max-height: min(605px, 100% - 48px); } ha-md-list { background: none; 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 1020083e3a73..1b500b34f925 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 @@ -13,11 +13,13 @@ 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 { generateEncryptionKey } from "../../../../data/backup"; +import { + downloadEmergencyKit, + generateEncryptionKey, +} from "../../../../data/backup"; import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; import { haStyle, haStyleDialog } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; -import { fileDownload } from "../../../../util/file_download"; import { showToast } from "../../../../util/toast"; import type { ChangeBackupEncryptionKeyDialogParams } from "./show-dialog-change-backup-encryption-key"; @@ -203,18 +205,18 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog { return nothing; } - private _copyKeyToClipboard() { - copyToClipboard(this._newEncryptionKey); + private async _copyKeyToClipboard() { + await copyToClipboard(this._newEncryptionKey); showToast(this, { message: this.hass.localize("ui.common.copied_clipboard"), }); } - private _copyOldKeyToClipboard() { + private async _copyOldKeyToClipboard() { if (!this._params?.currentKey) { return; } - copyToClipboard(this._params.currentKey); + await copyToClipboard(this._params.currentKey); showToast(this, { message: this.hass.localize("ui.common.copied_clipboard"), }); @@ -224,22 +226,14 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog { if (!this._params?.currentKey) { return; } - fileDownload( - "data:text/plain;charset=utf-8," + - encodeURIComponent(this._params.currentKey), - "emergency_kit_old.txt" - ); + downloadEmergencyKit(this.hass, this._params.currentKey, "old"); } private _downloadNew() { if (!this._newEncryptionKey) { return; } - fileDownload( - "data:text/plain;charset=utf-8," + - encodeURIComponent(this._newEncryptionKey), - "emergency_kit.txt" - ); + downloadEmergencyKit(this.hass, this._newEncryptionKey); } private async _submit() { diff --git a/src/panels/config/backup/dialogs/dialog-generate-backup.ts b/src/panels/config/backup/dialogs/dialog-generate-backup.ts index eb5d53407e73..95ddb898093d 100644 --- a/src/panels/config/backup/dialogs/dialog-generate-backup.ts +++ b/src/panels/config/backup/dialogs/dialog-generate-backup.ts @@ -99,7 +99,9 @@ class DialogGenerateBackup extends LitElement implements HassDialog { const { agents } = await fetchBackupAgentsInfo(this.hass); this._agentIds = agents .map((agent) => agent.agent_id) - .filter((id) => id !== CLOUD_AGENT || this._params?.cloudStatus.logged_in) + .filter( + (id) => id !== CLOUD_AGENT || this._params?.cloudStatus?.logged_in + ) .sort(compareAgents); } diff --git a/src/panels/config/backup/dialogs/dialog-set-backup-encryption-key.ts b/src/panels/config/backup/dialogs/dialog-set-backup-encryption-key.ts index ccb5c7c87b55..956b969dc157 100644 --- a/src/panels/config/backup/dialogs/dialog-set-backup-encryption-key.ts +++ b/src/panels/config/backup/dialogs/dialog-set-backup-encryption-key.ts @@ -11,11 +11,13 @@ 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 { generateEncryptionKey } from "../../../../data/backup"; +import { + downloadEmergencyKit, + generateEncryptionKey, +} from "../../../../data/backup"; import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; import { haStyle, haStyleDialog } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; -import { fileDownload } from "../../../../util/file_download"; import type { SetBackupEncryptionKeyDialogParams } from "./show-dialog-set-backup-encryption-key"; const STEPS = ["new", "save"] as const; @@ -162,11 +164,7 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog { if (!this._newEncryptionKey) { return; } - fileDownload( - "data:text/plain;charset=utf-8," + - encodeURIComponent(this._newEncryptionKey), - "emergency_kit.txt" - ); + downloadEmergencyKit(this.hass, this._newEncryptionKey); } private _encryptionKeyChanged(ev) { 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 578f80bf1344..c411d6f0d7bd 100644 --- a/src/panels/config/backup/dialogs/show-dialog-backup_onboarding.ts +++ b/src/panels/config/backup/dialogs/show-dialog-backup_onboarding.ts @@ -4,7 +4,7 @@ import type { CloudStatus } from "../../../../data/cloud"; export interface BackupOnboardingDialogParams { submit?: (value: boolean) => void; cancel?: () => void; - cloudStatus: CloudStatus; + cloudStatus?: CloudStatus; } const loadDialog = () => import("./dialog-backup-onboarding"); diff --git a/src/panels/config/backup/dialogs/show-dialog-generate-backup.ts b/src/panels/config/backup/dialogs/show-dialog-generate-backup.ts index ccd57deb44c6..75d139d8f8cb 100644 --- a/src/panels/config/backup/dialogs/show-dialog-generate-backup.ts +++ b/src/panels/config/backup/dialogs/show-dialog-generate-backup.ts @@ -5,7 +5,7 @@ import type { CloudStatus } from "../../../../data/cloud"; export interface GenerateBackupDialogParams { submit?: (response: GenerateBackupParams) => void; cancel?: () => void; - cloudStatus: CloudStatus; + cloudStatus?: CloudStatus; } export const loadGenerateBackupDialog = () => diff --git a/src/panels/config/backup/ha-config-backup-backups.ts b/src/panels/config/backup/ha-config-backup-backups.ts index 8c3cf4a1ef16..f6063ba21ce5 100644 --- a/src/panels/config/backup/ha-config-backup-backups.ts +++ b/src/panels/config/backup/ha-config-backup-backups.ts @@ -78,7 +78,7 @@ const TYPE_ORDER: Array = ["automatic", "manual", "imported"]; class HaConfigBackupBackups extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public cloudStatus!: CloudStatus; + @property({ attribute: false }) public cloudStatus?: CloudStatus; @property({ type: Boolean }) public narrow = false; @@ -167,7 +167,6 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { title: "Locations", showNarrow: true, minWidth: "60px", - maxWidth: "120px", template: (backup) => html`
${(backup.agent_ids || []).map((agentId) => { @@ -181,7 +180,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { `; } @@ -190,7 +189,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { `; } @@ -209,6 +208,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { referrerpolicy="no-referrer" alt=${name} slot="graphic" + style="flex-shrink: 0;" /> `; })} diff --git a/src/panels/config/backup/ha-config-backup-overview.ts b/src/panels/config/backup/ha-config-backup-overview.ts index 40614715a60f..0a132b799f68 100644 --- a/src/panels/config/backup/ha-config-backup-overview.ts +++ b/src/panels/config/backup/ha-config-backup-overview.ts @@ -26,7 +26,6 @@ import "../../../layouts/hass-subpage"; import "../../../layouts/hass-tabs-subpage-data-table"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant, Route } from "../../../types"; -import "./components/ha-backup-summary-card"; import "./components/overview/ha-backup-overview-backups"; import "./components/overview/ha-backup-overview-onboarding"; import "./components/overview/ha-backup-overview-progress"; @@ -42,7 +41,7 @@ import { showUploadBackupDialog } from "./dialogs/show-dialog-upload-backup"; class HaConfigBackupOverview extends LitElement { @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/backup/ha-config-backup-settings.ts b/src/panels/config/backup/ha-config-backup-settings.ts index ad894e6414af..e4b7326adaa7 100644 --- a/src/panels/config/backup/ha-config-backup-settings.ts +++ b/src/panels/config/backup/ha-config-backup-settings.ts @@ -141,9 +141,9 @@ class HaConfigBackupSettings extends LitElement {

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 characters below or download them as an emergency kit file. - Encryption keeps your backups private and secure. + access your backup, allowing it to be restored. Download them as + an emergency kit file and store it somewhere safe. Encryption + keeps your backups private and secure.

` @@ -123,7 +123,7 @@ export const showRepairsFlowDialog = ( ${description ? html` @@ -220,7 +220,7 @@ export const showRepairsFlowDialog = ( return html`${renderIssueDescription(hass, issue)}${description ? html` @@ -254,7 +254,7 @@ export const showRepairsFlowDialog = ( ${description ? html` diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index c75f8c151af0..c1d9bc39ca62 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -139,7 +139,8 @@ export class HaScriptEditor extends SubscribeMixin( changedProps.has("entityRegistry") ) { const script = this.entityRegistry.find( - (entity: EntityRegistryEntry) => entity.unique_id === this._newScriptId + (entity: EntityRegistryEntry) => + entity.platform === "script" && entity.unique_id === this._newScriptId ); if (script) { this._entityRegCreated(script); @@ -164,7 +165,8 @@ export class HaScriptEditor extends SubscribeMixin( .narrow=${this.narrow} .route=${this.route} .backCallback=${this._backTapped} - .header=${!this._config.alias ? "" : this._config.alias} + .header=${this._config.alias || + this.hass.localize("ui.panel.config.script.editor.default_name")} > ${this.scriptId && !this.narrow ? html` @@ -487,9 +489,7 @@ export class HaScriptEditor extends SubscribeMixin( if (changedProps.has("scriptId") && !this.scriptId && this.hass) { const initData = getScriptEditorInitData(); this._dirty = !!initData; - const baseConfig: Partial = { - alias: this.hass.localize("ui.panel.config.script.editor.default_name"), - }; + const baseConfig: Partial = {}; if (!initData || !("use_blueprint" in initData)) { baseConfig.sequence = []; } @@ -894,6 +894,15 @@ export class HaScriptEditor extends SubscribeMixin( const id = this.scriptId || this._entityId || Date.now(); this._saving = true; + + let entityRegPromise: Promise | undefined; + if (this._entityRegistryUpdate !== undefined && !this.scriptId) { + this._newScriptId = id.toString(); + entityRegPromise = new Promise((resolve) => { + this._entityRegCreated = resolve; + }); + } + try { await this.hass!.callApi( "POST", @@ -902,23 +911,20 @@ export class HaScriptEditor extends SubscribeMixin( ); if (this._entityRegistryUpdate !== undefined) { - let entityId = id.toString().startsWith("script.") - ? id.toString() - : `script.${id}`; + let entityId = this._entityId; // wait for new script to appear in entity registry - if (!this.scriptId) { - const script = await new Promise((resolve) => { - this._entityRegCreated = resolve; - }); + if (entityRegPromise) { + const script = await entityRegPromise; entityId = script.entity_id; } - await updateEntityRegistryEntry(this.hass, entityId, { + await updateEntityRegistryEntry(this.hass, entityId!, { categories: { script: this._entityRegistryUpdate.category || null, }, labels: this._entityRegistryUpdate.labels || [], + area_id: this._entityRegistryUpdate.area || null, }); } diff --git a/src/panels/config/script/ha-script-trace.ts b/src/panels/config/script/ha-script-trace.ts index 08b0561312cc..2523d0a1bf15 100644 --- a/src/panels/config/script/ha-script-trace.ts +++ b/src/panels/config/script/ha-script-trace.ts @@ -178,7 +178,9 @@ export class HaScriptTrace extends LitElement { @@ -198,7 +200,9 @@ export class HaScriptTrace extends LitElement { diff --git a/src/panels/developer-tools/state/developer-tools-state.ts b/src/panels/developer-tools/state/developer-tools-state.ts index a6ec61a221f8..d9204e0bdf06 100644 --- a/src/panels/developer-tools/state/developer-tools-state.ts +++ b/src/panels/developer-tools/state/developer-tools-state.ts @@ -324,10 +324,10 @@ class HaPanelDevState extends LitElement { `; } - private _copyEntity(ev) { + private async _copyEntity(ev) { ev.preventDefault(); const entity = (ev.currentTarget! as any).entity; - copyToClipboard(entity.entity_id); + await copyToClipboard(entity.entity_id); } private _entitySelected(ev) { diff --git a/src/panels/lovelace/components/hui-generic-entity-row.ts b/src/panels/lovelace/components/hui-generic-entity-row.ts index 40ee38175bb5..d24800bd7b45 100644 --- a/src/panels/lovelace/components/hui-generic-entity-row.ts +++ b/src/panels/lovelace/components/hui-generic-entity-row.ts @@ -23,7 +23,7 @@ export class HuiGenericEntityRow extends LitElement { @property({ attribute: false }) public config?: EntitiesCardEntityConfig; - @property({ attribute: false }) public secondaryText?: string; + @property({ attribute: "secondary-text" }) public secondaryText?: string; @property({ attribute: "hide-name", type: Boolean }) public hideName = false; 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 58481f516495..ec1a102a8dba 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 @@ -115,14 +115,24 @@ export class HuiViewBackgroundEditor extends LitElement { }; } - background = { - transparency: 100, - alignment: "center", - size: "auto", - repeat: "no-repeat", - attachment: "scroll", - ...background, - }; + if (!background) { + background = { + transparency: 33, + alignment: "center", + size: "cover", + repeat: "repeat", + attachment: "fixed", + }; + } else { + background = { + transparency: 100, + alignment: "center", + size: "cover", + repeat: "no-repeat", + attachment: "scroll", + ...background, + }; + } return html` ` : html`${this._step.type === "abort" ? html` ` : this._step.type === "form" ? html`