@camstack/addon-provider-homematic 1.2.131 → 1.2.132
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/addon.js +167 -5
- package/dist/addon.mjs +167 -5
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -26173,11 +26173,14 @@ var HardwareClassIdSchema = _enum([
|
|
|
26173
26173
|
"intel-n-series",
|
|
26174
26174
|
"apple-m-base",
|
|
26175
26175
|
"apple-m-pro",
|
|
26176
|
+
"amd-phoenix",
|
|
26176
26177
|
"unknown"
|
|
26177
26178
|
]);
|
|
26178
26179
|
var AcceleratorKindSchema = _enum([
|
|
26179
26180
|
"intel-npu",
|
|
26180
26181
|
"intel-igpu",
|
|
26182
|
+
"amd-igpu",
|
|
26183
|
+
"amd-npu",
|
|
26181
26184
|
"x86-cpu",
|
|
26182
26185
|
"arm-cpu",
|
|
26183
26186
|
"apple-ane",
|
|
@@ -36795,7 +36798,13 @@ var TopologyNodeSchema = object({
|
|
|
36795
36798
|
})).readonly(),
|
|
36796
36799
|
processes: array(TopologyProcessSchema).readonly(),
|
|
36797
36800
|
categories: array(TopologyCategorySchema).readonly(),
|
|
36798
|
-
rootPackage: TopologyRootPackageSchema.nullable()
|
|
36801
|
+
rootPackage: TopologyRootPackageSchema.nullable(),
|
|
36802
|
+
/**
|
|
36803
|
+
* How an online AGENT reaches the hub (mesh mTLS Release A): `tls` once it has
|
|
36804
|
+
* completed a mutual-TLS handshake, `clear` otherwise. Absent for the hub,
|
|
36805
|
+
* for offline rows and when the hub cannot tell.
|
|
36806
|
+
*/
|
|
36807
|
+
meshTransport: _enum(["tls", "clear"]).optional()
|
|
36799
36808
|
});
|
|
36800
36809
|
var CapUsageEdgeSchema = object({
|
|
36801
36810
|
callerAddonId: string(),
|
|
@@ -37765,7 +37774,15 @@ var GpuInfoSchema = object({
|
|
|
37765
37774
|
name: string(),
|
|
37766
37775
|
memoryMB: number().optional()
|
|
37767
37776
|
});
|
|
37768
|
-
var NpuInfoSchema = object({
|
|
37777
|
+
var NpuInfoSchema = object({
|
|
37778
|
+
type: _enum([
|
|
37779
|
+
"apple-ane",
|
|
37780
|
+
"intel-npu",
|
|
37781
|
+
"amd-xdna"
|
|
37782
|
+
]),
|
|
37783
|
+
name: string().optional(),
|
|
37784
|
+
generation: _enum(["xdna1", "xdna2"]).optional()
|
|
37785
|
+
});
|
|
37769
37786
|
var CoralInfoSchema = object({
|
|
37770
37787
|
type: literal("coral-edgetpu"),
|
|
37771
37788
|
bus: string().optional()
|
|
@@ -56693,6 +56710,8 @@ var STATE_SUMMARY_VARS = [
|
|
|
56693
56710
|
[...RULE_TEXT],
|
|
56694
56711
|
[...RULE_TEXT],
|
|
56695
56712
|
[...RULE_TEXT],
|
|
56713
|
+
[...RULE_TEXT],
|
|
56714
|
+
[...RULE_TEXT],
|
|
56696
56715
|
([...RULE_TEXT], [...UPDATES]),
|
|
56697
56716
|
([...RULE_TEXT], [...PACKAGE_UPDATES]),
|
|
56698
56717
|
([...RULE_TEXT], [...PACKAGE_UPDATES]),
|
|
@@ -80251,6 +80270,19 @@ function sameConnection(a, b) {
|
|
|
80251
80270
|
return a.host === b.host && a.enableHmipRf === b.enableHmipRf && a.enableBidcosRf === b.enableBidcosRf && a.enableGroups === b.enableGroups && a.enableBidcosWired === b.enableBidcosWired && a.callbackHost === b.callbackHost && a.callbackPort === b.callbackPort && a.username === b.username && a.password === b.password && a.tls === b.tls;
|
|
80252
80271
|
}
|
|
80253
80272
|
/**
|
|
80273
|
+
* Delays (ms) between background restore retries after a failed
|
|
80274
|
+
* `facade.start()` (e.g. CCU `ECONNREFUSED`). Six attempts ≈ 2+5+10+20+30+60 s.
|
|
80275
|
+
* Exported for specs that drive the schedule with fake timers.
|
|
80276
|
+
*/
|
|
80277
|
+
var RESTORE_RETRY_DELAYS_MS = [
|
|
80278
|
+
2e3,
|
|
80279
|
+
5e3,
|
|
80280
|
+
1e4,
|
|
80281
|
+
2e4,
|
|
80282
|
+
3e4,
|
|
80283
|
+
6e4
|
|
80284
|
+
];
|
|
80285
|
+
/**
|
|
80254
80286
|
* Manages N live CCU connections ("brokers"), each backed by an
|
|
80255
80287
|
* {@link HmIntegrationManager}. Replaces the old single-CCU
|
|
80256
80288
|
* `homematic-gateway.ts` singleton.
|
|
@@ -80296,6 +80328,10 @@ var HmBrokerRegistry = class {
|
|
|
80296
80328
|
* typically in tests) facade creation is skipped.
|
|
80297
80329
|
*/
|
|
80298
80330
|
#useDefaultFactory;
|
|
80331
|
+
/** Pending restore-retry timers — cleared on {@link shutdown}. */
|
|
80332
|
+
#retryTimers = /* @__PURE__ */ new Map();
|
|
80333
|
+
/** True after {@link shutdown} — blocks further restore retries. */
|
|
80334
|
+
#shuttingDown = false;
|
|
80299
80335
|
constructor(logger, deps = {}) {
|
|
80300
80336
|
this.#logger = logger;
|
|
80301
80337
|
this.#onBrokerConnected = deps.onBrokerConnected ?? (() => void 0);
|
|
@@ -80320,10 +80356,13 @@ var HmBrokerRegistry = class {
|
|
|
80320
80356
|
* Restore previously-persisted broker entries on addon boot.
|
|
80321
80357
|
*
|
|
80322
80358
|
* Seeds the id counter from existing ids so future allocations never
|
|
80323
|
-
* collide. Starts each manager best-effort: a failed start is
|
|
80324
|
-
*
|
|
80359
|
+
* collide. Starts each manager best-effort: a failed start is rolled back
|
|
80360
|
+
* out of `#managers` (so a later attempt can rebuild) and scheduled for a
|
|
80361
|
+
* bounded background retry — a transient CCU `ECONNREFUSED` must not leave
|
|
80362
|
+
* the broker dead until an operator restarts the addon.
|
|
80325
80363
|
*/
|
|
80326
80364
|
async restore(entries) {
|
|
80365
|
+
this.#shuttingDown = false;
|
|
80327
80366
|
for (const entry of entries) this.#seedCounter(entry.id);
|
|
80328
80367
|
for (const entry of entries) try {
|
|
80329
80368
|
await this.#startManager(entry);
|
|
@@ -80333,6 +80372,7 @@ var HmBrokerRegistry = class {
|
|
|
80333
80372
|
tags: { brokerId: entry.id },
|
|
80334
80373
|
meta: { error: msg }
|
|
80335
80374
|
});
|
|
80375
|
+
this.#scheduleRestoreRetry(entry, 0);
|
|
80336
80376
|
}
|
|
80337
80377
|
}
|
|
80338
80378
|
/**
|
|
@@ -80340,6 +80380,9 @@ var HmBrokerRegistry = class {
|
|
|
80340
80380
|
* shutdown.
|
|
80341
80381
|
*/
|
|
80342
80382
|
async shutdown() {
|
|
80383
|
+
this.#shuttingDown = true;
|
|
80384
|
+
for (const timer of this.#retryTimers.values()) clearTimeout(timer);
|
|
80385
|
+
this.#retryTimers.clear();
|
|
80343
80386
|
const ids = Array.from(this.#managers.keys());
|
|
80344
80387
|
await Promise.all(ids.map(async (id) => {
|
|
80345
80388
|
const mgr = this.#managers.get(id);
|
|
@@ -80531,6 +80574,10 @@ var HmBrokerRegistry = class {
|
|
|
80531
80574
|
* Build (via the factory), register, and start the manager for one entry.
|
|
80532
80575
|
* If a manager for the same id already exists (e.g. a duplicate restore
|
|
80533
80576
|
* call), skip construction and only re-index the integration FK.
|
|
80577
|
+
*
|
|
80578
|
+
* On `start()` failure the half-built manager is abandoned so a retry can
|
|
80579
|
+
* construct a fresh one — leaving it in `#managers` used to make every
|
|
80580
|
+
* subsequent `#startManager` a no-op.
|
|
80534
80581
|
*/
|
|
80535
80582
|
async #startManager(entry) {
|
|
80536
80583
|
if (this.#managers.has(entry.id)) {
|
|
@@ -80547,13 +80594,92 @@ var HmBrokerRegistry = class {
|
|
|
80547
80594
|
});
|
|
80548
80595
|
this.#managers.set(entry.id, mgr);
|
|
80549
80596
|
if (entry.integrationId !== void 0) this.#integrationToBroker.set(entry.integrationId, entry.id);
|
|
80550
|
-
|
|
80597
|
+
try {
|
|
80598
|
+
await mgr.start();
|
|
80599
|
+
} catch (err) {
|
|
80600
|
+
await this.#abandonFailedStart(entry.id, mgr);
|
|
80601
|
+
throw err;
|
|
80602
|
+
}
|
|
80603
|
+
const pending = this.#retryTimers.get(entry.id);
|
|
80604
|
+
if (pending !== void 0) {
|
|
80605
|
+
clearTimeout(pending);
|
|
80606
|
+
this.#retryTimers.delete(entry.id);
|
|
80607
|
+
}
|
|
80551
80608
|
this.#logger.info("HmBrokerRegistry: manager started", {
|
|
80552
80609
|
tags: { brokerId: entry.id },
|
|
80553
80610
|
meta: { name: entry.name }
|
|
80554
80611
|
});
|
|
80555
80612
|
}
|
|
80556
80613
|
/**
|
|
80614
|
+
* Drop a manager that never reached `connected` so a later attempt can
|
|
80615
|
+
* rebuild it. Best-effort `stop()`; never throws.
|
|
80616
|
+
*/
|
|
80617
|
+
async #abandonFailedStart(id, mgr) {
|
|
80618
|
+
this.#managers.delete(id);
|
|
80619
|
+
this.#facades.delete(id);
|
|
80620
|
+
homematicFacades.set(id, null);
|
|
80621
|
+
try {
|
|
80622
|
+
await mgr.stop();
|
|
80623
|
+
} catch (err) {
|
|
80624
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
80625
|
+
this.#logger.debug("HmBrokerRegistry: abandon stop failed", {
|
|
80626
|
+
tags: { brokerId: id },
|
|
80627
|
+
meta: { error: msg }
|
|
80628
|
+
});
|
|
80629
|
+
}
|
|
80630
|
+
}
|
|
80631
|
+
/**
|
|
80632
|
+
* Schedule attempt `attemptIndex` of {@link RESTORE_RETRY_DELAYS_MS} for
|
|
80633
|
+
* `entry`. No-ops when the registry is shutting down or the schedule is
|
|
80634
|
+
* exhausted (warns once at the end).
|
|
80635
|
+
*/
|
|
80636
|
+
#scheduleRestoreRetry(entry, attemptIndex) {
|
|
80637
|
+
if (this.#shuttingDown) return;
|
|
80638
|
+
const delay = RESTORE_RETRY_DELAYS_MS[attemptIndex];
|
|
80639
|
+
if (delay === void 0) {
|
|
80640
|
+
this.#logger.warn("HmBrokerRegistry: restore retries exhausted", {
|
|
80641
|
+
tags: { brokerId: entry.id },
|
|
80642
|
+
meta: { attempts: RESTORE_RETRY_DELAYS_MS.length }
|
|
80643
|
+
});
|
|
80644
|
+
return;
|
|
80645
|
+
}
|
|
80646
|
+
const existing = this.#retryTimers.get(entry.id);
|
|
80647
|
+
if (existing !== void 0) clearTimeout(existing);
|
|
80648
|
+
const timer = setTimeout(() => {
|
|
80649
|
+
this.#retryTimers.delete(entry.id);
|
|
80650
|
+
this.#runRestoreRetry(entry, attemptIndex);
|
|
80651
|
+
}, delay);
|
|
80652
|
+
this.#retryTimers.set(entry.id, timer);
|
|
80653
|
+
this.#logger.info("HmBrokerRegistry: scheduling restore retry", {
|
|
80654
|
+
tags: { brokerId: entry.id },
|
|
80655
|
+
meta: {
|
|
80656
|
+
attempt: attemptIndex + 1,
|
|
80657
|
+
delayMs: delay
|
|
80658
|
+
}
|
|
80659
|
+
});
|
|
80660
|
+
}
|
|
80661
|
+
async #runRestoreRetry(entry, attemptIndex) {
|
|
80662
|
+
if (this.#shuttingDown) return;
|
|
80663
|
+
if (this.#managers.has(entry.id)) return;
|
|
80664
|
+
try {
|
|
80665
|
+
await this.#startManager(entry);
|
|
80666
|
+
this.#logger.info("HmBrokerRegistry: restore retry succeeded", {
|
|
80667
|
+
tags: { brokerId: entry.id },
|
|
80668
|
+
meta: { attempt: attemptIndex + 1 }
|
|
80669
|
+
});
|
|
80670
|
+
} catch (err) {
|
|
80671
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
80672
|
+
this.#logger.warn("HmBrokerRegistry: restore retry failed", {
|
|
80673
|
+
tags: { brokerId: entry.id },
|
|
80674
|
+
meta: {
|
|
80675
|
+
attempt: attemptIndex + 1,
|
|
80676
|
+
error: msg
|
|
80677
|
+
}
|
|
80678
|
+
});
|
|
80679
|
+
this.#scheduleRestoreRetry(entry, attemptIndex + 1);
|
|
80680
|
+
}
|
|
80681
|
+
}
|
|
80682
|
+
/**
|
|
80557
80683
|
* Allocate the next monotonic id in the `hm_NNN` sequence and advance
|
|
80558
80684
|
* the counter.
|
|
80559
80685
|
*/
|
|
@@ -83010,6 +83136,19 @@ var HmContainerDevice = class HmContainerDevice extends BaseDevice {
|
|
|
83010
83136
|
}
|
|
83011
83137
|
}
|
|
83012
83138
|
/**
|
|
83139
|
+
* Stamp `feature-probe.lastProbedAt` only when the CCU device is on the
|
|
83140
|
+
* facade. An empty accessory-spec list + a stamped probe is how the kernel
|
|
83141
|
+
* orphan-prunes linked children (`device-cap-proxy` safety gate): a restore
|
|
83142
|
+
* that races a CCU `ECONNREFUSED` used to delete every accessory and fence
|
|
83143
|
+
* their identities for 5 minutes. Leaving `lastProbedAt === 0` until the
|
|
83144
|
+
* device is present keeps the gate closed; `reprobe()` on broker connect
|
|
83145
|
+
* completes the probe and spawns/reconciles children for real.
|
|
83146
|
+
*/
|
|
83147
|
+
async onProbe() {
|
|
83148
|
+
if (this.resolveHmDevice() === null) return;
|
|
83149
|
+
await super.onProbe();
|
|
83150
|
+
}
|
|
83151
|
+
/**
|
|
83013
83152
|
* Derive the accessory children LIVE from the facade descriptor. Each child
|
|
83014
83153
|
* is a {@link HmEntitySpec} (the shared per-entity decomposition) wrapped into
|
|
83015
83154
|
* an `AccessoryChildSpec`: the `stableIdSuffix` IS the spec `key`, the broker
|
|
@@ -83149,6 +83288,7 @@ var HomematicProviderAddon = class extends BaseDeviceProvider {
|
|
|
83149
83288
|
this.registry.setOnBrokerConnected((brokerId) => {
|
|
83150
83289
|
this.ctx.logger.info("Homematic: broker connected", { meta: { brokerId } });
|
|
83151
83290
|
this.setBrokerDevicesOnline(brokerId, true);
|
|
83291
|
+
this.reprobeBrokerContainers(brokerId);
|
|
83152
83292
|
});
|
|
83153
83293
|
this.registry.setOnBrokerDisconnected((brokerId) => {
|
|
83154
83294
|
this.setBrokerDevicesOnline(brokerId, false);
|
|
@@ -83543,6 +83683,28 @@ var HomematicProviderAddon = class extends BaseDeviceProvider {
|
|
|
83543
83683
|
count: n
|
|
83544
83684
|
} });
|
|
83545
83685
|
}
|
|
83686
|
+
/**
|
|
83687
|
+
* Reprobe every adopted {@link HmContainerDevice} for `brokerId` so the
|
|
83688
|
+
* kernel re-runs accessory reconcile against the live facade. Best-effort:
|
|
83689
|
+
* one container's failure must not block the rest (same discipline as HA's
|
|
83690
|
+
* post-connect reconcile).
|
|
83691
|
+
*/
|
|
83692
|
+
async reprobeBrokerContainers(brokerId) {
|
|
83693
|
+
const reg = this.ctx.kernel.deviceRegistry;
|
|
83694
|
+
if (!reg) return;
|
|
83695
|
+
const containers = reg.getAllForAddon(this.addonId).filter((d) => d instanceof HmContainerDevice && d.config.get("brokerId") === brokerId);
|
|
83696
|
+
for (const container of containers) try {
|
|
83697
|
+
await container.reprobe();
|
|
83698
|
+
} catch (err) {
|
|
83699
|
+
this.ctx.logger.warn("Homematic: container reprobe failed", {
|
|
83700
|
+
tags: { deviceId: container.id },
|
|
83701
|
+
meta: {
|
|
83702
|
+
brokerId,
|
|
83703
|
+
error: errMsg(err)
|
|
83704
|
+
}
|
|
83705
|
+
});
|
|
83706
|
+
}
|
|
83707
|
+
}
|
|
83546
83708
|
/** Return the live registry or throw if the addon is not initialised. */
|
|
83547
83709
|
requireRegistry() {
|
|
83548
83710
|
if (!this.registry) throw new Error("Homematic provider not initialised");
|
package/dist/addon.mjs
CHANGED
|
@@ -26174,11 +26174,14 @@ var HardwareClassIdSchema = _enum([
|
|
|
26174
26174
|
"intel-n-series",
|
|
26175
26175
|
"apple-m-base",
|
|
26176
26176
|
"apple-m-pro",
|
|
26177
|
+
"amd-phoenix",
|
|
26177
26178
|
"unknown"
|
|
26178
26179
|
]);
|
|
26179
26180
|
var AcceleratorKindSchema = _enum([
|
|
26180
26181
|
"intel-npu",
|
|
26181
26182
|
"intel-igpu",
|
|
26183
|
+
"amd-igpu",
|
|
26184
|
+
"amd-npu",
|
|
26182
26185
|
"x86-cpu",
|
|
26183
26186
|
"arm-cpu",
|
|
26184
26187
|
"apple-ane",
|
|
@@ -36796,7 +36799,13 @@ var TopologyNodeSchema = object({
|
|
|
36796
36799
|
})).readonly(),
|
|
36797
36800
|
processes: array(TopologyProcessSchema).readonly(),
|
|
36798
36801
|
categories: array(TopologyCategorySchema).readonly(),
|
|
36799
|
-
rootPackage: TopologyRootPackageSchema.nullable()
|
|
36802
|
+
rootPackage: TopologyRootPackageSchema.nullable(),
|
|
36803
|
+
/**
|
|
36804
|
+
* How an online AGENT reaches the hub (mesh mTLS Release A): `tls` once it has
|
|
36805
|
+
* completed a mutual-TLS handshake, `clear` otherwise. Absent for the hub,
|
|
36806
|
+
* for offline rows and when the hub cannot tell.
|
|
36807
|
+
*/
|
|
36808
|
+
meshTransport: _enum(["tls", "clear"]).optional()
|
|
36800
36809
|
});
|
|
36801
36810
|
var CapUsageEdgeSchema = object({
|
|
36802
36811
|
callerAddonId: string(),
|
|
@@ -37766,7 +37775,15 @@ var GpuInfoSchema = object({
|
|
|
37766
37775
|
name: string(),
|
|
37767
37776
|
memoryMB: number().optional()
|
|
37768
37777
|
});
|
|
37769
|
-
var NpuInfoSchema = object({
|
|
37778
|
+
var NpuInfoSchema = object({
|
|
37779
|
+
type: _enum([
|
|
37780
|
+
"apple-ane",
|
|
37781
|
+
"intel-npu",
|
|
37782
|
+
"amd-xdna"
|
|
37783
|
+
]),
|
|
37784
|
+
name: string().optional(),
|
|
37785
|
+
generation: _enum(["xdna1", "xdna2"]).optional()
|
|
37786
|
+
});
|
|
37770
37787
|
var CoralInfoSchema = object({
|
|
37771
37788
|
type: literal("coral-edgetpu"),
|
|
37772
37789
|
bus: string().optional()
|
|
@@ -56694,6 +56711,8 @@ var STATE_SUMMARY_VARS = [
|
|
|
56694
56711
|
[...RULE_TEXT],
|
|
56695
56712
|
[...RULE_TEXT],
|
|
56696
56713
|
[...RULE_TEXT],
|
|
56714
|
+
[...RULE_TEXT],
|
|
56715
|
+
[...RULE_TEXT],
|
|
56697
56716
|
([...RULE_TEXT], [...UPDATES]),
|
|
56698
56717
|
([...RULE_TEXT], [...PACKAGE_UPDATES]),
|
|
56699
56718
|
([...RULE_TEXT], [...PACKAGE_UPDATES]),
|
|
@@ -80252,6 +80271,19 @@ function sameConnection(a, b) {
|
|
|
80252
80271
|
return a.host === b.host && a.enableHmipRf === b.enableHmipRf && a.enableBidcosRf === b.enableBidcosRf && a.enableGroups === b.enableGroups && a.enableBidcosWired === b.enableBidcosWired && a.callbackHost === b.callbackHost && a.callbackPort === b.callbackPort && a.username === b.username && a.password === b.password && a.tls === b.tls;
|
|
80253
80272
|
}
|
|
80254
80273
|
/**
|
|
80274
|
+
* Delays (ms) between background restore retries after a failed
|
|
80275
|
+
* `facade.start()` (e.g. CCU `ECONNREFUSED`). Six attempts ≈ 2+5+10+20+30+60 s.
|
|
80276
|
+
* Exported for specs that drive the schedule with fake timers.
|
|
80277
|
+
*/
|
|
80278
|
+
var RESTORE_RETRY_DELAYS_MS = [
|
|
80279
|
+
2e3,
|
|
80280
|
+
5e3,
|
|
80281
|
+
1e4,
|
|
80282
|
+
2e4,
|
|
80283
|
+
3e4,
|
|
80284
|
+
6e4
|
|
80285
|
+
];
|
|
80286
|
+
/**
|
|
80255
80287
|
* Manages N live CCU connections ("brokers"), each backed by an
|
|
80256
80288
|
* {@link HmIntegrationManager}. Replaces the old single-CCU
|
|
80257
80289
|
* `homematic-gateway.ts` singleton.
|
|
@@ -80297,6 +80329,10 @@ var HmBrokerRegistry = class {
|
|
|
80297
80329
|
* typically in tests) facade creation is skipped.
|
|
80298
80330
|
*/
|
|
80299
80331
|
#useDefaultFactory;
|
|
80332
|
+
/** Pending restore-retry timers — cleared on {@link shutdown}. */
|
|
80333
|
+
#retryTimers = /* @__PURE__ */ new Map();
|
|
80334
|
+
/** True after {@link shutdown} — blocks further restore retries. */
|
|
80335
|
+
#shuttingDown = false;
|
|
80300
80336
|
constructor(logger, deps = {}) {
|
|
80301
80337
|
this.#logger = logger;
|
|
80302
80338
|
this.#onBrokerConnected = deps.onBrokerConnected ?? (() => void 0);
|
|
@@ -80321,10 +80357,13 @@ var HmBrokerRegistry = class {
|
|
|
80321
80357
|
* Restore previously-persisted broker entries on addon boot.
|
|
80322
80358
|
*
|
|
80323
80359
|
* Seeds the id counter from existing ids so future allocations never
|
|
80324
|
-
* collide. Starts each manager best-effort: a failed start is
|
|
80325
|
-
*
|
|
80360
|
+
* collide. Starts each manager best-effort: a failed start is rolled back
|
|
80361
|
+
* out of `#managers` (so a later attempt can rebuild) and scheduled for a
|
|
80362
|
+
* bounded background retry — a transient CCU `ECONNREFUSED` must not leave
|
|
80363
|
+
* the broker dead until an operator restarts the addon.
|
|
80326
80364
|
*/
|
|
80327
80365
|
async restore(entries) {
|
|
80366
|
+
this.#shuttingDown = false;
|
|
80328
80367
|
for (const entry of entries) this.#seedCounter(entry.id);
|
|
80329
80368
|
for (const entry of entries) try {
|
|
80330
80369
|
await this.#startManager(entry);
|
|
@@ -80334,6 +80373,7 @@ var HmBrokerRegistry = class {
|
|
|
80334
80373
|
tags: { brokerId: entry.id },
|
|
80335
80374
|
meta: { error: msg }
|
|
80336
80375
|
});
|
|
80376
|
+
this.#scheduleRestoreRetry(entry, 0);
|
|
80337
80377
|
}
|
|
80338
80378
|
}
|
|
80339
80379
|
/**
|
|
@@ -80341,6 +80381,9 @@ var HmBrokerRegistry = class {
|
|
|
80341
80381
|
* shutdown.
|
|
80342
80382
|
*/
|
|
80343
80383
|
async shutdown() {
|
|
80384
|
+
this.#shuttingDown = true;
|
|
80385
|
+
for (const timer of this.#retryTimers.values()) clearTimeout(timer);
|
|
80386
|
+
this.#retryTimers.clear();
|
|
80344
80387
|
const ids = Array.from(this.#managers.keys());
|
|
80345
80388
|
await Promise.all(ids.map(async (id) => {
|
|
80346
80389
|
const mgr = this.#managers.get(id);
|
|
@@ -80532,6 +80575,10 @@ var HmBrokerRegistry = class {
|
|
|
80532
80575
|
* Build (via the factory), register, and start the manager for one entry.
|
|
80533
80576
|
* If a manager for the same id already exists (e.g. a duplicate restore
|
|
80534
80577
|
* call), skip construction and only re-index the integration FK.
|
|
80578
|
+
*
|
|
80579
|
+
* On `start()` failure the half-built manager is abandoned so a retry can
|
|
80580
|
+
* construct a fresh one — leaving it in `#managers` used to make every
|
|
80581
|
+
* subsequent `#startManager` a no-op.
|
|
80535
80582
|
*/
|
|
80536
80583
|
async #startManager(entry) {
|
|
80537
80584
|
if (this.#managers.has(entry.id)) {
|
|
@@ -80548,13 +80595,92 @@ var HmBrokerRegistry = class {
|
|
|
80548
80595
|
});
|
|
80549
80596
|
this.#managers.set(entry.id, mgr);
|
|
80550
80597
|
if (entry.integrationId !== void 0) this.#integrationToBroker.set(entry.integrationId, entry.id);
|
|
80551
|
-
|
|
80598
|
+
try {
|
|
80599
|
+
await mgr.start();
|
|
80600
|
+
} catch (err) {
|
|
80601
|
+
await this.#abandonFailedStart(entry.id, mgr);
|
|
80602
|
+
throw err;
|
|
80603
|
+
}
|
|
80604
|
+
const pending = this.#retryTimers.get(entry.id);
|
|
80605
|
+
if (pending !== void 0) {
|
|
80606
|
+
clearTimeout(pending);
|
|
80607
|
+
this.#retryTimers.delete(entry.id);
|
|
80608
|
+
}
|
|
80552
80609
|
this.#logger.info("HmBrokerRegistry: manager started", {
|
|
80553
80610
|
tags: { brokerId: entry.id },
|
|
80554
80611
|
meta: { name: entry.name }
|
|
80555
80612
|
});
|
|
80556
80613
|
}
|
|
80557
80614
|
/**
|
|
80615
|
+
* Drop a manager that never reached `connected` so a later attempt can
|
|
80616
|
+
* rebuild it. Best-effort `stop()`; never throws.
|
|
80617
|
+
*/
|
|
80618
|
+
async #abandonFailedStart(id, mgr) {
|
|
80619
|
+
this.#managers.delete(id);
|
|
80620
|
+
this.#facades.delete(id);
|
|
80621
|
+
homematicFacades.set(id, null);
|
|
80622
|
+
try {
|
|
80623
|
+
await mgr.stop();
|
|
80624
|
+
} catch (err) {
|
|
80625
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
80626
|
+
this.#logger.debug("HmBrokerRegistry: abandon stop failed", {
|
|
80627
|
+
tags: { brokerId: id },
|
|
80628
|
+
meta: { error: msg }
|
|
80629
|
+
});
|
|
80630
|
+
}
|
|
80631
|
+
}
|
|
80632
|
+
/**
|
|
80633
|
+
* Schedule attempt `attemptIndex` of {@link RESTORE_RETRY_DELAYS_MS} for
|
|
80634
|
+
* `entry`. No-ops when the registry is shutting down or the schedule is
|
|
80635
|
+
* exhausted (warns once at the end).
|
|
80636
|
+
*/
|
|
80637
|
+
#scheduleRestoreRetry(entry, attemptIndex) {
|
|
80638
|
+
if (this.#shuttingDown) return;
|
|
80639
|
+
const delay = RESTORE_RETRY_DELAYS_MS[attemptIndex];
|
|
80640
|
+
if (delay === void 0) {
|
|
80641
|
+
this.#logger.warn("HmBrokerRegistry: restore retries exhausted", {
|
|
80642
|
+
tags: { brokerId: entry.id },
|
|
80643
|
+
meta: { attempts: RESTORE_RETRY_DELAYS_MS.length }
|
|
80644
|
+
});
|
|
80645
|
+
return;
|
|
80646
|
+
}
|
|
80647
|
+
const existing = this.#retryTimers.get(entry.id);
|
|
80648
|
+
if (existing !== void 0) clearTimeout(existing);
|
|
80649
|
+
const timer = setTimeout(() => {
|
|
80650
|
+
this.#retryTimers.delete(entry.id);
|
|
80651
|
+
this.#runRestoreRetry(entry, attemptIndex);
|
|
80652
|
+
}, delay);
|
|
80653
|
+
this.#retryTimers.set(entry.id, timer);
|
|
80654
|
+
this.#logger.info("HmBrokerRegistry: scheduling restore retry", {
|
|
80655
|
+
tags: { brokerId: entry.id },
|
|
80656
|
+
meta: {
|
|
80657
|
+
attempt: attemptIndex + 1,
|
|
80658
|
+
delayMs: delay
|
|
80659
|
+
}
|
|
80660
|
+
});
|
|
80661
|
+
}
|
|
80662
|
+
async #runRestoreRetry(entry, attemptIndex) {
|
|
80663
|
+
if (this.#shuttingDown) return;
|
|
80664
|
+
if (this.#managers.has(entry.id)) return;
|
|
80665
|
+
try {
|
|
80666
|
+
await this.#startManager(entry);
|
|
80667
|
+
this.#logger.info("HmBrokerRegistry: restore retry succeeded", {
|
|
80668
|
+
tags: { brokerId: entry.id },
|
|
80669
|
+
meta: { attempt: attemptIndex + 1 }
|
|
80670
|
+
});
|
|
80671
|
+
} catch (err) {
|
|
80672
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
80673
|
+
this.#logger.warn("HmBrokerRegistry: restore retry failed", {
|
|
80674
|
+
tags: { brokerId: entry.id },
|
|
80675
|
+
meta: {
|
|
80676
|
+
attempt: attemptIndex + 1,
|
|
80677
|
+
error: msg
|
|
80678
|
+
}
|
|
80679
|
+
});
|
|
80680
|
+
this.#scheduleRestoreRetry(entry, attemptIndex + 1);
|
|
80681
|
+
}
|
|
80682
|
+
}
|
|
80683
|
+
/**
|
|
80558
80684
|
* Allocate the next monotonic id in the `hm_NNN` sequence and advance
|
|
80559
80685
|
* the counter.
|
|
80560
80686
|
*/
|
|
@@ -83011,6 +83137,19 @@ var HmContainerDevice = class HmContainerDevice extends BaseDevice {
|
|
|
83011
83137
|
}
|
|
83012
83138
|
}
|
|
83013
83139
|
/**
|
|
83140
|
+
* Stamp `feature-probe.lastProbedAt` only when the CCU device is on the
|
|
83141
|
+
* facade. An empty accessory-spec list + a stamped probe is how the kernel
|
|
83142
|
+
* orphan-prunes linked children (`device-cap-proxy` safety gate): a restore
|
|
83143
|
+
* that races a CCU `ECONNREFUSED` used to delete every accessory and fence
|
|
83144
|
+
* their identities for 5 minutes. Leaving `lastProbedAt === 0` until the
|
|
83145
|
+
* device is present keeps the gate closed; `reprobe()` on broker connect
|
|
83146
|
+
* completes the probe and spawns/reconciles children for real.
|
|
83147
|
+
*/
|
|
83148
|
+
async onProbe() {
|
|
83149
|
+
if (this.resolveHmDevice() === null) return;
|
|
83150
|
+
await super.onProbe();
|
|
83151
|
+
}
|
|
83152
|
+
/**
|
|
83014
83153
|
* Derive the accessory children LIVE from the facade descriptor. Each child
|
|
83015
83154
|
* is a {@link HmEntitySpec} (the shared per-entity decomposition) wrapped into
|
|
83016
83155
|
* an `AccessoryChildSpec`: the `stableIdSuffix` IS the spec `key`, the broker
|
|
@@ -83150,6 +83289,7 @@ var HomematicProviderAddon = class extends BaseDeviceProvider {
|
|
|
83150
83289
|
this.registry.setOnBrokerConnected((brokerId) => {
|
|
83151
83290
|
this.ctx.logger.info("Homematic: broker connected", { meta: { brokerId } });
|
|
83152
83291
|
this.setBrokerDevicesOnline(brokerId, true);
|
|
83292
|
+
this.reprobeBrokerContainers(brokerId);
|
|
83153
83293
|
});
|
|
83154
83294
|
this.registry.setOnBrokerDisconnected((brokerId) => {
|
|
83155
83295
|
this.setBrokerDevicesOnline(brokerId, false);
|
|
@@ -83544,6 +83684,28 @@ var HomematicProviderAddon = class extends BaseDeviceProvider {
|
|
|
83544
83684
|
count: n
|
|
83545
83685
|
} });
|
|
83546
83686
|
}
|
|
83687
|
+
/**
|
|
83688
|
+
* Reprobe every adopted {@link HmContainerDevice} for `brokerId` so the
|
|
83689
|
+
* kernel re-runs accessory reconcile against the live facade. Best-effort:
|
|
83690
|
+
* one container's failure must not block the rest (same discipline as HA's
|
|
83691
|
+
* post-connect reconcile).
|
|
83692
|
+
*/
|
|
83693
|
+
async reprobeBrokerContainers(brokerId) {
|
|
83694
|
+
const reg = this.ctx.kernel.deviceRegistry;
|
|
83695
|
+
if (!reg) return;
|
|
83696
|
+
const containers = reg.getAllForAddon(this.addonId).filter((d) => d instanceof HmContainerDevice && d.config.get("brokerId") === brokerId);
|
|
83697
|
+
for (const container of containers) try {
|
|
83698
|
+
await container.reprobe();
|
|
83699
|
+
} catch (err) {
|
|
83700
|
+
this.ctx.logger.warn("Homematic: container reprobe failed", {
|
|
83701
|
+
tags: { deviceId: container.id },
|
|
83702
|
+
meta: {
|
|
83703
|
+
brokerId,
|
|
83704
|
+
error: errMsg(err)
|
|
83705
|
+
}
|
|
83706
|
+
});
|
|
83707
|
+
}
|
|
83708
|
+
}
|
|
83547
83709
|
/** Return the live registry or throw if the addon is not initialised. */
|
|
83548
83710
|
requireRegistry() {
|
|
83549
83711
|
if (!this.registry) throw new Error("Homematic provider not initialised");
|
package/package.json
CHANGED