Supervisor network changes (#7676)

This commit is contained in:
Joakim Sørensen 2020-11-19 22:51:29 +01:00 committed by GitHub
parent 0b896ddfb1
commit c409ba149d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 428 additions and 116 deletions

View File

@ -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<HassioNetworkDialogParams> {
@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<void> {
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._interfaces = params.network.interfaces.sort((a, b) => {
return a.primary > b.primary ? -1 : 1;
});
this._device = this._network[this._curTabIndex];
this._device.data.nameservers = String(this._device.data.nameservers);
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
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
</ha-header-bar>
${this._network.length > 1
${this._interfaces.length > 1
? html` <mwc-tab-bar
.activeIndex=${this._curTabIndex}
@MDCTabBar:activated=${this._handleTabActivated}
>${this._network.map(
>${this._interfaces.map(
(device) =>
html`<mwc-tab
.id=${device.interface}
@ -129,81 +132,296 @@ export class DialogHassioNetwork extends LitElement
private _renderTab() {
return html` <div class="form container">
${IP_VERSIONS.map((version) =>
this._interface![version] ? this._renderIPConfiguration(version) : ""
)}
${this._interface?.type === "wireless"
? html`
<ha-expansion-panel outlined>
<span slot="title">Wi-Fi</span>
${this._interface?.wifi?.ssid
? html`<p>Connected to: ${this._interface?.wifi?.ssid}</p>`
: ""}
<mwc-button
class="scan"
@click=${this._scanForAP}
.disabled=${this._scanning}
>
${this._scanning
? html`<ha-circular-progress active size="small">
</ha-circular-progress>`
: "Scan for accesspoints"}
</mwc-button>
${this._accessPoints &&
this._accessPoints.accesspoints &&
this._accessPoints.accesspoints.length !== 0
? html`
<mwc-list>
${this._accessPoints.accesspoints
.filter((ap) => ap.ssid)
.map(
(ap) =>
html`
<mwc-list-item
twoline
@click=${this._selectAP}
.activated=${ap.ssid ===
this._wifiConfiguration?.ssid}
.ap=${ap}
>
<span>${ap.ssid}</span>
<span slot="secondary">
${ap.mac} - Strength: ${ap.signal}
</span>
</mwc-list-item>
`
)}
</mwc-list>
`
: ""}
${this._wifiConfiguration
? html`
<div class="radio-row">
<ha-formfield label="open">
<ha-radio
@change=${this._handleRadioValueChangedAp}
.ap=${this._wifiConfiguration}
value="open"
name="auth"
.checked=${this._wifiConfiguration.auth ===
undefined ||
this._wifiConfiguration.auth === "open"}
>
</ha-radio>
</ha-formfield>
<ha-formfield label="wep">
<ha-radio
@change=${this._handleRadioValueChangedAp}
.ap=${this._wifiConfiguration}
value="wep"
name="auth"
.checked=${this._wifiConfiguration.auth === "wep"}
>
</ha-radio>
</ha-formfield>
<ha-formfield label="wpa-psk">
<ha-radio
@change=${this._handleRadioValueChangedAp}
.ap=${this._wifiConfiguration}
value="wpa-psk"
name="auth"
.checked=${this._wifiConfiguration.auth ===
"wpa-psk"}
>
</ha-radio>
</ha-formfield>
</div>
${this._wifiConfiguration.auth === "wpa-psk" ||
this._wifiConfiguration.auth === "wep"
? html`
<paper-input
class="flex-auto"
type="password"
id="psk"
label="Password"
version="wifi"
@value-changed=${this
._handleInputValueChangedWifi}
>
</paper-input>
`
: ""}
`
: ""}
</ha-expansion-panel>
`
: ""}
${this._dirty
? html`<div class="warning">
If you are changing the Wi-Fi, IP or gateway addresses, you might
lose the connection!
</div>`
: ""}
</div>
<div class="buttons">
<mwc-button label="close" @click=${this.closeDialog}> </mwc-button>
<mwc-button @click=${this._updateNetwork} .disabled=${!this._dirty}>
${this._processing
? html`<ha-circular-progress active size="small">
</ha-circular-progress>`
: "Save"}
</mwc-button>
</div>`;
}
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) {
showAlertDialog(this, {
title: "Failed to scan for accesspoints",
text: extractApiErrorMessage(err),
});
} finally {
this._scanning = false;
}
}
private _renderIPConfiguration(version: string) {
return html`
<ha-expansion-panel outlined>
<span slot="title">IPv${version.charAt(version.length - 1)}</span>
<div class="radio-row">
<ha-formfield label="DHCP">
<ha-radio
@change=${this._handleRadioValueChanged}
value="dhcp"
name="method"
?checked=${this._device!.data.method === "dhcp"}
.version=${version}
value="auto"
name="${version}method"
.checked=${this._interface![version]?.method === "auto"}
>
</ha-radio>
</ha-formfield>
<ha-formfield label="Static">
<ha-radio
@change=${this._handleRadioValueChanged}
.version=${version}
value="static"
name="method"
?checked=${this._device!.data.method === "static"}
name="${version}method"
.checked=${this._interface![version]?.method === "static"}
>
</ha-radio>
</ha-formfield>
${this._device!.data.method !== "dhcp"
? html` <paper-input
<ha-formfield label="Disabled" class="warning">
<ha-radio
@change=${this._handleRadioValueChanged}
.version=${version}
value="disabled"
name="${version}method"
.checked=${this._interface![version]?.method === "disabled"}
>
</ha-radio>
</ha-formfield>
</div>
${this._interface![version].method === "static"
? html`
<paper-input
class="flex-auto"
id="ip_address"
id="address"
label="IP address/Netmask"
.value="${this._device!.data.ip_address}"
.version=${version}
.value=${this._toString(this._interface![version].address)}
@value-changed=${this._handleInputValueChanged}
></paper-input>
>
</paper-input>
<paper-input
class="flex-auto"
id="gateway"
label="Gateway address"
.value="${this._device!.data.gateway}"
.version=${version}
.value=${this._interface![version].gateway}
@value-changed=${this._handleInputValueChanged}
></paper-input>
>
</paper-input>
<paper-input
class="flex-auto"
id="nameservers"
label="DNS servers"
.value="${this._device!.data.nameservers as string}"
.version=${version}
.value=${this._toString(this._interface![version].nameservers)}
@value-changed=${this._handleInputValueChanged}
></paper-input>
NB!: If you are changing IP or gateway addresses, you might lose
the connection.`
>
</paper-input>
`
: ""}
</div>
<div class="buttons">
<mwc-button label="close" @click=${this.closeDialog}> </mwc-button>
<mwc-button @click=${this._updateNetwork} ?disabled=${!this._dirty}>
${this._prosessing
? html`<ha-circular-progress active></ha-circular-progress>`
: "Update"}
</mwc-button>
</div>`;
</ha-expansion-panel>
`;
}
_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._prosessing = true;
let options: Partial<NetworkInterface> = {
method: this._device!.data.method,
this._processing = true;
let interfaceOptions: Partial<NetworkInterface> = {};
IP_VERSIONS.forEach((version) => {
interfaceOptions[version] = {
method: this._interface![version]?.method || "auto",
};
if (options.method !== "dhcp") {
options = {
...options,
address: this._device!.data.ip_address,
gateway: this._device!.data.gateway,
dns: String(this._device!.data.nameservers).split(","),
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._device!.interface, options);
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;
}
`,
];
}

View File

@ -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<ActionDetail>) {

View File

@ -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<IpConfiguration>;
ipv6?: Partial<IpConfiguration>;
type: "ethernet" | "wireless" | "vlan";
wifi?: Partial<WifiConfiguration>;
}
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<HassioResponse<AccessPoints>>(
"GET",
`hassio/network/interface/${network_interface}/accesspoints`
)
);
};