diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts index df915c398d..94ceca01b5 100644 --- a/src/panels/config/ha-panel-config.ts +++ b/src/panels/config/ha-panel-config.ts @@ -397,6 +397,10 @@ class HaPanelConfig extends HassRouterPage { tag: "ha-config-energy", load: () => import("./energy/ha-config-energy"), }, + hardware: { + tag: "ha-config-hardware", + load: () => import("./hardware/ha-config-hardware"), + }, integrations: { tag: "ha-config-integrations", load: () => import("./integrations/ha-config-integrations"), diff --git a/src/panels/config/hardware/dialog-hardware-available.ts b/src/panels/config/hardware/dialog-hardware-available.ts new file mode 100644 index 0000000000..9413c8acb7 --- /dev/null +++ b/src/panels/config/hardware/dialog-hardware-available.ts @@ -0,0 +1,213 @@ +import { mdiClose } from "@mdi/js"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { stringCompare } from "../../../common/string/compare"; +import "../../../components/ha-dialog"; +import "../../../components/ha-expansion-panel"; +import "../../../components/ha-icon-next"; +import "../../../components/search-input"; +import { extractApiErrorMessage } from "../../../data/hassio/common"; +import { + fetchHassioHardwareInfo, + HassioHardwareInfo, +} from "../../../data/hassio/hardware"; +import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; +import type { HassDialog } from "../../../dialogs/make-dialog-manager"; +import { dump } from "../../../resources/js-yaml-dump"; +import { haStyle, haStyleDialog } from "../../../resources/styles"; +import type { HomeAssistant } from "../../../types"; + +const _filterDevices = memoizeOne( + (showAdvanced: boolean, hardware: HassioHardwareInfo, filter: string) => + hardware.devices + .filter( + (device) => + (showAdvanced || + ["tty", "gpio", "input"].includes(device.subsystem)) && + (device.by_id?.toLowerCase().includes(filter) || + device.name.toLowerCase().includes(filter) || + device.dev_path.toLocaleLowerCase().includes(filter) || + JSON.stringify(device.attributes) + .toLocaleLowerCase() + .includes(filter)) + ) + .sort((a, b) => stringCompare(a.name, b.name)) +); + +@customElement("ha-dialog-hardware-available") +class DialogHardwareAvailable extends LitElement implements HassDialog { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _hardware?: HassioHardwareInfo; + + @state() private _filter?: string; + + public async showDialog(): Promise> { + try { + this._hardware = await fetchHassioHardwareInfo(this.hass); + } catch (err: any) { + await showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.hardware.available_hardware.failed_to_get" + ), + text: extractApiErrorMessage(err), + }); + } + } + + public closeDialog(): void { + this._hardware = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render(): TemplateResult { + if (!this._hardware) { + return html``; + } + + const devices = _filterDevices( + this.hass.userData?.showAdvanced || false, + this._hardware, + (this._filter || "").toLowerCase() + ); + + return html` + +
+

+ ${this.hass.localize( + "ui.panel.config.hardware.available_hardware.title" + )} +

+ + + +
+ ${devices.map( + (device) => + html` + +
+ + ${this.hass.localize( + "ui.panel.config.hardware.available_hardware.subsystem" + )}: + + ${device.subsystem} +
+
+ + ${this.hass.localize( + "ui.panel.config.hardware.available_hardware.device_path" + )}: + + ${device.dev_path} +
+ ${device.by_id + ? html` +
+ + ${this.hass.localize( + "ui.panel.config.hardware.available_hardware.id" + )}: + + ${device.by_id} +
+ ` + : ""} +
+ + ${this.hass.localize( + "ui.panel.config.hardware.available_hardware.attributes" + )}: + +
${dump(device.attributes, { indent: 2 })}
+
+
+ ` + )} +
+ `; + } + + private _handleSearchChange(ev: CustomEvent) { + this._filter = ev.detail.value; + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + haStyleDialog, + css` + ha-icon-button { + position: absolute; + right: 16px; + top: 10px; + text-decoration: none; + color: var(--primary-text-color); + } + h2 { + margin: 18px 42px 0 18px; + color: var(--primary-text-color); + } + ha-expansion-panel { + margin: 4px 0; + } + pre, + code { + background-color: var(--markdown-code-background-color, none); + border-radius: 3px; + } + pre { + padding: 16px; + overflow: auto; + line-height: 1.45; + font-family: var(--code-font-family, monospace); + } + code { + font-size: 85%; + padding: 0.2em 0.4em; + } + search-input { + margin: 8px 16px 0; + display: block; + } + .device-property { + display: flex; + justify-content: space-between; + } + .attributes { + margin-top: 12px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-dialog-hardware-available": DialogHardwareAvailable; + } +} diff --git a/src/panels/config/hardware/ha-config-hardware.ts b/src/panels/config/hardware/ha-config-hardware.ts new file mode 100644 index 0000000000..6b22057e59 --- /dev/null +++ b/src/panels/config/hardware/ha-config-hardware.ts @@ -0,0 +1,257 @@ +import "@material/mwc-list/mwc-list-item"; +import { mdiDotsVertical } from "@mdi/js"; +import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { isComponentLoaded } from "../../../common/config/is_component_loaded"; +import "../../../components/buttons/ha-progress-button"; +import "../../../components/ha-alert"; +import "../../../components/ha-button-menu"; +import "../../../components/ha-card"; +import "../../../components/ha-settings-row"; +import { + extractApiErrorMessage, + ignoreSupervisorError, +} from "../../../data/hassio/common"; +import { + fetchHassioHassOsInfo, + fetchHassioHostInfo, + HassioHassOSInfo, + HassioHostInfo, + rebootHost, + shutdownHost, +} from "../../../data/hassio/host"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../../../dialogs/generic/show-dialog-box"; +import "../../../layouts/hass-subpage"; +import { haStyle } from "../../../resources/styles"; +import type { HomeAssistant } from "../../../types"; +import { showhardwareAvailableDialog } from "./show-dialog-hardware-available"; + +@customElement("ha-config-hardware") +class HaConfigHardware extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public narrow!: boolean; + + @state() private _error?: { code: string; message: string }; + + @state() private _OSData?: HassioHassOSInfo; + + @state() private _hostData?: HassioHostInfo; + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + if (isComponentLoaded(this.hass, "hassio")) { + this._load(); + } + } + + protected render(): TemplateResult { + return html` + + ${this._error + ? html` + ${this._error.message || this._error.code} + ` + : ""} + ${this._OSData && this._hostData + ? html` +
+ +
+ + ${this.hass.localize( + "ui.panel.config.hardware.board" + )} +
+ ${this._OSData.board} +
+
+
+
+
+ ${this._hostData.features.includes("reboot") + ? html` + + ${this.hass.localize( + "ui.panel.config.hardware.reboot_host" + )} + + ` + : ""} + ${this._hostData.features.includes("shutdown") + ? html` + + ${this.hass.localize( + "ui.panel.config.hardware.shutdown_host" + )} + + ` + : ""} +
+ + + + ${this.hass.localize( + "ui.panel.config.hardware.available_hardware.title" + )} + + +
+
+
+ ` + : ""} +
+ `; + } + + private async _load() { + try { + this._OSData = await fetchHassioHassOsInfo(this.hass); + this._hostData = await fetchHassioHostInfo(this.hass); + } catch (err: any) { + this._error = err.message || err; + } + } + + private async _openHardware() { + showhardwareAvailableDialog(this); + } + + private async _hostReboot(ev: CustomEvent): Promise { + const button = ev.currentTarget as any; + button.progress = true; + + const confirmed = await showConfirmationDialog(this, { + title: this.hass.localize("ui.panel.config.hardware.reboot_host"), + text: this.hass.localize("ui.panel.config.hardware.reboot_host_confirm"), + confirmText: this.hass.localize("ui.panel.config.hardware.reboot_host"), + dismissText: this.hass.localize("common.cancel"), + }); + + if (!confirmed) { + button.progress = false; + return; + } + + try { + await rebootHost(this.hass); + } catch (err: any) { + // Ignore connection errors, these are all expected + if (this.hass.connection.connected && !ignoreSupervisorError(err)) { + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.hardware.failed_to_reboot_host" + ), + text: extractApiErrorMessage(err), + }); + } + } + button.progress = false; + } + + private async _hostShutdown(ev: CustomEvent): Promise { + const button = ev.currentTarget as any; + button.progress = true; + + const confirmed = await showConfirmationDialog(this, { + title: this.hass.localize("ui.panel.config.hardware.shutdown_host"), + text: this.hass.localize( + "ui.panel.config.hardware.shutdown_host_confirm" + ), + confirmText: this.hass.localize("ui.panel.config.hardware.shutdown_host"), + dismissText: this.hass.localize("common.cancel"), + }); + + if (!confirmed) { + button.progress = false; + return; + } + + try { + await shutdownHost(this.hass); + } catch (err: any) { + // Ignore connection errors, these are all expected + if (this.hass.connection.connected && !ignoreSupervisorError(err)) { + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.hardware.failed_to_shutdown_host" + ), + text: extractApiErrorMessage(err), + }); + } + } + button.progress = false; + } + + static styles = [ + haStyle, + css` + .content { + padding: 28px 20px 0; + max-width: 1040px; + margin: 0 auto; + } + ha-card { + max-width: 500px; + margin: 0 auto; + height: 100%; + justify-content: space-between; + flex-direction: column; + display: flex; + } + .card-content { + display: flex; + justify-content: space-between; + flex-direction: column; + padding: 16px 16px 0 16px; + } + ha-button-menu { + color: var(--secondary-text-color); + --mdc-menu-min-width: 200px; + } + .card-actions { + height: 48px; + border-top: none; + display: flex; + justify-content: space-between; + align-items: center; + } + .buttons { + display: flex; + align-items: center; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-config-hardware": HaConfigHardware; + } +} diff --git a/src/panels/config/hardware/show-dialog-hardware-available.ts b/src/panels/config/hardware/show-dialog-hardware-available.ts new file mode 100644 index 0000000000..c11356ae8b --- /dev/null +++ b/src/panels/config/hardware/show-dialog-hardware-available.ts @@ -0,0 +1,12 @@ +import { fireEvent } from "../../../common/dom/fire_event"; + +export const loadHardwareAvailableDialog = () => + import("./dialog-hardware-available"); + +export const showhardwareAvailableDialog = (element: HTMLElement): void => { + fireEvent(element, "show-dialog", { + dialogTag: "ha-dialog-hardware-available", + dialogImport: loadHardwareAvailableDialog, + dialogParams: {}, + }); +}; diff --git a/src/panels/config/zone/dialog-core-zone-detail.ts b/src/panels/config/zone/dialog-core-zone-detail.ts index 7c388bd964..13f06113b9 100644 --- a/src/panels/config/zone/dialog-core-zone-detail.ts +++ b/src/panels/config/zone/dialog-core-zone-detail.ts @@ -10,7 +10,8 @@ import { stopPropagation } from "../../../common/dom/stop_propagation"; import { currencies } from "../../../components/currency-datalist"; import { createCloseHeading } from "../../../components/ha-dialog"; import "../../../components/ha-formfield"; -import { HaRadio } from "../../../components/ha-radio"; +import "../../../components/ha-radio"; +import type { HaRadio } from "../../../components/ha-radio"; import "../../../components/ha-select"; import "../../../components/ha-textfield"; import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core"; diff --git a/src/translations/en.json b/src/translations/en.json index a24bccef9f..6dd926e542 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1465,7 +1465,22 @@ "internal_url_https_error_description": "You have configured an HTTPS certificate in Home Assistant. This means that your internal URL needs to be set to a domain covered by the certficate." }, "hardware": { - "caption": "Hardware" + "caption": "Hardware", + "available_hardware": { + "failed_to_get": "Failed to get available hardware", + "title": "Available hardware", + "subsystem": "Subsystem", + "device_path": "Device path", + "id": "ID", + "attributes": "Attributes" + }, + "reboot_host": "Reboot host", + "reboot_host_confirm": "Are you sure you want to reboot your host?", + "failed_to_reboot_host": "Failed to reboot host", + "shutdown_host": "Shutdown host", + "shutdown_host_confirm": "Are you sure you want to shutdown your host?", + "failed_to_shutdown_host": "Failed to shutdown host", + "board": "Board" }, "info": { "caption": "Info",