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-button/mwc-button";
import "@material/mwc-icon-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";
import "@material/mwc-tab-bar"; import "@material/mwc-tab-bar";
import { mdiClose } from "@mdi/js"; import { mdiClose } from "@mdi/js";
@ -16,18 +18,22 @@ import {
} from "lit-element"; } from "lit-element";
import { cache } from "lit-html/directives/cache"; import { cache } from "lit-html/directives/cache";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-chips";
import "../../../../src/components/ha-circular-progress"; import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog"; import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel";
import "../../../../src/components/ha-formfield"; import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-header-bar"; import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-radio"; import "../../../../src/components/ha-radio";
import type { HaRadio } from "../../../../src/components/ha-radio";
import "../../../../src/components/ha-related-items"; import "../../../../src/components/ha-related-items";
import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-svg-icon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { import {
AccessPoints,
accesspointScan,
NetworkInterface, NetworkInterface,
updateNetworkInterface, updateNetworkInterface,
WifiConfiguration,
} from "../../../../src/data/hassio/network"; } from "../../../../src/data/hassio/network";
import { import {
showAlertDialog, showAlertDialog,
@ -38,54 +44,51 @@ import { haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types"; import type { HomeAssistant } from "../../../../src/types";
import { HassioNetworkDialogParams } from "./show-dialog-network"; import { HassioNetworkDialogParams } from "./show-dialog-network";
const IP_VERSIONS = ["ipv4", "ipv6"];
@customElement("dialog-hassio-network") @customElement("dialog-hassio-network")
export class DialogHassioNetwork extends LitElement export class DialogHassioNetwork extends LitElement
implements HassDialog<HassioNetworkDialogParams> { implements HassDialog<HassioNetworkDialogParams> {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@internalProperty() private _prosessing = false; @internalProperty() private _accessPoints?: AccessPoints;
@internalProperty() private _params?: HassioNetworkDialogParams;
@internalProperty() private _network!: {
interface: string;
data: NetworkInterface;
}[];
@internalProperty() private _curTabIndex = 0; @internalProperty() private _curTabIndex = 0;
@internalProperty() private _device?: {
interface: string;
data: NetworkInterface;
};
@internalProperty() private _dirty = false; @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> { public async showDialog(params: HassioNetworkDialogParams): Promise<void> {
this._params = params; this._params = params;
this._dirty = false; this._dirty = false;
this._curTabIndex = 0; this._curTabIndex = 0;
this._network = Object.keys(params.network?.interfaces) this._interfaces = params.network.interfaces.sort((a, b) => {
.map((device) => ({ return a.primary > b.primary ? -1 : 1;
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._interface = { ...this._interfaces[this._curTabIndex] };
this._device.data.nameservers = String(this._device.data.nameservers);
await this.updateComplete; await this.updateComplete;
} }
public closeDialog(): void { public closeDialog(): void {
this._params = undefined; this._params = undefined;
this._prosessing = false; this._processing = false;
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._params || !this._network) { if (!this._params || !this._interface) {
return html``; return html``;
} }
@ -107,11 +110,11 @@ export class DialogHassioNetwork extends LitElement
<ha-svg-icon .path=${mdiClose}></ha-svg-icon> <ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button> </mwc-icon-button>
</ha-header-bar> </ha-header-bar>
${this._network.length > 1 ${this._interfaces.length > 1
? html` <mwc-tab-bar ? html` <mwc-tab-bar
.activeIndex=${this._curTabIndex} .activeIndex=${this._curTabIndex}
@MDCTabBar:activated=${this._handleTabActivated} @MDCTabBar:activated=${this._handleTabActivated}
>${this._network.map( >${this._interfaces.map(
(device) => (device) =>
html`<mwc-tab html`<mwc-tab
.id=${device.interface} .id=${device.interface}
@ -129,81 +132,296 @@ export class DialogHassioNetwork extends LitElement
private _renderTab() { private _renderTab() {
return html` <div class="form container"> 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-formfield label="DHCP">
<ha-radio <ha-radio
@change=${this._handleRadioValueChanged} @change=${this._handleRadioValueChanged}
value="dhcp" .version=${version}
name="method" value="auto"
?checked=${this._device!.data.method === "dhcp"} name="${version}method"
.checked=${this._interface![version]?.method === "auto"}
> >
</ha-radio> </ha-radio>
</ha-formfield> </ha-formfield>
<ha-formfield label="Static"> <ha-formfield label="Static">
<ha-radio <ha-radio
@change=${this._handleRadioValueChanged} @change=${this._handleRadioValueChanged}
.version=${version}
value="static" value="static"
name="method" name="${version}method"
?checked=${this._device!.data.method === "static"} .checked=${this._interface![version]?.method === "static"}
> >
</ha-radio> </ha-radio>
</ha-formfield> </ha-formfield>
${this._device!.data.method !== "dhcp" <ha-formfield label="Disabled" class="warning">
? html` <paper-input <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" class="flex-auto"
id="ip_address" id="address"
label="IP address/Netmask" label="IP address/Netmask"
.value="${this._device!.data.ip_address}" .version=${version}
.value=${this._toString(this._interface![version].address)}
@value-changed=${this._handleInputValueChanged} @value-changed=${this._handleInputValueChanged}
></paper-input> >
</paper-input>
<paper-input <paper-input
class="flex-auto" class="flex-auto"
id="gateway" id="gateway"
label="Gateway address" label="Gateway address"
.value="${this._device!.data.gateway}" .version=${version}
.value=${this._interface![version].gateway}
@value-changed=${this._handleInputValueChanged} @value-changed=${this._handleInputValueChanged}
></paper-input> >
</paper-input>
<paper-input <paper-input
class="flex-auto" class="flex-auto"
id="nameservers" id="nameservers"
label="DNS servers" label="DNS servers"
.value="${this._device!.data.nameservers as string}" .version=${version}
.value=${this._toString(this._interface![version].nameservers)}
@value-changed=${this._handleInputValueChanged} @value-changed=${this._handleInputValueChanged}
></paper-input> >
NB!: If you are changing IP or gateway addresses, you might lose </paper-input>
the connection.` `
: ""} : ""}
</div> </ha-expansion-panel>
<div class="buttons"> `;
<mwc-button label="close" @click=${this.closeDialog}> </mwc-button> }
<mwc-button @click=${this._updateNetwork} ?disabled=${!this._dirty}>
${this._prosessing _toArray(data: string | string[]): string[] {
? html`<ha-circular-progress active></ha-circular-progress>` if (Array.isArray(data)) {
: "Update"} if (data && typeof data[0] === "string") {
</mwc-button> data = data[0];
</div>`; }
}
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() { private async _updateNetwork() {
this._prosessing = true; this._processing = true;
let options: Partial<NetworkInterface> = { let interfaceOptions: Partial<NetworkInterface> = {};
method: this._device!.data.method,
IP_VERSIONS.forEach((version) => {
interfaceOptions[version] = {
method: this._interface![version]?.method || "auto",
}; };
if (options.method !== "dhcp") { if (this._interface![version]?.method === "static") {
options = { interfaceOptions[version] = {
...options, ...interfaceOptions[version],
address: this._device!.data.ip_address, address: this._toArray(this._interface![version]?.address),
gateway: this._device!.data.gateway, gateway: this._interface![version]?.gateway,
dns: String(this._device!.data.nameservers).split(","), 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 { try {
await updateNetworkInterface(this.hass, this._device!.interface, options); await updateNetworkInterface(
this.hass,
this._interface!.interface,
interfaceOptions
);
} catch (err) { } catch (err) {
showAlertDialog(this, { showAlertDialog(this, {
title: "Failed to change network settings", title: "Failed to change network settings",
text: extractApiErrorMessage(err), text: extractApiErrorMessage(err),
}); });
this._prosessing = false; this._processing = false;
return; return;
} }
this._params?.loadData(); this._params?.loadData();
@ -219,40 +437,73 @@ export class DialogHassioNetwork extends LitElement
dismissText: "no", dismissText: "no",
}); });
if (!confirm) { if (!confirm) {
this.requestUpdate("_device"); this.requestUpdate("_interface");
return; return;
} }
} }
this._curTabIndex = ev.detail.index; this._curTabIndex = ev.detail.index;
this._device = this._network[ev.detail.index]; this._interface = { ...this._interfaces[ev.detail.index] };
this._device.data.nameservers = String(this._device.data.nameservers);
} }
private _handleRadioValueChanged(ev: CustomEvent): void { 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; return;
} }
this._dirty = true; this._dirty = true;
this._device!.data.method = value; this._interface[version]!.method = value;
this.requestUpdate("_device"); 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 { private _handleInputValueChanged(ev: CustomEvent): void {
const value: string | null | undefined = (ev.target as PaperInputElement) const value: string | null | undefined = (ev.target as PaperInputElement)
.value; .value;
const version = (ev.target as any).version as "ipv4" | "ipv6";
const id = (ev.target as PaperInputElement).id; 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; return;
} }
this._dirty = true; 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[] { static get styles(): CSSResult[] {
@ -299,12 +550,16 @@ export class DialogHassioNetwork extends LitElement
--mdc-theme-primary: var(--error-color); --mdc-theme-primary: var(--error-color);
} }
mwc-button.scan {
margin-left: 8px;
}
:host([rtl]) app-toolbar { :host([rtl]) app-toolbar {
direction: rtl; direction: rtl;
text-align: right; text-align: right;
} }
.container { .container {
padding: 20px 24px; padding: 0 8px 4px;
} }
.form { .form {
margin-bottom: 53px; margin-bottom: 53px;
@ -322,6 +577,23 @@ export class DialogHassioNetwork extends LitElement
padding-bottom: max(env(safe-area-inset-bottom), 8px); padding-bottom: max(env(safe-area-inset-bottom), 8px);
background-color: var(--mdc-theme-surface, #fff); 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 { class HassioHostInfo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public hostInfo!: HassioHostInfoType; @property({ attribute: false }) public hostInfo!: HassioHostInfoType;
@property({ attribute: false }) public hassioInfo!: HassioInfo; @property({ attribute: false }) public hassioInfo!: HassioInfo;
@ -193,12 +193,10 @@ class HassioHostInfo extends LitElement {
} }
private _primaryIpAddress = memoizeOne((network_info: NetworkInfo) => { private _primaryIpAddress = memoizeOne((network_info: NetworkInfo) => {
if (!network_info) { if (!network_info || !network_info.interfaces) {
return ""; return "";
} }
return Object.keys(network_info?.interfaces) return network_info.interfaces.find((a) => a.primary)?.ipv4?.address![0];
.map((device) => network_info.interfaces[device])
.find((device) => device.primary)?.ip_address;
}); });
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) { private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {

View File

@ -1,24 +1,54 @@
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { hassioApiResultExtractor, HassioResponse } from "./common"; import { hassioApiResultExtractor, HassioResponse } from "./common";
export interface NetworkInterface { interface IpConfiguration {
address: string[];
gateway: string; gateway: string;
id: string; method: "disabled" | "static" | "auto";
ip_address: string; nameservers: string[];
address?: string;
method: "static" | "dhcp";
nameservers: string[] | string;
dns?: string[];
primary: boolean;
type: string;
} }
export interface NetworkInterfaces { export interface NetworkInterface {
[key: string]: 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 { export interface NetworkInfo {
interfaces: NetworkInterfaces; interfaces: NetworkInterface[];
docker: DockerNetwork;
} }
export const fetchNetworkInfo = async (hass: HomeAssistant) => { export const fetchNetworkInfo = async (hass: HomeAssistant) => {
@ -41,3 +71,15 @@ export const updateNetworkInterface = async (
options options
); );
}; };
export const accesspointScan = async (
hass: HomeAssistant,
network_interface: string
) => {
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<AccessPoints>>(
"GET",
`hassio/network/interface/${network_interface}/accesspoints`
)
);
};