Skip to content

Commit 4808463

Browse files
Estimate backup size (#27423)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
1 parent 5fb3cab commit 4808463

File tree

2 files changed

+170
-1
lines changed

2 files changed

+170
-1
lines changed

src/panels/config/backup/components/config/ha-backup-config-data.ts

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
mdiChartBox,
33
mdiCog,
44
mdiFolder,
5+
mdiInformation,
56
mdiPlayBoxMultiple,
67
mdiPuzzle,
78
} from "@mdi/js";
@@ -11,17 +12,23 @@ import { customElement, property, state } from "lit/decorators";
1112
import memoizeOne from "memoize-one";
1213
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
1314
import { fireEvent } from "../../../../../common/dom/fire_event";
15+
import "../../../../../components/ha-alert";
1416
import "../../../../../components/ha-button";
1517
import "../../../../../components/ha-expansion-panel";
1618
import "../../../../../components/ha-md-list";
1719
import "../../../../../components/ha-md-list-item";
1820
import "../../../../../components/ha-md-select";
1921
import type { HaMdSelect } from "../../../../../components/ha-md-select";
2022
import "../../../../../components/ha-md-select-option";
23+
import "../../../../../components/ha-spinner";
2124
import "../../../../../components/ha-switch";
2225
import type { HaSwitch } from "../../../../../components/ha-switch";
26+
import "../../../../../components/ha-tooltip";
2327
import { fetchHassioAddonsInfo } from "../../../../../data/hassio/addon";
28+
import type { HostDisksUsage } from "../../../../../data/hassio/host";
29+
import { fetchHostDisksUsage } from "../../../../../data/hassio/host";
2430
import type { HomeAssistant } from "../../../../../types";
31+
import { bytesToString } from "../../../../../util/bytes-to-string";
2532
import "../ha-backup-addons-picker";
2633
import type { BackupAddonItem } from "../ha-backup-addons-picker";
2734
import { getRecorderInfo } from "../../../../../data/recorder";
@@ -78,11 +85,14 @@ class HaBackupConfigData extends LitElement {
7885

7986
@state() private _showDbOption = true;
8087

88+
@state() private _storageInfo?: HostDisksUsage | null;
89+
8190
protected firstUpdated(changedProperties: PropertyValues): void {
8291
super.firstUpdated(changedProperties);
8392
this._checkDbOption();
8493
if (isComponentLoaded(this.hass, "hassio")) {
8594
this._fetchAddons();
95+
this._fetchStorageInfo();
8696
}
8797
}
8898

@@ -114,10 +124,68 @@ class HaBackupConfigData extends LitElement {
114124
}
115125
}
116126

127+
private async _fetchStorageInfo() {
128+
try {
129+
this._storageInfo = await fetchHostDisksUsage(this.hass);
130+
} catch (_err: any) {
131+
this._storageInfo = null;
132+
}
133+
}
134+
117135
private _hasLocalAddons(addons: BackupAddonItem[]): boolean {
118136
return addons.some((addon) => addon.slug === "local");
119137
}
120138

