Merge branch 'rc'

This commit is contained in:
Bram Kragten 2024-12-24 16:24:59 +01:00
commit c05054e4ff
45 changed files with 374 additions and 248 deletions

View File

@ -27,7 +27,7 @@
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@babel/runtime": "7.26.0", "@babel/runtime": "7.26.0",
"@braintree/sanitize-url": "7.1.0", "@braintree/sanitize-url": "7.1.1",
"@codemirror/autocomplete": "6.18.4", "@codemirror/autocomplete": "6.18.4",
"@codemirror/commands": "6.7.1", "@codemirror/commands": "6.7.1",
"@codemirror/language": "6.10.7", "@codemirror/language": "6.10.7",
@ -139,7 +139,7 @@
"tinykeys": "3.0.0", "tinykeys": "3.0.0",
"tsparticles-engine": "2.12.0", "tsparticles-engine": "2.12.0",
"tsparticles-preset-links": "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-data": "7.1.9",
"vis-network": "9.1.9", "vis-network": "9.1.9",
"vue": "2.7.16", "vue": "2.7.16",

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20241223.1" version = "20241224.0"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "The Home Assistant frontend" description = "The Home Assistant frontend"
readme = "README.md" readme = "README.md"

View File

@ -61,6 +61,8 @@ export class HaChartBase extends LitElement {
@state() private _chartHeight?: number; @state() private _chartHeight?: number;
@state() private _legendHeight?: number;
@state() private _tooltip?: Tooltip; @state() private _tooltip?: Tooltip;
@state() private _hiddenDatasets: Set<number> = new Set(); @state() private _hiddenDatasets: Set<number> = new Set();
@ -214,10 +216,22 @@ export class HaChartBase extends LitElement {
this.chart.update("none"); 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() { protected render() {
return html` return html`
${this.options?.plugins?.legend?.display === true ${this.options?.plugins?.legend?.display === true
? html`<div class="chartLegend"> ? html`<div class="chart-legend">
<ul> <ul>
${this._datasetOrder.map((index) => { ${this._datasetOrder.map((index) => {
const dataset = this.data.datasets[index]; const dataset = this.data.datasets[index];
@ -249,7 +263,7 @@ export class HaChartBase extends LitElement {
</div>` </div>`
: ""} : ""}
<div <div
class="animationContainer" class="animation-container"
style=${styleMap({ style=${styleMap({
height: `${this.height || this._chartHeight || 0}px`, height: `${this.height || this._chartHeight || 0}px`,
overflow: this._chartHeight ? "initial" : "hidden", overflow: this._chartHeight ? "initial" : "hidden",
@ -288,7 +302,7 @@ export class HaChartBase extends LitElement {
</div> </div>
${this._tooltip ${this._tooltip
? html`<div ? html`<div
class="chartTooltip ${classMap({ class="chart-tooltip ${classMap({
[this._tooltip.yAlign]: true, [this._tooltip.yAlign]: true,
})}" })}"
style=${styleMap({ style=${styleMap({
@ -298,7 +312,7 @@ export class HaChartBase extends LitElement {
> >
<div class="title">${this._tooltip.title}</div> <div class="title">${this._tooltip.title}</div>
${this._tooltip.beforeBody ${this._tooltip.beforeBody
? html`<div class="beforeBody"> ? html`<div class="before-body">
${this._tooltip.beforeBody} ${this._tooltip.beforeBody}
</div>` </div>`
: ""} : ""}
@ -456,6 +470,7 @@ export class HaChartBase extends LitElement {
private _handleChartScroll(ev: MouseEvent) { private _handleChartScroll(ev: MouseEvent) {
const modifier = isMac ? "metaKey" : "ctrlKey"; const modifier = isMac ? "metaKey" : "ctrlKey";
this._tooltip = undefined;
if (!ev[modifier] && !this._showZoomHint) { if (!ev[modifier] && !this._showZoomHint) {
this._showZoomHint = true; this._showZoomHint = true;
setTimeout(() => { setTimeout(() => {
@ -498,15 +513,20 @@ export class HaChartBase extends LitElement {
this._tooltip = undefined; this._tooltip = undefined;
return; return;
} }
const boundingBox = this.getBoundingClientRect();
this._tooltip = { this._tooltip = {
...context.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: left:
this.chart!.canvas.offsetLeft +
clamp( clamp(
context.tooltip.caretX, boundingBox.x + context.tooltip.caretX,
100, boundingBox.x + 100,
this.clientWidth - 100 - this._paddingYAxisInternal boundingBox.x + boundingBox.width - 100
) - ) -
100 + 100 +
"px", "px",
@ -525,16 +545,13 @@ export class HaChartBase extends LitElement {
return css` return css`
:host { :host {
display: block; display: block;
position: var(--chart-base-position, relative); position: relative;
} }
.animationContainer { .animation-container {
overflow: hidden; overflow: hidden;
height: 0; height: 0;
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1); transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
} }
.chart-container {
position: relative;
}
canvas { canvas {
max-height: var(--chart-max-height, 400px); max-height: var(--chart-max-height, 400px);
} }
@ -542,10 +559,10 @@ export class HaChartBase extends LitElement {
/* allow scrolling if the chart is not zoomed */ /* allow scrolling if the chart is not zoomed */
touch-action: pan-y !important; touch-action: pan-y !important;
} }
.chartLegend { .chart-legend {
text-align: center; text-align: center;
} }
.chartLegend li { .chart-legend li {
cursor: pointer; cursor: pointer;
display: inline-grid; display: inline-grid;
grid-auto-flow: column; grid-auto-flow: column;
@ -554,16 +571,16 @@ export class HaChartBase extends LitElement {
align-items: center; align-items: center;
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
.chartLegend .hidden { .chart-legend .hidden {
text-decoration: line-through; text-decoration: line-through;
} }
.chartLegend .label { .chart-legend .label {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
} }
.chartLegend .bullet, .chart-legend .bullet,
.chartTooltip .bullet { .chart-tooltip .bullet {
border-width: 1px; border-width: 1px;
border-style: solid; border-style: solid;
border-radius: 50%; border-radius: 50%;
@ -577,13 +594,13 @@ export class HaChartBase extends LitElement {
margin-inline-start: initial; margin-inline-start: initial;
direction: var(--direction); direction: var(--direction);
} }
.chartTooltip .bullet { .chart-tooltip .bullet {
align-self: baseline; align-self: baseline;
} }
.chartTooltip { .chart-tooltip {
padding: 8px; padding: 8px;
font-size: 90%; font-size: 90%;
position: absolute; position: fixed;
background: rgba(80, 80, 80, 0.9); background: rgba(80, 80, 80, 0.9);
color: white; color: white;
border-radius: 4px; border-radius: 4px;
@ -596,17 +613,17 @@ export class HaChartBase extends LitElement {
box-sizing: border-box; box-sizing: border-box;
direction: var(--direction); direction: var(--direction);
} }
.chartLegend ul, .chart-legend ul,
.chartTooltip ul { .chart-tooltip ul {
display: inline-block; display: inline-block;
padding: 0 0px; padding: 0 0px;
margin: 8px 0 0 0; margin: 8px 0 0 0;
width: 100%; width: 100%;
} }
.chartTooltip ul { .chart-tooltip ul {
margin: 0 4px; margin: 0 4px;
} }
.chartTooltip li { .chart-tooltip li {
display: flex; display: flex;
white-space: pre-line; white-space: pre-line;
word-break: break-word; word-break: break-word;
@ -614,16 +631,16 @@ export class HaChartBase extends LitElement {
line-height: 16px; line-height: 16px;
padding: 4px 0; padding: 4px 0;
} }
.chartTooltip .title { .chart-tooltip .title {
text-align: center; text-align: center;
font-weight: 500; font-weight: 500;
word-break: break-word; word-break: break-word;
direction: ltr; direction: ltr;
} }
.chartTooltip .footer { .chart-tooltip .footer {
font-weight: 500; font-weight: 500;
} }
.chartTooltip .beforeBody { .chart-tooltip .before-body {
text-align: center; text-align: center;
font-weight: 300; font-weight: 300;
word-break: break-all; word-break: break-all;

View File

@ -210,11 +210,9 @@ class DialogMediaManage extends LitElement {
href="/config/storage" href="/config/storage"
@click=${this.closeDialog} @click=${this.closeDialog}
> >
${this.hass ${this.hass.localize(
.localize(
"ui.components.media-browser.file_management.tip_storage_panel" "ui.components.media-browser.file_management.tip_storage_panel"
) )}
.toLowerCase()}
</a>`, </a>`,
} }
)} )}

View File

@ -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 { LocalizeFunc } from "../common/translations/localize";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import { domainToName } from "./integration"; 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 { export const enum BackupScheduleState {
NEVER = "never", NEVER = "never",
@ -282,3 +292,49 @@ export const generateEncryptionKey = () => {
}); });
return result; 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);
}
);

View File

@ -50,7 +50,7 @@ export const showConfigFlowDialog = (
return description return description
? html` ? html`
<ha-markdown allowsvg breaks .content=${description}></ha-markdown> <ha-markdown allow-svg breaks .content=${description}></ha-markdown>
` `
: step.reason; : step.reason;
}, },
@ -71,7 +71,7 @@ export const showConfigFlowDialog = (
); );
return description return description
? html` ? html`
<ha-markdown allowsvg breaks .content=${description}></ha-markdown> <ha-markdown allow-svg breaks .content=${description}></ha-markdown>
` `
: ""; : "";
}, },
@ -163,7 +163,7 @@ export const showConfigFlowDialog = (
${description ${description
? html` ? html`
<ha-markdown <ha-markdown
allowsvg allow-svg
breaks breaks
.content=${description} .content=${description}
></ha-markdown> ></ha-markdown>
@ -184,7 +184,7 @@ export const showConfigFlowDialog = (
${description ${description
? html` ? html`
<ha-markdown <ha-markdown
allowsvg allow-svg
breaks breaks
.content=${description} .content=${description}
></ha-markdown> ></ha-markdown>
@ -214,7 +214,7 @@ export const showConfigFlowDialog = (
); );
return description return description
? html` ? html`
<ha-markdown allowsvg breaks .content=${description}></ha-markdown> <ha-markdown allow-svg breaks .content=${description}></ha-markdown>
` `
: ""; : "";
}, },
@ -234,7 +234,7 @@ export const showConfigFlowDialog = (
); );
return description return description
? html` ? html`
<ha-markdown allowsvg breaks .content=${description}></ha-markdown> <ha-markdown allow-svg breaks .content=${description}></ha-markdown>
` `
: ""; : "";
}, },

View File

@ -61,7 +61,7 @@ export const showOptionsFlowDialog = (
? html` ? html`
<ha-markdown <ha-markdown
breaks breaks
allowsvg allow-svg
.content=${description} .content=${description}
></ha-markdown> ></ha-markdown>
` `
@ -85,7 +85,7 @@ export const showOptionsFlowDialog = (
return description return description
? html` ? html`
<ha-markdown <ha-markdown
allowsvg allow-svg
breaks breaks
.content=${description} .content=${description}
></ha-markdown> ></ha-markdown>
@ -183,7 +183,7 @@ export const showOptionsFlowDialog = (
return description return description
? html` ? html`
<ha-markdown <ha-markdown
allowsvg allow-svg
breaks breaks
.content=${description} .content=${description}
></ha-markdown> ></ha-markdown>
@ -207,7 +207,7 @@ export const showOptionsFlowDialog = (
return description return description
? html` ? html`
<ha-markdown <ha-markdown
allowsvg allow-svg
breaks breaks
.content=${description} .content=${description}
></ha-markdown> ></ha-markdown>

View File

@ -51,7 +51,6 @@ class StepFlowAbort extends LitElement {
} }
private async _handleMissingCreds() { private async _handleMissingCreds() {
this._flowDone();
// Prompt to enter credentials and restart integration setup // Prompt to enter credentials and restart integration setup
showAddApplicationCredentialDialog(this.params.dialogParentElement!, { showAddApplicationCredentialDialog(this.params.dialogParentElement!, {
selectedDomain: this.domain, selectedDomain: this.domain,
@ -64,6 +63,7 @@ class StepFlowAbort extends LitElement {
}); });
}, },
}); });
this._flowDone();
} }
private _flowDone(): void { private _flowDone(): void {

View File

@ -213,9 +213,10 @@ class MoreInfoMediaPlayer extends LitElement {
ha-icon-button[action="turn_off"], ha-icon-button[action="turn_off"],
ha-icon-button[action="turn_on"] { ha-icon-button[action="turn_on"] {
margin-inline-end: auto;
margin-right: auto; margin-right: auto;
margin-left: inherit; margin-left: inherit;
margin-inline-start: inherit;
margin-inline-end: auto;
} }
.controls { .controls {

View File

@ -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 */ /* 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; --vertical-align-dialog: flex-start;
--dialog-surface-margin-top: 40px; --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; --dialog-content-padding: 0;
--chart-base-position: static;
} }
.content { .content {

View File

@ -40,6 +40,7 @@ import { loadVirtualizer } from "../../resources/virtualizer";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { showConfirmationDialog } from "../generic/show-dialog-box"; import { showConfirmationDialog } from "../generic/show-dialog-box";
import { QuickBarMode, type QuickBarParams } from "./show-dialog-quick-bar"; import { QuickBarMode, type QuickBarParams } from "./show-dialog-quick-bar";
import { computeDeviceName } from "../../data/device_registry";
interface QuickBarItem extends ScorableTextItem { interface QuickBarItem extends ScorableTextItem {
primaryText: string; primaryText: string;
@ -522,12 +523,14 @@ export class QuickBar extends LitElement {
} }
private _generateDeviceItems(): DeviceItem[] { private _generateDeviceItems(): DeviceItem[] {
return Object.keys(this.hass.devices) return Object.values(this.hass.devices)
.map((deviceId) => { .filter((device) => !device.disabled_by)
const device = this.hass.devices[deviceId]; .map((device) => {
const area = this.hass.areas[device.area_id!]; const area = device.area_id
? this.hass.areas[device.area_id]
: undefined;
const deviceItem = { const deviceItem = {
primaryText: device.name!, primaryText: computeDeviceName(device, this.hass),
deviceId: device.id, deviceId: device.id,
area: area?.name, area: area?.name,
action: () => navigate(`/config/devices/device/${device.id}`), action: () => navigate(`/config/devices/device/${device.id}`),

View File

@ -14,6 +14,7 @@ import "../../category/ha-category-picker";
import "../../../../components/ha-expansion-panel"; import "../../../../components/ha-expansion-panel";
import "../../../../components/chips/ha-chip-set"; import "../../../../components/chips/ha-chip-set";
import "../../../../components/chips/ha-assist-chip"; import "../../../../components/chips/ha-assist-chip";
import "../../../../components/ha-area-picker";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles"; import { haStyle, haStyleDialog } from "../../../../resources/styles";
@ -57,6 +58,7 @@ class DialogAutomationRename extends LitElement implements HassDialog {
); );
this._newDescription = params.config.description || ""; this._newDescription = params.config.description || "";
this._entryUpdates = params.entityRegistryUpdate || { this._entryUpdates = params.entityRegistryUpdate || {
area: params.entityRegistryEntry?.area_id || "",
labels: params.entityRegistryEntry?.labels || [], labels: params.entityRegistryEntry?.labels || [],
category: params.entityRegistryEntry?.categories[params.domain] || "", category: params.entityRegistryEntry?.categories[params.domain] || "",
}; };
@ -66,6 +68,7 @@ class DialogAutomationRename extends LitElement implements HassDialog {
this._newIcon ? "icon" : "", this._newIcon ? "icon" : "",
this._entryUpdates.category ? "category" : "", this._entryUpdates.category ? "category" : "",
this._entryUpdates.labels.length > 0 ? "labels" : "", this._entryUpdates.labels.length > 0 ? "labels" : "",
this._entryUpdates.area ? "area" : "",
]; ];
} }
@ -193,6 +196,14 @@ class DialogAutomationRename extends LitElement implements HassDialog {
@value-changed=${this._registryEntryChanged} @value-changed=${this._registryEntryChanged}
></ha-labels-picker>` ></ha-labels-picker>`
: nothing} : nothing}
${this._visibleOptionals.includes("area")
? html` <ha-area-picker
id="area"
.hass=${this.hass}
.value=${this._entryUpdates.area}
@value-changed=${this._registryEntryChanged}
></ha-area-picker>`
: nothing}
<ha-chip-set> <ha-chip-set>
${this._renderOptionalChip( ${this._renderOptionalChip(
@ -209,6 +220,12 @@ class DialogAutomationRename extends LitElement implements HassDialog {
) )
) )
: nothing} : nothing}
${this._renderOptionalChip(
"area",
this.hass.localize(
"ui.panel.config.automation.editor.dialog.add_area"
)
)}
${this._renderOptionalChip( ${this._renderOptionalChip(
"category", "category",
this.hass.localize( this.hass.localize(
@ -311,12 +328,14 @@ class DialogAutomationRename extends LitElement implements HassDialog {
ha-icon-picker, ha-icon-picker,
ha-category-picker, ha-category-picker,
ha-labels-picker, ha-labels-picker,
ha-area-picker,
ha-chip-set { ha-chip-set {
display: block; display: block;
} }
ha-icon-picker, ha-icon-picker,
ha-category-picker, ha-category-picker,
ha-labels-picker, ha-labels-picker,
ha-area-picker,
ha-chip-set { ha-chip-set {
margin-top: 16px; margin-top: 16px;
} }

View File

@ -13,6 +13,7 @@ interface BaseRenameDialogParams {
} }
export interface EntityRegistryUpdate { export interface EntityRegistryUpdate {
area: string;
labels: string[]; labels: string[];
category: string; category: string;
} }

View File

@ -167,10 +167,11 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
if ( if (
this._entityRegCreated && this._entityRegCreated &&
this._newAutomationId && this._newAutomationId &&
changedProps.has("entityRegistry") changedProps.has("_entityRegistry")
) { ) {
const automation = this._entityRegistry.find( const automation = this._entityRegistry.find(
(entity: EntityRegistryEntry) => (entity: EntityRegistryEntry) =>
entity.platform === "automation" &&
entity.unique_id === this._newAutomationId entity.unique_id === this._newAutomationId
); );
if (automation) { if (automation) {
@ -927,6 +928,14 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
this._saving = true; this._saving = true;
this._validationErrors = undefined; this._validationErrors = undefined;
let entityRegPromise: Promise<EntityRegistryEntry> | undefined;
if (this._entityRegistryUpdate !== undefined && !this._entityId) {
this._newAutomationId = id;
entityRegPromise = new Promise<EntityRegistryEntry>((resolve) => {
this._entityRegCreated = resolve;
});
}
try { try {
await saveAutomationConfig(this.hass, id, this._config!); await saveAutomationConfig(this.hass, id, this._config!);
@ -934,13 +943,8 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
let entityId = this._entityId; let entityId = this._entityId;
// wait for automation to appear in entity registry when creating a new automation // wait for automation to appear in entity registry when creating a new automation
if (!entityId) { if (entityRegPromise) {
this._newAutomationId = id; const automation = await entityRegPromise;
const automation = await new Promise<EntityRegistryEntry>(
(resolve) => {
this._entityRegCreated = resolve;
}
);
entityId = automation.entity_id; entityId = automation.entity_id;
} }
@ -950,6 +954,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
automation: this._entityRegistryUpdate.category || null, automation: this._entityRegistryUpdate.category || null,
}, },
labels: this._entityRegistryUpdate.labels || [], labels: this._entityRegistryUpdate.labels || [],
area_id: this._entityRegistryUpdate.area || null,
}); });
} }
} }

View File

@ -51,7 +51,7 @@ class HaBackupConfigAgents extends LitElement {
private _description(agentId: string) { private _description(agentId: string) {
if (agentId === CLOUD_AGENT) { 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)) { if (isNetworkMountAgent(agentId)) {
return "Network storage"; return "Network storage";

View File

@ -6,9 +6,10 @@ import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item"; import "../../../../../components/ha-md-list-item";
import type { HomeAssistant } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";
import { showChangeBackupEncryptionKeyDialog } from "../../dialogs/show-dialog-change-backup-encryption-key"; 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 { showSetBackupEncryptionKeyDialog } from "../../dialogs/show-dialog-set-backup-encryption-key";
import { downloadEmergencyKit } from "../../../../../data/backup";
@customElement("ha-backup-config-encryption-key") @customElement("ha-backup-config-encryption-key")
class HaBackupConfigEncryptionKey extends LitElement { class HaBackupConfigEncryptionKey extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@ -64,10 +65,7 @@ class HaBackupConfigEncryptionKey extends LitElement {
if (!this._value) { if (!this._value) {
return; return;
} }
fileDownload( downloadEmergencyKit(this.hass, this._value);
"data:text/plain;charset=utf-8," + encodeURIComponent(this._value),
"emergency_kit.txt"
);
} }
private _change() { private _change() {

View File

@ -3,18 +3,21 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import { clamp } from "../../../../../common/number/clamp";
import type { HaCheckbox } from "../../../../../components/ha-checkbox"; import type { HaCheckbox } from "../../../../../components/ha-checkbox";
import "../../../../../components/ha-md-list"; import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item"; import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-md-select"; import "../../../../../components/ha-md-select";
import "../../../../../components/ha-md-textfield";
import type { HaMdSelect } from "../../../../../components/ha-md-select"; import type { HaMdSelect } from "../../../../../components/ha-md-select";
import "../../../../../components/ha-md-select-option"; import "../../../../../components/ha-md-select-option";
import "../../../../../components/ha-md-textfield";
import "../../../../../components/ha-switch"; import "../../../../../components/ha-switch";
import type { BackupConfig } from "../../../../../data/backup"; import type { BackupConfig } from "../../../../../data/backup";
import { BackupScheduleState } from "../../../../../data/backup"; import {
BackupScheduleState,
getFormattedBackupTime,
} from "../../../../../data/backup";
import type { HomeAssistant } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";
import { clamp } from "../../../../../common/number/clamp";
export type BackupConfigSchedule = Pick<BackupConfig, "schedule" | "retention">; export type BackupConfigSchedule = Pick<BackupConfig, "schedule" | "retention">;
@ -120,13 +123,12 @@ class HaBackupConfigSchedule extends LitElement {
protected render() { protected render() {
const data = this._getData(this.value); const data = this._getData(this.value);
const time = getFormattedBackupTime(this.hass.locale, this.hass.config);
return html` return html`
<ha-md-list> <ha-md-list>
<ha-md-list-item> <ha-md-list-item>
<span slot="headline">Use automatic backups</span> <span slot="headline">Use automatic backups</span>
<span slot="supporting-text">
How often you want to create a backup.
</span>
<ha-switch <ha-switch
slot="end" slot="end"
@ -148,35 +150,36 @@ class HaBackupConfigSchedule extends LitElement {
.value=${data.schedule} .value=${data.schedule}
> >
<ha-md-select-option .value=${BackupScheduleState.DAILY}> <ha-md-select-option .value=${BackupScheduleState.DAILY}>
<div slot="headline">Daily at 04:45</div> <div slot="headline">Daily at ${time}</div>
</ha-md-select-option> </ha-md-select-option>
<ha-md-select-option .value=${BackupScheduleState.MONDAY}> <ha-md-select-option .value=${BackupScheduleState.MONDAY}>
<div slot="headline">Monday at 04:45</div> <div slot="headline">Monday at ${time}</div>
</ha-md-select-option> </ha-md-select-option>
<ha-md-select-option .value=${BackupScheduleState.TUESDAY}> <ha-md-select-option .value=${BackupScheduleState.TUESDAY}>
<div slot="headline">Tuesday at 04:45</div> <div slot="headline">Tuesday at ${time}</div>
</ha-md-select-option> </ha-md-select-option>
<ha-md-select-option .value=${BackupScheduleState.WEDNESDAY}> <ha-md-select-option .value=${BackupScheduleState.WEDNESDAY}>
<div slot="headline">Wednesday at 04:45</div> <div slot="headline">Wednesday at ${time}</div>
</ha-md-select-option> </ha-md-select-option>
<ha-md-select-option .value=${BackupScheduleState.THURSDAY}> <ha-md-select-option .value=${BackupScheduleState.THURSDAY}>
<div slot="headline">Thursday at 04:45</div> <div slot="headline">Thursday at ${time}</div>
</ha-md-select-option> </ha-md-select-option>
<ha-md-select-option .value=${BackupScheduleState.FRIDAY}> <ha-md-select-option .value=${BackupScheduleState.FRIDAY}>
<div slot="headline">Friday at 04:45</div> <div slot="headline">Friday at ${time}</div>
</ha-md-select-option> </ha-md-select-option>
<ha-md-select-option .value=${BackupScheduleState.SATURDAY}> <ha-md-select-option .value=${BackupScheduleState.SATURDAY}>
<div slot="headline">Saturday at 04:45</div> <div slot="headline">Saturday at ${time}</div>
</ha-md-select-option> </ha-md-select-option>
<ha-md-select-option .value=${BackupScheduleState.SUNDAY}> <ha-md-select-option .value=${BackupScheduleState.SUNDAY}>
<div slot="headline">Sunday at 04:45</div> <div slot="headline">Sunday at ${time}</div>
</ha-md-select-option> </ha-md-select-option>
</ha-md-select> </ha-md-select>
</ha-md-list-item> </ha-md-list-item>
<ha-md-list-item> <ha-md-list-item>
<span slot="headline">Backups to keep</span> <span slot="headline">Backups to keep</span>
<span slot="supporting-text"> <span slot="supporting-text">
The number of backups that are saved Based on the maximum number of backups or how many days they
should be kept.
</span> </span>
<ha-md-select <ha-md-select
slot="end" slot="end"
@ -326,16 +329,13 @@ class HaBackupConfigSchedule extends LitElement {
@media all and (max-width: 450px) { @media all and (max-width: 450px) {
ha-md-select { ha-md-select {
min-width: 160px; min-width: 160px;
width: 160px;
} }
} }
ha-md-textfield#value { ha-md-textfield#value {
min-width: 70px; min-width: 70px;
width: 70px;
} }
ha-md-select#type { ha-md-select#type {
min-width: 100px; min-width: 100px;
width: 100px;
} }
`; `;
} }

View File

@ -2,7 +2,9 @@ import { mdiPuzzle } from "@mdi/js";
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup } from "lit";
import { LitElement, css, html } from "lit"; import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { stringCompare } from "../../../../common/string/compare";
import "../../../../components/ha-checkbox"; import "../../../../components/ha-checkbox";
import type { HaCheckbox } from "../../../../components/ha-checkbox"; import type { HaCheckbox } from "../../../../components/ha-checkbox";
import "../../../../components/ha-formfield"; import "../../../../components/ha-formfield";
@ -29,10 +31,16 @@ export class HaBackupAddonsPicker extends LitElement {
@property({ attribute: "hide-version", type: Boolean }) @property({ attribute: "hide-version", type: Boolean })
public hideVersion = false; public hideVersion = false;
private _addons = memoizeOne((addons: BackupAddonItem[]) =>
addons.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language)
)
);
protected render() { protected render() {
return html` return html`
<div class="items"> <div class="items">
${this.addons.map( ${this._addons(this.addons).map(
(item) => html` (item) => html`
<ha-formfield> <ha-formfield>
<ha-backup-formfield-label <ha-backup-formfield-label

View File

@ -300,22 +300,22 @@ export class HaBackupDataPicker extends LitElement {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
.section { .section {
margin-inline-start: -16px;
margin-inline-end: 0;
margin-left: -16px; margin-left: -16px;
margin-inline-start: -16px;
margin-inline-end: initial;
} }
.items { .items {
padding-inline-start: 40px;
padding-inline-end: 0;
padding-left: 40px; padding-left: 40px;
padding-inline-start: 40px;
padding-inline-end: initial;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
ha-backup-addons-picker { ha-backup-addons-picker {
display: block; display: block;
padding-inline-start: 40px;
padding-inline-end: 0;
padding-left: 40px; padding-left: 40px;
padding-inline-start: 40px;
padding-inline-end: initial;
} }
`; `;
} }

View File

@ -68,6 +68,9 @@ class HaBackupSummaryCard extends LitElement {
} }
static styles = css` static styles = css`
ha-card {
min-height: 74px;
}
.summary { .summary {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View File

@ -13,6 +13,7 @@ import type { BackupConfig } from "../../../../../data/backup";
import { import {
BackupScheduleState, BackupScheduleState,
computeBackupAgentName, computeBackupAgentName,
getFormattedBackupTime,
isLocalAgent, isLocalAgent,
} from "../../../../../data/backup"; } from "../../../../../data/backup";
import { haStyle } from "../../../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";
@ -43,30 +44,32 @@ class HaBackupBackupsSummary extends LitElement {
copiesText = `and keep backups for ${days} day(s)`; copiesText = `and keep backups for ${days} day(s)`;
} }
const time = getFormattedBackupTime(this.hass.locale, this.hass.config);
let scheduleText = ""; let scheduleText = "";
if (schedule === BackupScheduleState.DAILY) { if (schedule === BackupScheduleState.DAILY) {
scheduleText = `Daily at 04:45`; scheduleText = `Daily at ${time}`;
} }
if (schedule === BackupScheduleState.MONDAY) { if (schedule === BackupScheduleState.MONDAY) {
scheduleText = `Weekly on Mondays at 04:45`; scheduleText = `Weekly on Mondays at ${time}`;
} }
if (schedule === BackupScheduleState.TUESDAY) { if (schedule === BackupScheduleState.TUESDAY) {
scheduleText = `Weekly on Thuesdays at 04:45`; scheduleText = `Weekly on Tuesdays at ${time}`;
} }
if (schedule === BackupScheduleState.WEDNESDAY) { if (schedule === BackupScheduleState.WEDNESDAY) {
scheduleText = `Weekly on Wednesdays at 04:45`; scheduleText = `Weekly on Wednesdays at ${time}`;
} }
if (schedule === BackupScheduleState.THURSDAY) { if (schedule === BackupScheduleState.THURSDAY) {
scheduleText = `Weekly on Thursdays at 04:45`; scheduleText = `Weekly on Thursdays at ${time}`;
} }
if (schedule === BackupScheduleState.FRIDAY) { if (schedule === BackupScheduleState.FRIDAY) {
scheduleText = `Weekly on Fridays at 04:45`; scheduleText = `Weekly on Fridays at ${time}`;
} }
if (schedule === BackupScheduleState.SATURDAY) { if (schedule === BackupScheduleState.SATURDAY) {
scheduleText = `Weekly on Saturdays at 04:45`; scheduleText = `Weekly on Saturdays at ${time}`;
} }
if (schedule === BackupScheduleState.SUNDAY) { if (schedule === BackupScheduleState.SUNDAY) {
scheduleText = `Weekly on Sundays at 04:45`; scheduleText = `Weekly on Sundays at ${time}`;
} }
return scheduleText + " " + copiesText; return scheduleText + " " + copiesText;

View File

@ -1,10 +1,9 @@
import { mdiBackupRestore, mdiCalendar } from "@mdi/js"; import { mdiBackupRestore, mdiCalendar } from "@mdi/js";
import { addHours, differenceInDays, setHours, setMinutes } from "date-fns"; import { addHours, differenceInDays } from "date-fns";
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup } from "lit";
import { css, html, LitElement } from "lit"; import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { formatTime } from "../../../../../common/datetime/format_time";
import { relativeTime } from "../../../../../common/datetime/relative_time"; import { relativeTime } from "../../../../../common/datetime/relative_time";
import "../../../../../components/ha-button"; import "../../../../../components/ha-button";
import "../../../../../components/ha-card"; import "../../../../../components/ha-card";
@ -12,7 +11,10 @@ import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item"; import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-svg-icon"; import "../../../../../components/ha-svg-icon";
import type { BackupConfig, BackupContent } from "../../../../../data/backup"; import type { BackupConfig, BackupContent } from "../../../../../data/backup";
import { BackupScheduleState } from "../../../../../data/backup"; import {
BackupScheduleState,
getFormattedBackupTime,
} from "../../../../../data/backup";
import { haStyle } from "../../../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";
import "../ha-backup-summary-card"; import "../ha-backup-summary-card";
@ -29,20 +31,16 @@ class HaBackupOverviewBackups extends LitElement {
@property({ type: Boolean }) public fetching = false; @property({ type: Boolean }) public fetching = false;
private _lastBackup = memoizeOne((backups: BackupContent[]) => { private _lastSuccessfulBackup = memoizeOne((backups: BackupContent[]) => {
const sortedBackups = backups const sortedBackups = backups
.filter( .filter((backup) => backup.with_automatic_settings)
(backup) =>
backup.with_automatic_settings && !backup.failed_agent_ids?.length
)
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
return sortedBackups[0] as BackupContent | undefined; return sortedBackups[0] as BackupContent | undefined;
}); });
private _nextBackupDescription(schedule: BackupScheduleState) { private _nextBackupDescription(schedule: BackupScheduleState) {
const newDate = setMinutes(setHours(new Date(), 4), 45); const time = getFormattedBackupTime(this.hass.locale, this.hass.config);
const time = formatTime(newDate, this.hass.locale, this.hass.config);
switch (schedule) { switch (schedule) {
case BackupScheduleState.DAILY: 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) { const lastSuccessfulBackupDate = lastSuccessfulBackup
return html` ? new Date(lastSuccessfulBackup.date)
<ha-backup-summary-card : new Date(0);
heading="No automatic backup available"
description="You have no automatic backups yet."
status="warning"
>
</ha-backup-summary-card>
`;
}
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 lastAttempt = this.config.last_attempted_automatic_backup const lastAttempt = this.config.last_attempted_automatic_backup
? new Date(this.config.last_attempted_automatic_backup) ? new Date(this.config.last_attempted_automatic_backup)
: undefined; : 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.`; const lastAttemptDescription = `The last automatic backup trigged ${relativeTime(lastAttempt, this.hass.locale, now, true)} wasn't successful.`;
return html` return html`
<ha-backup-summary-card <ha-backup-summary-card
heading=${`Last automatic backup failed`} heading="Last automatic backup failed"
status="error" status="error"
> >
<ha-md-list> <ha-md-list>
@ -131,10 +119,25 @@ class HaBackupOverviewBackups extends LitElement {
`; `;
} }
if (!lastSuccessfulBackup) {
return html`
<ha-backup-summary-card
heading="No automatic backup available"
description="You have no automatic backups yet."
status="warning"
>
</ha-backup-summary-card>
`;
}
const nextBackupDescription = this._nextBackupDescription(
this.config.schedule.state
);
const numberOfDays = differenceInDays( const numberOfDays = differenceInDays(
// Subtract a few hours to avoid showing as overdue if it's just a few hours (e.g. daylight saving) // 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), addHours(now, -OVERDUE_MARGIN_HOURS),
lastBackupDate lastSuccessfulBackupDate
); );
const isOverdue = const isOverdue =
@ -216,12 +219,13 @@ class HaBackupOverviewBackups extends LitElement {
animation-timing-function: linear; animation-timing-function: linear;
animation-duration: 1.2s; animation-duration: 1.2s;
border-radius: 4px; border-radius: 4px;
height: 20px; height: 16px;
margin: 2px 0;
background: linear-gradient( background: linear-gradient(
to right, to right,
rgb(247, 249, 250) 8%, var(--card-background-color) 8%,
rgb(235, 238, 240) 18%, var(--secondary-background-color) 18%,
rgb(247, 249, 250) 33% var(--card-background-color) 33%
) )
0% 0% / 936px 104px; 0% 0% / 936px 104px;
} }

View File

@ -24,6 +24,7 @@ import {
BackupScheduleState, BackupScheduleState,
CLOUD_AGENT, CLOUD_AGENT,
CORE_LOCAL_AGENT, CORE_LOCAL_AGENT,
downloadEmergencyKit,
generateEncryptionKey, generateEncryptionKey,
HASSIO_LOCAL_AGENT, HASSIO_LOCAL_AGENT,
updateBackupConfig, updateBackupConfig,
@ -31,7 +32,6 @@ import {
import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles"; import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { fileDownload } from "../../../../util/file_download";
import { showToast } from "../../../../util/toast"; import { showToast } from "../../../../util/toast";
import "../components/config/ha-backup-config-agents"; import "../components/config/ha-backup-config-agents";
import "../components/config/ha-backup-config-data"; import "../components/config/ha-backup-config-data";
@ -101,7 +101,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
agents.push(CORE_LOCAL_AGENT); agents.push(CORE_LOCAL_AGENT);
} }
// Enable cloud location if logged in // Enable cloud location if logged in
if (this._params.cloudStatus.logged_in) { if (this._params.cloudStatus?.logged_in) {
agents.push(CLOUD_AGENT); agents.push(CLOUD_AGENT);
} }
@ -327,12 +327,6 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
`; `;
case "setup": case "setup":
return html` return html`
<p>
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.
</p>
<ha-md-list class="full"> <ha-md-list class="full">
<ha-md-list-item type="button" @click=${this._done}> <ha-md-list-item type="button" @click=${this._done}>
<span slot="headline">Recommended settings</span> <span slot="headline">Recommended settings</span>
@ -398,14 +392,11 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
if (!key) { if (!key) {
return; return;
} }
fileDownload( downloadEmergencyKit(this.hass, key);
"data:text/plain;charset=utf-8," + encodeURIComponent(key),
"emergency_kit.txt"
);
} }
private _copyKeyToClipboard() { private async _copyKeyToClipboard() {
copyToClipboard(this._config!.create_backup.password!); await copyToClipboard(this._config!.create_backup.password!);
showToast(this, { showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"), message: this.hass.localize("ui.common.copied_clipboard"),
}); });
@ -471,6 +462,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
width: 90vw; width: 90vw;
max-width: 560px; max-width: 560px;
--dialog-content-padding: 8px 24px; --dialog-content-padding: 8px 24px;
max-height: min(605px, 100% - 48px);
} }
ha-md-list { ha-md-list {
background: none; background: none;

View File

@ -13,11 +13,13 @@ import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import "../../../../components/ha-md-list"; import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item"; import "../../../../components/ha-md-list-item";
import "../../../../components/ha-password-field"; 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 type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles"; import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { fileDownload } from "../../../../util/file_download";
import { showToast } from "../../../../util/toast"; import { showToast } from "../../../../util/toast";
import type { ChangeBackupEncryptionKeyDialogParams } from "./show-dialog-change-backup-encryption-key"; import type { ChangeBackupEncryptionKeyDialogParams } from "./show-dialog-change-backup-encryption-key";
@ -203,18 +205,18 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
return nothing; return nothing;
} }
private _copyKeyToClipboard() { private async _copyKeyToClipboard() {
copyToClipboard(this._newEncryptionKey); await copyToClipboard(this._newEncryptionKey);
showToast(this, { showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"), message: this.hass.localize("ui.common.copied_clipboard"),
}); });
} }
private _copyOldKeyToClipboard() { private async _copyOldKeyToClipboard() {
if (!this._params?.currentKey) { if (!this._params?.currentKey) {
return; return;
} }
copyToClipboard(this._params.currentKey); await copyToClipboard(this._params.currentKey);
showToast(this, { showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"), message: this.hass.localize("ui.common.copied_clipboard"),
}); });
@ -224,22 +226,14 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
if (!this._params?.currentKey) { if (!this._params?.currentKey) {
return; return;
} }
fileDownload( downloadEmergencyKit(this.hass, this._params.currentKey, "old");
"data:text/plain;charset=utf-8," +
encodeURIComponent(this._params.currentKey),
"emergency_kit_old.txt"
);
} }
private _downloadNew() { private _downloadNew() {
if (!this._newEncryptionKey) { if (!this._newEncryptionKey) {
return; return;
} }
fileDownload( downloadEmergencyKit(this.hass, this._newEncryptionKey);
"data:text/plain;charset=utf-8," +
encodeURIComponent(this._newEncryptionKey),
"emergency_kit.txt"
);
} }
private async _submit() { private async _submit() {

View File

@ -99,7 +99,9 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
const { agents } = await fetchBackupAgentsInfo(this.hass); const { agents } = await fetchBackupAgentsInfo(this.hass);
this._agentIds = agents this._agentIds = agents
.map((agent) => agent.agent_id) .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); .sort(compareAgents);
} }

View File

@ -11,11 +11,13 @@ import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import "../../../../components/ha-md-list"; import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item"; import "../../../../components/ha-md-list-item";
import "../../../../components/ha-password-field"; 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 type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles"; import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { fileDownload } from "../../../../util/file_download";
import type { SetBackupEncryptionKeyDialogParams } from "./show-dialog-set-backup-encryption-key"; import type { SetBackupEncryptionKeyDialogParams } from "./show-dialog-set-backup-encryption-key";
const STEPS = ["new", "save"] as const; const STEPS = ["new", "save"] as const;
@ -162,11 +164,7 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
if (!this._newEncryptionKey) { if (!this._newEncryptionKey) {
return; return;
} }
fileDownload( downloadEmergencyKit(this.hass, this._newEncryptionKey);
"data:text/plain;charset=utf-8," +
encodeURIComponent(this._newEncryptionKey),
"emergency_kit.txt"
);
} }
private _encryptionKeyChanged(ev) { private _encryptionKeyChanged(ev) {

View File

@ -4,7 +4,7 @@ import type { CloudStatus } from "../../../../data/cloud";
export interface BackupOnboardingDialogParams { export interface BackupOnboardingDialogParams {
submit?: (value: boolean) => void; submit?: (value: boolean) => void;
cancel?: () => void; cancel?: () => void;
cloudStatus: CloudStatus; cloudStatus?: CloudStatus;
} }
const loadDialog = () => import("./dialog-backup-onboarding"); const loadDialog = () => import("./dialog-backup-onboarding");

View File

@ -5,7 +5,7 @@ import type { CloudStatus } from "../../../../data/cloud";
export interface GenerateBackupDialogParams { export interface GenerateBackupDialogParams {
submit?: (response: GenerateBackupParams) => void; submit?: (response: GenerateBackupParams) => void;
cancel?: () => void; cancel?: () => void;
cloudStatus: CloudStatus; cloudStatus?: CloudStatus;
} }
export const loadGenerateBackupDialog = () => export const loadGenerateBackupDialog = () =>

View File

@ -78,7 +78,7 @@ const TYPE_ORDER: Array<BackupType> = ["automatic", "manual", "imported"];
class HaConfigBackupBackups extends SubscribeMixin(LitElement) { class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public cloudStatus!: CloudStatus; @property({ attribute: false }) public cloudStatus?: CloudStatus;
@property({ type: Boolean }) public narrow = false; @property({ type: Boolean }) public narrow = false;
@ -167,7 +167,6 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
title: "Locations", title: "Locations",
showNarrow: true, showNarrow: true,
minWidth: "60px", minWidth: "60px",
maxWidth: "120px",
template: (backup) => html` template: (backup) => html`
<div style="display: flex; gap: 4px;"> <div style="display: flex; gap: 4px;">
${(backup.agent_ids || []).map((agentId) => { ${(backup.agent_ids || []).map((agentId) => {
@ -181,7 +180,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
<ha-svg-icon <ha-svg-icon
.path=${mdiHarddisk} .path=${mdiHarddisk}
title=${name} title=${name}
slot="graphic" style="flex-shrink: 0;"
></ha-svg-icon> ></ha-svg-icon>
`; `;
} }
@ -190,7 +189,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
<ha-svg-icon <ha-svg-icon
.path=${mdiNas} .path=${mdiNas}
title=${name} title=${name}
slot="graphic" style="flex-shrink: 0;"
></ha-svg-icon> ></ha-svg-icon>
`; `;
} }
@ -209,6 +208,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
referrerpolicy="no-referrer" referrerpolicy="no-referrer"
alt=${name} alt=${name}
slot="graphic" slot="graphic"
style="flex-shrink: 0;"
/> />
`; `;
})} })}

View File

@ -26,7 +26,6 @@ import "../../../layouts/hass-subpage";
import "../../../layouts/hass-tabs-subpage-data-table"; import "../../../layouts/hass-tabs-subpage-data-table";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route } from "../../../types"; 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-backups";
import "./components/overview/ha-backup-overview-onboarding"; import "./components/overview/ha-backup-overview-onboarding";
import "./components/overview/ha-backup-overview-progress"; import "./components/overview/ha-backup-overview-progress";
@ -42,7 +41,7 @@ import { showUploadBackupDialog } from "./dialogs/show-dialog-upload-backup";
class HaConfigBackupOverview extends LitElement { class HaConfigBackupOverview extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public cloudStatus!: CloudStatus; @property({ attribute: false }) public cloudStatus?: CloudStatus;
@property({ type: Boolean }) public narrow = false; @property({ type: Boolean }) public narrow = false;

View File

@ -141,9 +141,9 @@ class HaConfigBackupSettings extends LitElement {
<div class="card-content"> <div class="card-content">
<p> <p>
Keep this encryption key in a safe place, as you will need it to Keep this encryption key in a safe place, as you will need it to
access your backup, allowing it to be restored. Either record access your backup, allowing it to be restored. Download them as
the characters below or download them as an emergency kit file. an emergency kit file and store it somewhere safe. Encryption
Encryption keeps your backups private and secure. keeps your backups private and secure.
</p> </p>
<ha-backup-config-encryption-key <ha-backup-config-encryption-key
.hass=${this.hass} .hass=${this.hass}

View File

@ -75,7 +75,9 @@ export class DialogTryTts extends LitElement {
<ha-textarea <ha-textarea
autogrow autogrow
id="message" id="message"
label="Message" .label=${this.hass.localize(
"ui.panel.config.cloud.account.tts.dialog.message"
)}
.value=${this._message || .value=${this._message ||
this.hass.localize( this.hass.localize(
"ui.panel.config.cloud.account.tts.dialog.example_message", "ui.panel.config.cloud.account.tts.dialog.example_message",

View File

@ -247,6 +247,13 @@ export class SystemLogCard extends LitElement {
padding-top: 8px; padding-top: 8px;
} }
:host {
direction: var(--direction);
}
mwc-list {
direction: ltr;
}
.header { .header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -293,13 +300,7 @@ export class SystemLogCard extends LitElement {
border-top: 1px solid var(--divider-color); border-top: 1px solid var(--divider-color);
} }
.card-actions,
.empty-content {
direction: var(--direction);
}
.row-secondary { .row-secondary {
direction: var(--direction);
text-align: left; text-align: left;
} }
`; `;

View File

@ -105,6 +105,9 @@ export class HaConfigLovelaceRescources extends LitElement {
}, },
delete: { delete: {
title: "", title: "",
label: localize(
"ui.panel.config.lovelace.resources.picker.headers.delete"
),
type: "icon-button", type: "icon-button",
minWidth: "48px", minWidth: "48px",
maxWidth: "48px", maxWidth: "48px",

View File

@ -99,7 +99,7 @@ class DialogRepairsIssue extends LitElement {
: ""} : ""}
<ha-markdown <ha-markdown
id="dialog-repairs-issue-description" id="dialog-repairs-issue-description"
allowsvg allow-svg
breaks breaks
@click=${this._clickHandler} @click=${this._clickHandler}
.content=${this.hass.localize( .content=${this.hass.localize(

View File

@ -90,7 +90,7 @@ export const showRepairsFlowDialog = (
? html` ? html`
<ha-markdown <ha-markdown
breaks breaks
allowsvg allow-svg
.content=${description} .content=${description}
></ha-markdown> ></ha-markdown>
` `
@ -123,7 +123,7 @@ export const showRepairsFlowDialog = (
${description ${description
? html` ? html`
<ha-markdown <ha-markdown
allowsvg allow-svg
breaks breaks
.content=${description} .content=${description}
></ha-markdown> ></ha-markdown>
@ -220,7 +220,7 @@ export const showRepairsFlowDialog = (
return html`${renderIssueDescription(hass, issue)}${description return html`${renderIssueDescription(hass, issue)}${description
? html` ? html`
<ha-markdown <ha-markdown
allowsvg allow-svg
breaks breaks
.content=${description} .content=${description}
></ha-markdown> ></ha-markdown>
@ -254,7 +254,7 @@ export const showRepairsFlowDialog = (
${description ${description
? html` ? html`
<ha-markdown <ha-markdown
allowsvg allow-svg
breaks breaks
.content=${description} .content=${description}
></ha-markdown> ></ha-markdown>

View File

@ -139,7 +139,8 @@ export class HaScriptEditor extends SubscribeMixin(
changedProps.has("entityRegistry") changedProps.has("entityRegistry")
) { ) {
const script = this.entityRegistry.find( const script = this.entityRegistry.find(
(entity: EntityRegistryEntry) => entity.unique_id === this._newScriptId (entity: EntityRegistryEntry) =>
entity.platform === "script" && entity.unique_id === this._newScriptId
); );
if (script) { if (script) {
this._entityRegCreated(script); this._entityRegCreated(script);
@ -164,7 +165,8 @@ export class HaScriptEditor extends SubscribeMixin(
.narrow=${this.narrow} .narrow=${this.narrow}
.route=${this.route} .route=${this.route}
.backCallback=${this._backTapped} .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 ${this.scriptId && !this.narrow
? html` ? html`
@ -487,9 +489,7 @@ export class HaScriptEditor extends SubscribeMixin(
if (changedProps.has("scriptId") && !this.scriptId && this.hass) { if (changedProps.has("scriptId") && !this.scriptId && this.hass) {
const initData = getScriptEditorInitData(); const initData = getScriptEditorInitData();
this._dirty = !!initData; this._dirty = !!initData;
const baseConfig: Partial<ScriptConfig> = { const baseConfig: Partial<ScriptConfig> = {};
alias: this.hass.localize("ui.panel.config.script.editor.default_name"),
};
if (!initData || !("use_blueprint" in initData)) { if (!initData || !("use_blueprint" in initData)) {
baseConfig.sequence = []; baseConfig.sequence = [];
} }
@ -894,6 +894,15 @@ export class HaScriptEditor extends SubscribeMixin(
const id = this.scriptId || this._entityId || Date.now(); const id = this.scriptId || this._entityId || Date.now();
this._saving = true; this._saving = true;
let entityRegPromise: Promise<EntityRegistryEntry> | undefined;
if (this._entityRegistryUpdate !== undefined && !this.scriptId) {
this._newScriptId = id.toString();
entityRegPromise = new Promise<EntityRegistryEntry>((resolve) => {
this._entityRegCreated = resolve;
});
}
try { try {
await this.hass!.callApi( await this.hass!.callApi(
"POST", "POST",
@ -902,23 +911,20 @@ export class HaScriptEditor extends SubscribeMixin(
); );
if (this._entityRegistryUpdate !== undefined) { if (this._entityRegistryUpdate !== undefined) {
let entityId = id.toString().startsWith("script.") let entityId = this._entityId;
? id.toString()
: `script.${id}`;
// wait for new script to appear in entity registry // wait for new script to appear in entity registry
if (!this.scriptId) { if (entityRegPromise) {
const script = await new Promise<EntityRegistryEntry>((resolve) => { const script = await entityRegPromise;
this._entityRegCreated = resolve;
});
entityId = script.entity_id; entityId = script.entity_id;
} }
await updateEntityRegistryEntry(this.hass, entityId, { await updateEntityRegistryEntry(this.hass, entityId!, {
categories: { categories: {
script: this._entityRegistryUpdate.category || null, script: this._entityRegistryUpdate.category || null,
}, },
labels: this._entityRegistryUpdate.labels || [], labels: this._entityRegistryUpdate.labels || [],
area_id: this._entityRegistryUpdate.area || null,
}); });
} }

View File

@ -178,7 +178,9 @@ export class HaScriptTrace extends LitElement {
<ha-icon-button <ha-icon-button
.disabled=${this._traces[this._traces.length - 1].run_id === .disabled=${this._traces[this._traces.length - 1].run_id ===
this._runId} this._runId}
label="Older trace" .label=${this.hass!.localize(
"ui.panel.config.automation.trace.older_trace"
)}
@click=${this._pickOlderTrace} @click=${this._pickOlderTrace}
.path=${mdiRayEndArrow} .path=${mdiRayEndArrow}
></ha-icon-button> ></ha-icon-button>
@ -198,7 +200,9 @@ export class HaScriptTrace extends LitElement {
</select> </select>
<ha-icon-button <ha-icon-button
.disabled=${this._traces[0].run_id === this._runId} .disabled=${this._traces[0].run_id === this._runId}
label="Newer trace" .label=${this.hass!.localize(
"ui.panel.config.automation.trace.newer_trace"
)}
@click=${this._pickNewerTrace} @click=${this._pickNewerTrace}
.path=${mdiRayStartArrow} .path=${mdiRayStartArrow}
></ha-icon-button> ></ha-icon-button>

View File

@ -324,10 +324,10 @@ class HaPanelDevState extends LitElement {
`; `;
} }
private _copyEntity(ev) { private async _copyEntity(ev) {
ev.preventDefault(); ev.preventDefault();
const entity = (ev.currentTarget! as any).entity; const entity = (ev.currentTarget! as any).entity;
copyToClipboard(entity.entity_id); await copyToClipboard(entity.entity_id);
} }
private _entitySelected(ev) { private _entitySelected(ev) {

View File

@ -23,7 +23,7 @@ export class HuiGenericEntityRow extends LitElement {
@property({ attribute: false }) public config?: EntitiesCardEntityConfig; @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; @property({ attribute: "hide-name", type: Boolean }) public hideName = false;

View File

@ -115,14 +115,24 @@ export class HuiViewBackgroundEditor extends LitElement {
}; };
} }
if (!background) {
background = {
transparency: 33,
alignment: "center",
size: "cover",
repeat: "repeat",
attachment: "fixed",
};
} else {
background = { background = {
transparency: 100, transparency: 100,
alignment: "center", alignment: "center",
size: "auto", size: "cover",
repeat: "no-repeat", repeat: "no-repeat",
attachment: "scroll", attachment: "scroll",
...background, ...background,
}; };
}
return html` return html`
<ha-form <ha-form

View File

@ -52,8 +52,8 @@ export class HUIViewBackground extends LitElement {
background?: string | LovelaceViewBackgroundConfig background?: string | LovelaceViewBackgroundConfig
) { ) {
if (typeof background === "object" && background.image) { if (typeof background === "object" && background.image) {
const size = background.size ?? "auto";
const alignment = background.alignment ?? "center"; const alignment = background.alignment ?? "center";
const size = background.size ?? "cover";
const repeat = background.repeat ?? "no-repeat"; const repeat = background.repeat ?? "no-repeat";
return `${alignment} / ${size} ${repeat} url('${background.image}')`; return `${alignment} / ${size} ${repeat} url('${background.image}')`;
} }

View File

@ -88,7 +88,7 @@ class HaMfaModuleSetupFlow extends LitElement {
</div>` </div>`
: html`${this._step.type === "abort" : html`${this._step.type === "abort"
? html` <ha-markdown ? html` <ha-markdown
allowsvg allow-svg
breaks breaks
.content=${this.hass.localize( .content=${this.hass.localize(
`component.auth.mfa_setup.${this._step.handler}.abort.${this._step.reason}` `component.auth.mfa_setup.${this._step.handler}.abort.${this._step.reason}`
@ -103,7 +103,7 @@ class HaMfaModuleSetupFlow extends LitElement {
</p>` </p>`
: this._step.type === "form" : this._step.type === "form"
? html`<ha-markdown ? html`<ha-markdown
allowsvg allow-svg
breaks breaks
.content=${this.hass.localize( .content=${this.hass.localize(
`component.auth.mfa_setup.${ `component.auth.mfa_setup.${

View File

@ -936,7 +936,7 @@
"delete": "Delete {count}", "delete": "Delete {count}",
"deleting": "Deleting {count}", "deleting": "Deleting {count}",
"tip_media_storage": "[%key:ui::panel::config::tips::media_storage%]", "tip_media_storage": "[%key:ui::panel::config::tips::media_storage%]",
"tip_storage_panel": "[%key:ui::panel::config::storage::caption%]" "tip_storage_panel": "storage"
}, },
"class": { "class": {
"album": "Album", "album": "Album",
@ -2685,7 +2685,8 @@
"picker": { "picker": {
"headers": { "headers": {
"url": "URL", "url": "URL",
"type": "Type" "type": "Type",
"delete": "Delete"
}, },
"no_resources": "No resources", "no_resources": "No resources",
"add_resource": "Add resource" "add_resource": "Add resource"
@ -2957,8 +2958,8 @@
"traces_not_available": "[%key:ui::panel::config::automation::editor::traces_not_available%]", "traces_not_available": "[%key:ui::panel::config::automation::editor::traces_not_available%]",
"edit_category": "Edit category", "edit_category": "Edit category",
"assign_category": "Assign category", "assign_category": "Assign category",
"no_category_support": "You can't assign an category to this automation", "no_category_support": "You can't assign a category to this automation",
"no_category_entity_reg": "To assign an category to an automation it needs to have a unique ID.", "no_category_entity_reg": "To assign a category to an automation it needs to have a unique ID.",
"search": "Search {number} automations", "search": "Search {number} automations",
"headers": { "headers": {
"toggle": "Enable/disable", "toggle": "Enable/disable",
@ -3717,7 +3718,8 @@
"add_description": "Add description", "add_description": "Add description",
"add_icon": "Add icon", "add_icon": "Add icon",
"add_category": "Add category", "add_category": "Add category",
"add_labels": "Add labels" "add_labels": "Add labels",
"add_area": "Add area"
} }
}, },
"trace": { "trace": {
@ -4117,6 +4119,7 @@
"try": "Try", "try": "Try",
"dialog": { "dialog": {
"header": "Try text-to-speech", "header": "Try text-to-speech",
"message": "Message",
"example_message": "Hello {name}, you can play any text on any supported media player!", "example_message": "Hello {name}, you can play any text on any supported media player!",
"target": "Target", "target": "Target",
"target_browser": "Browser", "target_browser": "Browser",

View File

@ -1234,10 +1234,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@braintree/sanitize-url@npm:7.1.0": "@braintree/sanitize-url@npm:7.1.1":
version: 7.1.0 version: 7.1.1
resolution: "@braintree/sanitize-url@npm:7.1.0" resolution: "@braintree/sanitize-url@npm:7.1.1"
checksum: 10/b25cc5358bedfd97d8378d23ab43493e56a805bd82fdb092088bdd9db6aa3f6c32859d36526f570fb2c67a5a4f9ce579aacd52c3872db4285e4c34fb9947dfc0 checksum: 10/a8a5535c5a0a459ba593a018c554b35493dff004fd09d7147db67243df83bce3d410b89ee7dc2d95cce195b85b877c72f8ca149e1040110a945d193c67293af0
languageName: node languageName: node
linkType: hard linkType: hard
@ -9117,7 +9117,7 @@ __metadata:
"@babel/preset-env": "npm:7.26.0" "@babel/preset-env": "npm:7.26.0"
"@babel/preset-typescript": "npm:7.26.0" "@babel/preset-typescript": "npm:7.26.0"
"@babel/runtime": "npm:7.26.0" "@babel/runtime": "npm:7.26.0"
"@braintree/sanitize-url": "npm:7.1.0" "@braintree/sanitize-url": "npm:7.1.1"
"@bundle-stats/plugin-webpack-filter": "npm:4.17.0" "@bundle-stats/plugin-webpack-filter": "npm:4.17.0"
"@codemirror/autocomplete": "npm:6.18.4" "@codemirror/autocomplete": "npm:6.18.4"
"@codemirror/commands": "npm:6.7.1" "@codemirror/commands": "npm:6.7.1"
@ -9296,7 +9296,7 @@ __metadata:
tsparticles-engine: "npm:2.12.0" tsparticles-engine: "npm:2.12.0"
tsparticles-preset-links: "npm:2.12.0" tsparticles-preset-links: "npm:2.12.0"
typescript: "npm:5.7.2" typescript: "npm:5.7.2"
ua-parser-js: "npm:1.0.39" ua-parser-js: "npm:1.0.40"
vis-data: "npm:7.1.9" vis-data: "npm:7.1.9"
vis-network: "npm:9.1.9" vis-network: "npm:9.1.9"
vitest: "npm:2.1.8" vitest: "npm:2.1.8"
@ -14440,12 +14440,12 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"ua-parser-js@npm:1.0.39": "ua-parser-js@npm:1.0.40":
version: 1.0.39 version: 1.0.40
resolution: "ua-parser-js@npm:1.0.39" resolution: "ua-parser-js@npm:1.0.40"
bin: bin:
ua-parser-js: script/cli.js ua-parser-js: script/cli.js
checksum: 10/dd4026b6ece8a34a0d39b6de5542154c4506077d8def8647a300a29e1b3ffa0e23f5c8eeeb8101df6162b7b3eb3597d0b4adb031ae6104cbdb730d6ebc07f3c0 checksum: 10/7fced5f74ed570c83addffd4d367888d90c58803ff4bdd4a7b04b3f01d293263b8605e92ac560eb1c6a201ef3b11fcc46f3dbcbe764fbe54974924d542bc0135
languageName: node languageName: node
linkType: hard linkType: hard