mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-27 19:26:36 +00:00
Merge branch 'dev' into rc
This commit is contained in:
commit
f416b1b5da
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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<number> = 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`<div class="chartLegend">
|
||||
? html`<div class="chart-legend">
|
||||
<ul>
|
||||
${this._datasetOrder.map((index) => {
|
||||
const dataset = this.data.datasets[index];
|
||||
@ -249,7 +263,7 @@ export class HaChartBase extends LitElement {
|
||||
</div>`
|
||||
: ""}
|
||||
<div
|
||||
class="animationContainer"
|
||||
class="animation-container"
|
||||
style=${styleMap({
|
||||
height: `${this.height || this._chartHeight || 0}px`,
|
||||
overflow: this._chartHeight ? "initial" : "hidden",
|
||||
@ -288,7 +302,7 @@ export class HaChartBase extends LitElement {
|
||||
</div>
|
||||
${this._tooltip
|
||||
? html`<div
|
||||
class="chartTooltip ${classMap({
|
||||
class="chart-tooltip ${classMap({
|
||||
[this._tooltip.yAlign]: true,
|
||||
})}"
|
||||
style=${styleMap({
|
||||
@ -298,7 +312,7 @@ export class HaChartBase extends LitElement {
|
||||
>
|
||||
<div class="title">${this._tooltip.title}</div>
|
||||
${this._tooltip.beforeBody
|
||||
? html`<div class="beforeBody">
|
||||
? html`<div class="before-body">
|
||||
${this._tooltip.beforeBody}
|
||||
</div>`
|
||||
: ""}
|
||||
@ -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;
|
||||
|
@ -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"
|
||||
)}
|
||||
</a>`,
|
||||
}
|
||||
)}
|
||||
|
@ -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);
|
||||
}
|
||||
);
|
||||
|
@ -50,7 +50,7 @@ export const showConfigFlowDialog = (
|
||||
|
||||
return description
|
||||
? html`
|
||||
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
|
||||
<ha-markdown allow-svg breaks .content=${description}></ha-markdown>
|
||||
`
|
||||
: step.reason;
|
||||
},
|
||||
@ -71,7 +71,7 @@ export const showConfigFlowDialog = (
|
||||
);
|
||||
return description
|
||||
? 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
|
||||
? html`
|
||||
<ha-markdown
|
||||
allowsvg
|
||||
allow-svg
|
||||
breaks
|
||||
.content=${description}
|
||||
></ha-markdown>
|
||||
@ -184,7 +184,7 @@ export const showConfigFlowDialog = (
|
||||
${description
|
||||
? html`
|
||||
<ha-markdown
|
||||
allowsvg
|
||||
allow-svg
|
||||
breaks
|
||||
.content=${description}
|
||||
></ha-markdown>
|
||||
@ -214,7 +214,7 @@ export const showConfigFlowDialog = (
|
||||
);
|
||||
return description
|
||||
? 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
|
||||
? html`
|
||||
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
|
||||
<ha-markdown allow-svg breaks .content=${description}></ha-markdown>
|
||||
`
|
||||
: "";
|
||||
},
|
||||
|
@ -61,7 +61,7 @@ export const showOptionsFlowDialog = (
|
||||
? html`
|
||||
<ha-markdown
|
||||
breaks
|
||||
allowsvg
|
||||
allow-svg
|
||||
.content=${description}
|
||||
></ha-markdown>
|
||||
`
|
||||
@ -85,7 +85,7 @@ export const showOptionsFlowDialog = (
|
||||
return description
|
||||
? html`
|
||||
<ha-markdown
|
||||
allowsvg
|
||||
allow-svg
|
||||
breaks
|
||||
.content=${description}
|
||||
></ha-markdown>
|
||||
@ -183,7 +183,7 @@ export const showOptionsFlowDialog = (
|
||||
return description
|
||||
? html`
|
||||
<ha-markdown
|
||||
allowsvg
|
||||
allow-svg
|
||||
breaks
|
||||
.content=${description}
|
||||
></ha-markdown>
|
||||
@ -207,7 +207,7 @@ export const showOptionsFlowDialog = (
|
||||
return description
|
||||
? html`
|
||||
<ha-markdown
|
||||
allowsvg
|
||||
allow-svg
|
||||
breaks
|
||||
.content=${description}
|
||||
></ha-markdown>
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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}`),
|
||||
|
@ -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}
|
||||
></ha-labels-picker>`
|
||||
: 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>
|
||||
${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;
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ interface BaseRenameDialogParams {
|
||||
}
|
||||
|
||||
export interface EntityRegistryUpdate {
|
||||
area: string;
|
||||
labels: string[];
|
||||
category: string;
|
||||
}
|
||||
|
@ -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<EntityRegistryEntry> | undefined;
|
||||
if (this._entityRegistryUpdate !== undefined && !this._entityId) {
|
||||
this._newAutomationId = id;
|
||||
entityRegPromise = new Promise<EntityRegistryEntry>((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<EntityRegistryEntry>(
|
||||
(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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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";
|
||||
|
@ -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() {
|
||||
|
@ -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<BackupConfig, "schedule" | "retention">;
|
||||
|
||||
@ -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`
|
||||
<ha-md-list>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">Use automatic backups</span>
|
||||
<span slot="supporting-text">
|
||||
How often you want to create a backup.
|
||||
</span>
|
||||
|
||||
<ha-switch
|
||||
slot="end"
|
||||
@ -148,35 +150,36 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
.value=${data.schedule}
|
||||
>
|
||||
<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 .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 .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 .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 .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 .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 .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 .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>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">Backups to keep</span>
|
||||
<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>
|
||||
<ha-md-select
|
||||
slot="end"
|
||||
@ -326,16 +329,13 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
@media all and (max-width: 450px) {
|
||||
ha-md-select {
|
||||
min-width: 160px;
|
||||
width: 160px;
|
||||
}
|
||||
}
|
||||
ha-md-textfield#value {
|
||||
min-width: 70px;
|
||||
width: 70px;
|
||||
}
|
||||
ha-md-select#type {
|
||||
min-width: 100px;
|
||||
width: 100px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@ -2,7 +2,9 @@ import { mdiPuzzle } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stringCompare } from "../../../../common/string/compare";
|
||||
import "../../../../components/ha-checkbox";
|
||||
import type { HaCheckbox } from "../../../../components/ha-checkbox";
|
||||
import "../../../../components/ha-formfield";
|
||||
@ -29,10 +31,16 @@ export class HaBackupAddonsPicker extends LitElement {
|
||||
@property({ attribute: "hide-version", type: Boolean })
|
||||
public hideVersion = false;
|
||||
|
||||
private _addons = memoizeOne((addons: BackupAddonItem[]) =>
|
||||
addons.sort((a, b) =>
|
||||
stringCompare(a.name, b.name, this.hass.locale.language)
|
||||
)
|
||||
);
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="items">
|
||||
${this.addons.map(
|
||||
${this._addons(this.addons).map(
|
||||
(item) => html`
|
||||
<ha-formfield>
|
||||
<ha-backup-formfield-label
|
||||
|
@ -300,22 +300,22 @@ export class HaBackupDataPicker extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
.section {
|
||||
margin-inline-start: -16px;
|
||||
margin-inline-end: 0;
|
||||
margin-left: -16px;
|
||||
margin-inline-start: -16px;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
.items {
|
||||
padding-inline-start: 40px;
|
||||
padding-inline-end: 0;
|
||||
padding-left: 40px;
|
||||
padding-inline-start: 40px;
|
||||
padding-inline-end: initial;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
ha-backup-addons-picker {
|
||||
display: block;
|
||||
padding-inline-start: 40px;
|
||||
padding-inline-end: 0;
|
||||
padding-left: 40px;
|
||||
padding-inline-start: 40px;
|
||||
padding-inline-end: initial;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@ -68,6 +68,9 @@ class HaBackupSummaryCard extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-card {
|
||||
min-height: 74px;
|
||||
}
|
||||
.summary {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -13,6 +13,7 @@ import type { BackupConfig } from "../../../../../data/backup";
|
||||
import {
|
||||
BackupScheduleState,
|
||||
computeBackupAgentName,
|
||||
getFormattedBackupTime,
|
||||
isLocalAgent,
|
||||
} from "../../../../../data/backup";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
@ -43,30 +44,32 @@ class HaBackupBackupsSummary extends LitElement {
|
||||
copiesText = `and keep backups for ${days} day(s)`;
|
||||
}
|
||||
|
||||
const time = getFormattedBackupTime(this.hass.locale, this.hass.config);
|
||||
|
||||
let scheduleText = "";
|
||||
if (schedule === BackupScheduleState.DAILY) {
|
||||
scheduleText = `Daily at 04:45`;
|
||||
scheduleText = `Daily at ${time}`;
|
||||
}
|
||||
if (schedule === BackupScheduleState.MONDAY) {
|
||||
scheduleText = `Weekly on Mondays at 04:45`;
|
||||
scheduleText = `Weekly on Mondays at ${time}`;
|
||||
}
|
||||
if (schedule === BackupScheduleState.TUESDAY) {
|
||||
scheduleText = `Weekly on Thuesdays at 04:45`;
|
||||
scheduleText = `Weekly on Tuesdays at ${time}`;
|
||||
}
|
||||
if (schedule === BackupScheduleState.WEDNESDAY) {
|
||||
scheduleText = `Weekly on Wednesdays at 04:45`;
|
||||
scheduleText = `Weekly on Wednesdays at ${time}`;
|
||||
}
|
||||
if (schedule === BackupScheduleState.THURSDAY) {
|
||||
scheduleText = `Weekly on Thursdays at 04:45`;
|
||||
scheduleText = `Weekly on Thursdays at ${time}`;
|
||||
}
|
||||
if (schedule === BackupScheduleState.FRIDAY) {
|
||||
scheduleText = `Weekly on Fridays at 04:45`;
|
||||
scheduleText = `Weekly on Fridays at ${time}`;
|
||||
}
|
||||
if (schedule === BackupScheduleState.SATURDAY) {
|
||||
scheduleText = `Weekly on Saturdays at 04:45`;
|
||||
scheduleText = `Weekly on Saturdays at ${time}`;
|
||||
}
|
||||
if (schedule === BackupScheduleState.SUNDAY) {
|
||||
scheduleText = `Weekly on Sundays at 04:45`;
|
||||
scheduleText = `Weekly on Sundays at ${time}`;
|
||||
}
|
||||
|
||||
return scheduleText + " " + copiesText;
|
||||
|
@ -1,10 +1,9 @@
|
||||
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 { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { formatTime } from "../../../../../common/datetime/format_time";
|
||||
import { relativeTime } from "../../../../../common/datetime/relative_time";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-card";
|
||||
@ -12,7 +11,10 @@ import "../../../../../components/ha-md-list";
|
||||
import "../../../../../components/ha-md-list-item";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import type { BackupConfig, BackupContent } from "../../../../../data/backup";
|
||||
import { BackupScheduleState } from "../../../../../data/backup";
|
||||
import {
|
||||
BackupScheduleState,
|
||||
getFormattedBackupTime,
|
||||
} from "../../../../../data/backup";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import "../ha-backup-summary-card";
|
||||
@ -29,20 +31,16 @@ class HaBackupOverviewBackups extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public fetching = false;
|
||||
|
||||
private _lastBackup = memoizeOne((backups: BackupContent[]) => {
|
||||
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`
|
||||
<ha-backup-summary-card
|
||||
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 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`
|
||||
<ha-backup-summary-card
|
||||
heading=${`Last automatic backup failed`}
|
||||
heading="Last automatic backup failed"
|
||||
status="error"
|
||||
>
|
||||
<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(
|
||||
// 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;
|
||||
}
|
||||
|
@ -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`
|
||||
<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-item type="button" @click=${this._done}>
|
||||
<span slot="headline">Recommended settings</span>
|
||||
@ -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;
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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");
|
||||
|
@ -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 = () =>
|
||||
|
@ -78,7 +78,7 @@ const TYPE_ORDER: Array<BackupType> = ["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`
|
||||
<div style="display: flex; gap: 4px;">
|
||||
${(backup.agent_ids || []).map((agentId) => {
|
||||
@ -181,7 +180,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
<ha-svg-icon
|
||||
.path=${mdiHarddisk}
|
||||
title=${name}
|
||||
slot="graphic"
|
||||
style="flex-shrink: 0;"
|
||||
></ha-svg-icon>
|
||||
`;
|
||||
}
|
||||
@ -190,7 +189,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
<ha-svg-icon
|
||||
.path=${mdiNas}
|
||||
title=${name}
|
||||
slot="graphic"
|
||||
style="flex-shrink: 0;"
|
||||
></ha-svg-icon>
|
||||
`;
|
||||
}
|
||||
@ -209,6 +208,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
referrerpolicy="no-referrer"
|
||||
alt=${name}
|
||||
slot="graphic"
|
||||
style="flex-shrink: 0;"
|
||||
/>
|
||||
`;
|
||||
})}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -141,9 +141,9 @@ class HaConfigBackupSettings extends LitElement {
|
||||
<div class="card-content">
|
||||
<p>
|
||||
Keep this encryption key in a safe place, as you will need it to
|
||||
access your backup, allowing it to be restored. Either record
|
||||
the 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.
|
||||
</p>
|
||||
<ha-backup-config-encryption-key
|
||||
.hass=${this.hass}
|
||||
|
@ -75,7 +75,9 @@ export class DialogTryTts extends LitElement {
|
||||
<ha-textarea
|
||||
autogrow
|
||||
id="message"
|
||||
label="Message"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.tts.dialog.message"
|
||||
)}
|
||||
.value=${this._message ||
|
||||
this.hass.localize(
|
||||
"ui.panel.config.cloud.account.tts.dialog.example_message",
|
||||
|
@ -247,6 +247,13 @@ export class SystemLogCard extends LitElement {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
:host {
|
||||
direction: var(--direction);
|
||||
}
|
||||
mwc-list {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@ -293,13 +300,7 @@ export class SystemLogCard extends LitElement {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.card-actions,
|
||||
.empty-content {
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.row-secondary {
|
||||
direction: var(--direction);
|
||||
text-align: left;
|
||||
}
|
||||
`;
|
||||
|
@ -105,6 +105,9 @@ export class HaConfigLovelaceRescources extends LitElement {
|
||||
},
|
||||
delete: {
|
||||
title: "",
|
||||
label: localize(
|
||||
"ui.panel.config.lovelace.resources.picker.headers.delete"
|
||||
),
|
||||
type: "icon-button",
|
||||
minWidth: "48px",
|
||||
maxWidth: "48px",
|
||||
|
@ -99,7 +99,7 @@ class DialogRepairsIssue extends LitElement {
|
||||
: ""}
|
||||
<ha-markdown
|
||||
id="dialog-repairs-issue-description"
|
||||
allowsvg
|
||||
allow-svg
|
||||
breaks
|
||||
@click=${this._clickHandler}
|
||||
.content=${this.hass.localize(
|
||||
|
@ -90,7 +90,7 @@ export const showRepairsFlowDialog = (
|
||||
? html`
|
||||
<ha-markdown
|
||||
breaks
|
||||
allowsvg
|
||||
allow-svg
|
||||
.content=${description}
|
||||
></ha-markdown>
|
||||
`
|
||||
@ -123,7 +123,7 @@ export const showRepairsFlowDialog = (
|
||||
${description
|
||||
? html`
|
||||
<ha-markdown
|
||||
allowsvg
|
||||
allow-svg
|
||||
breaks
|
||||
.content=${description}
|
||||
></ha-markdown>
|
||||
@ -220,7 +220,7 @@ export const showRepairsFlowDialog = (
|
||||
return html`${renderIssueDescription(hass, issue)}${description
|
||||
? html`
|
||||
<ha-markdown
|
||||
allowsvg
|
||||
allow-svg
|
||||
breaks
|
||||
.content=${description}
|
||||
></ha-markdown>
|
||||
@ -254,7 +254,7 @@ export const showRepairsFlowDialog = (
|
||||
${description
|
||||
? html`
|
||||
<ha-markdown
|
||||
allowsvg
|
||||
allow-svg
|
||||
breaks
|
||||
.content=${description}
|
||||
></ha-markdown>
|
||||
|
@ -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<ScriptConfig> = {
|
||||
alias: this.hass.localize("ui.panel.config.script.editor.default_name"),
|
||||
};
|
||||
const baseConfig: Partial<ScriptConfig> = {};
|
||||
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<EntityRegistryEntry> | undefined;
|
||||
if (this._entityRegistryUpdate !== undefined && !this.scriptId) {
|
||||
this._newScriptId = id.toString();
|
||||
entityRegPromise = new Promise<EntityRegistryEntry>((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<EntityRegistryEntry>((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,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -178,7 +178,9 @@ export class HaScriptTrace extends LitElement {
|
||||
<ha-icon-button
|
||||
.disabled=${this._traces[this._traces.length - 1].run_id ===
|
||||
this._runId}
|
||||
label="Older trace"
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.automation.trace.older_trace"
|
||||
)}
|
||||
@click=${this._pickOlderTrace}
|
||||
.path=${mdiRayEndArrow}
|
||||
></ha-icon-button>
|
||||
@ -198,7 +200,9 @@ export class HaScriptTrace extends LitElement {
|
||||
</select>
|
||||
<ha-icon-button
|
||||
.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}
|
||||
.path=${mdiRayStartArrow}
|
||||
></ha-icon-button>
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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`
|
||||
<ha-form
|
||||
|
@ -52,8 +52,8 @@ export class HUIViewBackground extends LitElement {
|
||||
background?: string | LovelaceViewBackgroundConfig
|
||||
) {
|
||||
if (typeof background === "object" && background.image) {
|
||||
const size = background.size ?? "auto";
|
||||
const alignment = background.alignment ?? "center";
|
||||
const size = background.size ?? "cover";
|
||||
const repeat = background.repeat ?? "no-repeat";
|
||||
return `${alignment} / ${size} ${repeat} url('${background.image}')`;
|
||||
}
|
||||
|
@ -88,7 +88,7 @@ class HaMfaModuleSetupFlow extends LitElement {
|
||||
</div>`
|
||||
: html`${this._step.type === "abort"
|
||||
? html` <ha-markdown
|
||||
allowsvg
|
||||
allow-svg
|
||||
breaks
|
||||
.content=${this.hass.localize(
|
||||
`component.auth.mfa_setup.${this._step.handler}.abort.${this._step.reason}`
|
||||
@ -103,7 +103,7 @@ class HaMfaModuleSetupFlow extends LitElement {
|
||||
</p>`
|
||||
: this._step.type === "form"
|
||||
? html`<ha-markdown
|
||||
allowsvg
|
||||
allow-svg
|
||||
breaks
|
||||
.content=${this.hass.localize(
|
||||
`component.auth.mfa_setup.${
|
||||
|
@ -936,7 +936,7 @@
|
||||
"delete": "Delete {count}",
|
||||
"deleting": "Deleting {count}",
|
||||
"tip_media_storage": "[%key:ui::panel::config::tips::media_storage%]",
|
||||
"tip_storage_panel": "[%key:ui::panel::config::storage::caption%]"
|
||||
"tip_storage_panel": "storage"
|
||||
},
|
||||
"class": {
|
||||
"album": "Album",
|
||||
@ -2685,7 +2685,8 @@
|
||||
"picker": {
|
||||
"headers": {
|
||||
"url": "URL",
|
||||
"type": "Type"
|
||||
"type": "Type",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"no_resources": "No resources",
|
||||
"add_resource": "Add resource"
|
||||
@ -2957,8 +2958,8 @@
|
||||
"traces_not_available": "[%key:ui::panel::config::automation::editor::traces_not_available%]",
|
||||
"edit_category": "Edit category",
|
||||
"assign_category": "Assign category",
|
||||
"no_category_support": "You can't assign an category to this automation",
|
||||
"no_category_entity_reg": "To assign an category to an automation it needs to have a unique ID.",
|
||||
"no_category_support": "You can't assign a category to this automation",
|
||||
"no_category_entity_reg": "To assign a category to an automation it needs to have a unique ID.",
|
||||
"search": "Search {number} automations",
|
||||
"headers": {
|
||||
"toggle": "Enable/disable",
|
||||
@ -3717,7 +3718,8 @@
|
||||
"add_description": "Add description",
|
||||
"add_icon": "Add icon",
|
||||
"add_category": "Add category",
|
||||
"add_labels": "Add labels"
|
||||
"add_labels": "Add labels",
|
||||
"add_area": "Add area"
|
||||
}
|
||||
},
|
||||
"trace": {
|
||||
@ -4117,6 +4119,7 @@
|
||||
"try": "Try",
|
||||
"dialog": {
|
||||
"header": "Try text-to-speech",
|
||||
"message": "Message",
|
||||
"example_message": "Hello {name}, you can play any text on any supported media player!",
|
||||
"target": "Target",
|
||||
"target_browser": "Browser",
|
||||
|
20
yarn.lock
20
yarn.lock
@ -1234,10 +1234,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@braintree/sanitize-url@npm:7.1.0":
|
||||
version: 7.1.0
|
||||
resolution: "@braintree/sanitize-url@npm:7.1.0"
|
||||
checksum: 10/b25cc5358bedfd97d8378d23ab43493e56a805bd82fdb092088bdd9db6aa3f6c32859d36526f570fb2c67a5a4f9ce579aacd52c3872db4285e4c34fb9947dfc0
|
||||
"@braintree/sanitize-url@npm:7.1.1":
|
||||
version: 7.1.1
|
||||
resolution: "@braintree/sanitize-url@npm:7.1.1"
|
||||
checksum: 10/a8a5535c5a0a459ba593a018c554b35493dff004fd09d7147db67243df83bce3d410b89ee7dc2d95cce195b85b877c72f8ca149e1040110a945d193c67293af0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -9117,7 +9117,7 @@ __metadata:
|
||||
"@babel/preset-env": "npm:7.26.0"
|
||||
"@babel/preset-typescript": "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"
|
||||
"@codemirror/autocomplete": "npm:6.18.4"
|
||||
"@codemirror/commands": "npm:6.7.1"
|
||||
@ -9296,7 +9296,7 @@ __metadata:
|
||||
tsparticles-engine: "npm:2.12.0"
|
||||
tsparticles-preset-links: "npm:2.12.0"
|
||||
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-network: "npm:9.1.9"
|
||||
vitest: "npm:2.1.8"
|
||||
@ -14440,12 +14440,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ua-parser-js@npm:1.0.39":
|
||||
version: 1.0.39
|
||||
resolution: "ua-parser-js@npm:1.0.39"
|
||||
"ua-parser-js@npm:1.0.40":
|
||||
version: 1.0.40
|
||||
resolution: "ua-parser-js@npm:1.0.40"
|
||||
bin:
|
||||
ua-parser-js: script/cli.js
|
||||
checksum: 10/dd4026b6ece8a34a0d39b6de5542154c4506077d8def8647a300a29e1b3ffa0e23f5c8eeeb8101df6162b7b3eb3597d0b4adb031ae6104cbdb730d6ebc07f3c0
|
||||
checksum: 10/7fced5f74ed570c83addffd4d367888d90c58803ff4bdd4a7b04b3f01d293263b8605e92ac560eb1c6a201ef3b11fcc46f3dbcbe764fbe54974924d542bc0135
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user