-
-
Notifications
You must be signed in to change notification settings - Fork 51
Allow for generic Asterisk users with SIP Core, rather than the HA user only #186
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -34,6 +34,28 @@ export interface User { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| password: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Per-device SIP credentials stored in localStorage under 'sipcore-force-user'. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * When present, bypasses Home Assistant user matching entirely. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Set on a device via the browser console (one-time setup): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @example | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * localStorage.setItem('sipcore-force-user', JSON.stringify({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * extension: "100", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * password: "mypassword", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * display_name: "Front Door Panel" // optional | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Clear to revert to normal HA user matching: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @example | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * localStorage.removeItem('sipcore-force-user'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface ForceUser { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| extension: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| password: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| display_name?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface ICEConfig extends RTCConfiguration { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Timeout in milliseconds for ICE gathering */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| iceGatheringTimeout?: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -248,6 +270,40 @@ export class SIPCore { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.debug(`Audio input set to ${deviceId}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Per-device forced SIP user credentials, stored in localStorage. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * When set, this device will always register with these credentials, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * ignoring the global `users` list and the logged-in HA user. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Set: `localStorage.setItem('sipcore-force-user', JSON.stringify({ extension, password, display_name? }))` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Clear: `localStorage.removeItem('sipcore-force-user')` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| get ForceUser(): ForceUser | null { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const raw = localStorage.getItem("sipcore-force-user"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!raw) return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const parsed = JSON.parse(raw); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof parsed.extension === "string" && typeof parsed.password === "string") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return parsed as ForceUser; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.warn("sipcore-force-user in localStorage is missing required fields (extension, password). Ignoring."); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.warn("sipcore-force-user in localStorage is not valid JSON. Ignoring."); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| set ForceUser(forceUser: ForceUser | null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (forceUser === null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| localStorage.removeItem("sipcore-force-user"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.info("sipcore-force-user cleared. Will use HA user matching on next reload."); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| localStorage.setItem("sipcore-force-user", JSON.stringify(forceUser)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.info(`sipcore-force-user set to extension ${forceUser.extension}. Reload to apply.`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+298
to
+306
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private async setupAudio() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.incomingAudio = new Audio(this.config.incomingRingtoneUrl); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -319,6 +375,24 @@ export class SIPCore { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private async setupUser(): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 0. Check for a per-device forced user stored in localStorage. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // This takes priority over everything — the global users list and | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // the logged-in HA user are both ignored. Intended for kiosk tablets | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // or shared dashboards that should always register as a specific | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Asterisk extension regardless of who is logged into HA. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const forceUser = this.ForceUser; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (forceUser) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.user = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ha_username: "", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| display_name: forceUser.display_name ?? forceUser.extension, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| extension: forceUser.extension, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| password: forceUser.password, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.info(`sipcore-force-user active: registering as extension ${this.user.extension} (${this.user.display_name})`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.ua = this.setupUA(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const currentUserId = this.hass.user.id; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const currentUserName = this.hass.user.name; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -411,6 +485,19 @@ export class SIPCore { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async startCall(extension: string) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.ua.call(extension, await this.callOptions()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Re-applies user selection immediately, called by `sip-user-card` when | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * its YAML config changes. Stops the current UA, re-runs `setupUser()` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * with the latest `ForceUser` value from localStorage, then restarts. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async applyForceUser(): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.info("sip-user-card config changed, re-registering..."); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.info("sip-user-card config changed, re-registering..."); | |
| console.info("sip-user-card config changed, re-registering..."); | |
| if (!this.ua) { | |
| console.warn("UA not initialized; cannot re-register."); | |
| return; | |
| } |
Copilot
AI
Feb 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The applyForceUser method has no error handling. If setupUser() or ua.start() fail, the error will propagate uncaught to the caller (sip-user-card). Add try-catch error handling and log meaningful error messages to help users diagnose configuration issues.
| this.ua.stop(); | |
| await this.setupUser(); | |
| this.ua.start(); | |
| try { | |
| this.ua.stop(); | |
| } catch (error) { | |
| console.error( | |
| "Failed to stop existing SIP UA during re-registration. Continuing with reconfiguration anyway.", | |
| error, | |
| ); | |
| } | |
| try { | |
| await this.setupUser(); | |
| } catch (error) { | |
| console.error( | |
| "Failed to set up SIP user after sip-user-card config change. " + | |
| "Please check ForceUser, SIP credentials, and configuration.", | |
| error, | |
| ); | |
| return; | |
| } | |
| try { | |
| this.ua.start(); | |
| } catch (error) { | |
| console.error( | |
| "Failed to start SIP UA after re-registration. " + | |
| "Please verify the WebSocket URL and SIP server availability.", | |
| error, | |
| ); | |
| return; | |
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,153 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { LitElement, html, css } from "lit"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { customElement, state } from "lit/decorators.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { sipCore } from "./sip-core"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| declare global { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface Window { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| customCards?: Array<{ type: string; name: string; preview: boolean; description: string }>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface SIPUserCardConfig { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| extension: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| password: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| display_name?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * A zero-height Lovelace card that forces SIP Core to register with a specific | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Asterisk extension on this device, regardless of which HA user is logged in. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Add to a dashboard's YAML to configure a tablet or kiosk: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @example | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * type: custom:sip-user-card | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * extension: "100" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * password: "mypassword" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * display_name: "Front Door Panel" # optional | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Removing the card from the dashboard clears the override and reverts to | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * normal Home Assistant user matching. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| @customElement("sip-user-card") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| class SIPUserCard extends LitElement { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| @state() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| private config: SIPUserCardConfig | undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| static get styles() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return css` | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| :host { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| display: block; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* Zero height — this card is purely a config carrier */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| height: 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| overflow: hidden; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| .status { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| display: flex; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| align-items: center; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| gap: 6px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| padding: 4px 8px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| font-size: 12px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| color: var(--secondary-text-color); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| font-family: var(--paper-font-body1_-_font-family); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| .dot { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| width: 8px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| height: 8px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| border-radius: 50%; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| flex-shrink: 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| .dot.registered { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| background-color: var(--label-badge-green, #4caf50); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| .dot.unregistered { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| background-color: var(--label-badge-red, #f44336); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+45
to
+69
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| .status { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 4px 8px; | |
| font-size: 12px; | |
| color: var(--secondary-text-color); | |
| font-family: var(--paper-font-body1_-_font-family); | |
| } | |
| .dot { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| flex-shrink: 0; | |
| } | |
| .dot.registered { | |
| background-color: var(--label-badge-green, #4caf50); | |
| } | |
| .dot.unregistered { | |
| background-color: var(--label-badge-red, #f44336); | |
| } |
Copilot
AI
Feb 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The validation only checks for the presence of config.password but doesn't validate if it's a non-empty string. An empty string would pass validation but would likely cause authentication failures. Add validation: if (!config.password || config.password.trim() === '') to ensure the password is not empty. Apply the same check to config.extension.
| if (!config.extension) throw new Error("sip-user-card: 'extension' is required"); | |
| if (!config.password) throw new Error("sip-user-card: 'password' is required"); | |
| if (!config.extension || config.extension.trim() === "") { | |
| throw new Error("sip-user-card: 'extension' is required and cannot be empty"); | |
| } | |
| if (!config.password || config.password.trim() === "") { | |
| throw new Error("sip-user-card: 'password' is required and cannot be empty"); | |
| } |
Copilot
AI
Feb 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The change detection at line 81-84 doesn't check if display_name changed. If a user only updates the display name in the YAML config, the credentials won't be re-applied and the old display name will remain active until the page reloads. Include display_name in the change detection: currentForceUser.display_name !== config.display_name.
| currentForceUser.password !== config.password; | |
| currentForceUser.password !== config.password || | |
| currentForceUser.display_name !== config.display_name; |
Copilot
AI
Feb 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Storing plaintext SIP passwords in localStorage poses a significant security risk. localStorage is accessible to any JavaScript running on the same origin, making credentials vulnerable to XSS attacks. Consider encrypting the password before storage, or implement a more secure credential storage mechanism (such as using the Web Crypto API with a user-derived key, or storing only a session token that can be revoked).
Copilot
AI
Feb 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When multiple browser tabs/windows have different dashboards open simultaneously, all tabs share the same localStorage. If Tab A has a dashboard with sip-user-card forcing extension "100" and Tab B has a different dashboard with sip-user-card forcing extension "200", whichever tab loads last will overwrite localStorage and both tabs will attempt to register as the same extension. This defeats the purpose of the feature. Consider using sessionStorage instead of localStorage, or implement a tab-specific identifier to prevent cross-tab conflicts.
Copilot
AI
Feb 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The call to sipCore.applyForceUser() is not awaited. Since this is an async method that performs UA stop/restart operations, not awaiting it could lead to the card being marked as configured before the registration is complete. Consider adding await or handle the promise to detect registration failures and notify the user.
| sipCore.applyForceUser(); | |
| sipCore.applyForceUser().catch((err) => { | |
| console.error("sip-user-card: Failed to apply forced SIP user", err); | |
| }); |
Copilot
AI
Feb 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The setConfig method can be called multiple times by Home Assistant during re-renders. If credentials change rapidly or the method is called while applyForceUser() is still executing (which is async), this could lead to race conditions where multiple UA stop/start cycles overlap. Consider adding a debounce mechanism or a flag to prevent concurrent re-registration operations.
Copilot
AI
Feb 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The disconnectedCallback leaves credentials in localStorage even after the card is removed from the dashboard. This contradicts the documentation in the class comment (line 29-30) which states "Removing the card from the dashboard clears the override and reverts to normal Home Assistant user matching." The credentials should be cleared when the card is removed to match the documented behavior: sipCore.ForceUser = null;
| // Credentials remain in localStorage — this device stays registered as | |
| // the forced extension until manually cleared via: | |
| // localStorage.removeItem('sipcore-force-user') | |
| // Clear forced credentials so removing the card reverts to normal | |
| // Home Assistant user matching, as documented. | |
| sipCore.ForceUser = null; |
Copilot
AI
Feb 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The card's render method displays a status indicator despite claiming to be a "zero-height" configuration carrier. The CSS sets height: 0 and overflow: hidden, which will hide the rendered content. This creates a misleading implementation where visual status elements are rendered but never visible to users. Either remove the render method and return an empty template, or remove the zero-height constraint and make the status visible.
| const registered = sipCore.registered; | |
| const ext = this.config?.extension ?? "?"; | |
| const name = this.config?.display_name ?? ext; | |
| return html` | |
| <div class="status"> | |
| <span class="dot ${registered ? "registered" : "unregistered"}"></span> | |
| <span>${name} (${ext}) — ${registered ? "Registered" : "Unregistered"}</span> | |
| </div> | |
| `; | |
| // This card acts purely as a zero-height configuration carrier for sipCore. | |
| // It intentionally renders no visible UI. | |
| return html``; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The setter logs "Reload to apply" (line 304), but the card's
setConfigmethod immediately callsapplyForceUser()which applies changes without requiring a reload. This creates inconsistent messaging. Update the log message to reflect that the change will be applied immediately when using the card, or remove the misleading "Reload to apply" text.