139+
private _estimateBackupSize = memoizeOne(
140+
(
141+
data: FormData,
142+
storageInfo: HostDisksUsage | null | undefined,
143+
addonsLength: number
144+
): {
145+
compressedBytes: number;
146+
addonsNotAccurate: boolean;
147+
} | null => {
148+
if (!storageInfo?.children) {
149+
return null;
150+
}
151+
152+
let totalBytes = 0;
153+
154+
const segments: Record<string, number> = {};
155+
storageInfo.children.forEach((child) => {
156+
segments[child.id] = child.used_bytes;
157+
});
158+
159+
if (data.homeassistant) {
160+
totalBytes += segments.homeassistant ?? 0;
161+
}
162+
if (data.media) {
163+
totalBytes += segments.media ?? 0;
164+
}
165+
if (data.share) {
166+
totalBytes += segments.share ?? 0;
167+
}
168+
169+
if (
170+
data.addons_mode === "all" ||
171+
(data.addons_mode === "custom" && data.addons.length > 0)
172+
) {
173+
// It would be better if we could receive individual addon sizes in the WS request instead
174+
totalBytes +=
175+
(segments.addons_data ?? 0) + (segments.addons_config ?? 0);
176+
}
177+
178+
return {
179+
// Estimate compressed size (40% reduction typical for gzip)
180+
compressedBytes: Math.round(totalBytes * 0.6),
181+
addonsNotAccurate:
182+
data.addons_mode === "custom" &&
183+
data.addons.length > 0 &&
184+
data.addons.length !== addonsLength,
185+
};
186+
}
187+
);
188+
121189
private _getData = memoizeOne(
122190
(value: BackupConfigData | undefined, showAddon: boolean): FormData => {
123191
if (!value) {
@@ -171,6 +239,7 @@ class HaBackupConfigData extends LitElement {
171239
const isHassio = isComponentLoaded(this.hass, "hassio");
172240

173241
return html`
242+
${this._renderSizeEstimate()}
174243
<ha-md-list>
175244
<ha-md-list-item>
176245
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
@@ -381,7 +450,103 @@ class HaBackupConfigData extends LitElement {
381450
});
382451
}
383452

453+
private _renderSizeEstimate() {
454+
if (!isComponentLoaded(this.hass, "hassio")) {
455+
return nothing;
456+
}
457+
458+
const data = this._getData(this.value, this._showAddons);
459+
460+
if (
461+
!(
462+
data.homeassistant ||
463+
data.database ||
464+
data.media ||
465+
data.share ||
466+
data.local_addons ||
467+
data.addons_mode === "all" ||
468+
(data.addons_mode === "custom" && data.addons.length > 0)
469+
)
470+
) {
471+
return nothing;
472+
}
473+
474+
if (this._storageInfo === undefined) {
475+
return html`
476+
<ha-alert alert-type="info">
477+
<ha-spinner slot="icon"></ha-spinner>
478+
${this.hass.localize(
479+
"ui.panel.config.backup.data.estimated_size_loading"
480+
)}
481+
</ha-alert>
482+
`;
483+
}
484+
485+
if (!this._storageInfo || !this._storageInfo.children) {
486+
return nothing;
487+
}
488+
489+
const result = this._estimateBackupSize(
490+
data,
491+
this._storageInfo,
492+
this._addons.length
493+
);
494+
if (result === null) {
495+
return nothing;
496+
}
497+
498+
const { compressedBytes, addonsNotAccurate } = result;
499+
500+
return html`
501+
<span class="estimated-size">
502+
<span class="estimated-size-heading">
503+
${this.hass.localize("ui.panel.config.backup.data.estimated_size")}
504+
<ha-svg-icon
505+
id="estimated-size-info"
506+
.path=${mdiInformation}
507+
></ha-svg-icon>
508+
<ha-tooltip for="estimated-size-info" placement="right">
509+
${this.hass.localize(
510+
"ui.panel.config.backup.data.estimated_size_disclaimer"
511+
)}
512+
${addonsNotAccurate
513+
? html`<br /><br />${this.hass.localize(
514+
"ui.panel.config.backup.data.estimated_size_disclaimer_addons_custom"
515+
)}`
516+
: nothing}
517+
</ha-tooltip>
518+
</span>
519+
<span class="estimated-size-value">
520+
${bytesToString(compressedBytes)}
521+
</span>
522+
</span>
523+
`;
524+
}
525+
384526
static styles = css`
527+
.estimated-size {
528+
display: block;
529+
margin-top: var(--ha-space-2);
530+
font-size: var(--ha-font-size-m);
531+
}
532+
.estimated-size-heading {
533+
font-size: var(--ha-font-size-m);
534+
line-height: var(--ha-line-height-expanded);
535+
}
536+
.estimated-size-heading ha-svg-icon {
537+
--mdc-icon-size: 16px;
538+
color: var(--secondary-text-color);
539+
margin-inline-start: var(--ha-space-1);
540+
vertical-align: middle;
541+
}
542+
.estimated-size-value {
543+
display: block;
544+
font-size: var(--ha-font-size-s);
545+
color: var(--secondary-text-color);
546+
}
547+
ha-spinner {
548+
--ha-spinner-size: 24px;
549+
}
385550
ha-md-list {
386551
background: none;
387552
--md-list-item-leading-space: 0;

src/translations/en.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2674,7 +2674,11 @@
26742674
"addons_description": "Select what add-ons you want to include.",
26752675
"addons_all": "All",
26762676
"addons_none": "None",
2677-
"addons_custom": "Custom"
2677+
"addons_custom": "Custom",
2678+
"estimated_size": "Estimated backup size",
2679+
"estimated_size_disclaimer": "This is an approximation based on storage usage and compression. The actual backup size will vary.",
2680+
"estimated_size_disclaimer_addons_custom": "This estimate includes all add-ons, and not individual add-on sizes.",
2681+
"estimated_size_loading": "Loading size estimate..."
26782682
},
26792683
"data_picker": {
26802684
"settings": "Settings",

0 commit comments

Comments
 (0)