Skip to content

Allow for generic Asterisk users with SIP Core, rather than the HA user only#186

Open
dovy6 wants to merge 4 commits intoTECH7Fox:mainfrom
dovy6:main
Open

Allow for generic Asterisk users with SIP Core, rather than the HA user only#186
dovy6 wants to merge 4 commits intoTECH7Fox:mainfrom
dovy6:main

Conversation

@dovy6
Copy link

@dovy6 dovy6 commented Feb 17, 2026

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_contacts collision, 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-card acts as an invisible configuration carrier. When placed on a dashboard, it writes the provided SIP credentials directly into the browser's localStorage (sipcore-force-user).

I modified sip-core.ts to intercept the setupUser() flow. On boot, it checks localStorage first. If an override exists, it completely ignores the global users list 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:

type: custom:sip-user-card
extension: "200"
password: "mypassword"
display_name: "Office Tablet"

added sip-user-card
Allow for using sip-user-card to force registering a different user than the logged in HA user
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, and applyForceUser() 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();
Copy link

Copilot AI Feb 24, 2026

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.

Suggested change
sipCore.applyForceUser();
sipCore.applyForceUser().catch((err) => {
console.error("sip-user-card: Failed to apply forced SIP user", err);
});

Copilot uses AI. Check for mistakes.
Comment on lines +129 to +138
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>
`;
Copy link

Copilot AI Feb 24, 2026

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.

Suggested change
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``;

Copilot uses AI. Check for mistakes.
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);
}
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
.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 uses AI. Check for mistakes.
Comment on lines +89 to +94
// Write to localStorage
sipCore.ForceUser = {
extension: config.extension,
password: config.password,
display_name: config.display_name,
};
Copy link

Copilot AI Feb 24, 2026

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 uses AI. Check for mistakes.
Comment on lines +74 to +101
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;
}
Copy link

Copilot AI Feb 24, 2026

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 uses AI. Check for mistakes.
Comment on lines +119 to +121
// Credentials remain in localStorage — this device stays registered as
// the forced extension until manually cleared via:
// localStorage.removeItem('sipcore-force-user')
Copy link

Copilot AI Feb 24, 2026

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;

Suggested change
// 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 uses AI. Check for mistakes.
Comment on lines +298 to +306
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.`);
}
}
Copy link

Copilot AI Feb 24, 2026

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.

Copilot uses AI. Check for mistakes.
* with the latest `ForceUser` value from localStorage, then restarts.
*/
async applyForceUser(): Promise<void> {
console.info("sip-user-card config changed, re-registering...");
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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().

Suggested change
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 uses AI. Check for mistakes.
Comment on lines +298 to +306
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.`);
}
}
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
const configChanged =
!currentForceUser ||
currentForceUser.extension !== config.extension ||
currentForceUser.password !== config.password;
Copy link

Copilot AI Feb 24, 2026

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.

Suggested change
currentForceUser.password !== config.password;
currentForceUser.password !== config.password ||
currentForceUser.display_name !== config.display_name;

Copilot uses AI. Check for mistakes.
@TECH7Fox
Copy link
Owner

Thanks @dovy6 for the PR! Will try it out soon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants