@riddix/hamh 2.1.0-alpha.835 → 2.1.0-alpha.836
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.
package/dist/backend/cli.js
CHANGED
|
@@ -132058,6 +132058,12 @@ var init_bridge_config_schema = __esm({
|
|
|
132058
132058
|
description: "Anchor each device to its Home Assistant entity registry id instead of the entity id, so renaming an entity no longer re-adds it in your controller (Alexa, Google Home, Apple Home) and keeps groups and automations. Records are seeded from the start, so enabling this later is safe and never re-adds existing devices. Default off.",
|
|
132059
132059
|
type: "boolean",
|
|
132060
132060
|
default: false
|
|
132061
|
+
},
|
|
132062
|
+
wedgeWatchdog: {
|
|
132063
|
+
title: "Wedge Watchdog (Apple 'Updating' workaround)",
|
|
132064
|
+
description: "Rotate the one session that looks wedged, subscriptions still alive but no inbound request from the controller for about 45 minutes, earlier than the blind session rotation. Targets Apple Home tiles stuck on 'Updating' where the controller keeps acking but stops consuming data. A false positive only triggers a transparent reconnect, the same as normal rotation. Default off.",
|
|
132065
|
+
type: "boolean",
|
|
132066
|
+
default: false
|
|
132061
132067
|
}
|
|
132062
132068
|
}
|
|
132063
132069
|
};
|
|
@@ -156805,7 +156811,23 @@ function seedExistingSessionStarts(startedAt, sessions, now = Date.now()) {
|
|
|
156805
156811
|
}
|
|
156806
156812
|
}
|
|
156807
156813
|
|
|
156814
|
+
// src/services/bridges/wedge-watchdog.ts
|
|
156815
|
+
var WEDGE_WARMUP_MS = 15 * 60 * 1e3;
|
|
156816
|
+
var WEDGE_IM_SILENCE_MS = 45 * 60 * 1e3;
|
|
156817
|
+
var WEDGE_MIN_ROTATE_INTERVAL_MS = 60 * 60 * 1e3;
|
|
156818
|
+
function decideWedgeRotation(input) {
|
|
156819
|
+
const {
|
|
156820
|
+
subscriptionCount,
|
|
156821
|
+
sessionAgeMs,
|
|
156822
|
+
lastImRequestMsAgo,
|
|
156823
|
+
lastRotatedMsAgo
|
|
156824
|
+
} = input;
|
|
156825
|
+
const imSilenceMs = lastImRequestMsAgo == null ? sessionAgeMs : lastImRequestMsAgo;
|
|
156826
|
+
return subscriptionCount > 0 && sessionAgeMs > WEDGE_WARMUP_MS && imSilenceMs > WEDGE_IM_SILENCE_MS && (lastRotatedMsAgo == null || lastRotatedMsAgo > WEDGE_MIN_ROTATE_INTERVAL_MS);
|
|
156827
|
+
}
|
|
156828
|
+
|
|
156808
156829
|
// src/services/bridges/bridge.ts
|
|
156830
|
+
var imWrapMarker = /* @__PURE__ */ Symbol("hamh.wedgeImWrap");
|
|
156809
156831
|
var AUTO_FORCE_SYNC_INTERVAL_MS = 9e4;
|
|
156810
156832
|
var SHUTDOWN_SESSION_CLOSE_TIMEOUT_MS = 2500;
|
|
156811
156833
|
var MDNS_ADDRESS_CHECK_INTERVAL_MS = 6e4;
|
|
@@ -156853,6 +156875,12 @@ var Bridge = class {
|
|
|
156853
156875
|
sessionStartedAt = /* @__PURE__ */ new Map();
|
|
156854
156876
|
rotationTimer = null;
|
|
156855
156877
|
maxSessionAgeMs = 0;
|
|
156878
|
+
// Wedge watchdog: last inbound Interaction Model request time per session and
|
|
156879
|
+
// last time the watchdog rotated it, both keyed by the long-lived session
|
|
156880
|
+
// object so they clear when the session goes away.
|
|
156881
|
+
lastImRequestAt = /* @__PURE__ */ new WeakMap();
|
|
156882
|
+
wedgeLastRotatedAt = /* @__PURE__ */ new WeakMap();
|
|
156883
|
+
wedgeWatchdogTimer = null;
|
|
156856
156884
|
// Watches the advertised interface addresses so a dynamic ISP IPv6 prefix
|
|
156857
156885
|
// change forces a fresh operational announcement (#415).
|
|
156858
156886
|
mdnsAddressTimer = null;
|
|
@@ -156912,6 +156940,8 @@ var Bridge = class {
|
|
|
156912
156940
|
const nowMs = Date.now();
|
|
156913
156941
|
const lastActiveMsAgo = typeof s.activeTimestamp === "number" && s.activeTimestamp > 0 ? nowMs - s.activeTimestamp : null;
|
|
156914
156942
|
const lastAnyActivityMsAgo = typeof s.timestamp === "number" ? nowMs - s.timestamp : null;
|
|
156943
|
+
const lastImAt = this.lastImRequestAt.get(s);
|
|
156944
|
+
const lastImRequestMsAgo = lastImAt != null ? nowMs - lastImAt : null;
|
|
156915
156945
|
const startedAt = this.sessionStartedAt.get(s.id);
|
|
156916
156946
|
return {
|
|
156917
156947
|
id: s.id,
|
|
@@ -156920,6 +156950,7 @@ var Bridge = class {
|
|
|
156920
156950
|
subscriptionCount: subCount,
|
|
156921
156951
|
lastActiveMsAgo,
|
|
156922
156952
|
lastAnyActivityMsAgo,
|
|
156953
|
+
lastImRequestMsAgo,
|
|
156923
156954
|
isPeerActive: Boolean(s.isPeerActive),
|
|
156924
156955
|
ageMsFromOpen: startedAt != null ? nowMs - startedAt : null
|
|
156925
156956
|
};
|
|
@@ -157086,6 +157117,7 @@ var Bridge = class {
|
|
|
157086
157117
|
this.wireSessionDiagnostics();
|
|
157087
157118
|
this.wireFabricWarnings();
|
|
157088
157119
|
this.startSessionRotation();
|
|
157120
|
+
this.startWedgeWatchdog();
|
|
157089
157121
|
this.startMdnsAddressWatch();
|
|
157090
157122
|
logMemoryUsage(this.log, "bridge running");
|
|
157091
157123
|
diagnosticEventBus.emit("bridge_started", `Bridge started`, {
|
|
@@ -157285,6 +157317,29 @@ ${e?.toString()}`);
|
|
|
157285
157317
|
};
|
|
157286
157318
|
sessionManager.sessions.added.on(this.sessionAddedHandler);
|
|
157287
157319
|
sessionManager.sessions.deleted.on(this.sessionDeletedHandler);
|
|
157320
|
+
this.wireImRequestTracking();
|
|
157321
|
+
} catch {
|
|
157322
|
+
}
|
|
157323
|
+
}
|
|
157324
|
+
// Stamp the time of every inbound Interaction Model request per session by
|
|
157325
|
+
// wrapping InteractionServer.onNewExchange. The wedge watchdog reads these to
|
|
157326
|
+
// tell a live-but-consuming controller from one that only keeps acking.
|
|
157327
|
+
wireImRequestTracking() {
|
|
157328
|
+
try {
|
|
157329
|
+
const is = this.server.env.get(InteractionServer);
|
|
157330
|
+
const marked = is;
|
|
157331
|
+
if (marked[imWrapMarker]) {
|
|
157332
|
+
return;
|
|
157333
|
+
}
|
|
157334
|
+
const original = is.onNewExchange.bind(is);
|
|
157335
|
+
is.onNewExchange = (exchange, message) => {
|
|
157336
|
+
const session = exchange.session;
|
|
157337
|
+
if (session) {
|
|
157338
|
+
this.lastImRequestAt.set(session, Date.now());
|
|
157339
|
+
}
|
|
157340
|
+
return original(exchange, message);
|
|
157341
|
+
};
|
|
157342
|
+
marked[imWrapMarker] = true;
|
|
157288
157343
|
} catch {
|
|
157289
157344
|
}
|
|
157290
157345
|
}
|
|
@@ -157447,6 +157502,7 @@ ${e?.toString()}`);
|
|
|
157447
157502
|
}
|
|
157448
157503
|
this.staleSessionTimers.clear();
|
|
157449
157504
|
this.stopSessionRotation();
|
|
157505
|
+
this.stopWedgeWatchdog();
|
|
157450
157506
|
this.stopMdnsAddressWatch();
|
|
157451
157507
|
this.sessionStartedAt.clear();
|
|
157452
157508
|
}
|
|
@@ -157512,6 +157568,72 @@ ${e?.toString()}`);
|
|
|
157512
157568
|
this.rotationTimer = null;
|
|
157513
157569
|
}
|
|
157514
157570
|
}
|
|
157571
|
+
// Opt-in wedge watchdog: reuse the rotation check cadence (5min) to look for
|
|
157572
|
+
// the one session wedged on "Updating" and rotate just that one.
|
|
157573
|
+
startWedgeWatchdog() {
|
|
157574
|
+
this.stopWedgeWatchdog();
|
|
157575
|
+
if (!this.dataProvider.featureFlags?.wedgeWatchdog) {
|
|
157576
|
+
return;
|
|
157577
|
+
}
|
|
157578
|
+
this.wedgeWatchdogTimer = setInterval(
|
|
157579
|
+
() => this.runWedgeWatchdogCheck(),
|
|
157580
|
+
ROTATION_CHECK_INTERVAL_MS
|
|
157581
|
+
);
|
|
157582
|
+
this.log.info(
|
|
157583
|
+
`Wedge watchdog: checking every ${ROTATION_CHECK_INTERVAL_MS / 6e4}min`
|
|
157584
|
+
);
|
|
157585
|
+
}
|
|
157586
|
+
stopWedgeWatchdog() {
|
|
157587
|
+
if (this.wedgeWatchdogTimer) {
|
|
157588
|
+
clearInterval(this.wedgeWatchdogTimer);
|
|
157589
|
+
this.wedgeWatchdogTimer = null;
|
|
157590
|
+
}
|
|
157591
|
+
}
|
|
157592
|
+
// Rotate exactly the sessions the pure rule flags as wedged. Closing is the
|
|
157593
|
+
// same graceful-then-force path age rotation uses, so a false positive just
|
|
157594
|
+
// re-CASEs the controller.
|
|
157595
|
+
runWedgeWatchdogCheck() {
|
|
157596
|
+
try {
|
|
157597
|
+
const sessionManager = this.server.env.get(SessionManager);
|
|
157598
|
+
const now = Date.now();
|
|
157599
|
+
const closes = [];
|
|
157600
|
+
for (const s of [...sessionManager.sessions]) {
|
|
157601
|
+
if (s.isClosing) continue;
|
|
157602
|
+
const startedAt = this.sessionStartedAt.get(s.id);
|
|
157603
|
+
const sessionAgeMs = startedAt != null ? now - startedAt : 0;
|
|
157604
|
+
const lastImAt = this.lastImRequestAt.get(s);
|
|
157605
|
+
const lastImRequestMsAgo = lastImAt != null ? now - lastImAt : null;
|
|
157606
|
+
const lastRotatedAt = this.wedgeLastRotatedAt.get(s);
|
|
157607
|
+
const lastRotatedMsAgo = lastRotatedAt != null ? now - lastRotatedAt : null;
|
|
157608
|
+
if (!decideWedgeRotation({
|
|
157609
|
+
subscriptionCount: s.subscriptions.size,
|
|
157610
|
+
sessionAgeMs,
|
|
157611
|
+
lastImRequestMsAgo,
|
|
157612
|
+
lastRotatedMsAgo
|
|
157613
|
+
})) {
|
|
157614
|
+
continue;
|
|
157615
|
+
}
|
|
157616
|
+
const silenceMin = Math.round(
|
|
157617
|
+
(lastImRequestMsAgo ?? sessionAgeMs) / 6e4
|
|
157618
|
+
);
|
|
157619
|
+
this.log.info(
|
|
157620
|
+
`Wedge watchdog: rotating session ${s.id}, no inbound interaction for ${silenceMin}min`
|
|
157621
|
+
);
|
|
157622
|
+
this.wedgeLastRotatedAt.set(s, now);
|
|
157623
|
+
closes.push(
|
|
157624
|
+
s.initiateClose().catch(() => {
|
|
157625
|
+
return s.initiateForceClose({
|
|
157626
|
+
cause: new Error("wedge watchdog, forcing")
|
|
157627
|
+
});
|
|
157628
|
+
})
|
|
157629
|
+
);
|
|
157630
|
+
}
|
|
157631
|
+
if (closes.length > 0) {
|
|
157632
|
+
Promise.allSettled(closes).then(() => this.triggerMdnsReAnnounce());
|
|
157633
|
+
}
|
|
157634
|
+
} catch {
|
|
157635
|
+
}
|
|
157636
|
+
}
|
|
157515
157637
|
// Poll the interface addresses mDNS advertises and re-announce when they
|
|
157516
157638
|
// change. matter.js caches the operational records at first announcement, so
|
|
157517
157639
|
// a dynamic ISP IPv6 prefix change keeps advertising the dead global address
|
|
@@ -157643,6 +157765,7 @@ ${e?.toString()}`);
|
|
|
157643
157765
|
if (this.status.code === BridgeStatus.Running) {
|
|
157644
157766
|
this.startAutoForceSyncIfEnabled();
|
|
157645
157767
|
this.startSessionRotation();
|
|
157768
|
+
this.startWedgeWatchdog();
|
|
157646
157769
|
}
|
|
157647
157770
|
} catch (e) {
|
|
157648
157771
|
const reason = "Failed to update bridge due to error:";
|
|
@@ -174378,6 +174501,7 @@ init_dist();
|
|
|
174378
174501
|
init_esm7();
|
|
174379
174502
|
import * as os10 from "node:os";
|
|
174380
174503
|
init_diagnostic_event_bus();
|
|
174504
|
+
var imWrapMarker2 = /* @__PURE__ */ Symbol("hamh.wedgeImWrap");
|
|
174381
174505
|
var AUTO_FORCE_SYNC_INTERVAL_MS2 = 9e4;
|
|
174382
174506
|
var SHUTDOWN_SESSION_CLOSE_TIMEOUT_MS2 = 2500;
|
|
174383
174507
|
var MDNS_ADDRESS_CHECK_INTERVAL_MS2 = 6e4;
|
|
@@ -174410,6 +174534,12 @@ var ServerModeBridge = class {
|
|
|
174410
174534
|
sessionStartedAt = /* @__PURE__ */ new Map();
|
|
174411
174535
|
rotationTimer = null;
|
|
174412
174536
|
maxSessionAgeMs = 0;
|
|
174537
|
+
// Wedge watchdog: last inbound Interaction Model request time per session and
|
|
174538
|
+
// last time the watchdog rotated it, both keyed by the long-lived session
|
|
174539
|
+
// object so they clear when the session goes away.
|
|
174540
|
+
lastImRequestAt = /* @__PURE__ */ new WeakMap();
|
|
174541
|
+
wedgeLastRotatedAt = /* @__PURE__ */ new WeakMap();
|
|
174542
|
+
wedgeWatchdogTimer = null;
|
|
174413
174543
|
// Watches the advertised interface addresses so a dynamic ISP IPv6 prefix
|
|
174414
174544
|
// change forces a fresh operational announcement (#415).
|
|
174415
174545
|
mdnsAddressTimer = null;
|
|
@@ -174486,6 +174616,8 @@ var ServerModeBridge = class {
|
|
|
174486
174616
|
const nowMs = Date.now();
|
|
174487
174617
|
const lastActiveMsAgo = typeof s.activeTimestamp === "number" && s.activeTimestamp > 0 ? nowMs - s.activeTimestamp : null;
|
|
174488
174618
|
const lastAnyActivityMsAgo = typeof s.timestamp === "number" ? nowMs - s.timestamp : null;
|
|
174619
|
+
const lastImAt = this.lastImRequestAt.get(s);
|
|
174620
|
+
const lastImRequestMsAgo = lastImAt != null ? nowMs - lastImAt : null;
|
|
174489
174621
|
const startedAt = this.sessionStartedAt.get(s.id);
|
|
174490
174622
|
return {
|
|
174491
174623
|
id: s.id,
|
|
@@ -174494,6 +174626,7 @@ var ServerModeBridge = class {
|
|
|
174494
174626
|
subscriptionCount: subCount,
|
|
174495
174627
|
lastActiveMsAgo,
|
|
174496
174628
|
lastAnyActivityMsAgo,
|
|
174629
|
+
lastImRequestMsAgo,
|
|
174497
174630
|
isPeerActive: Boolean(s.isPeerActive),
|
|
174498
174631
|
ageMsFromOpen: startedAt != null ? nowMs - startedAt : null
|
|
174499
174632
|
};
|
|
@@ -174557,6 +174690,7 @@ var ServerModeBridge = class {
|
|
|
174557
174690
|
this.wireSessionDiagnostics();
|
|
174558
174691
|
this.wireFabricWarnings();
|
|
174559
174692
|
this.startSessionRotation();
|
|
174693
|
+
this.startWedgeWatchdog();
|
|
174560
174694
|
this.startMdnsAddressWatch();
|
|
174561
174695
|
this.scheduleWarmStart();
|
|
174562
174696
|
logMemoryUsage(this.log, "server mode bridge running");
|
|
@@ -174574,6 +174708,7 @@ ${e?.toString()}`);
|
|
|
174574
174708
|
}
|
|
174575
174709
|
async stop(code = BridgeStatus.Stopped, reason = "Manually stopped") {
|
|
174576
174710
|
this.stopSessionRotation();
|
|
174711
|
+
this.stopWedgeWatchdog();
|
|
174577
174712
|
this.stopMdnsAddressWatch();
|
|
174578
174713
|
this.unwireSessionDiagnostics();
|
|
174579
174714
|
this.unwireFabricWarnings();
|
|
@@ -174606,6 +174741,7 @@ ${e?.toString()}`);
|
|
|
174606
174741
|
if (this.status.code === BridgeStatus.Running) {
|
|
174607
174742
|
this.startAutoForceSyncIfEnabled();
|
|
174608
174743
|
this.startSessionRotation();
|
|
174744
|
+
this.startWedgeWatchdog();
|
|
174609
174745
|
}
|
|
174610
174746
|
} catch (e) {
|
|
174611
174747
|
const reason = "Failed to update server mode bridge due to error:";
|
|
@@ -174790,6 +174926,29 @@ ${e?.toString()}`);
|
|
|
174790
174926
|
sessionManager.sessions.added.on(this.sessionAddedHandler);
|
|
174791
174927
|
sessionManager.sessions.deleted.on(this.sessionDeletedHandler);
|
|
174792
174928
|
seedExistingSessionStarts(this.sessionStartedAt, sessionManager.sessions);
|
|
174929
|
+
this.wireImRequestTracking();
|
|
174930
|
+
} catch {
|
|
174931
|
+
}
|
|
174932
|
+
}
|
|
174933
|
+
// Stamp the time of every inbound Interaction Model request per session by
|
|
174934
|
+
// wrapping InteractionServer.onNewExchange. The wedge watchdog reads these to
|
|
174935
|
+
// tell a live-but-consuming controller from one that only keeps acking.
|
|
174936
|
+
wireImRequestTracking() {
|
|
174937
|
+
try {
|
|
174938
|
+
const is = this.server.env.get(InteractionServer);
|
|
174939
|
+
const marked = is;
|
|
174940
|
+
if (marked[imWrapMarker2]) {
|
|
174941
|
+
return;
|
|
174942
|
+
}
|
|
174943
|
+
const original = is.onNewExchange.bind(is);
|
|
174944
|
+
is.onNewExchange = (exchange, message) => {
|
|
174945
|
+
const session = exchange.session;
|
|
174946
|
+
if (session) {
|
|
174947
|
+
this.lastImRequestAt.set(session, Date.now());
|
|
174948
|
+
}
|
|
174949
|
+
return original(exchange, message);
|
|
174950
|
+
};
|
|
174951
|
+
marked[imWrapMarker2] = true;
|
|
174793
174952
|
} catch {
|
|
174794
174953
|
}
|
|
174795
174954
|
}
|
|
@@ -175007,6 +175166,72 @@ ${e?.toString()}`);
|
|
|
175007
175166
|
this.rotationTimer = null;
|
|
175008
175167
|
}
|
|
175009
175168
|
}
|
|
175169
|
+
// Opt-in wedge watchdog: reuse the rotation check cadence (5min) to look for
|
|
175170
|
+
// the one session wedged on "Updating" and rotate just that one.
|
|
175171
|
+
startWedgeWatchdog() {
|
|
175172
|
+
this.stopWedgeWatchdog();
|
|
175173
|
+
if (!this.dataProvider.featureFlags?.wedgeWatchdog) {
|
|
175174
|
+
return;
|
|
175175
|
+
}
|
|
175176
|
+
this.wedgeWatchdogTimer = setInterval(
|
|
175177
|
+
() => this.runWedgeWatchdogCheck(),
|
|
175178
|
+
ROTATION_CHECK_INTERVAL_MS
|
|
175179
|
+
);
|
|
175180
|
+
this.log.info(
|
|
175181
|
+
`Wedge watchdog: checking every ${ROTATION_CHECK_INTERVAL_MS / 6e4}min`
|
|
175182
|
+
);
|
|
175183
|
+
}
|
|
175184
|
+
stopWedgeWatchdog() {
|
|
175185
|
+
if (this.wedgeWatchdogTimer) {
|
|
175186
|
+
clearInterval(this.wedgeWatchdogTimer);
|
|
175187
|
+
this.wedgeWatchdogTimer = null;
|
|
175188
|
+
}
|
|
175189
|
+
}
|
|
175190
|
+
// Rotate exactly the sessions the pure rule flags as wedged. Closing is the
|
|
175191
|
+
// same graceful-then-force path age rotation uses, so a false positive just
|
|
175192
|
+
// re-CASEs the controller.
|
|
175193
|
+
runWedgeWatchdogCheck() {
|
|
175194
|
+
try {
|
|
175195
|
+
const sessionManager = this.server.env.get(SessionManager);
|
|
175196
|
+
const now = Date.now();
|
|
175197
|
+
const closes = [];
|
|
175198
|
+
for (const s of [...sessionManager.sessions]) {
|
|
175199
|
+
if (s.isClosing) continue;
|
|
175200
|
+
const startedAt = this.sessionStartedAt.get(s.id);
|
|
175201
|
+
const sessionAgeMs = startedAt != null ? now - startedAt : 0;
|
|
175202
|
+
const lastImAt = this.lastImRequestAt.get(s);
|
|
175203
|
+
const lastImRequestMsAgo = lastImAt != null ? now - lastImAt : null;
|
|
175204
|
+
const lastRotatedAt = this.wedgeLastRotatedAt.get(s);
|
|
175205
|
+
const lastRotatedMsAgo = lastRotatedAt != null ? now - lastRotatedAt : null;
|
|
175206
|
+
if (!decideWedgeRotation({
|
|
175207
|
+
subscriptionCount: s.subscriptions.size,
|
|
175208
|
+
sessionAgeMs,
|
|
175209
|
+
lastImRequestMsAgo,
|
|
175210
|
+
lastRotatedMsAgo
|
|
175211
|
+
})) {
|
|
175212
|
+
continue;
|
|
175213
|
+
}
|
|
175214
|
+
const silenceMin = Math.round(
|
|
175215
|
+
(lastImRequestMsAgo ?? sessionAgeMs) / 6e4
|
|
175216
|
+
);
|
|
175217
|
+
this.log.info(
|
|
175218
|
+
`Wedge watchdog: rotating session ${s.id}, no inbound interaction for ${silenceMin}min`
|
|
175219
|
+
);
|
|
175220
|
+
this.wedgeLastRotatedAt.set(s, now);
|
|
175221
|
+
closes.push(
|
|
175222
|
+
s.initiateClose().catch(() => {
|
|
175223
|
+
return s.initiateForceClose({
|
|
175224
|
+
cause: new Error("wedge watchdog, forcing")
|
|
175225
|
+
});
|
|
175226
|
+
})
|
|
175227
|
+
);
|
|
175228
|
+
}
|
|
175229
|
+
if (closes.length > 0) {
|
|
175230
|
+
Promise.allSettled(closes).then(() => this.triggerMdnsReAnnounce());
|
|
175231
|
+
}
|
|
175232
|
+
} catch {
|
|
175233
|
+
}
|
|
175234
|
+
}
|
|
175010
175235
|
// Poll the interface addresses mDNS advertises and re-announce when they
|
|
175011
175236
|
// change. matter.js caches the operational records at first announcement, so
|
|
175012
175237
|
// a dynamic ISP IPv6 prefix change keeps advertising the dead global address
|