diff --git a/hassio/src/dialogs/datadisk/dialog-hassio-datadisk.ts b/hassio/src/dialogs/datadisk/dialog-hassio-datadisk.ts new file mode 100644 index 0000000000..5c32a9e9e3 --- /dev/null +++ b/hassio/src/dialogs/datadisk/dialog-hassio-datadisk.ts @@ -0,0 +1,191 @@ +import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; +import "@polymer/paper-item/paper-item"; +import "@polymer/paper-listbox/paper-listbox"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../../../../src/common/dom/fire_event"; +import "../../../../src/components/ha-circular-progress"; +import "../../../../src/components/ha-markdown"; +import { + extractApiErrorMessage, + ignoreSupervisorError, +} from "../../../../src/data/hassio/common"; +import { + DatadiskList, + listDatadisks, + moveDatadisk, +} from "../../../../src/data/hassio/host"; +import { Supervisor } from "../../../../src/data/supervisor/supervisor"; +import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box"; +import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; +import { HomeAssistant } from "../../../../src/types"; +import { HassioDatatiskDialogParams } from "./show-dialog-hassio-datadisk"; + +const calculateMoveTime = memoizeOne((supervisor: Supervisor): number => { + const speed = supervisor.host.disk_life_time !== "" ? 30 : 10; + const moveTime = (supervisor.host.disk_used * 1000) / 60 / speed; + const rebootTime = (supervisor.host.startup_time * 4) / 60; + return Math.ceil((moveTime + rebootTime) / 10) * 10; +}); + +@customElement("dialog-hassio-datadisk") +class HassioDatadiskDialog extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private dialogParams?: HassioDatatiskDialogParams; + + @state() private selectedDevice?: string; + + @state() private devices?: DatadiskList["devices"]; + + @state() private moving = false; + + public showDialog(params: HassioDatatiskDialogParams) { + this.dialogParams = params; + listDatadisks(this.hass).then((data) => { + this.devices = data.devices; + }); + } + + public closeDialog(): void { + this.dialogParams = undefined; + this.selectedDevice = undefined; + this.devices = undefined; + this.moving = false; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render(): TemplateResult { + if (!this.dialogParams) { + return html``; + } + return html` + + ${this.moving + ? html` +

+ ${this.dialogParams.supervisor.localize( + "dialog.datadisk_move.moving" + )} +

+
+ + +

+ ${this.dialogParams.supervisor.localize( + "dialog.datadisk_move.moving_desc" + )} +

` + : html` +

+ ${this.dialogParams.supervisor.localize( + "dialog.datadisk_move.title" + )} +

+
+ ${this.devices?.length + ? html` + ${this.dialogParams.supervisor.localize( + "dialog.datadisk_move.description", + { + current_path: this.dialogParams.supervisor.os.data_disk, + time: calculateMoveTime(this.dialogParams.supervisor), + } + )} +

+ + + + ${this.devices.map( + (device) => html`${device}` + )} + + + ` + : this.devices === undefined + ? this.dialogParams.supervisor.localize( + "dialog.datadisk_move.loading_devices" + ) + : this.dialogParams.supervisor.localize( + "dialog.datadisk_move.no_devices" + )} + + + ${this.dialogParams.supervisor.localize( + "dialog.datadisk_move.cancel" + )} + + + + ${this.dialogParams.supervisor.localize( + "dialog.datadisk_move.move" + )} + `} +
+ `; + } + + private _select_device(event) { + this.selectedDevice = event.detail.value; + } + + private async _moveDatadisk() { + this.moving = true; + try { + await moveDatadisk(this.hass, this.selectedDevice!); + } catch (err) { + if (this.hass.connection.connected && !ignoreSupervisorError(err)) { + showAlertDialog(this, { + title: this.dialogParams!.supervisor.localize( + "system.host.failed_to_move" + ), + text: extractApiErrorMessage(err), + }); + this.closeDialog(); + } + } + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + haStyleDialog, + css` + paper-dropdown-menu { + width: 100%; + } + ha-circular-progress { + display: block; + margin: 32px; + text-align: center; + } + + .progress-text { + text-align: center; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-hassio-datadisk": HassioDatadiskDialog; + } +} diff --git a/hassio/src/dialogs/datadisk/show-dialog-hassio-datadisk.ts b/hassio/src/dialogs/datadisk/show-dialog-hassio-datadisk.ts new file mode 100644 index 0000000000..c7a7d92b85 --- /dev/null +++ b/hassio/src/dialogs/datadisk/show-dialog-hassio-datadisk.ts @@ -0,0 +1,17 @@ +import { fireEvent } from "../../../../src/common/dom/fire_event"; +import { Supervisor } from "../../../../src/data/supervisor/supervisor"; + +export interface HassioDatatiskDialogParams { + supervisor: Supervisor; +} + +export const showHassioDatadiskDialog = ( + element: HTMLElement, + dialogParams: HassioDatatiskDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-hassio-datadisk", + dialogImport: () => import("./dialog-hassio-datadisk"), + dialogParams, + }); +}; diff --git a/hassio/src/system/hassio-host-info.ts b/hassio/src/system/hassio-host-info.ts index 60256bbe64..5592d23ee0 100644 --- a/hassio/src/system/hassio-host-info.ts +++ b/hassio/src/system/hassio-host-info.ts @@ -1,5 +1,4 @@ import "@material/mwc-button"; -import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import "@material/mwc-list/mwc-list-item"; import { mdiDotsVertical } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; @@ -40,8 +39,9 @@ import { roundWithOneDecimal, } from "../../../src/util/calculate"; import "../components/supervisor-metric"; -import { showNetworkDialog } from "../dialogs/network/show-dialog-network"; +import { showHassioDatadiskDialog } from "../dialogs/datadisk/show-dialog-hassio-datadisk"; import { showHassioHardwareDialog } from "../dialogs/hardware/show-dialog-hassio-hardware"; +import { showNetworkDialog } from "../dialogs/network/show-dialog-network"; import { hassioStyle } from "../resources/hassio-style"; @customElement("hassio-host-info") @@ -180,20 +180,27 @@ class HassioHostInfo extends LitElement { ` : ""} - + - + this._handleMenuAction("hardware")}> ${this.supervisor.localize("system.host.hardware")} ${this.supervisor.host.features.includes("haos") - ? html` - ${this.supervisor.localize("system.host.import_from_usb")} - ` + ? html` this._handleMenuAction("import_from_usb")} + > + ${this.supervisor.localize("system.host.import_from_usb")} + + ${this.supervisor.host.features.includes("os_agent") && + atLeastVersion(this.supervisor.host.agent_version, 1, 2, 0) + ? html` this._handleMenuAction("move_datadisk")} + > + ${this.supervisor.localize("system.host.move_datadisk")} + ` + : ""} ` : ""} @@ -216,17 +223,26 @@ class HassioHostInfo extends LitElement { return network_info.interfaces.find((a) => a.primary)?.ipv4?.address![0]; }); - private async _handleMenuAction(ev: CustomEvent) { - switch (ev.detail.index) { - case 0: + private async _handleMenuAction(action: string) { + switch (action) { + case "hardware": await this._showHardware(); break; - case 1: + case "import_from_usb": await this._importFromUSB(); break; + case "move_datadisk": + await this._moveDatadisk(); + break; } } + private _moveDatadisk(): void { + showHassioDatadiskDialog(this, { + supervisor: this.supervisor, + }); + } + private async _showHardware(): Promise { let hardware; try { diff --git a/src/data/hassio/host.ts b/src/data/hassio/host.ts index 47d5d8f815..136bd17d57 100644 --- a/src/data/hassio/host.ts +++ b/src/data/hassio/host.ts @@ -3,6 +3,7 @@ import { HomeAssistant } from "../../types"; import { hassioApiResultExtractor, HassioResponse } from "./common"; export type HassioHostInfo = { + agent_version: string; chassis: string; cpe: string; deployment: string; @@ -14,6 +15,8 @@ export type HassioHostInfo = { hostname: string; kernel: string; operating_system: string; + boot_timestamp: number; + startup_time: number; }; export interface HassioHassOSInfo { @@ -22,6 +25,11 @@ export interface HassioHassOSInfo { update_available: boolean; version_latest: string | null; version: string | null; + data_disk: string; +} + +export interface DatadiskList { + devices: string[]; } export const fetchHassioHostInfo = async ( @@ -129,3 +137,34 @@ export const changeHostOptions = async (hass: HomeAssistant, options: any) => { options ); }; + +export const moveDatadisk = async (hass: HomeAssistant, device: string) => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + return hass.callWS({ + type: "supervisor/api", + endpoint: "/os/datadisk/move", + method: "post", + timeout: null, + data: { device }, + }); + } + + return hass.callApi>("POST", "hassio/os/datadisk/move"); +}; + +export const listDatadisks = async ( + hass: HomeAssistant +): Promise => { + if (atLeastVersion(hass.config.version, 2021, 2, 4)) { + return hass.callWS({ + type: "supervisor/api", + endpoint: "/os/datadisk/list", + method: "get", + timeout: null, + }); + } + + return hassioApiResultExtractor( + await hass.callApi>("GET", "/os/datadisk/list") + ); +}; diff --git a/src/translations/en.json b/src/translations/en.json index 0c60bfa1f3..79b310a422 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -4104,6 +4104,7 @@ "failed_to_shutdown": "Failed to shutdown the host", "failed_to_set_hostname": "Setting hostname failed", "failed_to_import_from_usb": "Failed to import from USB", + "failed_to_move": "Failed to move datadisk", "used_space": "Used space", "hostname": "Hostname", "change_hostname": "Change Hostname", @@ -4119,7 +4120,8 @@ "confirm_shutdown": "Are you sure you want to shutdown the host?", "shutdown_host": "Shutdown host", "hardware": "Hardware", - "import_from_usb": "Import from USB" + "import_from_usb": "Import from USB", + "move_datadisk": "Move datadisk" }, "core": { "cpu_usage": "Core CPU Usage", @@ -4206,6 +4208,17 @@ "id": "ID", "attributes": "Attributes", "device_path": "Device path" + }, + "datadisk_move": { + "title": "[%key:supervisor::system::host::move_datadisk%]", + "description": "You are currently using ''{current_path}'' as datadisk. Moving data disks will reboot your device and it's estimated to take {time} minutes. Your Home Assistant installation will not be accessible during this period. Do not disconnect the power during the move!", + "select_device": "Select new datadisk", + "no_devices": "No suitable attached devices found", + "moving_desc": "Rebooting and moving datadisk. Please have patience", + "moving": "Moving datadisk", + "loading_devices": "Loading devices", + "cancel": "[%key:ui::common::cancel%]", + "move": "Move" } } }