/** * @license * Copyright 2025-2026 Open Home Foundation * SPDX-License-Identifier: Apache-2.0 */ import "@material/web/button/filled-button"; import "@material/web/button/text-button"; import "@material/web/dialog/dialog"; import "@material/web/divider/divider"; import "@material/web/iconbutton/icon-button"; import "@material/web/switch/switch"; import { consume } from "@lit/context"; import type { MdDialog } from "@material/web/dialog/dialog.js"; import type { MdSwitch } from "@material/web/switch/switch.js"; import "@material/web/textfield/outlined-text-field"; import type { MdOutlinedTextField } from "@material/web/textfield/outlined-text-field.js"; import { MatterClient } from "@matter-server/ws-client"; import { mdiAccessPoint, mdiEye, mdiEyeOff, mdiWifi } from "@mdi/js"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators.js"; import { clientContext, tickContext } from "../../../client/client-context.js"; import { handleAsync } from "../../../util/async-handler.js"; import { DevModeService } from "../../../util/dev-mode-service.js"; import { preventDefault } from "../../../util/prevent_default.js"; import "../../../components/ha-svg-icon.js"; import { showAlertDialog } from "../../dialog-box/show-dialog-box.js"; import "./log-level-section.js"; @customElement("settings-dialog") export class SettingsDialog extends LitElement { @consume({ context: clientContext }) public client!: MatterClient; @consume({ context: tickContext, subscribe: true }) protected _tick = 0; @state() private _devMode = DevModeService.active; private _unsubscribeDev?: () => void; @property({ attribute: false }) public scrollToSection?: string; @state() private _expandedRow: "wifi" | "thread" | null = null; @state() private _credLoading = false; @state() private _showPassword = false; @query("#cred-wifi-ssid") private _wifiSsidField!: MdOutlinedTextField; @query("#cred-wifi-password") private _wifiPasswordField!: MdOutlinedTextField; @query("#cred-thread-dataset") private _threadDatasetField!: MdOutlinedTextField; override connectedCallback() { super.connectedCallback(); this._unsubscribeDev = DevModeService.subscribe(active => { this._devMode = active; }); } override disconnectedCallback() { super.disconnectedCallback(); this._unsubscribeDev?.(); } override firstUpdated() { const knownSections = new Set(["network-credentials"]); if (this.scrollToSection && knownSections.has(this.scrollToSection)) { requestAnimationFrame(() => { this.renderRoot.querySelector(`#${this.scrollToSection}`)?.scrollIntoView({ behavior: "smooth", block: "start", }); }); } } private _close() { this.shadowRoot!.querySelector("md-dialog")!.close(); } private _handleClosed() { this.parentNode!.removeChild(this); } private _onDevToggle(event: Event) { const target = event.target as MdSwitch; DevModeService.setActive(target.selected); } private _copyDevLink() { const url = new URL(window.location.href); url.searchParams.set("dev", "on"); navigator.clipboard?.writeText(url.toString()).catch(() => { // best-effort; clipboard may be unavailable }); } private _toggleExpand(row: "wifi" | "thread") { this._expandedRow = this._expandedRow === row ? null : row; this._showPassword = false; } private _cancelCred() { this._expandedRow = null; this._showPassword = false; } private _togglePassword() { this._showPassword = !this._showPassword; } private async _saveWifi() { const ssid = this._wifiSsidField.value.trim(); if (!ssid) { showAlertDialog({ title: "Validation error", text: "SSID is required" }); return; } const password = this._wifiPasswordField.value; if (!password) { showAlertDialog({ title: "Validation error", text: "Password is required" }); return; } this._credLoading = true; try { await this.client.setWifiCredentials(ssid, password); this._expandedRow = null; this._showPassword = false; } catch (err) { showAlertDialog({ title: "Error saving WiFi credentials", text: (err as Error).message }); } finally { this._credLoading = false; } } private async _removeWifi() { this._credLoading = true; try { await this.client.removeWifiCredentials(); this._expandedRow = null; this._showPassword = false; } catch (err) { showAlertDialog({ title: "Error removing WiFi credentials", text: (err as Error).message }); } finally { this._credLoading = false; } } private async _saveThread() { const dataset = this._threadDatasetField.value.trim(); if (!dataset) { showAlertDialog({ title: "Validation error", text: "Thread dataset is required" }); return; } if (!/^[0-9a-fA-F]*$/.test(dataset) || dataset.length % 2 !== 0) { showAlertDialog({ title: "Invalid Thread dataset", text: "Must be a hex string with even length (each byte is two hex characters)", }); return; } this._credLoading = true; try { await this.client.setThreadOperationalDataset(dataset); this._expandedRow = null; } catch (err) { showAlertDialog({ title: "Error saving Thread dataset", text: (err as Error).message }); } finally { this._credLoading = false; } } private async _removeThread() { this._credLoading = true; try { await this.client.removeThreadDataset(); this._expandedRow = null; this._showPassword = false; } catch (err) { showAlertDialog({ title: "Error removing Thread dataset", text: (err as Error).message }); } finally { this._credLoading = false; } } protected override render() { return html`
Settings

