@camstack/addon-provider-homeassistant 1.2.44 → 1.2.45
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 +1 -1
- package/dist/addon.mjs +1 -1
- package/dist/{dist-B1grYZFh.js → dist-BXcwGSn_.js} +495 -59
- package/dist/{dist-BjB1J9mO.mjs → dist-Dfp98hJL.mjs} +495 -59
- package/dist/ha-export/ha-export.addon.js +255 -59
- package/dist/ha-export/ha-export.addon.mjs +255 -59
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
//#endregion
|
|
6
|
-
const require_dist = require("../dist-
|
|
6
|
+
const require_dist = require("../dist-BXcwGSn_.js");
|
|
7
7
|
let node_crypto = require("node:crypto");
|
|
8
8
|
//#region src/ha-export/topics.ts
|
|
9
9
|
/**
|
|
@@ -4302,6 +4302,58 @@ function pruneBrokers(membership, knownBrokerIds) {
|
|
|
4302
4302
|
function enabledBrokerIds(store, knownBrokerIds) {
|
|
4303
4303
|
return knownBrokerIds.filter((brokerId) => store[`exportTo:${brokerId}`] === true);
|
|
4304
4304
|
}
|
|
4305
|
+
/** Same brokers, same order, same names — nothing for the form to re-render. */
|
|
4306
|
+
function sameBrokers(a, b) {
|
|
4307
|
+
if (a.length !== b.length) return false;
|
|
4308
|
+
return a.every((broker, index) => {
|
|
4309
|
+
const other = b[index];
|
|
4310
|
+
return other !== void 0 && other.id === broker.id && other.name === broker.name;
|
|
4311
|
+
});
|
|
4312
|
+
}
|
|
4313
|
+
/**
|
|
4314
|
+
* What a broker refresh persists — and, above all, what it must NOT.
|
|
4315
|
+
*
|
|
4316
|
+
* **A patch never names a key it did not change.** That reads like tidiness
|
|
4317
|
+
* and is not: `updateGlobalSettings` is a read-modify-write over the addon
|
|
4318
|
+
* store, so naming `membership` writes whatever `this.config.membership`
|
|
4319
|
+
* currently holds — and `this.config` is not always the store.
|
|
4320
|
+
* `BaseAddon.resolveConfig` falls back to the CONSTRUCTOR DEFAULTS whenever
|
|
4321
|
+
* the settings read answers empty, and the kernel's `readAddonStore` answers
|
|
4322
|
+
* empty for a FAILED read exactly as it does for a missing one, silently. A
|
|
4323
|
+
* boot-window read that misses therefore gives the addon `membership: {}`,
|
|
4324
|
+
* and the first refresh that merely re-records `knownBrokers` hands that
|
|
4325
|
+
* empty object to the store as if the operator had unexported everything.
|
|
4326
|
+
*
|
|
4327
|
+
* That is not a hypothetical. On 2026-08-24 the hub restarted at 21:49; the
|
|
4328
|
+
* export runner came up while the cap resolver was still answering
|
|
4329
|
+
* "no provider registered" for hub-resident caps; at 21:55:13 a refresh
|
|
4330
|
+
* found `knownBrokers` stale (`[]` vs `[ha_001]`) and wrote BOTH keys. The
|
|
4331
|
+
* two exported cameras (590 salone, 615 ingresso) were erased from the
|
|
4332
|
+
* store, `devices` fell from 4 to 2 (the two synthetic devices), and Home
|
|
4333
|
+
* Assistant kept every camera entity as a restored/unavailable orphan for
|
|
4334
|
+
* three days: no stream ("the hub does not export this camera"), no
|
|
4335
|
+
* doorbell, no camera state. Nothing logged it, because from the store's
|
|
4336
|
+
* point of view the addon simply asked for `{}`.
|
|
4337
|
+
*
|
|
4338
|
+
* With the keys split, that pass writes `knownBrokers` alone, the
|
|
4339
|
+
* `resolveConfig` that follows the write re-reads a store that still holds
|
|
4340
|
+
* the membership, and the next reconcile exports both cameras again.
|
|
4341
|
+
*
|
|
4342
|
+
* Identity, not deep equality, decides `membership`: `pruneBrokers` returns
|
|
4343
|
+
* the SAME reference when it removed nothing, which is precisely the
|
|
4344
|
+
* "nothing changed" signal this needs.
|
|
4345
|
+
*
|
|
4346
|
+
* @returns the patch to persist, or `null` when there is nothing to write.
|
|
4347
|
+
*/
|
|
4348
|
+
function brokerRefreshPatch(current, next) {
|
|
4349
|
+
const brokersChanged = !sameBrokers(next.knownBrokers, current.knownBrokers);
|
|
4350
|
+
const membershipChanged = next.membership !== current.membership;
|
|
4351
|
+
if (!brokersChanged && !membershipChanged) return null;
|
|
4352
|
+
return {
|
|
4353
|
+
...brokersChanged ? { knownBrokers: next.knownBrokers } : {},
|
|
4354
|
+
...membershipChanged ? { membership: next.membership } : {}
|
|
4355
|
+
};
|
|
4356
|
+
}
|
|
4305
4357
|
/**
|
|
4306
4358
|
* What a linked Home Assistant NEEDS to function — one entry per thing it
|
|
4307
4359
|
* actually calls with this token, each named. Not a blast radius, not a
|
|
@@ -5165,6 +5217,11 @@ function ruleEntity(ruleId) {
|
|
|
5165
5217
|
function ruleIdFromEntity(entity, rules) {
|
|
5166
5218
|
return rules.find((rule) => ruleEntity(rule.id) === entity)?.id ?? null;
|
|
5167
5219
|
}
|
|
5220
|
+
/** The entity id of one per-rule last-trigger fact. Derived from the rule
|
|
5221
|
+
* ID, same identity rule as {@link ruleEntity}. */
|
|
5222
|
+
function ruleTriggerEntity(ruleId, suffix) {
|
|
5223
|
+
return `${ruleEntity(ruleId)}_last_trigger_${suffix}`;
|
|
5224
|
+
}
|
|
5168
5225
|
function notificationCenterSpecs(input) {
|
|
5169
5226
|
return [
|
|
5170
5227
|
{
|
|
@@ -5205,8 +5262,10 @@ function notificationCenterSpecs(input) {
|
|
|
5205
5262
|
*
|
|
5206
5263
|
* Fixed count regardless of camera fleet size — the fan-out this design
|
|
5207
5264
|
* explicitly avoids (960 entities at 30 cameras × 32 rules) lives ONLY in
|
|
5208
|
-
* the per-rule switches
|
|
5209
|
-
*
|
|
5265
|
+
* the per-rule switches and per-rule trigger facts below, which scale
|
|
5266
|
+
* with the RULE count only (this module has no camera dimension at all).
|
|
5267
|
+
* These six are on ONE synthetic device and never repeat per camera or
|
|
5268
|
+
* per rule.
|
|
5210
5269
|
*/
|
|
5211
5270
|
{
|
|
5212
5271
|
entity: "last_trigger_at",
|
|
@@ -5259,7 +5318,44 @@ function notificationCenterSpecs(input) {
|
|
|
5259
5318
|
label: rule.name,
|
|
5260
5319
|
writable: true,
|
|
5261
5320
|
icon: "mdi:bell-cog"
|
|
5262
|
-
}))
|
|
5321
|
+
})),
|
|
5322
|
+
...input.rules.flatMap((rule) => [
|
|
5323
|
+
{
|
|
5324
|
+
entity: ruleTriggerEntity(rule.id, "at"),
|
|
5325
|
+
platform: "sensor",
|
|
5326
|
+
label: `${rule.name} last trigger`,
|
|
5327
|
+
deviceClass: "timestamp"
|
|
5328
|
+
},
|
|
5329
|
+
{
|
|
5330
|
+
entity: ruleTriggerEntity(rule.id, "camera"),
|
|
5331
|
+
platform: "sensor",
|
|
5332
|
+
label: `${rule.name} last trigger camera`,
|
|
5333
|
+
icon: "mdi:cctv"
|
|
5334
|
+
},
|
|
5335
|
+
(
|
|
5336
|
+
/** See {@link SyntheticLastTrigger.label}. Never blank. */
|
|
5337
|
+
{
|
|
5338
|
+
entity: ruleTriggerEntity(rule.id, "label"),
|
|
5339
|
+
platform: "sensor",
|
|
5340
|
+
label: `${rule.name} last trigger label`,
|
|
5341
|
+
icon: "mdi:tag-outline"
|
|
5342
|
+
}),
|
|
5343
|
+
{
|
|
5344
|
+
entity: ruleTriggerEntity(rule.id, "status"),
|
|
5345
|
+
platform: "sensor",
|
|
5346
|
+
label: `${rule.name} last trigger delivery status`,
|
|
5347
|
+
icon: "mdi:send-check-outline",
|
|
5348
|
+
entityCategory: "diagnostic"
|
|
5349
|
+
},
|
|
5350
|
+
(
|
|
5351
|
+
/** See {@link SyntheticLastTrigger.imageUrl} for the expired/
|
|
5352
|
+
* unreachable degrade: no value published rather than a dead link. */
|
|
5353
|
+
{
|
|
5354
|
+
entity: ruleTriggerEntity(rule.id, "image"),
|
|
5355
|
+
platform: "image",
|
|
5356
|
+
label: `${rule.name} last trigger image`
|
|
5357
|
+
})
|
|
5358
|
+
])
|
|
5263
5359
|
];
|
|
5264
5360
|
}
|
|
5265
5361
|
function serverSpecs(input) {
|
|
@@ -5433,6 +5529,39 @@ function syntheticPlans(input) {
|
|
|
5433
5529
|
return [plan(NOTIFICATION_CENTER_STABLE_ID, "Notification Center", notificationCenterSpecs(input)), plan(SERVER_STABLE_ID, "CamStack Server", serverSpecs(input))];
|
|
5434
5530
|
}
|
|
5435
5531
|
/**
|
|
5532
|
+
* The four state facts plus the picture, shared by the GLOBAL `last_trigger_*`
|
|
5533
|
+
* entities and every per-rule `rule_<slug>_last_trigger_*` one. `entityFor` is
|
|
5534
|
+
* the SAME function that named the entity in `notificationCenterSpecs` — a
|
|
5535
|
+
* `sensor` whose declared `state_topic` and its published topic come from two
|
|
5536
|
+
* different expressions is exactly the class of bug
|
|
5537
|
+
* `camera-entity-feeds.spec.ts` exists to catch on the camera side.
|
|
5538
|
+
*/
|
|
5539
|
+
function projectTriggerFacts(deviceKey, entityFor, t) {
|
|
5540
|
+
const values = [
|
|
5541
|
+
{
|
|
5542
|
+
topic: stateTopic(deviceKey, entityFor("at")),
|
|
5543
|
+
value: new Date(t.at).toISOString()
|
|
5544
|
+
},
|
|
5545
|
+
{
|
|
5546
|
+
topic: stateTopic(deviceKey, entityFor("camera")),
|
|
5547
|
+
value: t.deviceName
|
|
5548
|
+
},
|
|
5549
|
+
{
|
|
5550
|
+
topic: stateTopic(deviceKey, entityFor("label")),
|
|
5551
|
+
value: t.label
|
|
5552
|
+
},
|
|
5553
|
+
{
|
|
5554
|
+
topic: stateTopic(deviceKey, entityFor("status")),
|
|
5555
|
+
value: t.status
|
|
5556
|
+
}
|
|
5557
|
+
];
|
|
5558
|
+
if (t.imageUrl !== null) values.push({
|
|
5559
|
+
topic: stateTopic(deviceKey, entityFor("image")),
|
|
5560
|
+
value: t.imageUrl
|
|
5561
|
+
});
|
|
5562
|
+
return values;
|
|
5563
|
+
}
|
|
5564
|
+
/**
|
|
5436
5565
|
* Every synthetic value, for the reconcile to push.
|
|
5437
5566
|
*
|
|
5438
5567
|
* Uptime is published as an ISO timestamp of the BOOT INSTANT, not as a
|
|
@@ -5499,27 +5628,16 @@ function projectSynthetic(input, nowMs) {
|
|
|
5499
5628
|
}
|
|
5500
5629
|
];
|
|
5501
5630
|
if (input.lastTrigger !== null) {
|
|
5502
|
-
const t = input.lastTrigger;
|
|
5503
5631
|
values.push({
|
|
5504
|
-
topic: stateTopic(nc, "last_trigger_at"),
|
|
5505
|
-
value: new Date(t.at).toISOString()
|
|
5506
|
-
}, {
|
|
5507
5632
|
topic: stateTopic(nc, "last_trigger_rule"),
|
|
5508
|
-
value:
|
|
5509
|
-
}, {
|
|
5510
|
-
topic: stateTopic(nc, "last_trigger_camera"),
|
|
5511
|
-
value: t.deviceName
|
|
5512
|
-
}, {
|
|
5513
|
-
topic: stateTopic(nc, "last_trigger_label"),
|
|
5514
|
-
value: t.label
|
|
5515
|
-
}, {
|
|
5516
|
-
topic: stateTopic(nc, "last_trigger_status"),
|
|
5517
|
-
value: t.status
|
|
5518
|
-
});
|
|
5519
|
-
if (t.imageUrl !== null) values.push({
|
|
5520
|
-
topic: stateTopic(nc, "last_trigger_image"),
|
|
5521
|
-
value: t.imageUrl
|
|
5633
|
+
value: input.lastTrigger.ruleName
|
|
5522
5634
|
});
|
|
5635
|
+
values.push(...projectTriggerFacts(nc, (suffix) => `last_trigger_${suffix}`, input.lastTrigger));
|
|
5636
|
+
}
|
|
5637
|
+
for (const rule of input.rules) {
|
|
5638
|
+
const t = input.lastTriggerByRule.get(rule.id);
|
|
5639
|
+
if (t === void 0) continue;
|
|
5640
|
+
values.push(...projectTriggerFacts(nc, (suffix) => ruleTriggerEntity(rule.id, suffix), t));
|
|
5523
5641
|
}
|
|
5524
5642
|
if (input.alertsLastTitle !== null) values.push({
|
|
5525
5643
|
topic: stateTopic(srv, "alerts_last_title"),
|
|
@@ -5571,6 +5689,38 @@ function projectSynthetic(input, nowMs) {
|
|
|
5571
5689
|
return values;
|
|
5572
5690
|
}
|
|
5573
5691
|
//#endregion
|
|
5692
|
+
//#region src/ha-export/rule-triggers.ts
|
|
5693
|
+
function reduceOne(row) {
|
|
5694
|
+
return {
|
|
5695
|
+
ruleId: row.ruleId,
|
|
5696
|
+
ruleName: row.ruleName,
|
|
5697
|
+
deviceId: row.deviceId,
|
|
5698
|
+
at: row.createdAt,
|
|
5699
|
+
label: row.subject.label ?? row.subject.className,
|
|
5700
|
+
status: row.status,
|
|
5701
|
+
artifactId: row.artifactIds?.[0] ?? null
|
|
5702
|
+
};
|
|
5703
|
+
}
|
|
5704
|
+
/**
|
|
5705
|
+
* Reduce a newest-first page of history rows.
|
|
5706
|
+
*
|
|
5707
|
+
* A rule missing from `rows` (never fired, or its last fire fell outside the
|
|
5708
|
+
* page this pass could afford) is simply absent from `byRule` — the caller
|
|
5709
|
+
* renders that as `unknown`, never as an invented value.
|
|
5710
|
+
*/
|
|
5711
|
+
function reduceLastTriggers(rows) {
|
|
5712
|
+
const byRule = /* @__PURE__ */ new Map();
|
|
5713
|
+
let overall = null;
|
|
5714
|
+
for (const row of rows) {
|
|
5715
|
+
if (overall === null) overall = reduceOne(row);
|
|
5716
|
+
if (!byRule.has(row.ruleId)) byRule.set(row.ruleId, reduceOne(row));
|
|
5717
|
+
}
|
|
5718
|
+
return {
|
|
5719
|
+
overall,
|
|
5720
|
+
byRule
|
|
5721
|
+
};
|
|
5722
|
+
}
|
|
5723
|
+
//#endregion
|
|
5574
5724
|
//#region src/ha-export/reconcile-fingerprint.ts
|
|
5575
5725
|
function structureFingerprint(exported) {
|
|
5576
5726
|
return JSON.stringify([...exported.keys()].sort().map((key) => {
|
|
@@ -6450,14 +6600,16 @@ var HaExportAddon = class extends require_dist.BaseAddon {
|
|
|
6450
6600
|
}).catch(() => null);
|
|
6451
6601
|
const server = await this.ctx.api.serverManagement.getServerPackageStatus.query().catch(() => null);
|
|
6452
6602
|
const alerts = await this.readActiveAlerts();
|
|
6453
|
-
const lastTrigger = await this.buildLastTrigger(byId);
|
|
6454
6603
|
if (rules === null && topology === null && addons === null && server === null) return null;
|
|
6455
6604
|
this.syntheticRules = rules ?? this.syntheticRules;
|
|
6605
|
+
const effectiveRules = this.syntheticRules;
|
|
6606
|
+
const { lastTrigger, lastTriggerByRule } = await this.buildLastTrigger(byId, effectiveRules);
|
|
6456
6607
|
const { nodes, oldestFootageMs } = await this.buildSyntheticNodes(topology ?? []);
|
|
6457
6608
|
return {
|
|
6458
|
-
rules:
|
|
6609
|
+
rules: effectiveRules,
|
|
6459
6610
|
snoozed: anySnoozed,
|
|
6460
6611
|
lastTrigger,
|
|
6612
|
+
lastTriggerByRule,
|
|
6461
6613
|
nodes,
|
|
6462
6614
|
addonsRunning: addons?.running ?? 0,
|
|
6463
6615
|
addonsFailed: addons?.failed ?? 0,
|
|
@@ -6471,41 +6623,78 @@ var HaExportAddon = class extends require_dist.BaseAddon {
|
|
|
6471
6623
|
};
|
|
6472
6624
|
}
|
|
6473
6625
|
/**
|
|
6474
|
-
* The most recent notification-rule trigger
|
|
6475
|
-
* trigger, le ultime immagini... e da quale label"
|
|
6626
|
+
* The most recent notification-rule trigger, GLOBAL and PER RULE —
|
|
6627
|
+
* "conoscere gli ultimi trigger, le ultime immagini... e da quale label"
|
|
6628
|
+
* (operator, 2026-08-25), extended to "l'ultimo trigger PER REGOLA, con
|
|
6629
|
+
* immagine e metadati" (operator, 2026-08-27).
|
|
6630
|
+
*
|
|
6631
|
+
* ONE `getHistory` read serves BOTH: `reduceLastTriggers`
|
|
6632
|
+
* (`rule-triggers.ts`) reduces the same newest-first page into "the very
|
|
6633
|
+
* first row" (global) and "the first row per rule id" (per rule) — never
|
|
6634
|
+
* a query per rule, which is exactly the shape a previous session spent
|
|
6635
|
+
* hours removing from the events path. `NC_HISTORY_LIMIT_MAX` (the cap's
|
|
6636
|
+
* own ceiling) is used rather than the global feature's old `limit: 1`,
|
|
6637
|
+
* so a page wide enough to find every rule's last trigger is fetched in
|
|
6638
|
+
* this one call.
|
|
6476
6639
|
*
|
|
6477
|
-
* `
|
|
6478
|
-
*
|
|
6479
|
-
*
|
|
6640
|
+
* `rules` bounds the per-rule work to rules that still exist: a deleted
|
|
6641
|
+
* rule's row may still be in the page, but this never mints a signed URL
|
|
6642
|
+
* for it. `resolveArtifactUrl` calls this pass makes are therefore bounded
|
|
6643
|
+
* by the RULE count (~15 measured live), never by fleet size or history
|
|
6644
|
+
* depth — and de-duplicated by artefact id, since two rules matching the
|
|
6645
|
+
* same evaluated record can share one indexed still.
|
|
6646
|
+
*
|
|
6647
|
+
* Best-effort like every other synthetic source here: a failed read costs
|
|
6648
|
+
* only these entities, never the rest of the device.
|
|
6480
6649
|
*/
|
|
6481
|
-
async buildLastTrigger(byId) {
|
|
6650
|
+
async buildLastTrigger(byId, rules) {
|
|
6651
|
+
const NONE = {
|
|
6652
|
+
lastTrigger: null,
|
|
6653
|
+
lastTriggerByRule: /* @__PURE__ */ new Map()
|
|
6654
|
+
};
|
|
6482
6655
|
try {
|
|
6483
|
-
const { entries } = await this.ctx.api.notificationRules.getHistory.query({ filter: { limit:
|
|
6484
|
-
const
|
|
6485
|
-
|
|
6486
|
-
const
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6656
|
+
const { entries } = await this.ctx.api.notificationRules.getHistory.query({ filter: { limit: 500 } });
|
|
6657
|
+
const { overall, byRule } = reduceLastTriggers(entries);
|
|
6658
|
+
const urlCache = /* @__PURE__ */ new Map();
|
|
6659
|
+
const resolveUrl = async (artifactId, deviceId) => {
|
|
6660
|
+
if (artifactId === null) return null;
|
|
6661
|
+
const cached = urlCache.get(artifactId);
|
|
6662
|
+
if (cached !== void 0) return cached;
|
|
6663
|
+
const url = await this.ctx.api.notificationRules.resolveArtifactUrl.query({ artifactId }).then((r) => r.url).catch((err) => {
|
|
6664
|
+
this.ctx.logger.debug("ha-export: could not resolve a trigger image url", {
|
|
6665
|
+
tags: { deviceId },
|
|
6666
|
+
meta: {
|
|
6667
|
+
artifactId,
|
|
6668
|
+
error: errMsg(err)
|
|
6669
|
+
}
|
|
6670
|
+
});
|
|
6671
|
+
return null;
|
|
6495
6672
|
});
|
|
6496
|
-
|
|
6673
|
+
urlCache.set(artifactId, url);
|
|
6674
|
+
return url;
|
|
6675
|
+
};
|
|
6676
|
+
const toSynthetic = async (t) => ({
|
|
6677
|
+
at: t.at,
|
|
6678
|
+
ruleName: t.ruleName,
|
|
6679
|
+
deviceName: byId.get(t.deviceId)?.name ?? `camera ${t.deviceId}`,
|
|
6680
|
+
label: t.label,
|
|
6681
|
+
status: t.status,
|
|
6682
|
+
imageUrl: await resolveUrl(t.artifactId, t.deviceId)
|
|
6497
6683
|
});
|
|
6684
|
+
const lastTrigger = overall === null ? null : await toSynthetic(overall);
|
|
6685
|
+
const lastTriggerByRule = /* @__PURE__ */ new Map();
|
|
6686
|
+
for (const rule of rules) {
|
|
6687
|
+
const reduced = byRule.get(rule.id);
|
|
6688
|
+
if (reduced === void 0) continue;
|
|
6689
|
+
lastTriggerByRule.set(rule.id, await toSynthetic(reduced));
|
|
6690
|
+
}
|
|
6498
6691
|
return {
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
deviceName: byId.get(entry.deviceId)?.name ?? `camera ${entry.deviceId}`,
|
|
6502
|
-
label,
|
|
6503
|
-
status: entry.status,
|
|
6504
|
-
imageUrl
|
|
6692
|
+
lastTrigger,
|
|
6693
|
+
lastTriggerByRule
|
|
6505
6694
|
};
|
|
6506
6695
|
} catch (err) {
|
|
6507
|
-
this.ctx.logger.warn("ha-export: could not read the last notification
|
|
6508
|
-
return
|
|
6696
|
+
this.ctx.logger.warn("ha-export: could not read the last notification triggers", { meta: { error: errMsg(err) } });
|
|
6697
|
+
return NONE;
|
|
6509
6698
|
}
|
|
6510
6699
|
}
|
|
6511
6700
|
/**
|
|
@@ -6982,10 +7171,24 @@ var HaExportAddon = class extends require_dist.BaseAddon {
|
|
|
6982
7171
|
*/
|
|
6983
7172
|
const pruned = haBrokers.length === 0 ? this.config.membership : pruneBrokers(this.config.membership, haBrokers.map((broker) => broker.id));
|
|
6984
7173
|
if (haBrokers.length === 0 && Object.keys(this.config.membership).length > 0) this.ctx.logger.warn("ha-export: no Home Assistant broker answered — keeping the export membership rather than pruning it", { meta: { brokers: Object.keys(this.config.membership) } });
|
|
6985
|
-
|
|
7174
|
+
/**
|
|
7175
|
+
* ONE key per thing that actually changed — see `brokerRefreshPatch`.
|
|
7176
|
+
*
|
|
7177
|
+
* This used to write `knownBrokers` and `membership` together whenever
|
|
7178
|
+
* EITHER differed. A boot-window settings read that answers empty leaves
|
|
7179
|
+
* `this.config` on its constructor defaults, and that joint write then
|
|
7180
|
+
* persisted `membership: {}` over the operator's real export list on a
|
|
7181
|
+
* pass whose only news was the broker roster. It cost this hub three days
|
|
7182
|
+
* of Home Assistant: 2026-08-24 21:55:13.
|
|
7183
|
+
*/
|
|
7184
|
+
const patch = brokerRefreshPatch({
|
|
7185
|
+
knownBrokers: this.config.knownBrokers,
|
|
7186
|
+
membership: this.config.membership
|
|
7187
|
+
}, {
|
|
6986
7188
|
knownBrokers: known,
|
|
6987
7189
|
membership: pruned
|
|
6988
7190
|
});
|
|
7191
|
+
if (patch !== null) await this.updateGlobalSettings(patch);
|
|
6989
7192
|
await this.refreshEnabledBrokers(known);
|
|
6990
7193
|
const wanted = new Set(this.enabledBrokerIds());
|
|
6991
7194
|
for (const [brokerId, link] of this.links) {
|
|
@@ -7861,13 +8064,6 @@ function randomSecret() {
|
|
|
7861
8064
|
crypto.getRandomValues(bytes);
|
|
7862
8065
|
return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
7863
8066
|
}
|
|
7864
|
-
function sameBrokers(a, b) {
|
|
7865
|
-
if (a.length !== b.length) return false;
|
|
7866
|
-
return a.every((broker, index) => {
|
|
7867
|
-
const other = b[index];
|
|
7868
|
-
return other !== void 0 && other.id === broker.id && other.name === broker.name;
|
|
7869
|
-
});
|
|
7870
|
-
}
|
|
7871
8067
|
function toSnapshot(raw) {
|
|
7872
8068
|
if (raw === null || raw === void 0 || typeof raw !== "object") return {};
|
|
7873
8069
|
const out = {};
|