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