@camstack/addon-provider-homeassistant 1.2.43 → 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-DiprHh6i.js → dist-BXcwGSn_.js} +572 -61
- package/dist/{dist-Dbn3F-bB.mjs → dist-Dfp98hJL.mjs} +572 -61
- package/dist/ha-export/ha-export.addon.js +330 -13
- package/dist/ha-export/ha-export.addon.mjs +330 -13
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { At as EventCategory, C as buildAddonRouteProvider, Et as string, N as deviceExportCapability, X as oauthIntegrationCapability, a as COCO_TO_MACRO, c as FanDirectionSchema, i as CAMERA_SWITCH_ORDER, l as HvacModeSchema, o as CameraSwitchIdSchema, p as addonRoutesCapability, pt as BaseAddon, t as AlarmArmModeSchema, u as MediaPlayerRepeatSchema, vt as nodePin } from "../dist-
|
|
1
|
+
import { At as EventCategory, C as buildAddonRouteProvider, Et as string, N as deviceExportCapability, X as oauthIntegrationCapability, a as COCO_TO_MACRO, c as FanDirectionSchema, i as CAMERA_SWITCH_ORDER, l as HvacModeSchema, o as CameraSwitchIdSchema, p as addonRoutesCapability, pt as BaseAddon, t as AlarmArmModeSchema, u as MediaPlayerRepeatSchema, vt as nodePin } from "../dist-Dfp98hJL.mjs";
|
|
2
2
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
3
3
|
//#region src/ha-export/topics.ts
|
|
4
4
|
/**
|
|
@@ -4297,6 +4297,58 @@ function pruneBrokers(membership, knownBrokerIds) {
|
|
|
4297
4297
|
function enabledBrokerIds(store, knownBrokerIds) {
|
|
4298
4298
|
return knownBrokerIds.filter((brokerId) => store[`exportTo:${brokerId}`] === true);
|
|
4299
4299
|
}
|
|
4300
|
+
/** Same brokers, same order, same names — nothing for the form to re-render. */
|
|
4301
|
+
function sameBrokers(a, b) {
|
|
4302
|
+
if (a.length !== b.length) return false;
|
|
4303
|
+
return a.every((broker, index) => {
|
|
4304
|
+
const other = b[index];
|
|
4305
|
+
return other !== void 0 && other.id === broker.id && other.name === broker.name;
|
|
4306
|
+
});
|
|
4307
|
+
}
|
|
4308
|
+
/**
|
|
4309
|
+
* What a broker refresh persists — and, above all, what it must NOT.
|
|
4310
|
+
*
|
|
4311
|
+
* **A patch never names a key it did not change.** That reads like tidiness
|
|
4312
|
+
* and is not: `updateGlobalSettings` is a read-modify-write over the addon
|
|
4313
|
+
* store, so naming `membership` writes whatever `this.config.membership`
|
|
4314
|
+
* currently holds — and `this.config` is not always the store.
|
|
4315
|
+
* `BaseAddon.resolveConfig` falls back to the CONSTRUCTOR DEFAULTS whenever
|
|
4316
|
+
* the settings read answers empty, and the kernel's `readAddonStore` answers
|
|
4317
|
+
* empty for a FAILED read exactly as it does for a missing one, silently. A
|
|
4318
|
+
* boot-window read that misses therefore gives the addon `membership: {}`,
|
|
4319
|
+
* and the first refresh that merely re-records `knownBrokers` hands that
|
|
4320
|
+
* empty object to the store as if the operator had unexported everything.
|
|
4321
|
+
*
|
|
4322
|
+
* That is not a hypothetical. On 2026-08-24 the hub restarted at 21:49; the
|
|
4323
|
+
* export runner came up while the cap resolver was still answering
|
|
4324
|
+
* "no provider registered" for hub-resident caps; at 21:55:13 a refresh
|
|
4325
|
+
* found `knownBrokers` stale (`[]` vs `[ha_001]`) and wrote BOTH keys. The
|
|
4326
|
+
* two exported cameras (590 salone, 615 ingresso) were erased from the
|
|
4327
|
+
* store, `devices` fell from 4 to 2 (the two synthetic devices), and Home
|
|
4328
|
+
* Assistant kept every camera entity as a restored/unavailable orphan for
|
|
4329
|
+
* three days: no stream ("the hub does not export this camera"), no
|
|
4330
|
+
* doorbell, no camera state. Nothing logged it, because from the store's
|
|
4331
|
+
* point of view the addon simply asked for `{}`.
|
|
4332
|
+
*
|
|
4333
|
+
* With the keys split, that pass writes `knownBrokers` alone, the
|
|
4334
|
+
* `resolveConfig` that follows the write re-reads a store that still holds
|
|
4335
|
+
* the membership, and the next reconcile exports both cameras again.
|
|
4336
|
+
*
|
|
4337
|
+
* Identity, not deep equality, decides `membership`: `pruneBrokers` returns
|
|
4338
|
+
* the SAME reference when it removed nothing, which is precisely the
|
|
4339
|
+
* "nothing changed" signal this needs.
|
|
4340
|
+
*
|
|
4341
|
+
* @returns the patch to persist, or `null` when there is nothing to write.
|
|
4342
|
+
*/
|
|
4343
|
+
function brokerRefreshPatch(current, next) {
|
|
4344
|
+
const brokersChanged = !sameBrokers(next.knownBrokers, current.knownBrokers);
|
|
4345
|
+
const membershipChanged = next.membership !== current.membership;
|
|
4346
|
+
if (!brokersChanged && !membershipChanged) return null;
|
|
4347
|
+
return {
|
|
4348
|
+
...brokersChanged ? { knownBrokers: next.knownBrokers } : {},
|
|
4349
|
+
...membershipChanged ? { membership: next.membership } : {}
|
|
4350
|
+
};
|
|
4351
|
+
}
|
|
4300
4352
|
/**
|
|
4301
4353
|
* What a linked Home Assistant NEEDS to function — one entry per thing it
|
|
4302
4354
|
* actually calls with this token, each named. Not a blast radius, not a
|
|
@@ -5160,6 +5212,11 @@ function ruleEntity(ruleId) {
|
|
|
5160
5212
|
function ruleIdFromEntity(entity, rules) {
|
|
5161
5213
|
return rules.find((rule) => ruleEntity(rule.id) === entity)?.id ?? null;
|
|
5162
5214
|
}
|
|
5215
|
+
/** The entity id of one per-rule last-trigger fact. Derived from the rule
|
|
5216
|
+
* ID, same identity rule as {@link ruleEntity}. */
|
|
5217
|
+
function ruleTriggerEntity(ruleId, suffix) {
|
|
5218
|
+
return `${ruleEntity(ruleId)}_last_trigger_${suffix}`;
|
|
5219
|
+
}
|
|
5163
5220
|
function notificationCenterSpecs(input) {
|
|
5164
5221
|
return [
|
|
5165
5222
|
{
|
|
@@ -5190,13 +5247,110 @@ function notificationCenterSpecs(input) {
|
|
|
5190
5247
|
writable: true,
|
|
5191
5248
|
options: SNOOZE_OPTIONS
|
|
5192
5249
|
}),
|
|
5250
|
+
(
|
|
5251
|
+
/**
|
|
5252
|
+
* The last trigger, six entities: five facts plus its picture — "conoscere
|
|
5253
|
+
* gli ultimi trigger, le ultime immagini... e da quale label" (operator,
|
|
5254
|
+
* 2026-08-25). ENABLED by default like every other entity on this device
|
|
5255
|
+
* (see {@link buildComponents}'s docblock): a rule the operator wrote is a
|
|
5256
|
+
* rule they want to watch fire.
|
|
5257
|
+
*
|
|
5258
|
+
* Fixed count regardless of camera fleet size — the fan-out this design
|
|
5259
|
+
* explicitly avoids (960 entities at 30 cameras × 32 rules) lives ONLY in
|
|
5260
|
+
* the per-rule switches and per-rule trigger facts below, which scale
|
|
5261
|
+
* with the RULE count only (this module has no camera dimension at all).
|
|
5262
|
+
* These six are on ONE synthetic device and never repeat per camera or
|
|
5263
|
+
* per rule.
|
|
5264
|
+
*/
|
|
5265
|
+
{
|
|
5266
|
+
entity: "last_trigger_at",
|
|
5267
|
+
platform: "sensor",
|
|
5268
|
+
label: "Last trigger",
|
|
5269
|
+
deviceClass: "timestamp"
|
|
5270
|
+
}),
|
|
5271
|
+
{
|
|
5272
|
+
entity: "last_trigger_rule",
|
|
5273
|
+
platform: "sensor",
|
|
5274
|
+
label: "Last trigger rule",
|
|
5275
|
+
icon: "mdi:bell-ring-outline"
|
|
5276
|
+
},
|
|
5277
|
+
{
|
|
5278
|
+
entity: "last_trigger_camera",
|
|
5279
|
+
platform: "sensor",
|
|
5280
|
+
label: "Last trigger camera",
|
|
5281
|
+
icon: "mdi:cctv"
|
|
5282
|
+
},
|
|
5283
|
+
(
|
|
5284
|
+
/** The identified label, or the detected class when none was resolved —
|
|
5285
|
+
* see {@link SyntheticLastTrigger.label}. Never blank. */
|
|
5286
|
+
{
|
|
5287
|
+
entity: "last_trigger_label",
|
|
5288
|
+
platform: "sensor",
|
|
5289
|
+
label: "Last trigger label",
|
|
5290
|
+
icon: "mdi:tag-outline"
|
|
5291
|
+
}),
|
|
5292
|
+
(
|
|
5293
|
+
/** `pending` / `sent` / `dead` — the outbox row's own status, straight
|
|
5294
|
+
* from `getHistory`, never a second delivery ledger. */
|
|
5295
|
+
{
|
|
5296
|
+
entity: "last_trigger_status",
|
|
5297
|
+
platform: "sensor",
|
|
5298
|
+
label: "Last trigger delivery status",
|
|
5299
|
+
icon: "mdi:send-check-outline",
|
|
5300
|
+
entityCategory: "diagnostic"
|
|
5301
|
+
}),
|
|
5302
|
+
(
|
|
5303
|
+
/** See {@link SyntheticLastTrigger.imageUrl} for the expired/unreachable
|
|
5304
|
+
* degrade: no value is published rather than a link that 404s. */
|
|
5305
|
+
{
|
|
5306
|
+
entity: "last_trigger_image",
|
|
5307
|
+
platform: "image",
|
|
5308
|
+
label: "Last trigger image"
|
|
5309
|
+
}),
|
|
5193
5310
|
...input.rules.map((rule) => ({
|
|
5194
5311
|
entity: ruleEntity(rule.id),
|
|
5195
5312
|
platform: "switch",
|
|
5196
5313
|
label: rule.name,
|
|
5197
5314
|
writable: true,
|
|
5198
5315
|
icon: "mdi:bell-cog"
|
|
5199
|
-
}))
|
|
5316
|
+
})),
|
|
5317
|
+
...input.rules.flatMap((rule) => [
|
|
5318
|
+
{
|
|
5319
|
+
entity: ruleTriggerEntity(rule.id, "at"),
|
|
5320
|
+
platform: "sensor",
|
|
5321
|
+
label: `${rule.name} last trigger`,
|
|
5322
|
+
deviceClass: "timestamp"
|
|
5323
|
+
},
|
|
5324
|
+
{
|
|
5325
|
+
entity: ruleTriggerEntity(rule.id, "camera"),
|
|
5326
|
+
platform: "sensor",
|
|
5327
|
+
label: `${rule.name} last trigger camera`,
|
|
5328
|
+
icon: "mdi:cctv"
|
|
5329
|
+
},
|
|
5330
|
+
(
|
|
5331
|
+
/** See {@link SyntheticLastTrigger.label}. Never blank. */
|
|
5332
|
+
{
|
|
5333
|
+
entity: ruleTriggerEntity(rule.id, "label"),
|
|
5334
|
+
platform: "sensor",
|
|
5335
|
+
label: `${rule.name} last trigger label`,
|
|
5336
|
+
icon: "mdi:tag-outline"
|
|
5337
|
+
}),
|
|
5338
|
+
{
|
|
5339
|
+
entity: ruleTriggerEntity(rule.id, "status"),
|
|
5340
|
+
platform: "sensor",
|
|
5341
|
+
label: `${rule.name} last trigger delivery status`,
|
|
5342
|
+
icon: "mdi:send-check-outline",
|
|
5343
|
+
entityCategory: "diagnostic"
|
|
5344
|
+
},
|
|
5345
|
+
(
|
|
5346
|
+
/** See {@link SyntheticLastTrigger.imageUrl} for the expired/
|
|
5347
|
+
* unreachable degrade: no value published rather than a dead link. */
|
|
5348
|
+
{
|
|
5349
|
+
entity: ruleTriggerEntity(rule.id, "image"),
|
|
5350
|
+
platform: "image",
|
|
5351
|
+
label: `${rule.name} last trigger image`
|
|
5352
|
+
})
|
|
5353
|
+
])
|
|
5200
5354
|
];
|
|
5201
5355
|
}
|
|
5202
5356
|
function serverSpecs(input) {
|
|
@@ -5370,6 +5524,39 @@ function syntheticPlans(input) {
|
|
|
5370
5524
|
return [plan(NOTIFICATION_CENTER_STABLE_ID, "Notification Center", notificationCenterSpecs(input)), plan(SERVER_STABLE_ID, "CamStack Server", serverSpecs(input))];
|
|
5371
5525
|
}
|
|
5372
5526
|
/**
|
|
5527
|
+
* The four state facts plus the picture, shared by the GLOBAL `last_trigger_*`
|
|
5528
|
+
* entities and every per-rule `rule_<slug>_last_trigger_*` one. `entityFor` is
|
|
5529
|
+
* the SAME function that named the entity in `notificationCenterSpecs` — a
|
|
5530
|
+
* `sensor` whose declared `state_topic` and its published topic come from two
|
|
5531
|
+
* different expressions is exactly the class of bug
|
|
5532
|
+
* `camera-entity-feeds.spec.ts` exists to catch on the camera side.
|
|
5533
|
+
*/
|
|
5534
|
+
function projectTriggerFacts(deviceKey, entityFor, t) {
|
|
5535
|
+
const values = [
|
|
5536
|
+
{
|
|
5537
|
+
topic: stateTopic(deviceKey, entityFor("at")),
|
|
5538
|
+
value: new Date(t.at).toISOString()
|
|
5539
|
+
},
|
|
5540
|
+
{
|
|
5541
|
+
topic: stateTopic(deviceKey, entityFor("camera")),
|
|
5542
|
+
value: t.deviceName
|
|
5543
|
+
},
|
|
5544
|
+
{
|
|
5545
|
+
topic: stateTopic(deviceKey, entityFor("label")),
|
|
5546
|
+
value: t.label
|
|
5547
|
+
},
|
|
5548
|
+
{
|
|
5549
|
+
topic: stateTopic(deviceKey, entityFor("status")),
|
|
5550
|
+
value: t.status
|
|
5551
|
+
}
|
|
5552
|
+
];
|
|
5553
|
+
if (t.imageUrl !== null) values.push({
|
|
5554
|
+
topic: stateTopic(deviceKey, entityFor("image")),
|
|
5555
|
+
value: t.imageUrl
|
|
5556
|
+
});
|
|
5557
|
+
return values;
|
|
5558
|
+
}
|
|
5559
|
+
/**
|
|
5373
5560
|
* Every synthetic value, for the reconcile to push.
|
|
5374
5561
|
*
|
|
5375
5562
|
* Uptime is published as an ISO timestamp of the BOOT INSTANT, not as a
|
|
@@ -5435,6 +5622,18 @@ function projectSynthetic(input, nowMs) {
|
|
|
5435
5622
|
value: String(input.alertsActiveCount)
|
|
5436
5623
|
}
|
|
5437
5624
|
];
|
|
5625
|
+
if (input.lastTrigger !== null) {
|
|
5626
|
+
values.push({
|
|
5627
|
+
topic: stateTopic(nc, "last_trigger_rule"),
|
|
5628
|
+
value: input.lastTrigger.ruleName
|
|
5629
|
+
});
|
|
5630
|
+
values.push(...projectTriggerFacts(nc, (suffix) => `last_trigger_${suffix}`, input.lastTrigger));
|
|
5631
|
+
}
|
|
5632
|
+
for (const rule of input.rules) {
|
|
5633
|
+
const t = input.lastTriggerByRule.get(rule.id);
|
|
5634
|
+
if (t === void 0) continue;
|
|
5635
|
+
values.push(...projectTriggerFacts(nc, (suffix) => ruleTriggerEntity(rule.id, suffix), t));
|
|
5636
|
+
}
|
|
5438
5637
|
if (input.alertsLastTitle !== null) values.push({
|
|
5439
5638
|
topic: stateTopic(srv, "alerts_last_title"),
|
|
5440
5639
|
value: input.alertsLastTitle
|
|
@@ -5485,6 +5684,38 @@ function projectSynthetic(input, nowMs) {
|
|
|
5485
5684
|
return values;
|
|
5486
5685
|
}
|
|
5487
5686
|
//#endregion
|
|
5687
|
+
//#region src/ha-export/rule-triggers.ts
|
|
5688
|
+
function reduceOne(row) {
|
|
5689
|
+
return {
|
|
5690
|
+
ruleId: row.ruleId,
|
|
5691
|
+
ruleName: row.ruleName,
|
|
5692
|
+
deviceId: row.deviceId,
|
|
5693
|
+
at: row.createdAt,
|
|
5694
|
+
label: row.subject.label ?? row.subject.className,
|
|
5695
|
+
status: row.status,
|
|
5696
|
+
artifactId: row.artifactIds?.[0] ?? null
|
|
5697
|
+
};
|
|
5698
|
+
}
|
|
5699
|
+
/**
|
|
5700
|
+
* Reduce a newest-first page of history rows.
|
|
5701
|
+
*
|
|
5702
|
+
* A rule missing from `rows` (never fired, or its last fire fell outside the
|
|
5703
|
+
* page this pass could afford) is simply absent from `byRule` — the caller
|
|
5704
|
+
* renders that as `unknown`, never as an invented value.
|
|
5705
|
+
*/
|
|
5706
|
+
function reduceLastTriggers(rows) {
|
|
5707
|
+
const byRule = /* @__PURE__ */ new Map();
|
|
5708
|
+
let overall = null;
|
|
5709
|
+
for (const row of rows) {
|
|
5710
|
+
if (overall === null) overall = reduceOne(row);
|
|
5711
|
+
if (!byRule.has(row.ruleId)) byRule.set(row.ruleId, reduceOne(row));
|
|
5712
|
+
}
|
|
5713
|
+
return {
|
|
5714
|
+
overall,
|
|
5715
|
+
byRule
|
|
5716
|
+
};
|
|
5717
|
+
}
|
|
5718
|
+
//#endregion
|
|
5488
5719
|
//#region src/ha-export/reconcile-fingerprint.ts
|
|
5489
5720
|
function structureFingerprint(exported) {
|
|
5490
5721
|
return JSON.stringify([...exported.keys()].sort().map((key) => {
|
|
@@ -6254,7 +6485,7 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
6254
6485
|
* `brokerIds` is every enabled broker, because they belong to the server
|
|
6255
6486
|
* and not to a membership the operator picks per camera.
|
|
6256
6487
|
*/
|
|
6257
|
-
const synthetic = await this.buildSynthetic(snoozes);
|
|
6488
|
+
const synthetic = await this.buildSynthetic(snoozes, byId);
|
|
6258
6489
|
if (synthetic !== null) for (const plan of syntheticPlans(synthetic)) {
|
|
6259
6490
|
exported.set(plan.deviceKey, {
|
|
6260
6491
|
plan,
|
|
@@ -6337,7 +6568,7 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
6337
6568
|
* Returns `null` only when NOTHING answered, so the devices are not
|
|
6338
6569
|
* announced empty on a hub that is still booting.
|
|
6339
6570
|
*/
|
|
6340
|
-
async buildSynthetic(snoozes) {
|
|
6571
|
+
async buildSynthetic(snoozes, byId) {
|
|
6341
6572
|
const rules = await this.ctx.api.notificationRules.listRules.query({}).then((r) => r.rules.map((rule) => ({
|
|
6342
6573
|
id: rule.id,
|
|
6343
6574
|
name: rule.name,
|
|
@@ -6366,10 +6597,14 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
6366
6597
|
const alerts = await this.readActiveAlerts();
|
|
6367
6598
|
if (rules === null && topology === null && addons === null && server === null) return null;
|
|
6368
6599
|
this.syntheticRules = rules ?? this.syntheticRules;
|
|
6600
|
+
const effectiveRules = this.syntheticRules;
|
|
6601
|
+
const { lastTrigger, lastTriggerByRule } = await this.buildLastTrigger(byId, effectiveRules);
|
|
6369
6602
|
const { nodes, oldestFootageMs } = await this.buildSyntheticNodes(topology ?? []);
|
|
6370
6603
|
return {
|
|
6371
|
-
rules:
|
|
6604
|
+
rules: effectiveRules,
|
|
6372
6605
|
snoozed: anySnoozed,
|
|
6606
|
+
lastTrigger,
|
|
6607
|
+
lastTriggerByRule,
|
|
6373
6608
|
nodes,
|
|
6374
6609
|
addonsRunning: addons?.running ?? 0,
|
|
6375
6610
|
addonsFailed: addons?.failed ?? 0,
|
|
@@ -6383,6 +6618,81 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
6383
6618
|
};
|
|
6384
6619
|
}
|
|
6385
6620
|
/**
|
|
6621
|
+
* The most recent notification-rule trigger, GLOBAL and PER RULE —
|
|
6622
|
+
* "conoscere gli ultimi trigger, le ultime immagini... e da quale label"
|
|
6623
|
+
* (operator, 2026-08-25), extended to "l'ultimo trigger PER REGOLA, con
|
|
6624
|
+
* immagine e metadati" (operator, 2026-08-27).
|
|
6625
|
+
*
|
|
6626
|
+
* ONE `getHistory` read serves BOTH: `reduceLastTriggers`
|
|
6627
|
+
* (`rule-triggers.ts`) reduces the same newest-first page into "the very
|
|
6628
|
+
* first row" (global) and "the first row per rule id" (per rule) — never
|
|
6629
|
+
* a query per rule, which is exactly the shape a previous session spent
|
|
6630
|
+
* hours removing from the events path. `NC_HISTORY_LIMIT_MAX` (the cap's
|
|
6631
|
+
* own ceiling) is used rather than the global feature's old `limit: 1`,
|
|
6632
|
+
* so a page wide enough to find every rule's last trigger is fetched in
|
|
6633
|
+
* this one call.
|
|
6634
|
+
*
|
|
6635
|
+
* `rules` bounds the per-rule work to rules that still exist: a deleted
|
|
6636
|
+
* rule's row may still be in the page, but this never mints a signed URL
|
|
6637
|
+
* for it. `resolveArtifactUrl` calls this pass makes are therefore bounded
|
|
6638
|
+
* by the RULE count (~15 measured live), never by fleet size or history
|
|
6639
|
+
* depth — and de-duplicated by artefact id, since two rules matching the
|
|
6640
|
+
* same evaluated record can share one indexed still.
|
|
6641
|
+
*
|
|
6642
|
+
* Best-effort like every other synthetic source here: a failed read costs
|
|
6643
|
+
* only these entities, never the rest of the device.
|
|
6644
|
+
*/
|
|
6645
|
+
async buildLastTrigger(byId, rules) {
|
|
6646
|
+
const NONE = {
|
|
6647
|
+
lastTrigger: null,
|
|
6648
|
+
lastTriggerByRule: /* @__PURE__ */ new Map()
|
|
6649
|
+
};
|
|
6650
|
+
try {
|
|
6651
|
+
const { entries } = await this.ctx.api.notificationRules.getHistory.query({ filter: { limit: 500 } });
|
|
6652
|
+
const { overall, byRule } = reduceLastTriggers(entries);
|
|
6653
|
+
const urlCache = /* @__PURE__ */ new Map();
|
|
6654
|
+
const resolveUrl = async (artifactId, deviceId) => {
|
|
6655
|
+
if (artifactId === null) return null;
|
|
6656
|
+
const cached = urlCache.get(artifactId);
|
|
6657
|
+
if (cached !== void 0) return cached;
|
|
6658
|
+
const url = await this.ctx.api.notificationRules.resolveArtifactUrl.query({ artifactId }).then((r) => r.url).catch((err) => {
|
|
6659
|
+
this.ctx.logger.debug("ha-export: could not resolve a trigger image url", {
|
|
6660
|
+
tags: { deviceId },
|
|
6661
|
+
meta: {
|
|
6662
|
+
artifactId,
|
|
6663
|
+
error: errMsg(err)
|
|
6664
|
+
}
|
|
6665
|
+
});
|
|
6666
|
+
return null;
|
|
6667
|
+
});
|
|
6668
|
+
urlCache.set(artifactId, url);
|
|
6669
|
+
return url;
|
|
6670
|
+
};
|
|
6671
|
+
const toSynthetic = async (t) => ({
|
|
6672
|
+
at: t.at,
|
|
6673
|
+
ruleName: t.ruleName,
|
|
6674
|
+
deviceName: byId.get(t.deviceId)?.name ?? `camera ${t.deviceId}`,
|
|
6675
|
+
label: t.label,
|
|
6676
|
+
status: t.status,
|
|
6677
|
+
imageUrl: await resolveUrl(t.artifactId, t.deviceId)
|
|
6678
|
+
});
|
|
6679
|
+
const lastTrigger = overall === null ? null : await toSynthetic(overall);
|
|
6680
|
+
const lastTriggerByRule = /* @__PURE__ */ new Map();
|
|
6681
|
+
for (const rule of rules) {
|
|
6682
|
+
const reduced = byRule.get(rule.id);
|
|
6683
|
+
if (reduced === void 0) continue;
|
|
6684
|
+
lastTriggerByRule.set(rule.id, await toSynthetic(reduced));
|
|
6685
|
+
}
|
|
6686
|
+
return {
|
|
6687
|
+
lastTrigger,
|
|
6688
|
+
lastTriggerByRule
|
|
6689
|
+
};
|
|
6690
|
+
} catch (err) {
|
|
6691
|
+
this.ctx.logger.warn("ha-export: could not read the last notification triggers", { meta: { error: errMsg(err) } });
|
|
6692
|
+
return NONE;
|
|
6693
|
+
}
|
|
6694
|
+
}
|
|
6695
|
+
/**
|
|
6386
6696
|
* `alerts.list` — the conditions nobody said (Task D1). Best-effort: a
|
|
6387
6697
|
* failed read yields "nothing active", not a fabricated absence dressed up
|
|
6388
6698
|
* as a clean one — this addon logs the drop rather than pretending the
|
|
@@ -6856,10 +7166,24 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
6856
7166
|
*/
|
|
6857
7167
|
const pruned = haBrokers.length === 0 ? this.config.membership : pruneBrokers(this.config.membership, haBrokers.map((broker) => broker.id));
|
|
6858
7168
|
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) } });
|
|
6859
|
-
|
|
7169
|
+
/**
|
|
7170
|
+
* ONE key per thing that actually changed — see `brokerRefreshPatch`.
|
|
7171
|
+
*
|
|
7172
|
+
* This used to write `knownBrokers` and `membership` together whenever
|
|
7173
|
+
* EITHER differed. A boot-window settings read that answers empty leaves
|
|
7174
|
+
* `this.config` on its constructor defaults, and that joint write then
|
|
7175
|
+
* persisted `membership: {}` over the operator's real export list on a
|
|
7176
|
+
* pass whose only news was the broker roster. It cost this hub three days
|
|
7177
|
+
* of Home Assistant: 2026-08-24 21:55:13.
|
|
7178
|
+
*/
|
|
7179
|
+
const patch = brokerRefreshPatch({
|
|
7180
|
+
knownBrokers: this.config.knownBrokers,
|
|
7181
|
+
membership: this.config.membership
|
|
7182
|
+
}, {
|
|
6860
7183
|
knownBrokers: known,
|
|
6861
7184
|
membership: pruned
|
|
6862
7185
|
});
|
|
7186
|
+
if (patch !== null) await this.updateGlobalSettings(patch);
|
|
6863
7187
|
await this.refreshEnabledBrokers(known);
|
|
6864
7188
|
const wanted = new Set(this.enabledBrokerIds());
|
|
6865
7189
|
for (const [brokerId, link] of this.links) {
|
|
@@ -7735,13 +8059,6 @@ function randomSecret() {
|
|
|
7735
8059
|
crypto.getRandomValues(bytes);
|
|
7736
8060
|
return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
7737
8061
|
}
|
|
7738
|
-
function sameBrokers(a, b) {
|
|
7739
|
-
if (a.length !== b.length) return false;
|
|
7740
|
-
return a.every((broker, index) => {
|
|
7741
|
-
const other = b[index];
|
|
7742
|
-
return other !== void 0 && other.id === broker.id && other.name === broker.name;
|
|
7743
|
-
});
|
|
7744
|
-
}
|
|
7745
8062
|
function toSnapshot(raw) {
|
|
7746
8063
|
if (raw === null || raw === void 0 || typeof raw !== "object") return {};
|
|
7747
8064
|
const out = {};
|