mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-02 00:27:49 +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>;
|
||||
by_id: null | string;
|
||||
dev_path: string;
|
||||
|
||||
@@ -73,7 +73,6 @@ class DialogBox extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
type=${confirmPrompt ? "alert" : "standard"}
|
||||
?prevent-scrim-close=${confirmPrompt}
|
||||
@@ -104,6 +103,9 @@ class DialogBox extends LitElement {
|
||||
: nothing}
|
||||
${dialogTitle}
|
||||
</span>
|
||||
${this._params.subtitle
|
||||
? html`<span slot="subtitle">${this._params.subtitle}</span>`
|
||||
: nothing}
|
||||
</ha-dialog-header>
|
||||
<div id="dialog-box-description">
|
||||
${this._params.text ? html` <p>${this._params.text}</p> ` : ""}
|
||||
|
||||
@@ -5,6 +5,7 @@ interface BaseDialogBoxParams {
|
||||
confirmText?: string;
|
||||
text?: string | TemplateResult;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
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 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 { mdiFormatListBulletedType, mdiMemory } from "@mdi/js";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
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-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 { RouterOptions } from "../../../layouts/hass-router-page";
|
||||
import { HassRouterPage } from "../../../layouts/hass-router-page";
|
||||
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
|
||||
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 = {
|
||||
type: "line",
|
||||
color: DefaultPrimaryColor,
|
||||
areaStyle: {
|
||||
color: DefaultPrimaryColor + "2B",
|
||||
},
|
||||
symbolSize: 0,
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
},
|
||||
smooth: 0.25,
|
||||
if (isComponentLoaded(hass, "hassio")) {
|
||||
tabs.push({
|
||||
path: "/config/hardware/all",
|
||||
translationKey: "ui.panel.config.hardware.system_hardware.title",
|
||||
iconPath: mdiFormatListBulletedType,
|
||||
});
|
||||
}
|
||||
|
||||
return tabs;
|
||||
};
|
||||
|
||||
@customElement("ha-config-hardware")
|
||||
class HaConfigHardware extends SubscribeMixin(LitElement) {
|
||||
class HaConfigHardware extends HassRouterPage {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@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;
|
||||
|
||||
@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 updatePageEl(pageEl) {
|
||||
pageEl.hass = this.hass;
|
||||
pageEl.narrow = this.narrow;
|
||||
pageEl.route = this.routeTail;
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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 { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-dialog-footer";
|
||||
import "../../../components/ha-select";
|
||||
import type { HaSelectSelectEvent } from "../../../components/ha-select";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/ha-dialog";
|
||||
import {
|
||||
extractApiErrorMessage,
|
||||
ignoreSupervisorError,
|
||||
@@ -79,7 +79,7 @@ class MoveDatadiskDialog extends LitElement {
|
||||
this.closeDialog();
|
||||
await showAlertDialog(this, {
|
||||
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),
|
||||
});
|
||||
|
||||
@@ -1489,7 +1489,7 @@
|
||||
"network": "[%key:ui::panel::config::network::caption%]",
|
||||
"updates": "[%key:ui::panel::config::updates::caption%]",
|
||||
"repairs": "[%key:ui::panel::config::repairs::caption%]",
|
||||
"hardware": "[%key:ui::panel::config::hardware::caption%]",
|
||||
"hardware": "Hardware",
|
||||
"storage": "[%key:ui::panel::config::storage::caption%]",
|
||||
"general": "[%key:ui::panel::config::core::caption%]",
|
||||
"backups": "[%key:ui::panel::config::backup::caption%]",
|
||||
@@ -4222,12 +4222,13 @@
|
||||
"invalid_url": "Invalid URL"
|
||||
},
|
||||
"hardware": {
|
||||
"caption": "Hardware",
|
||||
"overview": "Overview",
|
||||
"description": "Configure your hub and connected hardware",
|
||||
"available_hardware": {
|
||||
"system_hardware": {
|
||||
"failed_to_get": "Failed to get available hardware",
|
||||
"title": "All hardware",
|
||||
"title": "System hardware",
|
||||
"search": "Search hardware",
|
||||
"name": "Name",
|
||||
"subsystem": "Subsystem",
|
||||
"device_path": "Device path",
|
||||
"id": "ID",
|
||||
|
||||
Reference in New Issue
Block a user