From c409ba149dbd6314f584daa7caed659a5d3ed26a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 19 Nov 2020 22:51:29 +0100 Subject: [PATCH] Supervisor network changes (#7676) --- .../dialogs/network/dialog-hassio-network.ts | 470 ++++++++++++++---- hassio/src/system/hassio-host-info.ts | 8 +- src/data/hassio/network.ts | 66 ++- 3 files changed, 428 insertions(+), 116 deletions(-) diff --git a/hassio/src/dialogs/network/dialog-hassio-network.ts b/hassio/src/dialogs/network/dialog-hassio-network.ts index 8569b8bc60..c019bc0032 100644 --- a/hassio/src/dialogs/network/dialog-hassio-network.ts +++ b/hassio/src/dialogs/network/dialog-hassio-network.ts @@ -1,5 +1,7 @@ import "@material/mwc-button/mwc-button"; import "@material/mwc-icon-button"; +import "@material/mwc-list/mwc-list"; +import "@material/mwc-list/mwc-list-item"; import "@material/mwc-tab"; import "@material/mwc-tab-bar"; import { mdiClose } from "@mdi/js"; @@ -16,18 +18,22 @@ import { } from "lit-element"; import { cache } from "lit-html/directives/cache"; import { fireEvent } from "../../../../src/common/dom/fire_event"; +import "../../../../src/components/ha-chips"; import "../../../../src/components/ha-circular-progress"; import "../../../../src/components/ha-dialog"; +import "../../../../src/components/ha-expansion-panel"; import "../../../../src/components/ha-formfield"; import "../../../../src/components/ha-header-bar"; import "../../../../src/components/ha-radio"; -import type { HaRadio } from "../../../../src/components/ha-radio"; import "../../../../src/components/ha-related-items"; import "../../../../src/components/ha-svg-icon"; import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; import { + AccessPoints, + accesspointScan, NetworkInterface, updateNetworkInterface, + WifiConfiguration, } from "../../../../src/data/hassio/network"; import { showAlertDialog, @@ -38,54 +44,51 @@ import { haStyleDialog } from "../../../../src/resources/styles"; import type { HomeAssistant } from "../../../../src/types"; import { HassioNetworkDialogParams } from "./show-dialog-network"; +const IP_VERSIONS = ["ipv4", "ipv6"]; + @customElement("dialog-hassio-network") export class DialogHassioNetwork extends LitElement implements HassDialog { @property({ attribute: false }) public hass!: HomeAssistant; - @internalProperty() private _prosessing = false; - - @internalProperty() private _params?: HassioNetworkDialogParams; - - @internalProperty() private _network!: { - interface: string; - data: NetworkInterface; - }[]; + @internalProperty() private _accessPoints?: AccessPoints; @internalProperty() private _curTabIndex = 0; - @internalProperty() private _device?: { - interface: string; - data: NetworkInterface; - }; - @internalProperty() private _dirty = false; + @internalProperty() private _interface?: NetworkInterface; + + @internalProperty() private _interfaces!: NetworkInterface[]; + + @internalProperty() private _params?: HassioNetworkDialogParams; + + @internalProperty() private _processing = false; + + @internalProperty() private _scanning = false; + + @internalProperty() private _wifiConfiguration?: WifiConfiguration; + public async showDialog(params: HassioNetworkDialogParams): Promise { this._params = params; this._dirty = false; this._curTabIndex = 0; - this._network = Object.keys(params.network?.interfaces) - .map((device) => ({ - interface: device, - data: params.network.interfaces[device], - })) - .sort((a, b) => { - return a.data.primary > b.data.primary ? -1 : 1; - }); - this._device = this._network[this._curTabIndex]; - this._device.data.nameservers = String(this._device.data.nameservers); + this._interfaces = params.network.interfaces.sort((a, b) => { + return a.primary > b.primary ? -1 : 1; + }); + this._interface = { ...this._interfaces[this._curTabIndex] }; + await this.updateComplete; } public closeDialog(): void { this._params = undefined; - this._prosessing = false; + this._processing = false; fireEvent(this, "dialog-closed", { dialog: this.localName }); } protected render(): TemplateResult { - if (!this._params || !this._network) { + if (!this._params || !this._interface) { return html``; } @@ -107,11 +110,11 @@ export class DialogHassioNetwork extends LitElement - ${this._network.length > 1 + ${this._interfaces.length > 1 ? html` ${this._network.map( + >${this._interfaces.map( (device) => html` - - - - - - - - - ${this._device!.data.method !== "dhcp" - ? html` - - - NB!: If you are changing IP or gateway addresses, you might lose - the connection.` + ${IP_VERSIONS.map((version) => + this._interface![version] ? this._renderIPConfiguration(version) : "" + )} + ${this._interface?.type === "wireless" + ? html` + + Wi-Fi + ${this._interface?.wifi?.ssid + ? html`

Connected to: ${this._interface?.wifi?.ssid}

` + : ""} + + ${this._scanning + ? html` + ` + : "Scan for accesspoints"} + + ${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`
+ If you are changing the Wi-Fi, IP or gateway addresses, you might + lose the connection! +
` : ""}
- - ${this._prosessing - ? html`` - : "Update"} + + ${this._processing + ? html` + ` + : "Save"}
`; } - private async _updateNetwork() { - this._prosessing = true; - let options: Partial = { - method: this._device!.data.method, - }; - if (options.method !== "dhcp") { - options = { - ...options, - address: this._device!.data.ip_address, - gateway: this._device!.data.gateway, - dns: String(this._device!.data.nameservers).split(","), - }; + private _selectAP(event) { + this._wifiConfiguration = event.currentTarget.ap; + this._dirty = true; + } + + private async _scanForAP() { + if (!this._interface) { + return; } + this._scanning = true; try { - await updateNetworkInterface(this.hass, this._device!.interface, options); + this._accessPoints = await accesspointScan( + this.hass, + this._interface.interface + ); + } catch (err) { + showAlertDialog(this, { + title: "Failed to scan for accesspoints", + text: extractApiErrorMessage(err), + }); + } finally { + this._scanning = false; + } + } + + private _renderIPConfiguration(version: string) { + return html` + + IPv${version.charAt(version.length - 1)} +
+ + + + + + + + + + + + +
+ ${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, + enabled: true, + 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, + }; + } + } + }); + + try { + await updateNetworkInterface( + this.hass, + this._interface!.interface, + interfaceOptions + ); } catch (err) { showAlertDialog(this, { title: "Failed to change network settings", text: extractApiErrorMessage(err), }); - this._prosessing = false; + this._processing = false; return; } this._params?.loadData(); @@ -219,40 +437,73 @@ export class DialogHassioNetwork extends LitElement dismissText: "no", }); if (!confirm) { - this.requestUpdate("_device"); + this.requestUpdate("_interface"); return; } } this._curTabIndex = ev.detail.index; - this._device = this._network[ev.detail.index]; - this._device.data.nameservers = String(this._device.data.nameservers); + this._interface = { ...this._interfaces[ev.detail.index] }; } private _handleRadioValueChanged(ev: CustomEvent): void { - const value = (ev.target as HaRadio).value as "dhcp" | "static"; + const value = (ev.target as any).value as "disabled" | "auto" | "static"; + const version = (ev.target as any).version as "ipv4" | "ipv6"; - if (!value || !this._device || this._device!.data.method === value) { + if ( + !value || + !this._interface || + this._interface[version]!.method === value + ) { return; } - this._dirty = true; - this._device!.data.method = value; - this.requestUpdate("_device"); + 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._device || this._device.data[id] === value) { + if ( + !value || + !this._interface || + this._toString(this._interface[version]![id]) === this._toString(value) + ) { return; } this._dirty = true; + this._interface[version]![id] = value; + } - this._device.data[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(): CSSResult[] { @@ -299,12 +550,16 @@ export class DialogHassioNetwork extends LitElement --mdc-theme-primary: var(--error-color); } + mwc-button.scan { + margin-left: 8px; + } + :host([rtl]) app-toolbar { direction: rtl; text-align: right; } .container { - padding: 20px 24px; + padding: 0 8px 4px; } .form { margin-bottom: 53px; @@ -322,6 +577,23 @@ export class DialogHassioNetwork extends LitElement padding-bottom: max(env(safe-area-inset-bottom), 8px); background-color: var(--mdc-theme-surface, #fff); } + .warning { + color: var(--error-color); + --primary-color: var(--error-color); + } + div.warning { + margin: 12px 4px -12px; + } + + ha-expansion-panel { + margin: 4px 0; + } + paper-input { + padding: 0 14px; + } + mwc-list-item { + --mdc-list-side-padding: 10px; + } `, ]; } diff --git a/hassio/src/system/hassio-host-info.ts b/hassio/src/system/hassio-host-info.ts index 5a2c5776bc..0f96ae0ace 100644 --- a/hassio/src/system/hassio-host-info.ts +++ b/hassio/src/system/hassio-host-info.ts @@ -53,7 +53,7 @@ import { hassioStyle } from "../resources/hassio-style"; class HassioHostInfo extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public hostInfo!: HassioHostInfoType; + @property({ attribute: false }) public hostInfo!: HassioHostInfoType; @property({ attribute: false }) public hassioInfo!: HassioInfo; @@ -193,12 +193,10 @@ class HassioHostInfo extends LitElement { } private _primaryIpAddress = memoizeOne((network_info: NetworkInfo) => { - if (!network_info) { + if (!network_info || !network_info.interfaces) { return ""; } - return Object.keys(network_info?.interfaces) - .map((device) => network_info.interfaces[device]) - .find((device) => device.primary)?.ip_address; + return network_info.interfaces.find((a) => a.primary)?.ipv4?.address![0]; }); private async _handleMenuAction(ev: CustomEvent) { diff --git a/src/data/hassio/network.ts b/src/data/hassio/network.ts index 5e2e7c20e1..542ee25d95 100644 --- a/src/data/hassio/network.ts +++ b/src/data/hassio/network.ts @@ -1,24 +1,54 @@ import { HomeAssistant } from "../../types"; import { hassioApiResultExtractor, HassioResponse } from "./common"; -export interface NetworkInterface { +interface IpConfiguration { + address: string[]; gateway: string; - id: string; - ip_address: string; - address?: string; - method: "static" | "dhcp"; - nameservers: string[] | string; - dns?: string[]; - primary: boolean; - type: string; + method: "disabled" | "static" | "auto"; + nameservers: string[]; } -export interface NetworkInterfaces { - [key: string]: NetworkInterface; +export interface NetworkInterface { + primary: boolean; + privacy: boolean; + interface: string; + enabled: boolean; + ipv4?: Partial; + ipv6?: Partial; + type: "ethernet" | "wireless" | "vlan"; + wifi?: Partial; +} + +interface DockerNetwork { + address: string; + dns: string; + gateway: string; + interface: string; +} + +interface AccessPoint { + mode: "infrastructure" | "mesh" | "adhoc" | "ap"; + ssid: string; + mac: string; + frequency: number; + signal: number; +} + +export interface AccessPoints { + accesspoints: AccessPoint[]; +} + +export interface WifiConfiguration { + mode: "infrastructure" | "mesh" | "adhoc" | "ap"; + auth: "open" | "wep" | "wpa-psk"; + ssid: string; + signal: number; + psk?: string; } export interface NetworkInfo { - interfaces: NetworkInterfaces; + interfaces: NetworkInterface[]; + docker: DockerNetwork; } export const fetchNetworkInfo = async (hass: HomeAssistant) => { @@ -41,3 +71,15 @@ export const updateNetworkInterface = async ( options ); }; + +export const accesspointScan = async ( + hass: HomeAssistant, + network_interface: string +) => { + return hassioApiResultExtractor( + await hass.callApi>( + "GET", + `hassio/network/interface/${network_interface}/accesspoints` + ) + ); +};