From bfa7bccfa68f226f5a2322880e541ffbb2c7b470 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 25 Apr 2022 16:21:03 +0200 Subject: [PATCH] Add supervisor network interface settings (#12403) --- src/panels/config/ha-panel-config.ts | 2 +- .../{core => network}/ha-config-network.ts | 0 .../ha-config-section-network.ts | 10 +- .../{core => network}/ha-config-url-form.ts | 0 .../config/network/supervisor-network.ts | 577 ++++++++++++++++++ src/translations/en.json | 19 +- 6 files changed, 604 insertions(+), 4 deletions(-) rename src/panels/config/{core => network}/ha-config-network.ts (100%) rename src/panels/config/{core => network}/ha-config-section-network.ts (79%) rename src/panels/config/{core => network}/ha-config-url-form.ts (100%) create mode 100644 src/panels/config/network/supervisor-network.ts diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts index 315c7d192c..df915c398d 100644 --- a/src/panels/config/ha-panel-config.ts +++ b/src/panels/config/ha-panel-config.ts @@ -407,7 +407,7 @@ class HaPanelConfig extends HassRouterPage { }, network: { tag: "ha-config-section-network", - load: () => import("./core/ha-config-section-network"), + load: () => import("./network/ha-config-section-network"), }, person: { tag: "ha-config-person", diff --git a/src/panels/config/core/ha-config-network.ts b/src/panels/config/network/ha-config-network.ts similarity index 100% rename from src/panels/config/core/ha-config-network.ts rename to src/panels/config/network/ha-config-network.ts diff --git a/src/panels/config/core/ha-config-section-network.ts b/src/panels/config/network/ha-config-section-network.ts similarity index 79% rename from src/panels/config/core/ha-config-section-network.ts rename to src/panels/config/network/ha-config-section-network.ts index 7f87ae1d6c..daf8b15fc4 100644 --- a/src/panels/config/core/ha-config-section-network.ts +++ b/src/panels/config/network/ha-config-section-network.ts @@ -1,9 +1,11 @@ import { css, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; +import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import "../../../layouts/hass-subpage"; import type { HomeAssistant, Route } from "../../../types"; import "./ha-config-network"; import "./ha-config-url-form"; +import "./supervisor-network"; @customElement("ha-config-section-network") class HaConfigSectionNetwork extends LitElement { @@ -22,6 +24,9 @@ class HaConfigSectionNetwork extends LitElement { .header=${this.hass.localize("ui.panel.config.network.caption")} >
+ ${isComponentLoaded(this.hass, "hassio") + ? html`` + : ""}
@@ -35,9 +40,10 @@ class HaConfigSectionNetwork extends LitElement { max-width: 1040px; margin: 0 auto; } - ha-config-network { + supervisor-network, + ha-config-url-form { display: block; - margin-top: 24px; + margin-bottom: 24px; } `; } diff --git a/src/panels/config/core/ha-config-url-form.ts b/src/panels/config/network/ha-config-url-form.ts similarity index 100% rename from src/panels/config/core/ha-config-url-form.ts rename to src/panels/config/network/ha-config-url-form.ts diff --git a/src/panels/config/network/supervisor-network.ts b/src/panels/config/network/supervisor-network.ts new file mode 100644 index 0000000000..5621a666e6 --- /dev/null +++ b/src/panels/config/network/supervisor-network.ts @@ -0,0 +1,577 @@ +import "@material/mwc-button/mwc-button"; +import "@material/mwc-list/mwc-list"; +import "@material/mwc-list/mwc-list-item"; +import "@material/mwc-tab"; +import "@material/mwc-tab-bar"; +import { PaperInputElement } from "@polymer/paper-input/paper-input"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { cache } from "lit/directives/cache"; +import "../../../components/ha-alert"; +import "../../../components/ha-circular-progress"; +import "../../../components/ha-expansion-panel"; +import "../../../components/ha-formfield"; +import "../../../components/ha-header-bar"; +import "../../../components/ha-icon-button"; +import "../../../components/ha-radio"; +import "../../../components/ha-related-items"; +import { extractApiErrorMessage } from "../../../data/hassio/common"; +import { + AccessPoints, + accesspointScan, + fetchNetworkInfo, + NetworkInterface, + updateNetworkInterface, + WifiConfiguration, +} from "../../../data/hassio/network"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../../../dialogs/generic/show-dialog-box"; +import type { HomeAssistant } from "../../../types"; +import "../../../components/ha-card"; + +const IP_VERSIONS = ["ipv4", "ipv6"]; + +@customElement("supervisor-network") +export class HassioNetwork extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _accessPoints?: AccessPoints; + + @state() private _curTabIndex = 0; + + @state() private _dirty = false; + + @state() private _interface?: NetworkInterface; + + @state() private _interfaces!: NetworkInterface[]; + + @state() private _processing = false; + + @state() private _scanning = false; + + @state() private _wifiConfiguration?: WifiConfiguration; + + protected firstUpdated() { + this._fetchNetworkInfo(); + } + + private async _fetchNetworkInfo() { + const network = await fetchNetworkInfo(this.hass); + this._interfaces = network.interfaces.sort((a, b) => + a.primary > b.primary ? -1 : 1 + ); + this._interface = { ...this._interfaces[this._curTabIndex] }; + } + + protected render(): TemplateResult { + if (!this._interface) { + return html``; + } + + return html` + + ${this._interfaces.length > 1 + ? html`${this._interfaces.map( + (device) => + html` + ` + )} + ` + : ""} + ${cache(this._renderTab())} + + `; + } + + private _renderTab() { + return html`
+ ${IP_VERSIONS.map((version) => + this._interface![version] ? this._renderIPConfiguration(version) : "" + )} + ${this._interface?.type === "wireless" + ? html` + + ${this._interface?.wifi?.ssid + ? html`

+ ${this.hass.localize( + "ui.panel.config.network.supervisor.connected_to", + "ssid", + this._interface?.wifi?.ssid + )} +

` + : ""} + + ${this._scanning + ? html` + ` + : this.hass.localize( + "ui.panel.config.network.supervisor.scan_ap" + )} + + ${this._accessPoints && + this._accessPoints.accesspoints && + this._accessPoints.accesspoints.length !== 0 + ? html` + + ${this._accessPoints.accesspoints + .filter((ap) => ap.ssid) + .map( + (ap) => + html` + + ${ap.ssid} + + ${ap.mac} - Strength: ${ap.signal} + + + ` + )} + + ` + : ""} + ${this._wifiConfiguration + ? html` +
+ + + + + + + + + + + + +
+ ${this._wifiConfiguration.auth === "wpa-psk" || + this._wifiConfiguration.auth === "wep" + ? html` + + + ` + : ""} + ` + : ""} +
+ ` + : ""} + ${this._dirty + ? html` + ${this.hass.localize( + "ui.panel.config.network.supervisor.warning" + )} + ` + : ""} +
+
+ + ${this._processing + ? html` + ` + : this.hass.localize("ui.common.save")} + +
`; + } + + private _selectAP(event) { + this._wifiConfiguration = event.currentTarget.ap; + this._dirty = true; + } + + private async _scanForAP() { + if (!this._interface) { + return; + } + this._scanning = true; + try { + this._accessPoints = await accesspointScan( + this.hass, + this._interface.interface + ); + } catch (err: any) { + showAlertDialog(this, { + title: "Failed to scan for accesspoints", + text: extractApiErrorMessage(err), + }); + } finally { + this._scanning = false; + } + } + + private _renderIPConfiguration(version: string) { + return html` + +
+ + + + + + + + + + + + +
+ ${this._interface![version].method === "static" + ? html` + + + + + + + ` + : ""} +
+ `; + } + + _toArray(data: string | string[]): string[] { + if (Array.isArray(data)) { + if (data && typeof data[0] === "string") { + data = data[0]; + } + } + if (!data) { + return []; + } + if (typeof data === "string") { + return data.replace(/ /g, "").split(","); + } + return data; + } + + _toString(data: string | string[]): string { + if (!data) { + return ""; + } + if (Array.isArray(data)) { + return data.join(", "); + } + return data; + } + + private async _updateNetwork() { + this._processing = true; + let interfaceOptions: Partial = {}; + + IP_VERSIONS.forEach((version) => { + interfaceOptions[version] = { + method: this._interface![version]?.method || "auto", + }; + if (this._interface![version]?.method === "static") { + interfaceOptions[version] = { + ...interfaceOptions[version], + address: this._toArray(this._interface![version]?.address), + gateway: this._interface![version]?.gateway, + nameservers: this._toArray(this._interface![version]?.nameservers), + }; + } + }); + + if (this._wifiConfiguration) { + interfaceOptions = { + ...interfaceOptions, + wifi: { + ssid: this._wifiConfiguration.ssid, + mode: this._wifiConfiguration.mode, + auth: this._wifiConfiguration.auth || "open", + }, + }; + if (interfaceOptions.wifi!.auth !== "open") { + interfaceOptions.wifi = { + ...interfaceOptions.wifi, + psk: this._wifiConfiguration.psk, + }; + } + } + + interfaceOptions.enabled = + this._wifiConfiguration !== undefined || + interfaceOptions.ipv4?.method !== "disabled" || + interfaceOptions.ipv6?.method !== "disabled"; + + try { + await updateNetworkInterface( + this.hass, + this._interface!.interface, + interfaceOptions + ); + this._dirty = false; + } catch (err: any) { + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.network.supervisor.failed_to_change" + ), + text: extractApiErrorMessage(err), + }); + } finally { + this._processing = false; + } + } + + private async _handleTabActivated(ev: CustomEvent): Promise { + if (this._dirty) { + const confirm = await showConfirmationDialog(this, { + text: this.hass.localize("ui.panel.config.network.supervisor.unsaved"), + confirmText: this.hass.localize("ui.common.yes"), + dismissText: this.hass.localize("ui.common.no"), + }); + if (!confirm) { + this.requestUpdate("_interface"); + return; + } + } + this._curTabIndex = ev.detail.index; + this._interface = { ...this._interfaces[ev.detail.index] }; + } + + private _handleRadioValueChanged(ev: CustomEvent): void { + const value = (ev.target as any).value as "disabled" | "auto" | "static"; + const version = (ev.target as any).version as "ipv4" | "ipv6"; + + if ( + !value || + !this._interface || + this._interface[version]!.method === value + ) { + return; + } + this._dirty = true; + + this._interface[version]!.method = value; + this.requestUpdate("_interface"); + } + + private _handleRadioValueChangedAp(ev: CustomEvent): void { + const value = (ev.target as any).value as string as + | "open" + | "wep" + | "wpa-psk"; + this._wifiConfiguration!.auth = value; + this._dirty = true; + this.requestUpdate("_wifiConfiguration"); + } + + private _handleInputValueChanged(ev: CustomEvent): void { + const value: string | null | undefined = (ev.target as PaperInputElement) + .value; + const version = (ev.target as any).version as "ipv4" | "ipv6"; + const id = (ev.target as PaperInputElement).id; + + if ( + !value || + !this._interface || + this._toString(this._interface[version]![id]) === this._toString(value) + ) { + return; + } + + this._dirty = true; + this._interface[version]![id] = value; + } + + private _handleInputValueChangedWifi(ev: CustomEvent): void { + const value: string | null | undefined = (ev.target as PaperInputElement) + .value; + const id = (ev.target as PaperInputElement).id; + + if ( + !value || + !this._wifiConfiguration || + this._wifiConfiguration![id] === value + ) { + return; + } + this._dirty = true; + this._wifiConfiguration![id] = value; + } + + static get styles(): CSSResultGroup { + return [ + css` + ha-header-bar { + --mdc-theme-on-primary: var(--primary-text-color); + --mdc-theme-primary: var(--mdc-theme-surface); + flex-shrink: 0; + } + + mwc-tab-bar { + border-bottom: 1px solid + var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12)); + margin-bottom: 24px; + } + + .content { + display: block; + padding: 20px 24px; + } + + mwc-button.warning { + --mdc-theme-primary: var(--error-color); + } + + mwc-button.scan { + margin-left: 8px; + } + + :host([rtl]) app-toolbar { + direction: rtl; + text-align: right; + } + ha-expansion-panel { + --expansion-panel-summary-padding: 0 16px; + margin: 4px 0; + } + paper-input { + padding: 0 14px; + } + mwc-list-item { + --mdc-list-side-padding: 10px; + } + .card-actions { + display: flex; + flex-direction: row-reverse; + justify-content: space-between; + align-items: center; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "supervisor-network": HassioNetwork; + } +} diff --git a/src/translations/en.json b/src/translations/en.json index c7fe10dbd8..a24bccef9f 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3108,7 +3108,24 @@ "caption": "Analytics" }, "network": { - "caption": "Network" + "caption": "Network", + "supervisor": { + "title": "Configure network interfaces", + "connected_to": "Connected to {ssid}", + "scan_ap": "Scan for access points", + "open": "Open", + "wep": "WEP", + "wpa": "wpa-psk", + "warning": "If you are changing the Wi-Fi, IP or gateway addresses, you might lose the connection!", + "static": "Static", + "dhcp": "DHCP", + "disabled": "Disabled", + "ip_netmask": "IP address/Netmask", + "gateway": "Gateway address", + "dns_servers": "DNS Servers", + "unsaved": "You have unsaved changes, these will get lost if you change tabs, do you want to continue?", + "failed_to_change": "Failed to change network settings" + } }, "storage": { "caption": "Storage",