Developer mode

Copy URL with dev enabled

Server log levels

Network credentials

WiFi ${this.client.serverInfo.wifi_credentials_set ? html`${this.client.serverInfo.wifi_ssid}` : html`Not configured`}
this._toggleExpand("wifi")} .disabled=${this._credLoading} >Edit
${this._expandedRow === "wifi" ? html`
Cancel ${this.client.serverInfo.wifi_credentials_set ? html` this._removeWifi())} .disabled=${this._credLoading} >Remove` : nothing} this._saveWifi())} .disabled=${this._credLoading} >Save
` : nothing}
Thread ${this.client.serverInfo.thread_credentials_set ? html`Thread network set` : html`Not configured`}
this._toggleExpand("thread")} .disabled=${this._credLoading} >Edit
${this._expandedRow === "thread" ? html`
Cancel ${this.client.serverInfo.thread_credentials_set ? html` this._removeThread())} .disabled=${this._credLoading} >Remove` : nothing} this._saveThread())} .disabled=${this._credLoading} >Save
` : nothing}

Used when commissioning new devices. Existing devices are not affected.

Close
`; } static override styles = css` md-dialog { min-width: 480px; max-width: 600px; } .section { padding: 8px 0 16px 0; } .section-title { margin: 0 0 12px 0; font-size: 0.95rem; font-weight: 500; color: var(--md-sys-color-on-surface); text-transform: uppercase; letter-spacing: 0.08em; } .toggle-row { display: flex; align-items: center; gap: 16px; justify-content: space-between; } .toggle-label { display: flex; flex-direction: column; gap: 4px; color: var(--md-sys-color-on-surface); font-size: 0.95rem; } .hint { font-size: 0.825rem; color: var(--md-sys-color-on-surface-variant); font-weight: 400; } .hint code { font-family: var(--monospace-font); background: var(--md-sys-color-surface-container-high); padding: 0 4px; border-radius: 3px; } .aux-row { margin-top: 8px; display: flex; justify-content: flex-end; } md-divider { margin: 12px 0; } .cred-row { display: flex; align-items: center; justify-content: space-between; padding: 4px 0; } .cred-row-thread { margin-top: 8px; } .cred-info { display: flex; align-items: center; gap: 10px; font-size: 0.9rem; color: var(--md-sys-color-on-surface); } .cred-label { font-weight: 500; min-width: 52px; } .cred-value { color: var(--md-sys-color-on-surface-variant); } .cred-unset { color: var(--md-sys-color-on-surface-variant); font-style: italic; } .cred-form { display: flex; flex-direction: column; gap: 10px; padding: 8px 0 4px 0; } .password-row { display: flex; align-items: center; gap: 4px; } .password-row md-outlined-text-field { flex: 1; } .form-actions { display: flex; gap: 4px; justify-content: flex-end; } .cred-hint { margin: 10px 0 0 0; font-size: 0.8rem; color: var(--md-sys-color-on-surface-variant); } .cred-info ha-svg-icon { width: 18px; height: 18px; color: var(--md-sys-color-on-surface-variant); } .password-row ha-svg-icon { width: 18px; height: 18px; color: var(--md-sys-color-on-surface-variant); } `; } declare global { interface HTMLElementTagNameMap { "settings-dialog": SettingsDialog; } }