Add Hardware Page to Configuration System Menu (#12405)

This commit is contained in:
Zack Barett 2022-04-25 10:30:53 -05:00 committed by GitHub
parent dee59486ba
commit 8e55c83996
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 504 additions and 2 deletions

View File

@ -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"),

View File

@ -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<Promise<void>> {
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`
<ha-dialog
open
hideActions
@closed=${this.closeDialog}
.heading=${this.hass.localize(
"ui.panel.config.hardware.available_hardware.title"
)}
>
<div class="header" slot="heading">
<h2>
${this.hass.localize(
"ui.panel.config.hardware.available_hardware.title"
)}
</h2>
<ha-icon-button
.label=${this.hass.localize("common.close")}
.path=${mdiClose}
dialogAction="close"
></ha-icon-button>
<search-input
.hass=${this.hass}
.filter=${this._filter}
@value-changed=${this._handleSearchChange}
.label=${this.hass.localize("common.search")}
>
</search-input>
</div>
${devices.map(
(device) =>
html`
<ha-expansion-panel
.header=${device.name}
.secondary=${device.by_id || undefined}
outlined
>
<div class="device-property">
<span>
${this.hass.localize(
"ui.panel.config.hardware.available_hardware.subsystem"
)}:
</span>
<span>${device.subsystem}</span>
</div>
<div class="device-property">
<span>
${this.hass.localize(
"ui.panel.config.hardware.available_hardware.device_path"
)}:
</span>
<code>${device.dev_path}</code>
</div>
${device.by_id
? html`
<div class="device-property">
<span>
${this.hass.localize(
"ui.panel.config.hardware.available_hardware.id"
)}:
</span>
<code>${device.by_id}</code>
</div>
`
: ""}
<div class="attributes">
<span>
${this.hass.localize(
"ui.panel.config.hardware.available_hardware.attributes"
)}:
</span>
<pre>${dump(device.attributes, { indent: 2 })}</pre>
</div>
</ha-expansion-panel>
`
)}
</ha-dialog>
`;
}
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;
}
}

View File

@ -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`
<hass-subpage
back-path="/config/system"
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.hardware.caption")}
>
${this._error
? html`
<ha-alert alert-type="error"
>${this._error.message || this._error.code}</ha-alert
>
`
: ""}
${this._OSData && this._hostData
? html`
<div class="content">
<ha-card outlined>
<div class="card-content">
<ha-settings-row>
<span slot="heading"
>${this.hass.localize(
"ui.panel.config.hardware.board"
)}</span
>
<div slot="description">
<span class="value">${this._OSData.board}</span>
</div>
</ha-settings-row>
</div>
<div class="card-actions">
<div class="buttons">
${this._hostData.features.includes("reboot")
? html`
<ha-progress-button
class="warning"
@click=${this._hostReboot}
>
${this.hass.localize(
"ui.panel.config.hardware.reboot_host"
)}
</ha-progress-button>
`
: ""}
${this._hostData.features.includes("shutdown")
? html`
<ha-progress-button
class="warning"
@click=${this._hostShutdown}
>
${this.hass.localize(
"ui.panel.config.hardware.shutdown_host"
)}
</ha-progress-button>
`
: ""}
</div>
<ha-button-menu corner="BOTTOM_START">
<ha-icon-button
.label=${this.hass.localize("common.menu")}
.path=${mdiDotsVertical}
slot="trigger"
></ha-icon-button>
<mwc-list-item
.action=${"hardware"}
@click=${this._openHardware}
>
${this.hass.localize(
"ui.panel.config.hardware.available_hardware.title"
)}
</mwc-list-item>
</ha-button-menu>
</div>
</ha-card>
</div>
`
: ""}
</hass-subpage>
`;
}
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<void> {
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<void> {
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;
}
}

View File

@ -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: {},
});
};

View File

@ -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";

View File

@ -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",