-
Notifications
You must be signed in to change notification settings - Fork 21
feat: custom node labels with HA name sync #413
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
9bea46e
feat: add custom node labels (server-side naming)
markvp dbd8c87
feat: add server-side Home Assistant name sync
markvp b969865
feat: add dashboard UI for HA integration and settings menu
markvp 4c2fa5f
Update packages/ws-controller/src/server/HomeAssistantClient.ts
markvp 391ec6b
Update packages/ws-controller/src/server/WebSocketControllerHandler.ts
markvp 695102c
Update packages/ws-controller/src/server/ConfigStorage.ts
markvp c3c361c
Update packages/dashboard/src/pages/components/node-details.ts
markvp 1d3cadb
Update packages/dashboard/src/pages/components/server-details.ts
markvp 3e93f81
fix: add timeout to HA API requests via AbortController
markvp 80ed341
Address PR #413 review feedback
markvp 714c517
Address second round of PR #413 review feedback
markvp 87ed998
Address third round of PR #413 review feedback
markvp 5e8e34c
Address fourth round of PR #413 review feedback
markvp c4774b2
Address fifth round of PR #413 review feedback
markvp 2c6a66d
Address sixth round of PR #413 review feedback
markvp de556aa
Proactive cleanup addressing review patterns across PR #413
markvp 1740f3f
Proactive hardening: validation, batching, accessibility, comments
markvp 672a2e7
Fix HA client precedence, nodeId key consistency, empty name guard
markvp cff4d7d
Merge branch 'main' into feat/custom-node-labels
markvp File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
88 changes: 88 additions & 0 deletions
88
packages/dashboard/src/components/dialog-box/input-dialog-box.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| /** | ||
| * @license | ||
| * Copyright 2025-2026 Open Home Foundation | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import "@material/web/button/text-button"; | ||
| import "@material/web/dialog/dialog"; | ||
| import "@material/web/textfield/outlined-text-field"; | ||
| import type { MdDialog } from "@material/web/dialog/dialog.js"; | ||
| import type { MdOutlinedTextField } from "@material/web/textfield/outlined-text-field.js"; | ||
| import { css, html, LitElement } from "lit"; | ||
| import { customElement, property } from "lit/decorators.js"; | ||
| import type { InputDialogBoxParams } from "./show-dialog-box.js"; | ||
|
|
||
| @customElement("input-dialog-box") | ||
| export class InputDialogBox extends LitElement { | ||
| @property({ attribute: false }) public params!: InputDialogBoxParams; | ||
|
|
||
| @property({ attribute: false }) public dialogResult!: (result: string | null) => void; | ||
|
|
||
| protected override render() { | ||
| const params = this.params; | ||
| return html` | ||
| <md-dialog open @closed=${this._handleClosed}> | ||
| ${params.title ? html`<div slot="headline">${params.title}</div>` : ""} | ||
| <div slot="content"> | ||
| ${params.text ? html`<p>${params.text}</p>` : ""} | ||
| <md-outlined-text-field | ||
| .value=${params.defaultValue ?? ""} | ||
| .label=${params.label ?? ""} | ||
| aria-label=${params.label || params.title || "Input"} | ||
| @keydown=${this._handleKeydown} | ||
| ></md-outlined-text-field> | ||
| </div> | ||
| <div slot="actions"> | ||
| <md-text-button @click=${this._cancel}>${params.cancelText ?? "Cancel"}</md-text-button> | ||
| <md-text-button @click=${this._confirm}>${params.confirmText ?? "OK"}</md-text-button> | ||
| </div> | ||
| </md-dialog> | ||
| `; | ||
| } | ||
|
|
||
| private _handleKeydown(e: KeyboardEvent) { | ||
| if (e.key === "Enter") { | ||
| e.preventDefault(); | ||
| e.stopPropagation(); | ||
| this._confirm(); | ||
| } | ||
| } | ||
markvp marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| private _cancel() { | ||
| if (this._resolved) return; | ||
| this._resolved = true; | ||
| this.dialogResult(null); | ||
| this.shadowRoot!.querySelector<MdDialog>("md-dialog")!.close(); | ||
| } | ||
|
|
||
| private _resolved = false; | ||
|
|
||
| private _confirm() { | ||
| if (this._resolved) return; | ||
| this._resolved = true; | ||
| const textField = this.shadowRoot!.querySelector<MdOutlinedTextField>("md-outlined-text-field")!; | ||
| this.dialogResult(textField.value); | ||
| this.shadowRoot!.querySelector<MdDialog>("md-dialog")!.close(); | ||
| } | ||
|
|
||
| private _handleClosed() { | ||
markvp marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (!this._resolved) { | ||
| this._resolved = true; | ||
| this.dialogResult(null); | ||
| } | ||
| this.remove(); | ||
| } | ||
|
|
||
| static override styles = css` | ||
| md-outlined-text-field { | ||
| width: 100%; | ||
| } | ||
| `; | ||
| } | ||
|
|
||
| declare global { | ||
| interface HTMLElementTagNameMap { | ||
| "input-dialog-box": InputDialogBox; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
192 changes: 192 additions & 0 deletions
192
packages/dashboard/src/components/dialogs/settings/ha-integration-dialog.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,192 @@ | ||
| /** | ||
| * @license | ||
| * Copyright 2025-2026 Open Home Foundation | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import "@material/web/button/text-button"; | ||
| import "@material/web/dialog/dialog"; | ||
| import "@material/web/textfield/outlined-text-field"; | ||
| import type { MdDialog } from "@material/web/dialog/dialog.js"; | ||
| import type { MdOutlinedTextField } from "@material/web/textfield/outlined-text-field.js"; | ||
| import { MatterClient } from "@matter-server/ws-client"; | ||
| import { css, html, LitElement, nothing } from "lit"; | ||
| import { customElement, property, query, state } from "lit/decorators.js"; | ||
| import { handleAsync } from "../../../util/async-handler.js"; | ||
|
|
||
| @customElement("ha-integration-dialog") | ||
| export class HaIntegrationDialog extends LitElement { | ||
| @property({ attribute: false }) | ||
| public client!: MatterClient; | ||
|
|
||
| @state() private _saving = false; | ||
| @state() private _syncing = false; | ||
| @state() private _syncResult: string | null = null; | ||
|
|
||
| @query("#ha-url") | ||
| private _urlField!: MdOutlinedTextField; | ||
|
|
||
| @query("#ha-token") | ||
| private _tokenField!: MdOutlinedTextField; | ||
|
|
||
| private get _haConfigured(): boolean { | ||
| return this.client.serverInfo.ha_credentials_set === true; | ||
| } | ||
|
|
||
| private async _save() { | ||
| const url = this._urlField.value.trim(); | ||
| const token = this._tokenField.value.trim(); | ||
|
|
||
| if (!url || !token) { | ||
| this._syncResult = "Please enter both URL and token."; | ||
| return; | ||
| } | ||
markvp marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| this._saving = true; | ||
| this._syncResult = null; | ||
| try { | ||
| await this.client.setHaCredentials(url, token); | ||
| this._syncResult = "Credentials saved."; | ||
| // Clear the token field after saving (it's stored server-side) | ||
| this._tokenField.value = ""; | ||
| } catch (err) { | ||
| this._syncResult = `Failed to save: ${err instanceof Error ? err.message : String(err)}`; | ||
| } finally { | ||
| this._saving = false; | ||
| } | ||
| } | ||
|
|
||
| private async _sync() { | ||
| this._syncing = true; | ||
| this._syncResult = null; | ||
| try { | ||
| const result = await this.client.syncHaNames(); | ||
| if (result.errors.length > 0) { | ||
| this._syncResult = `Synced ${result.synced} name(s) with ${result.errors.length} error(s).`; | ||
| } else if (result.synced === 0) { | ||
| this._syncResult = "No new names to sync."; | ||
| } else { | ||
| this._syncResult = `Synced ${result.synced} name(s) from Home Assistant.`; | ||
| } | ||
| } catch (err) { | ||
| this._syncResult = `Sync failed: ${err instanceof Error ? err.message : String(err)}`; | ||
| } finally { | ||
| this._syncing = false; | ||
| } | ||
| } | ||
|
|
||
| private async _clear() { | ||
| this._saving = true; | ||
| this._syncResult = null; | ||
| try { | ||
| await this.client.setHaCredentials("", ""); | ||
| this._syncResult = "Stored credentials cleared."; | ||
| this._urlField.value = ""; | ||
| this._tokenField.value = ""; | ||
| } catch (err) { | ||
| this._syncResult = `Failed to clear: ${err instanceof Error ? err.message : String(err)}`; | ||
| } finally { | ||
| this._saving = false; | ||
| } | ||
| } | ||
markvp marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| private _close() { | ||
| this.shadowRoot!.querySelector<MdDialog>("md-dialog")!.close(); | ||
| } | ||
|
|
||
| private _handleClosed() { | ||
| this.remove(); | ||
| } | ||
|
|
||
| protected override render() { | ||
| return html` | ||
| <md-dialog open @closed=${this._handleClosed}> | ||
| <div slot="headline">Home Assistant Integration</div> | ||
| <div slot="content"> | ||
| <p class="hint"> | ||
| Connect to Home Assistant to sync device names. | ||
| ${ | ||
| this._haConfigured | ||
| ? html` | ||
| <br /><span class="status-ok">Credentials saved</span> | ||
| ` | ||
| : nothing | ||
| } | ||
| </p> | ||
| <div class="form-field"> | ||
| <md-outlined-text-field | ||
| id="ha-url" | ||
| label="Home Assistant URL" | ||
| placeholder="http://homeassistant.local:8123" | ||
| type="url" | ||
| ></md-outlined-text-field> | ||
| </div> | ||
| <div class="form-field"> | ||
| <md-outlined-text-field | ||
| id="ha-token" | ||
| label="Access Token" | ||
| placeholder="Long-lived access token" | ||
| type="password" | ||
| ></md-outlined-text-field> | ||
| </div> | ||
| ${this._syncResult ? html`<p class="sync-result">${this._syncResult}</p>` : nothing} | ||
| </div> | ||
| <div slot="actions"> | ||
| <md-text-button | ||
| @click=${handleAsync(() => this._sync())} | ||
| ?disabled=${!this._haConfigured || this._syncing} | ||
| > | ||
| ${this._syncing ? "Syncing..." : "Sync Names from HA"} | ||
| </md-text-button> | ||
| <md-text-button | ||
| @click=${handleAsync(() => this._clear())} | ||
| ?disabled=${!this._haConfigured || this._saving} | ||
| > | ||
| Clear | ||
| </md-text-button> | ||
| <md-text-button @click=${this._close}>Cancel</md-text-button> | ||
| <md-text-button | ||
| @click=${handleAsync(() => this._save())} | ||
| ?disabled=${this._saving} | ||
| > | ||
| ${this._saving ? "Saving..." : "Save"} | ||
| </md-text-button> | ||
| </div> | ||
| </md-dialog> | ||
| `; | ||
| } | ||
|
|
||
| static override styles = css` | ||
| .hint { | ||
| font-size: 0.875rem; | ||
| color: var(--md-sys-color-on-surface-variant); | ||
| margin: 0 0 16px 0; | ||
| } | ||
|
|
||
| .status-ok { | ||
| color: var(--md-sys-color-primary); | ||
| font-weight: 500; | ||
| } | ||
|
|
||
| .form-field { | ||
| margin-bottom: 16px; | ||
| } | ||
|
|
||
| md-outlined-text-field { | ||
| width: 100%; | ||
| } | ||
|
|
||
| .sync-result { | ||
| font-size: 0.875rem; | ||
| color: var(--md-sys-color-on-surface-variant); | ||
| margin: 8px 0 0 0; | ||
| font-style: italic; | ||
| } | ||
| `; | ||
| } | ||
|
|
||
| declare global { | ||
| interface HTMLElementTagNameMap { | ||
| "ha-integration-dialog": HaIntegrationDialog; | ||
| } | ||
| } | ||
75 changes: 75 additions & 0 deletions
75
packages/dashboard/src/components/dialogs/settings/settings-menu-dialog.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| /** | ||
| * @license | ||
| * Copyright 2025-2026 Open Home Foundation | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import "@material/web/dialog/dialog"; | ||
| import "@material/web/list/list"; | ||
| import "@material/web/list/list-item"; | ||
| import type { MdDialog } from "@material/web/dialog/dialog.js"; | ||
| import { MatterClient } from "@matter-server/ws-client"; | ||
| import { mdiHomeAssistant, mdiMathLog } from "@mdi/js"; | ||
| import { css, html, LitElement } from "lit"; | ||
| import { customElement, property } from "lit/decorators.js"; | ||
| import "../../../components/ha-svg-icon"; | ||
| import { showHaIntegrationDialog } from "./show-ha-integration-dialog.js"; | ||
| import { showLogLevelDialog } from "./show-log-level-dialog.js"; | ||
|
|
||
| @customElement("settings-menu-dialog") | ||
| export class SettingsMenuDialog extends LitElement { | ||
| @property({ attribute: false }) | ||
| public client!: MatterClient; | ||
|
|
||
| private _openLogLevel() { | ||
| this._close(); | ||
| showLogLevelDialog(this.client); | ||
| } | ||
|
|
||
| private _openHaIntegration() { | ||
| this._close(); | ||
| showHaIntegrationDialog(this.client); | ||
| } | ||
|
|
||
| private _close() { | ||
| this.shadowRoot!.querySelector<MdDialog>("md-dialog")!.close(); | ||
| } | ||
|
|
||
| private _handleClosed() { | ||
| this.remove(); | ||
| } | ||
|
|
||
| protected override render() { | ||
| return html` | ||
| <md-dialog open @closed=${this._handleClosed}> | ||
| <div slot="headline">Settings</div> | ||
| <div slot="content"> | ||
| <md-list> | ||
| <md-list-item type="button" @click=${this._openLogLevel}> | ||
| <ha-svg-icon slot="start" .path=${mdiMathLog}></ha-svg-icon> | ||
| <div slot="headline">Log Level</div> | ||
| <div slot="supporting-text">Configure server log verbosity</div> | ||
| </md-list-item> | ||
| <md-list-item type="button" @click=${this._openHaIntegration}> | ||
| <ha-svg-icon slot="start" .path=${mdiHomeAssistant}></ha-svg-icon> | ||
| <div slot="headline">Home Assistant</div> | ||
| <div slot="supporting-text">Sync device names with Home Assistant</div> | ||
| </md-list-item> | ||
| </md-list> | ||
| </div> | ||
| </md-dialog> | ||
| `; | ||
| } | ||
|
|
||
| static override styles = css` | ||
| md-list { | ||
| padding: 0; | ||
| } | ||
| `; | ||
| } | ||
|
|
||
| declare global { | ||
| interface HTMLElementTagNameMap { | ||
| "settings-menu-dialog": SettingsMenuDialog; | ||
| } | ||
| } |
14 changes: 14 additions & 0 deletions
14
packages/dashboard/src/components/dialogs/settings/show-ha-integration-dialog.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| /** | ||
| * @license | ||
| * Copyright 2025-2026 Open Home Foundation | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import { MatterClient } from "@matter-server/ws-client"; | ||
|
|
||
| export const showHaIntegrationDialog = async (client: MatterClient) => { | ||
| await import("./ha-integration-dialog.js"); | ||
| const dialog = document.createElement("ha-integration-dialog"); | ||
| dialog.client = client; | ||
| document.body.appendChild(dialog); | ||
| }; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.