diff --git a/src/components/ha-network.ts b/src/components/ha-network.ts new file mode 100644 index 0000000000..a4163ae720 --- /dev/null +++ b/src/components/ha-network.ts @@ -0,0 +1,172 @@ +import "@polymer/paper-tooltip/paper-tooltip"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, state, property } from "lit/decorators"; +import { + Adapter, + NetworkConfig, + IPv6ConfiguredAddress, + IPv4ConfiguredAddress, +} from "../data/network"; +import { fireEvent } from "../common/dom/fire_event"; +import { haStyle } from "../resources/styles"; +import { HomeAssistant } from "../types"; +import "./ha-checkbox"; +import type { HaCheckbox } from "./ha-checkbox"; +import "./ha-settings-row"; +import "./ha-icon"; + +const format_addresses = ( + addresses: IPv6ConfiguredAddress[] | IPv4ConfiguredAddress[] +): TemplateResult[] => + addresses.map( + (address) => html`${address.address}/${address.network_prefix}` + ); + +const format_auto_detected_interfaces = ( + adapters: Adapter[] +): Array => + adapters.map((adapter) => + adapter.auto + ? html`${adapter.name} (${format_addresses(adapter.ipv4)} + ${format_addresses(adapter.ipv6)} )` + : "" + ); + +declare global { + interface HASSDomEvents { + "network-config-changed": { configured_adapters: string[] }; + } +} +@customElement("ha-network") +export class HaNetwork extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public networkConfig?: NetworkConfig; + + @state() private _expanded?: boolean; + + protected render(): TemplateResult { + if (this.networkConfig === undefined) { + return html``; + } + const configured_adapters = this.networkConfig.configured_adapters || []; + return html` + + + + + + Auto Configure + + Detected: + ${format_auto_detected_interfaces(this.networkConfig.adapters)} + + + ${configured_adapters.length || this._expanded + ? this.networkConfig.adapters.map( + (adapter) => + html` + + + + + + Adapter: ${adapter.name} + ${adapter.default + ? html` (Default)` + : ""} + + + ${format_addresses(adapter.ipv4)} + ${format_addresses(adapter.ipv6)} + + ` + ) + : ""} + `; + } + + private _handleAutoConfigureCheckboxClick(ev: Event) { + const checkbox = ev.currentTarget as HaCheckbox; + if (this.networkConfig === undefined) { + return; + } + + let configured_adapters = [...this.networkConfig.configured_adapters]; + + if (checkbox.checked) { + this._expanded = false; + configured_adapters = []; + } else { + this._expanded = true; + for (const adapter of this.networkConfig.adapters) { + if (adapter.default) { + configured_adapters = [adapter.name]; + break; + } + } + } + + fireEvent(this, "network-config-changed", { + configured_adapters: configured_adapters, + }); + } + + private _handleAdapterCheckboxClick(ev: Event) { + const checkbox = ev.currentTarget as HaCheckbox; + const adapter_name = (checkbox as any).name; + if (this.networkConfig === undefined) { + return; + } + + const configured_adapters = [...this.networkConfig.configured_adapters]; + + if (checkbox.checked) { + configured_adapters.push(adapter_name); + } else { + const index = configured_adapters.indexOf(adapter_name, 0); + configured_adapters.splice(index, 1); + } + + fireEvent(this, "network-config-changed", { + configured_adapters: configured_adapters, + }); + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + css` + .error { + color: var(--error-color); + } + + ha-settings-row { + padding: 0; + } + + span[slot="heading"], + span[slot="description"] { + cursor: pointer; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-network": HaNetwork; + } +} diff --git a/src/data/network.ts b/src/data/network.ts new file mode 100644 index 0000000000..5bfb391c00 --- /dev/null +++ b/src/data/network.ts @@ -0,0 +1,43 @@ +import { HomeAssistant } from "../types"; + +export interface IPv6ConfiguredAddress { + address: string; + flowinfo: number; + scope_id: number; + network_prefix: number; +} + +export interface IPv4ConfiguredAddress { + address: string; + network_prefix: number; +} + +export interface Adapter { + name: string; + enabled: boolean; + auto: boolean; + default: boolean; + ipv6: IPv6ConfiguredAddress[]; + ipv4: IPv4ConfiguredAddress[]; +} + +export interface NetworkConfig { + adapters: Adapter[]; + configured_adapters: string[]; +} + +export const getNetworkConfig = (hass: HomeAssistant) => + hass.callWS({ + type: "network", + }); + +export const setNetworkConfig = ( + hass: HomeAssistant, + configured_adapters: string[] +) => + hass.callWS({ + type: "network/configure", + config: { + configured_adapters: configured_adapters, + }, + }); diff --git a/src/panels/config/core/ha-config-network.ts b/src/panels/config/core/ha-config-network.ts new file mode 100644 index 0000000000..ec90bc8087 --- /dev/null +++ b/src/panels/config/core/ha-config-network.ts @@ -0,0 +1,131 @@ +import "@material/mwc-button/mwc-button"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { isComponentLoaded } from "../../../common/config/is_component_loaded"; +import "../../../components/ha-network"; +import "../../../components/ha-card"; +import "../../../components/ha-checkbox"; +import "../../../components/ha-settings-row"; +import { + NetworkConfig, + getNetworkConfig, + setNetworkConfig, +} from "../../../data/network"; +import { haStyle } from "../../../resources/styles"; +import type { HomeAssistant } from "../../../types"; + +@customElement("ha-config-network") +class ConfigNetwork extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _networkConfig?: NetworkConfig; + + @state() private _error?: string; + + protected render(): TemplateResult { + if (!this.hass.userData?.showAdvanced) { + return html``; + } + + const error = this._error + ? this._error + : !isComponentLoaded(this.hass, "network") + ? "Network integration not loaded" + : undefined; + + return html` + +
+ ${error ? html`
${error}
` : ""} +

+ Configure which network adapters integrations will use. Currently + this setting only affects multicast traffic. A restart is required + for these settings to apply. +

+ +
+
+ + ${this.hass.localize( + "ui.panel.config.core.section.core.core_config.save_button" + )} + +
+
+ `; + } + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + if (isComponentLoaded(this.hass, "network")) { + this._load(); + } + } + + private async _load() { + this._error = undefined; + try { + this._networkConfig = await getNetworkConfig(this.hass); + } catch (err) { + this._error = err.message || err; + } + } + + private async _save() { + this._error = undefined; + try { + await setNetworkConfig( + this.hass, + this._networkConfig?.configured_adapters || [] + ); + } catch (err) { + this._error = err.message || err; + } + } + + private _configChanged(event: CustomEvent): void { + this._networkConfig = { + ...this._networkConfig!, + configured_adapters: event.detail.configured_adapters, + }; + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + css` + .error { + color: var(--error-color); + } + + ha-settings-row { + padding: 0; + } + + .card-actions { + display: flex; + flex-direction: row-reverse; + justify-content: space-between; + align-items: center; + } + `, // row-reverse so we tab first to "save" + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-config-network": ConfigNetwork; + } +} diff --git a/src/panels/config/core/ha-config-section-core.js b/src/panels/config/core/ha-config-section-core.js index 4e50ab56c4..6c74b6f539 100644 --- a/src/panels/config/core/ha-config-section-core.js +++ b/src/panels/config/core/ha-config-section-core.js @@ -11,6 +11,7 @@ import "../ha-config-section"; import "./ha-config-analytics"; import "./ha-config-core-form"; import "./ha-config-name-form"; +import "./ha-config-network"; import "./ha-config-url-form"; /* @@ -30,6 +31,7 @@ class HaConfigSectionCore extends LocalizeMixin(PolymerElement) { + `;