From 1653588b7fedd7da9d3c5bae661e9d60bb0c0e66 Mon Sep 17 00:00:00 2001
From: dovy6 <76541192+dovy6@users.noreply.github.com>
Date: Tue, 17 Feb 2026 14:36:49 -0500
Subject: [PATCH 1/3] Update index.ts
added sip-user-card
---
src/index.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/index.ts b/src/index.ts
index 912812f..b49f575 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -2,3 +2,4 @@ import "./sip-core";
import "./sip-call-dialog";
import "./sip-call-card";
import "./sip-contacts-card";
+import "./sip-user-card";
From 669d0afa33665db8ad0bbdee4e9b03c848a99783 Mon Sep 17 00:00:00 2001
From: dovy6 <76541192+dovy6@users.noreply.github.com>
Date: Tue, 17 Feb 2026 14:37:45 -0500
Subject: [PATCH 2/3] Create sip-user-card.ts
---
src/sip-user-card.ts | 153 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 153 insertions(+)
create mode 100644 src/sip-user-card.ts
diff --git a/src/sip-user-card.ts b/src/sip-user-card.ts
new file mode 100644
index 0000000..28548b0
--- /dev/null
+++ b/src/sip-user-card.ts
@@ -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);
+ }
+ `;
+ }
+
+// Called by HA with the YAML config whenever the dashboard loads or config changes.
+ 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;
+ }
+
+ static getStubConfig(): SIPUserCardConfig {
+ return {
+ extension: "100",
+ password: "mypassword",
+ display_name: "Front Door Panel",
+ };
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ window.addEventListener("sipcore-update", this.updateHandler);
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ window.removeEventListener("sipcore-update", this.updateHandler);
+ // Credentials remain in localStorage — this device stays registered as
+ // the forced extension until manually cleared via:
+ // localStorage.removeItem('sipcore-force-user')
+ }
+
+ private updateHandler = () => {
+ this.requestUpdate();
+ };
+
+ render() {
+ const registered = sipCore.registered;
+ const ext = this.config?.extension ?? "?";
+ const name = this.config?.display_name ?? ext;
+
+ return html`
+
+
+ ${name} (${ext}) — ${registered ? "Registered" : "Unregistered"}
+
+ `;
+ }
+
+ // Card height hint for HA layout engine — report 0 so it takes no grid space.
+ getCardSize() {
+ return 0;
+ }
+}
+
+window.customCards = window.customCards || [];
+window.customCards.push({
+ type: "sip-user-card",
+ name: "SIP User Card",
+ preview: false,
+ description: "Forces SIP Core to register as a specific Asterisk extension on this device, regardless of the logged-in HA user. Useful for shared tablets and kiosk dashboards.",
+});
From 42baa143a4d9f1036032299bdaaa7ddd1faf7095 Mon Sep 17 00:00:00 2001
From: dovy6 <76541192+dovy6@users.noreply.github.com>
Date: Tue, 17 Feb 2026 14:39:28 -0500
Subject: [PATCH 3/3] Update sip-core.ts
Allow for using sip-user-card to force registering a different user than the logged in HA user
---
src/sip-core.ts | 87 +++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 87 insertions(+)
diff --git a/src/sip-core.ts b/src/sip-core.ts
index 216b737..1a5e58e 100644
--- a/src/sip-core.ts
+++ b/src/sip-core.ts
@@ -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.`);
+ }
+ }
private async setupAudio() {
this.incomingAudio = new Audio(this.config.incomingRingtoneUrl);
@@ -319,6 +375,24 @@ export class SIPCore {
}
private async setupUser(): Promise {
+ // 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 {
+ console.info("sip-user-card config changed, re-registering...");
+ this.ua.stop();
+ await this.setupUser();
+ this.ua.start();
+ this.triggerUpdate();
+ }
/** Dispatches a `sipcore-update` event */
triggerUpdate() {