Allow for generic Asterisk users with SIP Core, rather than the HA user only#186
Allow for generic Asterisk users with SIP Core, rather than the HA user only#186dovy6 wants to merge 4 commits intoTECH7Fox:mainfrom
Conversation
added sip-user-card
Allow for using sip-user-card to force registering a different user than the logged in HA user
There was a problem hiding this comment.
Pull request overview
This PR introduces a new sip-user-card Lovelace card that allows specific devices/browsers to register with a dedicated Asterisk extension, bypassing Home Assistant's standard user-matching logic. This solves the problem of multiple tablets or kiosks sharing a single HA user account and causing SIP registration conflicts.
Changes:
- Added
sip-user-card.ts- A new Lovelace card that stores device-specific SIP credentials in localStorage - Modified
sip-core.ts- Added ForceUser interface, getter/setter, andapplyForceUser()method to support per-device credential override - Updated
index.ts- Registered the new card component
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 14 comments.
| File | Description |
|---|---|
| src/sip-user-card.ts | New zero-height Lovelace card that writes SIP credentials to localStorage and triggers re-registration when config changes |
| src/sip-core.ts | Added ForceUser functionality with localStorage-based credential storage and priority checking in setupUser() flow |
| src/index.ts | Imported the new sip-user-card component |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| }; | ||
|
|
||
| // Tell SIP Core to drop the old connection and log in with the new one | ||
| sipCore.applyForceUser(); |
There was a problem hiding this comment.
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); | |
| }); |
| 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> | ||
| `; |
There was a problem hiding this comment.
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``; |
|
|
||
| .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); | ||
| } |
There was a problem hiding this comment.
The CSS styles for .status, .dot, .dot.registered, and .dot.unregistered (lines 46-69) are unused since the card has height: 0 and overflow: hidden, making the rendered content invisible. Remove these unused styles to reduce code bloat and confusion.
| .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); | |
| } |
| // Write to localStorage | ||
| sipCore.ForceUser = { | ||
| extension: config.extension, | ||
| password: config.password, | ||
| display_name: config.display_name, | ||
| }; |
There was a problem hiding this comment.
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).
| setConfig(config: SIPUserCardConfig) { | ||
| if (!config.extension) throw new Error("sip-user-card: 'extension' is required"); | ||
| if (!config.password) throw new Error("sip-user-card: 'password' is required"); | ||
|
|
||
| // GUARD: Only trigger a re-registration if the credentials actually changed. | ||
| // This prevents HA's aggressive Lovelace rendering from spamming the Asterisk server. | ||
| const currentForceUser = sipCore.ForceUser; | ||
| const configChanged = | ||
| !currentForceUser || | ||
| currentForceUser.extension !== config.extension || | ||
| currentForceUser.password !== config.password; | ||
|
|
||
| if (configChanged) { | ||
| console.info(`sip-user-card: New credentials detected for ${config.extension}. Updating localStorage...`); | ||
|
|
||
| // Write to localStorage | ||
| sipCore.ForceUser = { | ||
| extension: config.extension, | ||
| password: config.password, | ||
| display_name: config.display_name, | ||
| }; | ||
|
|
||
| // Tell SIP Core to drop the old connection and log in with the new one | ||
| sipCore.applyForceUser(); | ||
| } | ||
|
|
||
| this.config = config; | ||
| } |
There was a problem hiding this comment.
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.
| // Credentials remain in localStorage — this device stays registered as | ||
| // the forced extension until manually cleared via: | ||
| // localStorage.removeItem('sipcore-force-user') |
There was a problem hiding this comment.
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; |
| 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.`); | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| * with the latest `ForceUser` value from localStorage, then restarts. | ||
| */ | ||
| async applyForceUser(): Promise<void> { | ||
| console.info("sip-user-card config changed, re-registering..."); |
There was a problem hiding this comment.
The applyForceUser method calls this.ua.stop() without checking if this.ua is defined. If this method is called before the UA is initialized (e.g., during early page load or if initialization failed), it will throw a runtime error. Add a guard check: if (!this.ua) return; or ensure the UA is initialized before calling stop().
| 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; | |
| } |
| 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.`); | ||
| } | ||
| } |
There was a problem hiding this comment.
The ForceUser setter accepts null to clear the value, but the getter returns ForceUser | null. However, the interface ForceUser requires both extension and password as non-optional strings, while display_name is optional. The validation in the getter (line 287) correctly checks for the presence of these required fields, which is good. Consider adding similar validation in the setter to fail fast if an invalid ForceUser object is passed.
| const configChanged = | ||
| !currentForceUser || | ||
| currentForceUser.extension !== config.extension || | ||
| currentForceUser.password !== config.password; |
There was a problem hiding this comment.
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; |
|
Thanks @dovy6 for the PR! Will try it out soon. |
Description
This PR introduces a new, zero-height Lovelace card (
sip-user-card) that allows a specific device/browser to bypass the standard Home Assistant user-matching logic and force SIP Core to register as a specific Asterisk extension.The Problem
When using wall panels, kiosk tablets, or shared dashboards (like View Assist), these devices often share a single, generic Home Assistant user account. Because SIP Core ties the SIP extension directly to the logged-in HA user, multiple tablets will attempt to register to Asterisk using the exact same extension. This causes a
max_contactscollision, where the tablets endlessly kick each other off the PBX. The only current workaround is to bloat the HA instance with dummy user accounts for every tablet.The Solution
The
sip-user-cardacts as an invisible configuration carrier. When placed on a dashboard, it writes the provided SIP credentials directly into the browser'slocalStorage(sipcore-force-user).I modified
sip-core.tsto intercept thesetupUser()flow. On boot, it checkslocalStoragefirst. If an override exists, it completely ignores the globaluserslist and the HA user, registering the device as the specified extension.Usage Example
Adding this to a tablet's dashboard YAML permanently ties that physical tablet to Extension 200, regardless of who is logged into Home Assistant: