mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-18 07:56:44 +01:00
Add all hardware table (#30312)
* add all hardware table * copilot review * Updated tab names * update localize keys * Fix translations --------- Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
This commit is contained in:
@@ -15,7 +15,7 @@ interface HassioHardwareAudioList {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HardwareDevice {
|
export interface HardwareDevice {
|
||||||
attributes: Record<string, string>;
|
attributes: Record<string, string>;
|
||||||
by_id: null | string;
|
by_id: null | string;
|
||||||
dev_path: string;
|
dev_path: string;
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ class DialogBox extends LitElement {
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-dialog
|
<ha-dialog
|
||||||
.hass=${this.hass}
|
|
||||||
.open=${this._open}
|
.open=${this._open}
|
||||||
type=${confirmPrompt ? "alert" : "standard"}
|
type=${confirmPrompt ? "alert" : "standard"}
|
||||||
?prevent-scrim-close=${confirmPrompt}
|
?prevent-scrim-close=${confirmPrompt}
|
||||||
@@ -104,6 +103,9 @@ class DialogBox extends LitElement {
|
|||||||
: nothing}
|
: nothing}
|
||||||
${dialogTitle}
|
${dialogTitle}
|
||||||
</span>
|
</span>
|
||||||
|
${this._params.subtitle
|
||||||
|
? html`<span slot="subtitle">${this._params.subtitle}</span>`
|
||||||
|
: nothing}
|
||||||
</ha-dialog-header>
|
</ha-dialog-header>
|
||||||
<div id="dialog-box-description">
|
<div id="dialog-box-description">
|
||||||
${this._params.text ? html` <p>${this._params.text}</p> ` : ""}
|
${this._params.text ? html` <p>${this._params.text}</p> ` : ""}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ interface BaseDialogBoxParams {
|
|||||||
confirmText?: string;
|
confirmText?: string;
|
||||||
text?: string | TemplateResult;
|
text?: string | TemplateResult;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
warning?: boolean;
|
warning?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,226 +0,0 @@
|
|||||||
import { dump } from "js-yaml";
|
|
||||||
import type { CSSResultGroup } from "lit";
|
|
||||||
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 { stringCompare } from "../../../common/string/compare";
|
|
||||||
import "../../../components/ha-dialog";
|
|
||||||
import "../../../components/ha-expansion-panel";
|
|
||||||
import "../../../components/ha-icon-next";
|
|
||||||
import "../../../components/input/ha-input-search";
|
|
||||||
import type { HaInputSearch } from "../../../components/input/ha-input-search";
|
|
||||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
|
||||||
import type { HassioHardwareInfo } from "../../../data/hassio/hardware";
|
|
||||||
import { fetchHassioHardwareInfo } from "../../../data/hassio/hardware";
|
|
||||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
|
||||||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
|
||||||
import { haStyleScrollbar } from "../../../resources/styles";
|
|
||||||
import type { HomeAssistant } from "../../../types";
|
|
||||||
|
|
||||||
const _filterDevices = memoizeOne(
|
|
||||||
(
|
|
||||||
showAdvanced: boolean,
|
|
||||||
hardware: HassioHardwareInfo,
|
|
||||||
filter: string,
|
|
||||||
language: 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, language))
|
|
||||||
);
|
|
||||||
|
|
||||||
@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;
|
|
||||||
|
|
||||||
@state() private _open = false;
|
|
||||||
|
|
||||||
public async showDialog(): Promise<Promise<void>> {
|
|
||||||
try {
|
|
||||||
this._hardware = await fetchHassioHardwareInfo(this.hass);
|
|
||||||
this._open = true;
|
|
||||||
} catch (err: any) {
|
|
||||||
await showAlertDialog(this, {
|
|
||||||
title: this.hass.localize(
|
|
||||||
"ui.panel.config.hardware.available_hardware.failed_to_get"
|
|
||||||
),
|
|
||||||
text: extractApiErrorMessage(err),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public closeDialog(): boolean {
|
|
||||||
this._open = false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _dialogClosed() {
|
|
||||||
this._open = false;
|
|
||||||
this._hardware = undefined;
|
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render() {
|
|
||||||
if (!this._hardware) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
const devices = _filterDevices(
|
|
||||||
this.hass.userData?.showAdvanced || false,
|
|
||||||
this._hardware,
|
|
||||||
(this._filter || "").toLowerCase(),
|
|
||||||
this.hass.locale.language
|
|
||||||
);
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<ha-dialog
|
|
||||||
.hass=${this.hass}
|
|
||||||
.open=${this._open}
|
|
||||||
flexcontent
|
|
||||||
header-title=${this.hass.localize(
|
|
||||||
"ui.panel.config.hardware.available_hardware.title"
|
|
||||||
)}
|
|
||||||
@closed=${this._dialogClosed}
|
|
||||||
>
|
|
||||||
<div class="content-container">
|
|
||||||
<ha-input-search
|
|
||||||
appearance="outlined"
|
|
||||||
autofocus
|
|
||||||
.value=${this._filter}
|
|
||||||
@input=${this._handleSearchChange}
|
|
||||||
.placeholder=${this.hass.localize(
|
|
||||||
"ui.panel.config.hardware.available_hardware.search"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
</ha-input-search>
|
|
||||||
<div class="devices-container ha-scrollbar">
|
|
||||||
${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>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
<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>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ha-dialog>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleSearchChange(ev: InputEvent) {
|
|
||||||
this._filter = (ev.target as HaInputSearch).value ?? "";
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return [
|
|
||||||
haStyleScrollbar,
|
|
||||||
css`
|
|
||||||
ha-dialog {
|
|
||||||
--dialog-content-padding: 0;
|
|
||||||
}
|
|
||||||
.content-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.devices-container {
|
|
||||||
padding: var(--ha-space-6);
|
|
||||||
overflow-y: auto;
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
ha-expansion-panel {
|
|
||||||
flex: 1;
|
|
||||||
margin: 4px 0;
|
|
||||||
}
|
|
||||||
pre,
|
|
||||||
code {
|
|
||||||
background-color: var(--markdown-code-background-color, none);
|
|
||||||
border-radius: var(--ha-border-radius-sm);
|
|
||||||
}
|
|
||||||
pre {
|
|
||||||
padding: 16px;
|
|
||||||
overflow: auto;
|
|
||||||
line-height: var(--ha-line-height-normal);
|
|
||||||
font-family: var(--ha-font-family-code);
|
|
||||||
}
|
|
||||||
code {
|
|
||||||
font-size: var(--ha-font-size-s);
|
|
||||||
padding: 0.2em 0.4em;
|
|
||||||
}
|
|
||||||
ha-input-search {
|
|
||||||
padding: 0 var(--ha-space-5);
|
|
||||||
}
|
|
||||||
.device-property {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
.attributes {
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"ha-dialog-hardware-available": DialogHardwareAvailable;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
166
src/panels/config/hardware/ha-config-hardware-all.ts
Normal file
166
src/panels/config/hardware/ha-config-hardware-all.ts
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import { dump } from "js-yaml";
|
||||||
|
import type { CSSResultGroup } from "lit";
|
||||||
|
import { css, html, LitElement } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { until } from "lit/directives/until";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||||
|
import "../../../components/data-table/ha-data-table";
|
||||||
|
import type {
|
||||||
|
DataTableColumnContainer,
|
||||||
|
DataTableRowData,
|
||||||
|
RowClickedEvent,
|
||||||
|
} from "../../../components/data-table/ha-data-table";
|
||||||
|
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||||
|
import type {
|
||||||
|
HardwareDevice,
|
||||||
|
HassioHardwareInfo,
|
||||||
|
} from "../../../data/hassio/hardware";
|
||||||
|
import { fetchHassioHardwareInfo } from "../../../data/hassio/hardware";
|
||||||
|
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||||
|
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||||
|
import type { HomeAssistant, Route } from "../../../types";
|
||||||
|
import { hardwareTabs } from "./ha-config-hardware";
|
||||||
|
|
||||||
|
interface HardwareDeviceRow extends HardwareDevice {
|
||||||
|
id: string;
|
||||||
|
attributes_string: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("ha-config-hardware-all")
|
||||||
|
class HaConfigHardwareAll extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public route!: Route;
|
||||||
|
|
||||||
|
@state() private _hardware?: HassioHardwareInfo;
|
||||||
|
|
||||||
|
@state() private _error?: string;
|
||||||
|
|
||||||
|
private _columns = memoizeOne(
|
||||||
|
(localize: LocalizeFunc): DataTableColumnContainer<HardwareDeviceRow> => ({
|
||||||
|
name: {
|
||||||
|
title: localize("ui.panel.config.hardware.system_hardware.name"),
|
||||||
|
main: true,
|
||||||
|
sortable: true,
|
||||||
|
filterable: true,
|
||||||
|
flex: 2,
|
||||||
|
},
|
||||||
|
dev_path: {
|
||||||
|
title: localize("ui.panel.config.hardware.system_hardware.device_path"),
|
||||||
|
sortable: true,
|
||||||
|
filterable: true,
|
||||||
|
flex: 2,
|
||||||
|
},
|
||||||
|
by_id: {
|
||||||
|
title: localize("ui.panel.config.hardware.system_hardware.id"),
|
||||||
|
sortable: true,
|
||||||
|
filterable: true,
|
||||||
|
flex: 2,
|
||||||
|
},
|
||||||
|
subsystem: {
|
||||||
|
title: localize("ui.panel.config.hardware.system_hardware.subsystem"),
|
||||||
|
sortable: true,
|
||||||
|
filterable: true,
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
attributes_string: {
|
||||||
|
title: "",
|
||||||
|
filterable: true,
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
private _data = memoizeOne(
|
||||||
|
(showAdvanced: boolean, hardware: HassioHardwareInfo): DataTableRowData[] =>
|
||||||
|
hardware.devices
|
||||||
|
.filter(
|
||||||
|
(device) =>
|
||||||
|
showAdvanced || ["tty", "gpio", "input"].includes(device.subsystem)
|
||||||
|
)
|
||||||
|
.map((device) => ({
|
||||||
|
...device,
|
||||||
|
id: device.dev_path,
|
||||||
|
attributes_string: Object.entries(device.attributes)
|
||||||
|
.map(([key, value]) => `${key}: ${value}`)
|
||||||
|
.join(" "),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
protected firstUpdated(): void {
|
||||||
|
this._load();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
<hass-tabs-subpage-data-table
|
||||||
|
.hass=${this.hass}
|
||||||
|
.narrow=${this.narrow}
|
||||||
|
back-path="/config/system"
|
||||||
|
.route=${this.route}
|
||||||
|
.tabs=${hardwareTabs(this.hass)}
|
||||||
|
clickable
|
||||||
|
.columns=${this._columns(this.hass.localize)}
|
||||||
|
.data=${this._hardware
|
||||||
|
? this._data(
|
||||||
|
this.hass.userData?.showAdvanced || false,
|
||||||
|
this._hardware
|
||||||
|
)
|
||||||
|
: []}
|
||||||
|
.noDataText=${this._error ||
|
||||||
|
this.hass.localize("ui.panel.config.hardware.loading_system_data")}
|
||||||
|
@row-click=${this._handleRowClicked}
|
||||||
|
></hass-tabs-subpage-data-table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _load() {
|
||||||
|
try {
|
||||||
|
this._hardware = await fetchHassioHardwareInfo(this.hass);
|
||||||
|
} catch (err: any) {
|
||||||
|
this._error = extractApiErrorMessage(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleRowClicked(ev: CustomEvent<RowClickedEvent>) {
|
||||||
|
const id = ev.detail.id;
|
||||||
|
const device = this._hardware?.devices.find((dev) => dev.dev_path === id);
|
||||||
|
if (!device) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: device.name,
|
||||||
|
subtitle: this.hass.localize(
|
||||||
|
"ui.panel.config.hardware.system_hardware.attributes"
|
||||||
|
),
|
||||||
|
text: html`${until(this._renderHaCodeEditor(device))}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _renderHaCodeEditor(device: HardwareDevice) {
|
||||||
|
await import("../../../components/ha-code-editor");
|
||||||
|
|
||||||
|
return html`<ha-code-editor
|
||||||
|
mode="yaml"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.value=${dump(device.attributes, { indent: 2 })}
|
||||||
|
read-only
|
||||||
|
></ha-code-editor>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles: CSSResultGroup = css`
|
||||||
|
ha-code-editor {
|
||||||
|
direction: var(--direction);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-config-hardware-all": HaConfigHardwareAll;
|
||||||
|
}
|
||||||
|
}
|
||||||
571
src/panels/config/hardware/ha-config-hardware-overview.ts
Normal file
571
src/panels/config/hardware/ha-config-hardware-overview.ts
Normal file
@@ -0,0 +1,571 @@
|
|||||||
|
import { mdiPower } from "@mdi/js";
|
||||||
|
import type { SeriesOption } from "echarts/types/dist/shared";
|
||||||
|
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
|
import type { PropertyValues } from "lit";
|
||||||
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||||
|
import { round } from "../../../common/number/round";
|
||||||
|
import { blankBeforePercent } from "../../../common/translations/blank_before_percent";
|
||||||
|
import "../../../components/chart/ha-chart-base";
|
||||||
|
import "../../../components/ha-alert";
|
||||||
|
import "../../../components/ha-button";
|
||||||
|
import "../../../components/ha-card";
|
||||||
|
import "../../../components/ha-fade-in";
|
||||||
|
import "../../../components/ha-icon-button";
|
||||||
|
import "../../../components/ha-icon-next";
|
||||||
|
import "../../../components/ha-md-list-item";
|
||||||
|
import "../../../components/ha-spinner";
|
||||||
|
import type { ConfigEntry } from "../../../data/config_entries";
|
||||||
|
import { subscribeConfigEntries } from "../../../data/config_entries";
|
||||||
|
import type {
|
||||||
|
HardwareInfo,
|
||||||
|
SystemStatusStreamMessage,
|
||||||
|
} from "../../../data/hardware";
|
||||||
|
import { BOARD_NAMES } from "../../../data/hardware";
|
||||||
|
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||||
|
import type { HassioHassOSInfo } from "../../../data/hassio/host";
|
||||||
|
import { fetchHassioHassOsInfo } from "../../../data/hassio/host";
|
||||||
|
import { scanUSBDevices } from "../../../data/usb";
|
||||||
|
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
|
||||||
|
import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart";
|
||||||
|
import "../../../layouts/hass-tabs-subpage";
|
||||||
|
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||||
|
import type { ECOption } from "../../../resources/echarts/echarts";
|
||||||
|
import { haStyle } from "../../../resources/styles";
|
||||||
|
import { DefaultPrimaryColor } from "../../../resources/theme/color/color.globals";
|
||||||
|
import type { HomeAssistant, Route } from "../../../types";
|
||||||
|
import { hardwareBrandsUrl } from "../../../util/brands-url";
|
||||||
|
import { hardwareTabs } from "./ha-config-hardware";
|
||||||
|
|
||||||
|
const DATASAMPLES = 60;
|
||||||
|
|
||||||
|
const DATA_SET_CONFIG: SeriesOption = {
|
||||||
|
type: "line",
|
||||||
|
color: DefaultPrimaryColor,
|
||||||
|
areaStyle: {
|
||||||
|
color: DefaultPrimaryColor + "2B",
|
||||||
|
},
|
||||||
|
symbolSize: 0,
|
||||||
|
lineStyle: {
|
||||||
|
width: 1,
|
||||||
|
},
|
||||||
|
smooth: 0.25,
|
||||||
|
};
|
||||||
|
|
||||||
|
@customElement("ha-config-hardware-overview")
|
||||||
|
class HaConfigHardwareOverview extends SubscribeMixin(LitElement) {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public route!: Route;
|
||||||
|
|
||||||
|
@state() private _error?: string;
|
||||||
|
|
||||||
|
@state() private _OSData?: HassioHassOSInfo;
|
||||||
|
|
||||||
|
@state() private _hardwareInfo?: HardwareInfo;
|
||||||
|
|
||||||
|
@state() private _chartOptions?: ECOption;
|
||||||
|
|
||||||
|
@state() private _systemStatusData?: SystemStatusStreamMessage;
|
||||||
|
|
||||||
|
@state() private _configEntries?: Record<string, ConfigEntry>;
|
||||||
|
|
||||||
|
private _memoryEntries: [number, number | null][] = [];
|
||||||
|
|
||||||
|
private _cpuEntries: [number, number | null][] = [];
|
||||||
|
|
||||||
|
public hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||||
|
const subs = [
|
||||||
|
subscribeConfigEntries(
|
||||||
|
this.hass,
|
||||||
|
(messages) => {
|
||||||
|
let fullUpdate = false;
|
||||||
|
const newEntries: ConfigEntry[] = [];
|
||||||
|
messages.forEach((message) => {
|
||||||
|
if (message.type === null || message.type === "added") {
|
||||||
|
newEntries.push(message.entry);
|
||||||
|
if (message.type === null) {
|
||||||
|
fullUpdate = true;
|
||||||
|
}
|
||||||
|
} else if (message.type === "removed") {
|
||||||
|
if (this._configEntries) {
|
||||||
|
delete this._configEntries[message.entry.entry_id];
|
||||||
|
}
|
||||||
|
} else if (message.type === "updated") {
|
||||||
|
if (this._configEntries) {
|
||||||
|
const newEntry = message.entry;
|
||||||
|
this._configEntries[message.entry.entry_id] = newEntry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!newEntries.length && !fullUpdate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const entries = [
|
||||||
|
...(fullUpdate ? [] : Object.values(this._configEntries || {})),
|
||||||
|
...newEntries,
|
||||||
|
];
|
||||||
|
const configEntries: Record<string, ConfigEntry> = {};
|
||||||
|
for (const entry of entries) {
|
||||||
|
configEntries[entry.entry_id] = entry;
|
||||||
|
}
|
||||||
|
this._configEntries = configEntries;
|
||||||
|
},
|
||||||
|
{ type: ["hardware"] }
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isComponentLoaded(this.hass, "hardware")) {
|
||||||
|
subs.push(
|
||||||
|
this.hass.connection.subscribeMessage<SystemStatusStreamMessage>(
|
||||||
|
(message) => {
|
||||||
|
// Only store the last 60 entries
|
||||||
|
this._memoryEntries.shift();
|
||||||
|
this._cpuEntries.shift();
|
||||||
|
|
||||||
|
this._memoryEntries.push([
|
||||||
|
new Date(message.timestamp).getTime(),
|
||||||
|
message.memory_used_percent,
|
||||||
|
]);
|
||||||
|
this._cpuEntries.push([
|
||||||
|
new Date(message.timestamp).getTime(),
|
||||||
|
message.cpu_percent,
|
||||||
|
]);
|
||||||
|
|
||||||
|
this._systemStatusData = message;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "hardware/subscribe_system_status",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return subs;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected willUpdate(): void {
|
||||||
|
if (!this.hasUpdated && !this._chartOptions) {
|
||||||
|
this._chartOptions = {
|
||||||
|
xAxis: {
|
||||||
|
type: "time",
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: "value",
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
splitLine: {
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
formatter: (value: number) =>
|
||||||
|
value + blankBeforePercent(this.hass.locale) + "%",
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
scale: true,
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
top: 10,
|
||||||
|
bottom: 10,
|
||||||
|
left: 10,
|
||||||
|
right: 10,
|
||||||
|
containLabel: true,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: "axis",
|
||||||
|
valueFormatter: (value) =>
|
||||||
|
value + blankBeforePercent(this.hass.locale) + "%",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected firstUpdated(changedProps: PropertyValues) {
|
||||||
|
super.firstUpdated(changedProps);
|
||||||
|
this._load();
|
||||||
|
|
||||||
|
const date = new Date();
|
||||||
|
// Force graph to start drawing from the right
|
||||||
|
for (let i = 0; i < DATASAMPLES; i++) {
|
||||||
|
const t = new Date(date);
|
||||||
|
t.setSeconds(t.getSeconds() - 5 * (DATASAMPLES - i));
|
||||||
|
this._memoryEntries.push([t.getTime(), null]);
|
||||||
|
this._cpuEntries.push([t.getTime(), null]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
let boardId: string | undefined;
|
||||||
|
let boardName: string | undefined;
|
||||||
|
let imageURL: string | undefined;
|
||||||
|
let documentationURL: string | undefined;
|
||||||
|
let boardConfigEntries: ConfigEntry[] = [];
|
||||||
|
|
||||||
|
const boardData = this._hardwareInfo?.hardware.find(
|
||||||
|
(hw) => hw.board !== null
|
||||||
|
);
|
||||||
|
|
||||||
|
const dongles = this._hardwareInfo?.hardware.filter(
|
||||||
|
(hw) =>
|
||||||
|
hw.dongle !== null &&
|
||||||
|
(!hw.config_entries.length ||
|
||||||
|
hw.config_entries.some(
|
||||||
|
(entryId) =>
|
||||||
|
this._configEntries?.[entryId] &&
|
||||||
|
!this._configEntries[entryId].disabled_by
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (boardData) {
|
||||||
|
boardConfigEntries = boardData.config_entries
|
||||||
|
.map((id) => this._configEntries?.[id])
|
||||||
|
.filter(
|
||||||
|
(entry) => entry?.supports_options && !entry.disabled_by
|
||||||
|
) as ConfigEntry[];
|
||||||
|
boardId = boardData.board!.hassio_board_id;
|
||||||
|
boardName = boardData.name;
|
||||||
|
documentationURL = boardData.url;
|
||||||
|
imageURL = hardwareBrandsUrl(
|
||||||
|
{
|
||||||
|
category: "boards",
|
||||||
|
manufacturer: boardData.board!.manufacturer,
|
||||||
|
model: boardData.board!.model,
|
||||||
|
darkOptimized: this.hass.themes?.darkMode,
|
||||||
|
},
|
||||||
|
this.hass.auth.data.hassUrl
|
||||||
|
);
|
||||||
|
} else if (this._OSData?.board) {
|
||||||
|
boardId = this._OSData.board;
|
||||||
|
boardName = BOARD_NAMES[this._OSData.board];
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<hass-tabs-subpage
|
||||||
|
back-path="/config/system"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.narrow=${this.narrow}
|
||||||
|
.route=${this.route}
|
||||||
|
.tabs=${hardwareTabs(this.hass)}
|
||||||
|
>
|
||||||
|
${isComponentLoaded(this.hass, "hassio")
|
||||||
|
? html`
|
||||||
|
<ha-icon-button
|
||||||
|
slot="toolbar-icon"
|
||||||
|
.path=${mdiPower}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.hardware.restart_homeassistant"
|
||||||
|
)}
|
||||||
|
@click=${this._showRestartDialog}
|
||||||
|
></ha-icon-button>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
${this._error
|
||||||
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
|
: nothing}
|
||||||
|
<div class="content">
|
||||||
|
${boardName || isComponentLoaded(this.hass, "hassio")
|
||||||
|
? html`
|
||||||
|
<ha-card outlined>
|
||||||
|
<div class="card-content">
|
||||||
|
${imageURL
|
||||||
|
? html`<img
|
||||||
|
alt=""
|
||||||
|
src=${imageURL}
|
||||||
|
crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
/>`
|
||||||
|
: nothing}
|
||||||
|
<div class="board-info">
|
||||||
|
<p class="primary-text">
|
||||||
|
${boardName ||
|
||||||
|
this.hass.localize(
|
||||||
|
"ui.panel.config.hardware.generic_hardware"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
${boardId
|
||||||
|
? html`<p class="secondary-text">${boardId}</p>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${documentationURL
|
||||||
|
? html`
|
||||||
|
<ha-md-list-item
|
||||||
|
.href=${documentationURL}
|
||||||
|
type="link"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.config.hardware.documentation"
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
|
<span slot="supporting-text"
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.config.hardware.documentation_description"
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
|
<ha-icon-next slot="end"></ha-icon-next>
|
||||||
|
</ha-md-list-item>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
${boardConfigEntries.length
|
||||||
|
? html`<div class="card-actions">
|
||||||
|
<ha-button
|
||||||
|
.entry=${boardConfigEntries[0]}
|
||||||
|
@click=${this._openOptionsFlow}
|
||||||
|
appearance="plain"
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.hardware.configure"
|
||||||
|
)}
|
||||||
|
</ha-button>
|
||||||
|
</div>`
|
||||||
|
: nothing}
|
||||||
|
</ha-card>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
${dongles?.length
|
||||||
|
? html`<ha-card outlined>
|
||||||
|
${dongles.map((dongle) => {
|
||||||
|
const configEntry = dongle.config_entries
|
||||||
|
.map((id) => this._configEntries?.[id])
|
||||||
|
.filter(
|
||||||
|
(entry) => entry?.supports_options && !entry.disabled_by
|
||||||
|
)[0];
|
||||||
|
return html`<div class="row">
|
||||||
|
${dongle.name}${configEntry
|
||||||
|
? html`<ha-button
|
||||||
|
.entry=${configEntry}
|
||||||
|
@click=${this._openOptionsFlow}
|
||||||
|
appearance="filled"
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.hardware.configure"
|
||||||
|
)}
|
||||||
|
</ha-button>`
|
||||||
|
: nothing}
|
||||||
|
</div>`;
|
||||||
|
})}
|
||||||
|
</ha-card>`
|
||||||
|
: nothing}
|
||||||
|
${isComponentLoaded(this.hass, "hardware")
|
||||||
|
? html`<ha-card outlined>
|
||||||
|
<div class="header">
|
||||||
|
<div class="title">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.hardware.processor"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="value">
|
||||||
|
${this._systemStatusData
|
||||||
|
? html`${this._systemStatusData
|
||||||
|
.cpu_percent}${blankBeforePercent(
|
||||||
|
this.hass.locale
|
||||||
|
)}%`
|
||||||
|
: "-"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-content loading-container">
|
||||||
|
<ha-chart-base
|
||||||
|
.hass=${this.hass}
|
||||||
|
.data=${this._getChartData(this._cpuEntries)}
|
||||||
|
.options=${this._chartOptions}
|
||||||
|
></ha-chart-base>
|
||||||
|
${!this._systemStatusData
|
||||||
|
? html` <ha-fade-in delay="1000" class="loading-overlay">
|
||||||
|
<ha-spinner size="large"></ha-spinner>
|
||||||
|
</ha-fade-in>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
<ha-card outlined>
|
||||||
|
<div class="header">
|
||||||
|
<div class="title">
|
||||||
|
${this.hass.localize("ui.panel.config.hardware.memory")}
|
||||||
|
</div>
|
||||||
|
<div class="value">
|
||||||
|
${this._systemStatusData
|
||||||
|
? html`${round(
|
||||||
|
this._systemStatusData.memory_used_mb / 1024,
|
||||||
|
1
|
||||||
|
)}
|
||||||
|
GB /
|
||||||
|
${round(
|
||||||
|
(this._systemStatusData.memory_used_mb +
|
||||||
|
this._systemStatusData.memory_free_mb) /
|
||||||
|
1024,
|
||||||
|
0
|
||||||
|
)}
|
||||||
|
GB`
|
||||||
|
: "-"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-content loading-container">
|
||||||
|
<ha-chart-base
|
||||||
|
.hass=${this.hass}
|
||||||
|
.data=${this._getChartData(this._memoryEntries)}
|
||||||
|
.options=${this._chartOptions}
|
||||||
|
></ha-chart-base>
|
||||||
|
${!this._systemStatusData
|
||||||
|
? html`
|
||||||
|
<ha-fade-in delay="1000" class="loading-overlay">
|
||||||
|
<ha-spinner size="large"></ha-spinner>
|
||||||
|
</ha-fade-in>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
</ha-card>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
</hass-tabs-subpage>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _load() {
|
||||||
|
if (isComponentLoaded(this.hass, "usb")) {
|
||||||
|
await scanUSBDevices(this.hass);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isHassioLoaded = isComponentLoaded(this.hass, "hassio");
|
||||||
|
try {
|
||||||
|
if (isComponentLoaded(this.hass, "hardware")) {
|
||||||
|
this._hardwareInfo = await this.hass.callWS({ type: "hardware/info" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHassioLoaded && !this._hardwareInfo?.hardware.length) {
|
||||||
|
this._OSData = await fetchHassioHassOsInfo(this.hass);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
this._error = extractApiErrorMessage(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _openOptionsFlow(ev) {
|
||||||
|
const entry = ev.currentTarget.entry;
|
||||||
|
if (!entry) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showOptionsFlowDialog(this, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _showRestartDialog() {
|
||||||
|
showRestartDialog(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getChartData = memoizeOne(
|
||||||
|
(entries: [number, number | null][]): SeriesOption[] => [
|
||||||
|
{
|
||||||
|
...DATA_SET_CONFIG,
|
||||||
|
id: entries === this._cpuEntries ? "cpu" : "memory",
|
||||||
|
name:
|
||||||
|
entries === this._cpuEntries
|
||||||
|
? this.hass.localize("ui.panel.config.hardware.processor")
|
||||||
|
: this.hass.localize("ui.panel.config.hardware.memory"),
|
||||||
|
data: entries,
|
||||||
|
} as SeriesOption,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
static styles = [
|
||||||
|
haStyle,
|
||||||
|
css`
|
||||||
|
.content {
|
||||||
|
padding: 28px 20px 0;
|
||||||
|
max-width: 1040px;
|
||||||
|
margin: 0 auto;
|
||||||
|
--mdc-list-side-padding: 24px;
|
||||||
|
--mdc-list-vertical-padding: 0;
|
||||||
|
}
|
||||||
|
ha-card {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-direction: column;
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.card-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: rgba(var(--rgb-card-background-color), 0.75);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.card-content img {
|
||||||
|
max-width: 300px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
.board-info {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.primary-text {
|
||||||
|
font-size: var(--ha-font-size-l);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.secondary-text {
|
||||||
|
font-size: var(--ha-font-size-m);
|
||||||
|
margin-bottom: 0;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .title {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
font-size: var(--ha-font-size-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .value {
|
||||||
|
font-size: var(--ha-font-size-l);
|
||||||
|
}
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 48px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-alert {
|
||||||
|
--ha-alert-icon-size: 24px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-config-hardware-overview": HaConfigHardwareOverview;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,585 +1,66 @@
|
|||||||
import { mdiPower } from "@mdi/js";
|
import { mdiFormatListBulletedType, mdiMemory } from "@mdi/js";
|
||||||
import type { SeriesOption } from "echarts/types/dist/shared";
|
import { customElement, property } from "lit/decorators";
|
||||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
|
||||||
import type { PropertyValues } from "lit";
|
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||||
import { round } from "../../../common/number/round";
|
import type { RouterOptions } from "../../../layouts/hass-router-page";
|
||||||
import { blankBeforePercent } from "../../../common/translations/blank_before_percent";
|
import { HassRouterPage } from "../../../layouts/hass-router-page";
|
||||||
import "../../../components/chart/ha-chart-base";
|
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
|
||||||
import "../../../components/ha-alert";
|
|
||||||
import "../../../components/ha-button";
|
|
||||||
import "../../../components/ha-card";
|
|
||||||
import "../../../components/ha-fade-in";
|
|
||||||
import "../../../components/ha-icon-button";
|
|
||||||
import "../../../components/ha-icon-next";
|
|
||||||
import "../../../components/ha-md-list-item";
|
|
||||||
import "../../../components/ha-spinner";
|
|
||||||
import type { ConfigEntry } from "../../../data/config_entries";
|
|
||||||
import { subscribeConfigEntries } from "../../../data/config_entries";
|
|
||||||
import type {
|
|
||||||
HardwareInfo,
|
|
||||||
SystemStatusStreamMessage,
|
|
||||||
} from "../../../data/hardware";
|
|
||||||
import { BOARD_NAMES } from "../../../data/hardware";
|
|
||||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
|
||||||
import type { HassioHassOSInfo } from "../../../data/hassio/host";
|
|
||||||
import { fetchHassioHassOsInfo } from "../../../data/hassio/host";
|
|
||||||
import { scanUSBDevices } from "../../../data/usb";
|
|
||||||
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
|
|
||||||
import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart";
|
|
||||||
import "../../../layouts/hass-subpage";
|
|
||||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
|
||||||
import type { ECOption } from "../../../resources/echarts/echarts";
|
|
||||||
import { haStyle } from "../../../resources/styles";
|
|
||||||
import { DefaultPrimaryColor } from "../../../resources/theme/color/color.globals";
|
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import { hardwareBrandsUrl } from "../../../util/brands-url";
|
|
||||||
import { showhardwareAvailableDialog } from "./show-dialog-hardware-available";
|
|
||||||
|
|
||||||
const DATASAMPLES = 60;
|
export const hardwareTabs = (hass: HomeAssistant): PageNavigation[] => {
|
||||||
|
const tabs: PageNavigation[] = [
|
||||||
|
{
|
||||||
|
path: "/config/hardware/overview",
|
||||||
|
translationKey: "ui.panel.config.hardware.overview",
|
||||||
|
iconPath: mdiMemory,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const DATA_SET_CONFIG: SeriesOption = {
|
if (isComponentLoaded(hass, "hassio")) {
|
||||||
type: "line",
|
tabs.push({
|
||||||
color: DefaultPrimaryColor,
|
path: "/config/hardware/all",
|
||||||
areaStyle: {
|
translationKey: "ui.panel.config.hardware.system_hardware.title",
|
||||||
color: DefaultPrimaryColor + "2B",
|
iconPath: mdiFormatListBulletedType,
|
||||||
},
|
});
|
||||||
symbolSize: 0,
|
}
|
||||||
lineStyle: {
|
|
||||||
width: 1,
|
return tabs;
|
||||||
},
|
|
||||||
smooth: 0.25,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@customElement("ha-config-hardware")
|
@customElement("ha-config-hardware")
|
||||||
class HaConfigHardware extends SubscribeMixin(LitElement) {
|
class HaConfigHardware extends HassRouterPage {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ type: Boolean }) public narrow = false;
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
@state() private _error?: string;
|
protected routerOptions: RouterOptions = {
|
||||||
|
defaultPage: "overview",
|
||||||
|
routes: {
|
||||||
|
overview: {
|
||||||
|
tag: "ha-config-hardware-overview",
|
||||||
|
load: () => import("./ha-config-hardware-overview"),
|
||||||
|
cache: true,
|
||||||
|
},
|
||||||
|
all: {
|
||||||
|
tag: "ha-config-hardware-all",
|
||||||
|
load: () => import("./ha-config-hardware-all"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
beforeRender: (page) => {
|
||||||
|
if (
|
||||||
|
page === "all" &&
|
||||||
|
(!this.hass || !isComponentLoaded(this.hass, "hassio"))
|
||||||
|
) {
|
||||||
|
return "overview";
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
@state() private _OSData?: HassioHassOSInfo;
|
protected updatePageEl(pageEl) {
|
||||||
|
pageEl.hass = this.hass;
|
||||||
@state() private _hardwareInfo?: HardwareInfo;
|
pageEl.narrow = this.narrow;
|
||||||
|
pageEl.route = this.routeTail;
|
||||||
@state() private _chartOptions?: ECOption;
|
|
||||||
|
|
||||||
@state() private _systemStatusData?: SystemStatusStreamMessage;
|
|
||||||
|
|
||||||
@state() private _configEntries?: Record<string, ConfigEntry>;
|
|
||||||
|
|
||||||
private _memoryEntries: [number, number | null][] = [];
|
|
||||||
|
|
||||||
private _cpuEntries: [number, number | null][] = [];
|
|
||||||
|
|
||||||
public hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
|
||||||
const subs = [
|
|
||||||
subscribeConfigEntries(
|
|
||||||
this.hass,
|
|
||||||
(messages) => {
|
|
||||||
let fullUpdate = false;
|
|
||||||
const newEntries: ConfigEntry[] = [];
|
|
||||||
messages.forEach((message) => {
|
|
||||||
if (message.type === null || message.type === "added") {
|
|
||||||
newEntries.push(message.entry);
|
|
||||||
if (message.type === null) {
|
|
||||||
fullUpdate = true;
|
|
||||||
}
|
|
||||||
} else if (message.type === "removed") {
|
|
||||||
if (this._configEntries) {
|
|
||||||
delete this._configEntries[message.entry.entry_id];
|
|
||||||
}
|
|
||||||
} else if (message.type === "updated") {
|
|
||||||
if (this._configEntries) {
|
|
||||||
const newEntry = message.entry;
|
|
||||||
this._configEntries[message.entry.entry_id] = newEntry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!newEntries.length && !fullUpdate) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const entries = [
|
|
||||||
...(fullUpdate ? [] : Object.values(this._configEntries || {})),
|
|
||||||
...newEntries,
|
|
||||||
];
|
|
||||||
const configEntries: Record<string, ConfigEntry> = {};
|
|
||||||
for (const entry of entries) {
|
|
||||||
configEntries[entry.entry_id] = entry;
|
|
||||||
}
|
|
||||||
this._configEntries = configEntries;
|
|
||||||
},
|
|
||||||
{ type: ["hardware"] }
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (isComponentLoaded(this.hass, "hardware")) {
|
|
||||||
subs.push(
|
|
||||||
this.hass.connection.subscribeMessage<SystemStatusStreamMessage>(
|
|
||||||
(message) => {
|
|
||||||
// Only store the last 60 entries
|
|
||||||
this._memoryEntries.shift();
|
|
||||||
this._cpuEntries.shift();
|
|
||||||
|
|
||||||
this._memoryEntries.push([
|
|
||||||
new Date(message.timestamp).getTime(),
|
|
||||||
message.memory_used_percent,
|
|
||||||
]);
|
|
||||||
this._cpuEntries.push([
|
|
||||||
new Date(message.timestamp).getTime(),
|
|
||||||
message.cpu_percent,
|
|
||||||
]);
|
|
||||||
|
|
||||||
this._systemStatusData = message;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "hardware/subscribe_system_status",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return subs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected willUpdate(): void {
|
|
||||||
if (!this.hasUpdated && !this._chartOptions) {
|
|
||||||
this._chartOptions = {
|
|
||||||
xAxis: {
|
|
||||||
type: "time",
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
type: "value",
|
|
||||||
min: 0,
|
|
||||||
max: 100,
|
|
||||||
splitLine: {
|
|
||||||
show: true,
|
|
||||||
},
|
|
||||||
axisLabel: {
|
|
||||||
formatter: (value: number) =>
|
|
||||||
value + blankBeforePercent(this.hass.locale) + "%",
|
|
||||||
},
|
|
||||||
axisLine: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
scale: true,
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
top: 10,
|
|
||||||
bottom: 10,
|
|
||||||
left: 10,
|
|
||||||
right: 10,
|
|
||||||
containLabel: true,
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
trigger: "axis",
|
|
||||||
valueFormatter: (value) =>
|
|
||||||
value + blankBeforePercent(this.hass.locale) + "%",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected firstUpdated(changedProps: PropertyValues) {
|
|
||||||
super.firstUpdated(changedProps);
|
|
||||||
this._load();
|
|
||||||
|
|
||||||
const date = new Date();
|
|
||||||
// Force graph to start drawing from the right
|
|
||||||
for (let i = 0; i < DATASAMPLES; i++) {
|
|
||||||
const t = new Date(date);
|
|
||||||
t.setSeconds(t.getSeconds() - 5 * (DATASAMPLES - i));
|
|
||||||
this._memoryEntries.push([t.getTime(), null]);
|
|
||||||
this._cpuEntries.push([t.getTime(), null]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render() {
|
|
||||||
let boardId: string | undefined;
|
|
||||||
let boardName: string | undefined;
|
|
||||||
let imageURL: string | undefined;
|
|
||||||
let documentationURL: string | undefined;
|
|
||||||
let boardConfigEntries: ConfigEntry[] = [];
|
|
||||||
|
|
||||||
const boardData = this._hardwareInfo?.hardware.find(
|
|
||||||
(hw) => hw.board !== null
|
|
||||||
);
|
|
||||||
|
|
||||||
const dongles = this._hardwareInfo?.hardware.filter(
|
|
||||||
(hw) =>
|
|
||||||
hw.dongle !== null &&
|
|
||||||
(!hw.config_entries.length ||
|
|
||||||
hw.config_entries.some(
|
|
||||||
(entryId) =>
|
|
||||||
this._configEntries?.[entryId] &&
|
|
||||||
!this._configEntries[entryId].disabled_by
|
|
||||||
))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (boardData) {
|
|
||||||
boardConfigEntries = boardData.config_entries
|
|
||||||
.map((id) => this._configEntries?.[id])
|
|
||||||
.filter(
|
|
||||||
(entry) => entry?.supports_options && !entry.disabled_by
|
|
||||||
) as ConfigEntry[];
|
|
||||||
boardId = boardData.board!.hassio_board_id;
|
|
||||||
boardName = boardData.name;
|
|
||||||
documentationURL = boardData.url;
|
|
||||||
imageURL = hardwareBrandsUrl(
|
|
||||||
{
|
|
||||||
category: "boards",
|
|
||||||
manufacturer: boardData.board!.manufacturer,
|
|
||||||
model: boardData.board!.model,
|
|
||||||
darkOptimized: this.hass.themes?.darkMode,
|
|
||||||
},
|
|
||||||
this.hass.auth.data.hassUrl
|
|
||||||
);
|
|
||||||
} else if (this._OSData?.board) {
|
|
||||||
boardId = this._OSData.board;
|
|
||||||
boardName = BOARD_NAMES[this._OSData.board];
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<hass-subpage
|
|
||||||
back-path="/config/system"
|
|
||||||
.hass=${this.hass}
|
|
||||||
.narrow=${this.narrow}
|
|
||||||
.header=${this.hass.localize("ui.panel.config.hardware.caption")}
|
|
||||||
>
|
|
||||||
${isComponentLoaded(this.hass, "hassio")
|
|
||||||
? html`
|
|
||||||
<ha-icon-button
|
|
||||||
slot="toolbar-icon"
|
|
||||||
.path=${mdiPower}
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.panel.config.hardware.restart_homeassistant"
|
|
||||||
)}
|
|
||||||
@click=${this._showRestartDialog}
|
|
||||||
></ha-icon-button>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
${this._error
|
|
||||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
|
||||||
: nothing}
|
|
||||||
<div class="content">
|
|
||||||
${boardName || isComponentLoaded(this.hass, "hassio")
|
|
||||||
? html`
|
|
||||||
<ha-card outlined>
|
|
||||||
<div class="card-content">
|
|
||||||
${imageURL
|
|
||||||
? html`<img
|
|
||||||
alt=""
|
|
||||||
src=${imageURL}
|
|
||||||
crossorigin="anonymous"
|
|
||||||
referrerpolicy="no-referrer"
|
|
||||||
/>`
|
|
||||||
: nothing}
|
|
||||||
<div class="board-info">
|
|
||||||
<p class="primary-text">
|
|
||||||
${boardName ||
|
|
||||||
this.hass.localize(
|
|
||||||
"ui.panel.config.hardware.generic_hardware"
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
${boardId
|
|
||||||
? html`<p class="secondary-text">${boardId}</p>`
|
|
||||||
: nothing}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
${documentationURL
|
|
||||||
? html`
|
|
||||||
<ha-md-list-item
|
|
||||||
.href=${documentationURL}
|
|
||||||
type="link"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
>${this.hass.localize(
|
|
||||||
"ui.panel.config.hardware.documentation"
|
|
||||||
)}</span
|
|
||||||
>
|
|
||||||
<span slot="supporting-text"
|
|
||||||
>${this.hass.localize(
|
|
||||||
"ui.panel.config.hardware.documentation_description"
|
|
||||||
)}</span
|
|
||||||
>
|
|
||||||
<ha-icon-next slot="end"></ha-icon-next>
|
|
||||||
</ha-md-list-item>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
${boardConfigEntries.length ||
|
|
||||||
isComponentLoaded(this.hass, "hassio")
|
|
||||||
? html`<div class="card-actions">
|
|
||||||
${boardConfigEntries.length
|
|
||||||
? html`
|
|
||||||
<ha-button
|
|
||||||
.entry=${boardConfigEntries[0]}
|
|
||||||
@click=${this._openOptionsFlow}
|
|
||||||
appearance="plain"
|
|
||||||
>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.hardware.configure"
|
|
||||||
)}
|
|
||||||
</ha-button>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
${isComponentLoaded(this.hass, "hassio")
|
|
||||||
? html`
|
|
||||||
<ha-button
|
|
||||||
@click=${this._openHardware}
|
|
||||||
appearance="plain"
|
|
||||||
>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.hardware.available_hardware.title"
|
|
||||||
)}
|
|
||||||
</ha-button>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
</div>`
|
|
||||||
: nothing}
|
|
||||||
</ha-card>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
${dongles?.length
|
|
||||||
? html`<ha-card outlined>
|
|
||||||
${dongles.map((dongle) => {
|
|
||||||
const configEntry = dongle.config_entries
|
|
||||||
.map((id) => this._configEntries?.[id])
|
|
||||||
.filter(
|
|
||||||
(entry) => entry?.supports_options && !entry.disabled_by
|
|
||||||
)[0];
|
|
||||||
return html`<div class="row">
|
|
||||||
${dongle.name}${configEntry
|
|
||||||
? html`<ha-button
|
|
||||||
.entry=${configEntry}
|
|
||||||
@click=${this._openOptionsFlow}
|
|
||||||
appearance="filled"
|
|
||||||
>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.hardware.configure"
|
|
||||||
)}
|
|
||||||
</ha-button>`
|
|
||||||
: nothing}
|
|
||||||
</div>`;
|
|
||||||
})}
|
|
||||||
</ha-card>`
|
|
||||||
: nothing}
|
|
||||||
${isComponentLoaded(this.hass, "hardware")
|
|
||||||
? html`<ha-card outlined>
|
|
||||||
<div class="header">
|
|
||||||
<div class="title">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.hardware.processor"
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div class="value">
|
|
||||||
${this._systemStatusData
|
|
||||||
? html`${this._systemStatusData
|
|
||||||
.cpu_percent}${blankBeforePercent(
|
|
||||||
this.hass.locale
|
|
||||||
)}%`
|
|
||||||
: "-"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-content loading-container">
|
|
||||||
<ha-chart-base
|
|
||||||
.hass=${this.hass}
|
|
||||||
.data=${this._getChartData(this._cpuEntries)}
|
|
||||||
.options=${this._chartOptions}
|
|
||||||
></ha-chart-base>
|
|
||||||
${!this._systemStatusData
|
|
||||||
? html` <ha-fade-in delay="1000" class="loading-overlay">
|
|
||||||
<ha-spinner size="large"></ha-spinner>
|
|
||||||
</ha-fade-in>`
|
|
||||||
: nothing}
|
|
||||||
</div>
|
|
||||||
</ha-card>
|
|
||||||
<ha-card outlined>
|
|
||||||
<div class="header">
|
|
||||||
<div class="title">
|
|
||||||
${this.hass.localize("ui.panel.config.hardware.memory")}
|
|
||||||
</div>
|
|
||||||
<div class="value">
|
|
||||||
${this._systemStatusData
|
|
||||||
? html`${round(
|
|
||||||
this._systemStatusData.memory_used_mb / 1024,
|
|
||||||
1
|
|
||||||
)}
|
|
||||||
GB /
|
|
||||||
${round(
|
|
||||||
(this._systemStatusData.memory_used_mb +
|
|
||||||
this._systemStatusData.memory_free_mb) /
|
|
||||||
1024,
|
|
||||||
0
|
|
||||||
)}
|
|
||||||
GB`
|
|
||||||
: "-"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-content loading-container">
|
|
||||||
<ha-chart-base
|
|
||||||
.hass=${this.hass}
|
|
||||||
.data=${this._getChartData(this._memoryEntries)}
|
|
||||||
.options=${this._chartOptions}
|
|
||||||
></ha-chart-base>
|
|
||||||
${!this._systemStatusData
|
|
||||||
? html`
|
|
||||||
<ha-fade-in delay="1000" class="loading-overlay">
|
|
||||||
<ha-spinner size="large"></ha-spinner>
|
|
||||||
</ha-fade-in>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
</div>
|
|
||||||
</ha-card>`
|
|
||||||
: nothing}
|
|
||||||
</div>
|
|
||||||
</hass-subpage>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _load() {
|
|
||||||
if (isComponentLoaded(this.hass, "usb")) {
|
|
||||||
await scanUSBDevices(this.hass);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isHassioLoaded = isComponentLoaded(this.hass, "hassio");
|
|
||||||
try {
|
|
||||||
if (isComponentLoaded(this.hass, "hardware")) {
|
|
||||||
this._hardwareInfo = await this.hass.callWS({ type: "hardware/info" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isHassioLoaded && !this._hardwareInfo?.hardware.length) {
|
|
||||||
this._OSData = await fetchHassioHassOsInfo(this.hass);
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
this._error = extractApiErrorMessage(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _openOptionsFlow(ev) {
|
|
||||||
const entry = ev.currentTarget.entry;
|
|
||||||
if (!entry) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showOptionsFlowDialog(this, entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _openHardware() {
|
|
||||||
showhardwareAvailableDialog(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _showRestartDialog() {
|
|
||||||
showRestartDialog(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getChartData = memoizeOne(
|
|
||||||
(entries: [number, number | null][]): SeriesOption[] => [
|
|
||||||
{
|
|
||||||
...DATA_SET_CONFIG,
|
|
||||||
id: entries === this._cpuEntries ? "cpu" : "memory",
|
|
||||||
name:
|
|
||||||
entries === this._cpuEntries
|
|
||||||
? this.hass.localize("ui.panel.config.hardware.processor")
|
|
||||||
: this.hass.localize("ui.panel.config.hardware.memory"),
|
|
||||||
data: entries,
|
|
||||||
} as SeriesOption,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
static styles = [
|
|
||||||
haStyle,
|
|
||||||
css`
|
|
||||||
.content {
|
|
||||||
padding: 28px 20px 0;
|
|
||||||
max-width: 1040px;
|
|
||||||
margin: 0 auto;
|
|
||||||
--mdc-list-side-padding: 24px;
|
|
||||||
--mdc-list-vertical-padding: 0;
|
|
||||||
}
|
|
||||||
ha-card {
|
|
||||||
max-width: 600px;
|
|
||||||
margin: 0 auto;
|
|
||||||
height: 100%;
|
|
||||||
justify-content: space-between;
|
|
||||||
flex-direction: column;
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.card-content {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-container {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-overlay {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
background-color: rgba(var(--rgb-card-background-color), 0.75);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.card-content img {
|
|
||||||
max-width: 300px;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
.board-info {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.primary-text {
|
|
||||||
font-size: var(--ha-font-size-l);
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.secondary-text {
|
|
||||||
font-size: var(--ha-font-size-m);
|
|
||||||
margin-bottom: 0;
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
padding: 16px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header .title {
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
font-size: var(--ha-font-size-l);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header .value {
|
|
||||||
font-size: var(--ha-font-size-l);
|
|
||||||
}
|
|
||||||
.row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
height: 48px;
|
|
||||||
padding: 8px 16px;
|
|
||||||
}
|
|
||||||
.card-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-alert {
|
|
||||||
--ha-alert-icon-size: 24px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
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: {},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -4,11 +4,11 @@ import { customElement, property, state } from "lit/decorators";
|
|||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import "../../../components/ha-button";
|
import "../../../components/ha-button";
|
||||||
|
import "../../../components/ha-dialog";
|
||||||
import "../../../components/ha-dialog-footer";
|
import "../../../components/ha-dialog-footer";
|
||||||
import "../../../components/ha-select";
|
import "../../../components/ha-select";
|
||||||
import type { HaSelectSelectEvent } from "../../../components/ha-select";
|
import type { HaSelectSelectEvent } from "../../../components/ha-select";
|
||||||
import "../../../components/ha-spinner";
|
import "../../../components/ha-spinner";
|
||||||
import "../../../components/ha-dialog";
|
|
||||||
import {
|
import {
|
||||||
extractApiErrorMessage,
|
extractApiErrorMessage,
|
||||||
ignoreSupervisorError,
|
ignoreSupervisorError,
|
||||||
@@ -79,7 +79,7 @@ class MoveDatadiskDialog extends LitElement {
|
|||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
await showAlertDialog(this, {
|
await showAlertDialog(this, {
|
||||||
title: this.hass.localize(
|
title: this.hass.localize(
|
||||||
"ui.panel.config.hardware.available_hardware.failed_to_get"
|
"ui.panel.config.hardware.system_hardware.failed_to_get"
|
||||||
),
|
),
|
||||||
text: extractApiErrorMessage(err),
|
text: extractApiErrorMessage(err),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1489,7 +1489,7 @@
|
|||||||
"network": "[%key:ui::panel::config::network::caption%]",
|
"network": "[%key:ui::panel::config::network::caption%]",
|
||||||
"updates": "[%key:ui::panel::config::updates::caption%]",
|
"updates": "[%key:ui::panel::config::updates::caption%]",
|
||||||
"repairs": "[%key:ui::panel::config::repairs::caption%]",
|
"repairs": "[%key:ui::panel::config::repairs::caption%]",
|
||||||
"hardware": "[%key:ui::panel::config::hardware::caption%]",
|
"hardware": "Hardware",
|
||||||
"storage": "[%key:ui::panel::config::storage::caption%]",
|
"storage": "[%key:ui::panel::config::storage::caption%]",
|
||||||
"general": "[%key:ui::panel::config::core::caption%]",
|
"general": "[%key:ui::panel::config::core::caption%]",
|
||||||
"backups": "[%key:ui::panel::config::backup::caption%]",
|
"backups": "[%key:ui::panel::config::backup::caption%]",
|
||||||
@@ -4222,12 +4222,13 @@
|
|||||||
"invalid_url": "Invalid URL"
|
"invalid_url": "Invalid URL"
|
||||||
},
|
},
|
||||||
"hardware": {
|
"hardware": {
|
||||||
"caption": "Hardware",
|
"overview": "Overview",
|
||||||
"description": "Configure your hub and connected hardware",
|
"description": "Configure your hub and connected hardware",
|
||||||
"available_hardware": {
|
"system_hardware": {
|
||||||
"failed_to_get": "Failed to get available hardware",
|
"failed_to_get": "Failed to get available hardware",
|
||||||
"title": "All hardware",
|
"title": "System hardware",
|
||||||
"search": "Search hardware",
|
"search": "Search hardware",
|
||||||
|
"name": "Name",
|
||||||
"subsystem": "Subsystem",
|
"subsystem": "Subsystem",
|
||||||
"device_path": "Device path",
|
"device_path": "Device path",
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
|
|||||||
Reference in New Issue
Block a user