@riddix/hamh 2.1.0-alpha.839 → 2.1.0-alpha.840

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1767,8 +1767,8 @@ var init_Cancelable = __esm({
1767
1767
  };
1768
1768
  return result;
1769
1769
  }
1770
- static set logger(logger248) {
1771
- this.#logger = logger248;
1770
+ static set logger(logger250) {
1771
+ this.#logger = logger250;
1772
1772
  }
1773
1773
  static get logger() {
1774
1774
  return this.#logger;
@@ -3475,13 +3475,13 @@ var init_Logger = __esm({
3475
3475
  *
3476
3476
  * @deprecated use {@link destinations}
3477
3477
  */
3478
- static addLogger(identifier, logger248, options) {
3478
+ static addLogger(identifier, logger250, options) {
3479
3479
  if (identifier in this.destinations) {
3480
3480
  throw new ImplementationError(`Logger "${identifier}" already exists`);
3481
3481
  }
3482
3482
  const dest = LogDestination({ name: identifier });
3483
3483
  const legacy = adaptDestinationToLegacy(dest);
3484
- legacy.log = logger248;
3484
+ legacy.log = logger250;
3485
3485
  if (options?.defaultLogLevel !== void 0) {
3486
3486
  legacy.defaultLogLevel = options.defaultLogLevel;
3487
3487
  }
@@ -5687,8 +5687,8 @@ var init_Construction = __esm({
5687
5687
  }
5688
5688
  }
5689
5689
  #unhandledError(...args) {
5690
- const logger248 = Logger.get(this.#subject.constructor.name);
5691
- logger248.error(...args);
5690
+ const logger250 = Logger.get(this.#subject.constructor.name);
5691
+ logger250.error(...args);
5692
5692
  }
5693
5693
  #createErrorHandler(name) {
5694
5694
  return (e) => {
@@ -64070,8 +64070,8 @@ var init_NodeSession = __esm({
64070
64070
  return session?.type === SessionType.Unicast;
64071
64071
  }
64072
64072
  NodeSession2.is = is;
64073
- function logNew(logger248, operation, session, messenger, fabric, peerNodeId) {
64074
- logger248.info(
64073
+ function logNew(logger250, operation, session, messenger, fabric, peerNodeId) {
64074
+ logger250.info(
64075
64075
  session.via,
64076
64076
  `${operation} session with`,
64077
64077
  Diagnostic.strong(PeerAddress({ fabricIndex: fabric.fabricIndex, nodeId: peerNodeId }).toString()),
@@ -117714,11 +117714,11 @@ var init_Api = __esm({
117714
117714
  }
117715
117715
  Api2.resourceFor = resourceFor;
117716
117716
  function log(level, facility, id, ...message) {
117717
- let logger248 = loggers.get(facility);
117718
- if (!logger248) {
117719
- loggers.set(facility, logger248 = Logger.get(facility));
117717
+ let logger250 = loggers.get(facility);
117718
+ if (!logger250) {
117719
+ loggers.set(facility, logger250 = Logger.get(facility));
117720
117720
  }
117721
- logger248[level](Diagnostic.via(id || "(anon)"), message);
117721
+ logger250[level](Diagnostic.via(id || "(anon)"), message);
117722
117722
  }
117723
117723
  Api2.log = log;
117724
117724
  function logRequest(facility, id, method, target) {
@@ -132631,10 +132631,10 @@ var init_home_assistant_actions = __esm({
132631
132631
  circuitBreakerResetMs: 3e4
132632
132632
  };
132633
132633
  HomeAssistantActions = class extends Service {
132634
- constructor(logger248, client, config11) {
132634
+ constructor(logger250, client, config11) {
132635
132635
  super("HomeAssistantActions");
132636
132636
  this.client = client;
132637
- this.log = logger248.get(this);
132637
+ this.log = logger250.get(this);
132638
132638
  this.config = { ...defaultConfig, ...config11 };
132639
132639
  this.circuitBreaker = new CircuitBreaker(
132640
132640
  this.config.circuitBreakerThreshold,
@@ -132980,10 +132980,10 @@ var DiagnosticService = class {
132980
132980
  };
132981
132981
 
132982
132982
  // src/api/access-log.ts
132983
- function accessLogger(logger248) {
132983
+ function accessLogger(logger250) {
132984
132984
  return (req, res, next) => {
132985
132985
  res.on("finish", () => {
132986
- logger248.debug(
132986
+ logger250.debug(
132987
132987
  `${req.method} ${decodeURI(req.originalUrl)} ${res.statusCode} ${res.statusMessage} from ${req.socket.remoteAddress}`
132988
132988
  );
132989
132989
  });
@@ -137530,7 +137530,7 @@ var WebSocketApi = class {
137530
137530
 
137531
137531
  // src/api/web-api.ts
137532
137532
  var WebApi = class extends Service {
137533
- constructor(logger248, bridgeService, haClient, haRegistry, bridgeStorage, mappingStorage, identityStorage, lockCredentialStorage, settingsStorage, backupService, props) {
137533
+ constructor(logger250, bridgeService, haClient, haRegistry, bridgeStorage, mappingStorage, identityStorage, lockCredentialStorage, settingsStorage, backupService, props) {
137534
137534
  super("WebApi");
137535
137535
  this.bridgeService = bridgeService;
137536
137536
  this.haClient = haClient;
@@ -137542,8 +137542,8 @@ var WebApi = class extends Service {
137542
137542
  this.settingsStorage = settingsStorage;
137543
137543
  this.backupService = backupService;
137544
137544
  this.props = props;
137545
- this.logger = logger248;
137546
- this.log = logger248.get(this);
137545
+ this.logger = logger250;
137546
+ this.log = logger250.get(this);
137547
137547
  this.accessLogger = accessLogger(this.log.createChild("Access Log"));
137548
137548
  this.startTime = Date.now();
137549
137549
  this.wsApi = new WebSocketApi(
@@ -138734,10 +138734,10 @@ function isTransientConnectError(reason) {
138734
138734
  return msg.includes("socket hang up") || msg.includes("tls") || msg.includes("TLS");
138735
138735
  }
138736
138736
  var HomeAssistantClient = class extends Service {
138737
- constructor(logger248, options) {
138737
+ constructor(logger250, options) {
138738
138738
  super("HomeAssistantClient");
138739
138739
  this.options = options;
138740
- this.log = logger248.get(this);
138740
+ this.log = logger250.get(this);
138741
138741
  }
138742
138742
  options;
138743
138743
  static Options = /* @__PURE__ */ Symbol.for("HomeAssistantClientProps");
@@ -157335,11 +157335,11 @@ var AUTO_FORCE_SYNC_INTERVAL_MS = 9e4;
157335
157335
  var SHUTDOWN_SESSION_CLOSE_TIMEOUT_MS = 2500;
157336
157336
  var MDNS_ADDRESS_CHECK_INTERVAL_MS = 6e4;
157337
157337
  var Bridge = class {
157338
- constructor(env, logger248, dataProvider, endpointManager, serverOptions) {
157338
+ constructor(env, logger250, dataProvider, endpointManager, serverOptions) {
157339
157339
  this.dataProvider = dataProvider;
157340
157340
  this.endpointManager = endpointManager;
157341
157341
  this.serverOptions = serverOptions;
157342
- this.log = logger248.get(`Bridge / ${dataProvider.id}`);
157342
+ this.log = logger250.get(`Bridge / ${dataProvider.id}`);
157343
157343
  this.server = new BridgeServerNode(
157344
157344
  env,
157345
157345
  this.dataProvider,
@@ -172957,18 +172957,32 @@ var CameraAvStreamServer = class extends Base4 {
172957
172957
  })(CameraAvStreamServer || (CameraAvStreamServer = {}));
172958
172958
 
172959
172959
  // src/plugins/builtin/camera/webrtc-provider-server.ts
172960
+ init_esm();
172960
172961
  init_esm7();
172961
172962
  init_esm3();
172963
+ var logger244 = Logger.get("CameraWebRtc");
172962
172964
  var CameraWebRtcProviderServer = class extends WebRtcTransportProviderServer {
172963
172965
  solicitOffer(request) {
172964
172966
  const id = this.state.nextSessionId++;
172965
172967
  this.trackSession(id, request.streamUsage, request.originatingEndpointId);
172966
- void this.state.bridge.startSession(id, this.state.entityId).catch(() => {
172967
- });
172968
+ logger244.info(
172969
+ `solicitOffer session=${id} (${this.state.entityId}), deferred offer`
172970
+ );
172971
+ void this.state.bridge.startSession(id, this.state.entityId, {
172972
+ iceServers: request.iceServers,
172973
+ iceTransportPolicy: request.iceTransportPolicy
172974
+ }).catch(
172975
+ (err) => logger244.info(
172976
+ `solicitOffer startSession failed for ${this.state.entityId}: ${errText(err)}`
172977
+ )
172978
+ );
172968
172979
  return { webRtcSessionId: id, deferredOffer: true };
172969
172980
  }
172970
172981
  async provideOffer(request) {
172971
172982
  const id = request.webRtcSessionId ?? this.state.nextSessionId++;
172983
+ logger244.info(
172984
+ `provideOffer entry: entityId=${this.state.entityId} session=${id} (sdp ${request.sdp.length} chars)`
172985
+ );
172972
172986
  if (request.webRtcSessionId == null) {
172973
172987
  this.trackSession(
172974
172988
  id,
@@ -172976,30 +172990,62 @@ var CameraWebRtcProviderServer = class extends WebRtcTransportProviderServer {
172976
172990
  request.originatingEndpointId ?? EndpointNumber(0)
172977
172991
  );
172978
172992
  }
172979
- const answerSdp = await this.state.bridge.acceptControllerOffer(
172980
- id,
172981
- this.state.entityId,
172982
- request.sdp
172993
+ let answerSdp;
172994
+ try {
172995
+ answerSdp = await this.state.bridge.acceptControllerOffer(
172996
+ id,
172997
+ this.state.entityId,
172998
+ request.sdp,
172999
+ {
173000
+ iceServers: request.iceServers,
173001
+ iceTransportPolicy: request.iceTransportPolicy
173002
+ }
173003
+ );
173004
+ } catch (err) {
173005
+ const message = errText(err);
173006
+ logger244.info(
173007
+ `provideOffer failed for ${this.state.entityId} session=${id}: ${message}`
173008
+ );
173009
+ this.state.currentSessions = this.state.currentSessions.filter(
173010
+ (s) => s.id !== id
173011
+ );
173012
+ throw new StatusResponseError(
173013
+ `WebRTC offer failed: ${message}`,
173014
+ StatusCode.Failure
173015
+ );
173016
+ }
173017
+ logger244.info(
173018
+ `provideOffer answer computed for ${this.state.entityId} session=${id} (${answerSdp.length} chars); awaiting requestor delivery path`
172983
173019
  );
172984
173020
  void answerSdp;
172985
173021
  return { webRtcSessionId: id };
172986
173022
  }
172987
173023
  provideAnswer(request) {
173024
+ logger244.info(
173025
+ `provideAnswer session=${request.webRtcSessionId} (sdp ${request.sdp.length} chars, ${this.state.entityId})`
173026
+ );
172988
173027
  return this.state.bridge.acceptControllerAnswer(
172989
173028
  request.webRtcSessionId,
172990
173029
  request.sdp
172991
173030
  );
172992
173031
  }
172993
173032
  async provideIceCandidates(request) {
173033
+ logger244.info(
173034
+ `provideIceCandidates session=${request.webRtcSessionId}: ${request.iceCandidates.length} candidate(s) (${this.state.entityId})`
173035
+ );
172994
173036
  for (const c of request.iceCandidates) {
172995
173037
  await this.state.bridge.addControllerIceCandidate(
172996
173038
  request.webRtcSessionId,
172997
173039
  c.candidate,
172998
- c.sdpMid
173040
+ c.sdpMid,
173041
+ c.sdpmLineIndex
172999
173042
  );
173000
173043
  }
173001
173044
  }
173002
173045
  async endSession(request) {
173046
+ logger244.info(
173047
+ `endSession session=${request.webRtcSessionId} (${this.state.entityId})`
173048
+ );
173003
173049
  await this.state.bridge.endSession(request.webRtcSessionId);
173004
173050
  this.state.currentSessions = this.state.currentSessions.filter(
173005
173051
  (s) => s.id !== request.webRtcSessionId
@@ -173020,6 +173066,9 @@ var CameraWebRtcProviderServer = class extends WebRtcTransportProviderServer {
173020
173066
  ];
173021
173067
  }
173022
173068
  };
173069
+ function errText(err) {
173070
+ return err instanceof Error ? err.message : String(err);
173071
+ }
173023
173072
  ((CameraWebRtcProviderServer2) => {
173024
173073
  class State extends WebRtcTransportProviderServer.State {
173025
173074
  bridge;
@@ -173106,108 +173155,180 @@ function createCameraEndpointType(bridge, entityId, sensor = defaultSensorParams
173106
173155
  }
173107
173156
 
173108
173157
  // src/plugins/builtin/camera/webrtc-bridge.ts
173158
+ init_esm();
173109
173159
  import {
173110
173160
  createConnection as createConnection3,
173111
173161
  createLongLivedTokenAuth as createLongLivedTokenAuth2
173112
173162
  } from "home-assistant-js-websocket";
173113
173163
  import { RTCPeerConnection } from "werift";
173164
+ var logger245 = Logger.get("CameraWebRtc");
173165
+ var DEFAULT_HA_WEBRTC_TIMEOUT_MS = 15e3;
173166
+ var haWebRtcTimeoutMs = DEFAULT_HA_WEBRTC_TIMEOUT_MS;
173114
173167
  var WebRtcBridge = class {
173115
- constructor(config11) {
173168
+ constructor(config11, deps = {}) {
173116
173169
  this.config = config11;
173170
+ this.deps = deps;
173117
173171
  }
173118
173172
  config;
173173
+ deps;
173119
173174
  connection;
173120
173175
  sessions = /* @__PURE__ */ new Map();
173121
173176
  async ha() {
173122
173177
  if (this.connection) return this.connection;
173123
- const auth = createLongLivedTokenAuth2(
173124
- this.config.haUrl,
173125
- this.config.haToken
173126
- );
173127
- this.connection = await createConnection3({ auth });
173178
+ if (this.deps.connect) {
173179
+ this.connection = await this.deps.connect(this.config);
173180
+ } else {
173181
+ const auth = createLongLivedTokenAuth2(
173182
+ this.config.haUrl,
173183
+ this.config.haToken
173184
+ );
173185
+ this.connection = await createConnection3({ auth });
173186
+ }
173128
173187
  return this.connection;
173129
173188
  }
173130
173189
  // We offer to the controller; it answers. Pulls HA media first, then forwards.
173131
- async startSession(matterSessionId, entityId) {
173190
+ async startSession(matterSessionId, entityId, ice) {
173132
173191
  const haPeer = new RTCPeerConnection();
173133
- const controllerPeer = new RTCPeerConnection();
173134
- haPeer.onTrack.subscribe((track) => {
173135
- const transceiver = controllerPeer.addTransceiver(
173136
- track.kind,
173137
- { direction: "sendonly" }
173192
+ const controllerPeer = new RTCPeerConnection(
173193
+ this.controllerConfig(entityId, ice)
173194
+ );
173195
+ this.wireStateLogging(haPeer, "haPeer", entityId);
173196
+ this.wireStateLogging(controllerPeer, "controllerPeer", entityId);
173197
+ const senders = /* @__PURE__ */ new Map();
173198
+ senders.set(
173199
+ "video",
173200
+ controllerPeer.addTransceiver("video", { direction: "sendonly" })
173201
+ );
173202
+ this.forwardHaTracks(haPeer, senders, entityId);
173203
+ this.sessions.set(matterSessionId, { entityId, haPeer, controllerPeer });
173204
+ try {
173205
+ haPeer.addTransceiver("video", { direction: "recvonly" });
173206
+ haPeer.addTransceiver("audio", { direction: "recvonly" });
173207
+ const haOffer = await haPeer.createOffer();
173208
+ await haPeer.setLocalDescription(haOffer);
173209
+ const haOfferSdp = this.localSdp(haPeer);
173210
+ logger245.info(`HA offer sent for ${entityId} (${haOfferSdp.length} chars)`);
173211
+ const { answer, sessionId, unsubscribe } = await this.requestHaWebRtc(
173212
+ entityId,
173213
+ haOfferSdp,
173214
+ haPeer
173138
173215
  );
173139
- track.onReceiveRtp.subscribe((rtp) => transceiver.sender.sendRtp(rtp));
173140
- });
173141
- haPeer.addTransceiver("video", { direction: "recvonly" });
173142
- haPeer.addTransceiver("audio", { direction: "recvonly" });
173143
- const haOffer = await haPeer.createOffer();
173144
- await haPeer.setLocalDescription(haOffer);
173145
- const haAnswer = await this.requestHaWebRtc(entityId, haOffer.sdp);
173146
- await haPeer.setRemoteDescription({ type: "answer", sdp: haAnswer });
173147
- const controllerOffer = await controllerPeer.createOffer();
173148
- await controllerPeer.setLocalDescription(controllerOffer);
173149
- const iceCandidates = [];
173150
- controllerPeer.onIceCandidate.subscribe((c) => {
173151
- if (c?.candidate)
173152
- iceCandidates.push({
173153
- candidate: c.candidate,
173154
- sdpMid: c.sdpMid ?? null
173155
- });
173156
- });
173157
- this.sessions.set(matterSessionId, {
173158
- entityId,
173159
- haPeer,
173160
- controllerPeer
173161
- });
173162
- return { sdp: controllerOffer.sdp, iceCandidates };
173216
+ const session = this.sessions.get(matterSessionId);
173217
+ if (session) {
173218
+ session.haSessionId = sessionId;
173219
+ session.haUnsubscribe = unsubscribe;
173220
+ }
173221
+ await haPeer.setRemoteDescription({ type: "answer", sdp: answer });
173222
+ const iceCandidates = [];
173223
+ controllerPeer.onIceCandidate.subscribe((c) => {
173224
+ if (c?.candidate)
173225
+ iceCandidates.push({
173226
+ candidate: c.candidate,
173227
+ sdpMid: c.sdpMid ?? null
173228
+ });
173229
+ });
173230
+ const controllerOffer = await controllerPeer.createOffer();
173231
+ await controllerPeer.setLocalDescription(controllerOffer);
173232
+ const sdp = this.localSdp(controllerPeer);
173233
+ logger245.info(
173234
+ `controller offer created for ${entityId} (${sdp.length} chars)`
173235
+ );
173236
+ return { sdp, iceCandidates };
173237
+ } catch (err) {
173238
+ logger245.info(`startSession failed for ${entityId}: ${errText2(err)}`);
173239
+ await this.endSession(matterSessionId);
173240
+ throw err;
173241
+ }
173163
173242
  }
173164
173243
  // ProvideOffer flow: controller sent its offer. Pull HA media, answer it.
173165
173244
  // Returning the answer to the controller is the unverified Requestor path.
173166
- async acceptControllerOffer(matterSessionId, entityId, controllerOfferSdp) {
173245
+ async acceptControllerOffer(matterSessionId, entityId, controllerOfferSdp, ice) {
173167
173246
  const haPeer = new RTCPeerConnection();
173168
- const controllerPeer = new RTCPeerConnection();
173169
- haPeer.onTrack.subscribe((track) => {
173170
- const transceiver = controllerPeer.addTransceiver(track.kind, {
173171
- direction: "sendonly"
173172
- });
173173
- track.onReceiveRtp.subscribe((rtp) => transceiver.sender.sendRtp(rtp));
173174
- });
173247
+ const controllerPeer = new RTCPeerConnection(
173248
+ this.controllerConfig(entityId, ice)
173249
+ );
173250
+ this.wireStateLogging(haPeer, "haPeer", entityId);
173251
+ this.wireStateLogging(controllerPeer, "controllerPeer", entityId);
173252
+ const senders = /* @__PURE__ */ new Map();
173253
+ for (const kind of offerKinds(controllerOfferSdp)) {
173254
+ senders.set(
173255
+ kind,
173256
+ controllerPeer.addTransceiver(kind, { direction: "sendonly" })
173257
+ );
173258
+ }
173259
+ this.forwardHaTracks(haPeer, senders, entityId);
173175
173260
  this.sessions.set(matterSessionId, { entityId, haPeer, controllerPeer });
173176
- haPeer.addTransceiver("video", { direction: "recvonly" });
173177
- haPeer.addTransceiver("audio", { direction: "recvonly" });
173178
- const haOffer = await haPeer.createOffer();
173179
- await haPeer.setLocalDescription(haOffer);
173180
- const haAnswer = await this.requestHaWebRtc(entityId, haOffer.sdp);
173181
- await haPeer.setRemoteDescription({ type: "answer", sdp: haAnswer });
173182
- await controllerPeer.setRemoteDescription({
173183
- type: "offer",
173184
- sdp: controllerOfferSdp
173185
- });
173186
- const answer = await controllerPeer.createAnswer();
173187
- await controllerPeer.setLocalDescription(answer);
173188
- return answer.sdp;
173261
+ try {
173262
+ haPeer.addTransceiver("video", { direction: "recvonly" });
173263
+ haPeer.addTransceiver("audio", { direction: "recvonly" });
173264
+ const haOffer = await haPeer.createOffer();
173265
+ await haPeer.setLocalDescription(haOffer);
173266
+ const haOfferSdp = this.localSdp(haPeer);
173267
+ logger245.info(`HA offer sent for ${entityId} (${haOfferSdp.length} chars)`);
173268
+ const { answer, sessionId, unsubscribe } = await this.requestHaWebRtc(
173269
+ entityId,
173270
+ haOfferSdp,
173271
+ haPeer
173272
+ );
173273
+ const session = this.sessions.get(matterSessionId);
173274
+ if (session) {
173275
+ session.haSessionId = sessionId;
173276
+ session.haUnsubscribe = unsubscribe;
173277
+ }
173278
+ await haPeer.setRemoteDescription({ type: "answer", sdp: answer });
173279
+ await controllerPeer.setRemoteDescription({
173280
+ type: "offer",
173281
+ sdp: controllerOfferSdp
173282
+ });
173283
+ const controllerAnswer = await controllerPeer.createAnswer();
173284
+ await controllerPeer.setLocalDescription(controllerAnswer);
173285
+ const answerSdp = this.localSdp(controllerPeer);
173286
+ logger245.info(
173287
+ `controller answer created for ${entityId} (${answerSdp.length} chars)`
173288
+ );
173289
+ return answerSdp;
173290
+ } catch (err) {
173291
+ logger245.info(
173292
+ `acceptControllerOffer failed for ${entityId}: ${errText2(err)}`
173293
+ );
173294
+ await this.endSession(matterSessionId);
173295
+ throw err;
173296
+ }
173189
173297
  }
173190
173298
  /** Apply the Matter controller's SDP answer to the controller peer. */
173191
173299
  async acceptControllerAnswer(matterSessionId, sdp) {
173192
173300
  const session = this.sessions.get(matterSessionId);
173193
173301
  if (!session) return;
173302
+ logger245.debug(`controller answer applied for ${session.entityId}`);
173194
173303
  await session.controllerPeer.setRemoteDescription({ type: "answer", sdp });
173195
173304
  }
173196
173305
  /** Add a remote ICE candidate from the Matter controller. */
173197
- async addControllerIceCandidate(matterSessionId, candidate, sdpMid) {
173306
+ async addControllerIceCandidate(matterSessionId, candidate, sdpMid, sdpMLineIndex) {
173198
173307
  const session = this.sessions.get(matterSessionId);
173199
173308
  if (!session) return;
173309
+ logger245.debug(
173310
+ `controller ICE candidate for ${session.entityId}: ${candidate}`
173311
+ );
173200
173312
  await session.controllerPeer.addIceCandidate({
173201
173313
  candidate,
173202
- sdpMid: sdpMid ?? void 0
173314
+ sdpMid: sdpMid ?? void 0,
173315
+ sdpMLineIndex: sdpMLineIndex ?? void 0
173203
173316
  });
173204
173317
  }
173205
173318
  async endSession(matterSessionId) {
173206
173319
  const session = this.sessions.get(matterSessionId);
173207
173320
  if (!session) return;
173321
+ logger245.info(`ending session ${matterSessionId} (${session.entityId})`);
173208
173322
  this.sessions.delete(matterSessionId);
173209
- await session.haPeer.close();
173210
- await session.controllerPeer.close();
173323
+ if (session.haUnsubscribe) {
173324
+ await Promise.resolve(session.haUnsubscribe()).catch(
173325
+ (err) => logger245.debug(`HA unsubscribe failed: ${errText2(err)}`)
173326
+ );
173327
+ }
173328
+ await session.haPeer.close().catch(() => {
173329
+ });
173330
+ await session.controllerPeer.close().catch(() => {
173331
+ });
173211
173332
  if (session.haSessionId) {
173212
173333
  try {
173213
173334
  const conn = await this.ha();
@@ -173224,10 +173345,19 @@ var WebRtcBridge = class {
173224
173345
  // Grab a still JPEG via HA's camera proxy (for CaptureSnapshot).
173225
173346
  async snapshot(entityId) {
173226
173347
  const url2 = `${this.config.haUrl}/api/camera_proxy/${entityId}`;
173227
- const res = await fetch(url2, {
173228
- headers: { Authorization: `Bearer ${this.config.haToken}` }
173229
- });
173348
+ let res;
173349
+ try {
173350
+ res = await fetch(url2, {
173351
+ headers: { Authorization: `Bearer ${this.config.haToken}` }
173352
+ });
173353
+ } catch (err) {
173354
+ logger245.info(`snapshot fetch failed for ${entityId}: ${errText2(err)}`);
173355
+ throw err;
173356
+ }
173230
173357
  if (!res.ok) {
173358
+ logger245.info(
173359
+ `snapshot fetch failed for ${entityId}: camera_proxy ${res.status}`
173360
+ );
173231
173361
  throw new Error(`HA camera_proxy ${entityId}: ${res.status}`);
173232
173362
  }
173233
173363
  return new Uint8Array(await res.arrayBuffer());
@@ -173239,29 +173369,157 @@ var WebRtcBridge = class {
173239
173369
  this.connection?.close();
173240
173370
  this.connection = void 0;
173241
173371
  }
173242
- // Offer to HA's WebRTC, resolve on the first answer event off the same sub.
173243
- async requestHaWebRtc(entityId, offerSdp) {
173372
+ // Forward every track HA sends into the matching pre-added controller sender.
173373
+ forwardHaTracks(haPeer, senders, entityId) {
173374
+ haPeer.onTrack.subscribe((track) => {
173375
+ logger245.info(`onTrack ${track.kind} from HA (${entityId})`);
173376
+ const transceiver = senders.get(track.kind);
173377
+ if (!transceiver) {
173378
+ logger245.info(
173379
+ `no controller transceiver for ${track.kind} (${entityId})`
173380
+ );
173381
+ return;
173382
+ }
173383
+ let firstLogged = false;
173384
+ track.onReceiveRtp.subscribe((rtp) => {
173385
+ if (!firstLogged) {
173386
+ firstLogged = true;
173387
+ logger245.info(`first RTP forwarded for ${track.kind} (${entityId})`);
173388
+ }
173389
+ transceiver.sender.sendRtp(rtp);
173390
+ });
173391
+ });
173392
+ }
173393
+ // Log connection/ice state transitions (these events fire on change only).
173394
+ wireStateLogging(peer, label, entityId) {
173395
+ peer.connectionStateChange.subscribe(
173396
+ (s) => logger245.info(`${label} connectionState=${s} (${entityId})`)
173397
+ );
173398
+ peer.iceConnectionStateChange.subscribe(
173399
+ (s) => logger245.info(`${label} iceConnectionState=${s} (${entityId})`)
173400
+ );
173401
+ }
173402
+ // Map the Matter ICE struct onto werift's config. Only the controller peer
173403
+ // uses controller-supplied servers; the HA peer keeps werift defaults.
173404
+ controllerConfig(entityId, ice) {
173405
+ const iceServers = (ice?.iceServers ?? []).flatMap(
173406
+ (s) => (s.urLs ?? []).map((url2) => ({
173407
+ urls: url2,
173408
+ username: s.username,
173409
+ credential: s.credential
173410
+ }))
173411
+ );
173412
+ if (iceServers.length === 0) {
173413
+ logger245.info(`controller peer using default ICE servers (${entityId})`);
173414
+ return void 0;
173415
+ }
173416
+ const policy = ice?.iceTransportPolicy === "relay" || ice?.iceTransportPolicy === "all" ? ice.iceTransportPolicy : void 0;
173417
+ logger245.info(
173418
+ `controller peer using ${iceServers.length} controller ICE server(s)${policy ? ` policy=${policy}` : ""} (${entityId})`
173419
+ );
173420
+ return policy ? { iceServers, iceTransportPolicy: policy } : { iceServers };
173421
+ }
173422
+ localSdp(peer) {
173423
+ const local = peer.localDescription;
173424
+ if (!local) throw new Error("no local description");
173425
+ return local.sdp;
173426
+ }
173427
+ // Feed an HA trickle-ICE candidate into the HA peer.
173428
+ feedHaCandidate(entityId, haPeer, candidate) {
173429
+ const value = candidate?.candidate;
173430
+ if (!value) return;
173431
+ logger245.debug(`HA ICE candidate for ${entityId}: ${value}`);
173432
+ void haPeer.addIceCandidate({
173433
+ candidate: value,
173434
+ sdpMid: candidate?.sdpMid ?? void 0,
173435
+ sdpMLineIndex: candidate?.sdpMLineIndex ?? void 0
173436
+ }).catch(
173437
+ (e) => logger245.debug(
173438
+ `HA addIceCandidate failed for ${entityId}: ${errText2(e)}`
173439
+ )
173440
+ );
173441
+ }
173442
+ // Offer to HA's WebRTC and resolve on the first answer. Rejects on an error
173443
+ // reply or after a timeout; keeps the subscription open past the answer so HA
173444
+ // trickle candidates keep flowing into the HA peer until the session ends.
173445
+ async requestHaWebRtc(entityId, offerSdp, haPeer) {
173244
173446
  const conn = await this.ha();
173245
- return new Promise((resolve11, reject) => {
173447
+ let unsubscribe = () => {
173448
+ };
173449
+ let sessionId;
173450
+ const answer = await new Promise((resolve11, reject) => {
173246
173451
  let settled = false;
173452
+ let rejected = false;
173453
+ let timer;
173454
+ const settle = (fn) => {
173455
+ if (settled) return;
173456
+ settled = true;
173457
+ clearTimeout(timer);
173458
+ fn();
173459
+ };
173460
+ const fail = (err) => settle(() => {
173461
+ rejected = true;
173462
+ unsubscribe();
173463
+ reject(err);
173464
+ });
173465
+ timer = setTimeout(() => {
173466
+ logger245.info(
173467
+ `HA WebRTC timed out after ${haWebRtcTimeoutMs}ms for ${entityId}`
173468
+ );
173469
+ fail(
173470
+ new Error(`HA WebRTC request timed out after ${haWebRtcTimeoutMs}ms`)
173471
+ );
173472
+ }, haWebRtcTimeoutMs);
173247
173473
  conn.subscribeMessage(
173248
173474
  (msg) => {
173249
- if (msg.type === "answer" && msg.answer && !settled) {
173250
- settled = true;
173251
- const session = [...this.sessions.values()].find(
173252
- (s) => s.entityId === entityId
173253
- );
173254
- if (session && msg.session_id) {
173255
- session.haSessionId = msg.session_id;
173475
+ switch (msg.type) {
173476
+ case "session":
173477
+ if (msg.session_id) sessionId = msg.session_id;
173478
+ break;
173479
+ case "answer":
173480
+ if (msg.answer) {
173481
+ if (msg.session_id) sessionId = msg.session_id;
173482
+ const ans = msg.answer;
173483
+ settle(() => {
173484
+ logger245.info(
173485
+ `HA answer received for ${entityId} (${ans.length} chars)`
173486
+ );
173487
+ resolve11(ans);
173488
+ });
173489
+ }
173490
+ break;
173491
+ case "error": {
173492
+ const detail = msg.message ?? msg.code ?? "unknown error";
173493
+ logger245.info(`HA WebRTC error for ${entityId}: ${detail}`);
173494
+ fail(new Error(`HA WebRTC error: ${detail}`));
173495
+ break;
173256
173496
  }
173257
- resolve11(msg.answer);
173497
+ case "candidate":
173498
+ this.feedHaCandidate(entityId, haPeer, msg.candidate);
173499
+ break;
173258
173500
  }
173259
173501
  },
173260
173502
  { type: "camera/webrtc/offer", entity_id: entityId, offer: offerSdp }
173261
- ).catch(reject);
173503
+ ).then((unsub) => {
173504
+ unsubscribe = unsub;
173505
+ if (rejected) unsub();
173506
+ }).catch((err) => {
173507
+ fail(err instanceof Error ? err : new Error(String(err)));
173508
+ });
173262
173509
  });
173510
+ return { answer, sessionId, unsubscribe };
173263
173511
  }
173264
173512
  };
173513
+ function offerKinds(sdp) {
173514
+ const kinds = [];
173515
+ for (const m of sdp.matchAll(/^m=(video|audio)/gm)) {
173516
+ kinds.push(m[1]);
173517
+ }
173518
+ return kinds;
173519
+ }
173520
+ function errText2(err) {
173521
+ return err instanceof Error ? err.message : String(err);
173522
+ }
173265
173523
 
173266
173524
  // src/plugins/builtin/camera/camera-plugin.ts
173267
173525
  var CONFIG_KEY = "config";
@@ -173407,7 +173665,7 @@ import {
173407
173665
  getCollection
173408
173666
  } from "home-assistant-js-websocket";
173409
173667
  import { atLeastHaVersion } from "home-assistant-js-websocket/dist/util.js";
173410
- var logger244 = Logger.get("SubscribeEntities");
173668
+ var logger246 = Logger.get("SubscribeEntities");
173411
173669
  function processEvent(store, updates) {
173412
173670
  const state = { ...store.state };
173413
173671
  if (updates.a) {
@@ -173433,7 +173691,7 @@ function processEvent(store, updates) {
173433
173691
  for (const entityId in updates.c) {
173434
173692
  let entityState = state[entityId];
173435
173693
  if (!entityState) {
173436
- logger244.warn("Received state update for unknown entity", entityId);
173694
+ logger246.warn("Received state update for unknown entity", entityId);
173437
173695
  continue;
173438
173696
  }
173439
173697
  entityState = { ...entityState };
@@ -173522,7 +173780,7 @@ var subscribeEntities = (conn, onChange, entityIds) => {
173522
173780
  // src/services/bridges/entity-isolation-service.ts
173523
173781
  init_esm();
173524
173782
  init_diagnostic_event_bus();
173525
- var logger245 = Logger.get("EntityIsolation");
173783
+ var logger247 = Logger.get("EntityIsolation");
173526
173784
  var EntityIsolationServiceImpl = class {
173527
173785
  isolatedEntities = /* @__PURE__ */ new Map();
173528
173786
  isolationCallbacks = /* @__PURE__ */ new Map();
@@ -173587,13 +173845,13 @@ var EntityIsolationServiceImpl = class {
173587
173845
  }
173588
173846
  const parsed = this.parseEndpointPath(msg);
173589
173847
  if (!parsed) {
173590
- logger245.warn("Could not parse entity from error:", msg);
173848
+ logger247.warn("Could not parse entity from error:", msg);
173591
173849
  return false;
173592
173850
  }
173593
173851
  const { bridgeId, entityName } = parsed;
173594
173852
  const callback = this.isolationCallbacks.get(bridgeId);
173595
173853
  if (!callback) {
173596
- logger245.warn(
173854
+ logger247.warn(
173597
173855
  `No isolation callback registered for bridge ${bridgeId}, entity: ${entityName}`
173598
173856
  );
173599
173857
  return false;
@@ -173608,7 +173866,7 @@ var EntityIsolationServiceImpl = class {
173608
173866
  reason,
173609
173867
  failedAt: (/* @__PURE__ */ new Date()).toISOString()
173610
173868
  });
173611
- logger245.warn(
173869
+ logger247.warn(
173612
173870
  `Isolating entity "${entityName}" from bridge ${bridgeId} due to: ${reason}`
173613
173871
  );
173614
173872
  diagnosticEventBus.emit("entity_error", `Entity isolated: ${entityName}`, {
@@ -173620,7 +173878,7 @@ var EntityIsolationServiceImpl = class {
173620
173878
  await callback(entityName);
173621
173879
  return true;
173622
173880
  } catch (e) {
173623
- logger245.error(`Failed to isolate entity ${entityName}:`, e);
173881
+ logger247.error(`Failed to isolate entity ${entityName}:`, e);
173624
173882
  return false;
173625
173883
  }
173626
173884
  }
@@ -175138,11 +175396,11 @@ function makeWarmStartState(state, now = (/* @__PURE__ */ new Date()).toISOStrin
175138
175396
  return { ...state, last_updated: now };
175139
175397
  }
175140
175398
  var ServerModeBridge = class {
175141
- constructor(logger248, dataProvider, endpointManager, server) {
175399
+ constructor(logger250, dataProvider, endpointManager, server) {
175142
175400
  this.dataProvider = dataProvider;
175143
175401
  this.endpointManager = endpointManager;
175144
175402
  this.server = server;
175145
- this.log = logger248.get(`ServerModeBridge / ${dataProvider.id}`);
175403
+ this.log = logger250.get(`ServerModeBridge / ${dataProvider.id}`);
175146
175404
  }
175147
175405
  dataProvider;
175148
175406
  endpointManager;
@@ -176172,7 +176430,7 @@ function ServerModeVacuumDevice(homeAssistantEntity, includeOnOff = false, clean
176172
176430
  }
176173
176431
 
176174
176432
  // src/matter/endpoints/server-mode-vacuum-endpoint.ts
176175
- var logger246 = Logger.get("ServerModeVacuumEndpoint");
176433
+ var logger248 = Logger.get("ServerModeVacuumEndpoint");
176176
176434
  var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEndpoint {
176177
176435
  static async create(registry2, entityId, mapping, endpointId) {
176178
176436
  const deviceRegistry = registry2.deviceOf(entityId);
@@ -176185,7 +176443,7 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
176185
176443
  if (mapping?.disableBatteryMapping && mapping.batteryEntity) {
176186
176444
  effectiveMapping = { ...mapping, batteryEntity: void 0 };
176187
176445
  }
176188
- logger246.info(
176446
+ logger248.info(
176189
176447
  `${entityId}: device_id=${entity.device_id}, manualBattery=${mapping?.batteryEntity ?? "none"}`
176190
176448
  );
176191
176449
  if (entity.device_id) {
@@ -176200,15 +176458,15 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
176200
176458
  batteryEntity: batteryEntityId
176201
176459
  };
176202
176460
  registry2.markBatteryEntityUsed(batteryEntityId);
176203
- logger246.info(`${entityId}: Auto-assigned battery ${batteryEntityId}`);
176461
+ logger248.info(`${entityId}: Auto-assigned battery ${batteryEntityId}`);
176204
176462
  } else {
176205
176463
  const attrs = state.attributes;
176206
176464
  if (attrs.battery_level != null || attrs.battery != null) {
176207
- logger246.info(
176465
+ logger248.info(
176208
176466
  `${entityId}: No battery entity found, using battery attribute from vacuum state`
176209
176467
  );
176210
176468
  } else {
176211
- logger246.warn(
176469
+ logger248.warn(
176212
176470
  `${entityId}: No battery entity found for device ${entity.device_id}`
176213
176471
  );
176214
176472
  }
@@ -176223,7 +176481,7 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
176223
176481
  entityId: effectiveMapping?.entityId ?? entityId,
176224
176482
  cleaningModeEntity: vacuumEntities.cleaningModeEntity
176225
176483
  };
176226
- logger246.info(
176484
+ logger248.info(
176227
176485
  `${entityId}: Auto-assigned cleaningMode ${vacuumEntities.cleaningModeEntity}`
176228
176486
  );
176229
176487
  }
@@ -176233,7 +176491,7 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
176233
176491
  entityId: effectiveMapping?.entityId ?? entityId,
176234
176492
  suctionLevelEntity: vacuumEntities.suctionLevelEntity
176235
176493
  };
176236
- logger246.info(
176494
+ logger248.info(
176237
176495
  `${entityId}: Auto-assigned suctionLevel ${vacuumEntities.suctionLevelEntity}`
176238
176496
  );
176239
176497
  }
@@ -176243,7 +176501,7 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
176243
176501
  entityId: effectiveMapping?.entityId ?? entityId,
176244
176502
  mopIntensityEntity: vacuumEntities.mopIntensityEntity
176245
176503
  };
176246
- logger246.info(
176504
+ logger248.info(
176247
176505
  `${entityId}: Auto-assigned mopIntensity ${vacuumEntities.mopIntensityEntity}`
176248
176506
  );
176249
176507
  }
@@ -176253,7 +176511,7 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
176253
176511
  entityId: effectiveMapping?.entityId ?? entityId,
176254
176512
  currentRoomEntity: vacuumEntities.currentRoomEntity
176255
176513
  };
176256
- logger246.info(
176514
+ logger248.info(
176257
176515
  `${entityId}: Auto-assigned currentRoom ${vacuumEntities.currentRoomEntity}`
176258
176516
  );
176259
176517
  }
@@ -176268,7 +176526,7 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
176268
176526
  entityId: effectiveMapping?.entityId ?? entityId,
176269
176527
  cleanAreaRooms
176270
176528
  };
176271
- logger246.info(
176529
+ logger248.info(
176272
176530
  `${entityId}: Using ${cleanAreaRooms.length} HA areas via CLEAN_AREA`
176273
176531
  );
176274
176532
  }
@@ -176289,7 +176547,7 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
176289
176547
  rooms: roomsObj
176290
176548
  }
176291
176549
  };
176292
- logger246.info(
176550
+ logger248.info(
176293
176551
  `${entityId}: Auto-detected ${valetudoRooms.length} Valetudo segments`
176294
176552
  );
176295
176553
  } else {
@@ -176306,14 +176564,14 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
176306
176564
  rooms: roomsObj
176307
176565
  }
176308
176566
  };
176309
- logger246.info(
176567
+ logger248.info(
176310
176568
  `${entityId}: Auto-detected ${roborockRooms.length} Roborock rooms`
176311
176569
  );
176312
176570
  }
176313
176571
  }
176314
176572
  }
176315
176573
  } else {
176316
- logger246.warn(`${entityId}: No device_id, cannot auto-assign battery`);
176574
+ logger248.warn(`${entityId}: No device_id, cannot auto-assign battery`);
176317
176575
  }
176318
176576
  const payload = {
176319
176577
  entity_id: entityId,
@@ -176386,11 +176644,11 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
176386
176644
  }
176387
176645
  if (mappedChanged) {
176388
176646
  this.pendingMappedChange = true;
176389
- logger246.debug(
176647
+ logger248.debug(
176390
176648
  `Mapped entity change detected for ${this.entityId}, forcing update`
176391
176649
  );
176392
176650
  }
176393
- logger246.debug(
176651
+ logger248.debug(
176394
176652
  `State update received for ${this.entityId}: state=${state.state}`
176395
176653
  );
176396
176654
  this.lastState = state;
@@ -176968,10 +177226,10 @@ var BridgeEnvironmentFactory = class extends BridgeFactory {
176968
177226
  // src/core/ioc/app-environment.ts
176969
177227
  var AppEnvironment = class _AppEnvironment extends EnvironmentBase {
176970
177228
  constructor(rootEnv, options) {
176971
- const logger248 = rootEnv.get(LoggerService);
177229
+ const logger250 = rootEnv.get(LoggerService);
176972
177230
  super({
176973
177231
  id: "App",
176974
- log: logger248.get("AppContainer"),
177232
+ log: logger250.get("AppContainer"),
176975
177233
  parent: rootEnv
176976
177234
  });
176977
177235
  this.options = options;
@@ -176985,8 +177243,8 @@ var AppEnvironment = class _AppEnvironment extends EnvironmentBase {
176985
177243
  }
176986
177244
  construction;
176987
177245
  async init() {
176988
- const logger248 = this.get(LoggerService);
176989
- this.set(LoggerService, logger248);
177246
+ const logger250 = this.get(LoggerService);
177247
+ this.set(LoggerService, logger250);
176990
177248
  this.set(AppStorage, new AppStorage(await this.load(StorageService)));
176991
177249
  this.set(BridgeStorage, new BridgeStorage(await this.load(AppStorage)));
176992
177250
  this.set(
@@ -177007,7 +177265,7 @@ var AppEnvironment = class _AppEnvironment extends EnvironmentBase {
177007
177265
  );
177008
177266
  this.set(
177009
177267
  HomeAssistantClient,
177010
- new HomeAssistantClient(logger248, this.options.homeAssistant)
177268
+ new HomeAssistantClient(logger250, this.options.homeAssistant)
177011
177269
  );
177012
177270
  this.set(
177013
177271
  HomeAssistantConfig,
@@ -177015,7 +177273,7 @@ var AppEnvironment = class _AppEnvironment extends EnvironmentBase {
177015
177273
  );
177016
177274
  this.set(
177017
177275
  HomeAssistantActions,
177018
- new HomeAssistantActions(logger248, await this.load(HomeAssistantClient))
177276
+ new HomeAssistantActions(logger250, await this.load(HomeAssistantClient))
177019
177277
  );
177020
177278
  this.set(
177021
177279
  HomeAssistantRegistry,
@@ -177051,7 +177309,7 @@ var AppEnvironment = class _AppEnvironment extends EnvironmentBase {
177051
177309
  this.set(
177052
177310
  WebApi,
177053
177311
  new WebApi(
177054
- logger248,
177312
+ logger250,
177055
177313
  await this.load(BridgeService),
177056
177314
  await this.load(HomeAssistantClient),
177057
177315
  await this.load(HomeAssistantRegistry),
@@ -177072,7 +177330,7 @@ var AppEnvironment = class _AppEnvironment extends EnvironmentBase {
177072
177330
 
177073
177331
  // src/matter/patches/patch-level-control-tlv.ts
177074
177332
  init_esm();
177075
- var logger247 = Logger.get("PatchLevelControlTlv");
177333
+ var logger249 = Logger.get("PatchLevelControlTlv");
177076
177334
  var optionalFieldModels = /* @__PURE__ */ new WeakSet();
177077
177335
  var fieldModelMandatoryGetterPatched = false;
177078
177336
  function patchLevelControlTlv() {
@@ -177128,11 +177386,11 @@ function patchLevelControlTlv() {
177128
177386
  patched++;
177129
177387
  }
177130
177388
  if (patched > 0) {
177131
- logger247.info(
177389
+ logger249.info(
177132
177390
  `Patched ${patched} LevelControl TLV schema(s): transitionTime is now optional (Google Home compatibility)`
177133
177391
  );
177134
177392
  } else {
177135
- logger247.warn(
177393
+ logger249.warn(
177136
177394
  "Failed to patch LevelControl TLV schemas, field definitions not found. Google Home brightness adjustment may not work.",
177137
177395
  {
177138
177396
  moveToLevel: describeFields(