@camstack/addon-provider-homematic 1.2.33 → 1.2.35

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.
Files changed (3) hide show
  1. package/dist/addon.js +409 -30
  2. package/dist/addon.mjs +409 -30
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -7477,6 +7477,111 @@ var CameraSwitchGroupSchema = object({
7477
7477
  fetchedAt: number()
7478
7478
  });
7479
7479
  /**
7480
+ * Per-component log CHANNELS — the gate a hot path consults, and the registry
7481
+ * an addon declares its channels in.
7482
+ *
7483
+ * ## Two axes, deliberately separated
7484
+ *
7485
+ * - **DECLARATION** — which channels exist. Only the addon knows:
7486
+ * `stream-broker` knows webrtc/ICE/RTP, `provider-reolink` knows
7487
+ * baichuan/handshake. A hand-wired central list rots at the first addition,
7488
+ * and rots silently. So a channel is declared where it is consulted, and the
7489
+ * `log-channels` capability enumerates the declarations.
7490
+ * - **VALUE** — at which level, for which scope, until when. That stays ONE
7491
+ * thing: the logging settings document on the `system` cap. Two authorities
7492
+ * over the values is the exact defect
7493
+ * `docs/design/plans/2026-08-26-logging-per-componente.md` was written to
7494
+ * remove; re-introducing it from the cure side would be grotesque.
7495
+ *
7496
+ * Nothing in this file reads a clock, an env var or a store. The registry is
7497
+ * a MIRROR: it is moved only by {@link LogChannelRegistry.apply}, called off
7498
+ * the hot path with a value somebody actually read, and by
7499
+ * {@link LogChannelRegistry.tick}, called on a timer. A store read that fails
7500
+ * never reaches here, so it can neither disarm an armed channel nor arm a
7501
+ * disarmed one (D49).
7502
+ *
7503
+ * ## The canonical call shape
7504
+ *
7505
+ * ```ts
7506
+ * if (CH_RTP.on && CH_RTP.wants(deviceId)) {
7507
+ * CH_RTP.log(logger, 'rtp subscriber added', { tags: { deviceId }, meta: { ssrc } })
7508
+ * }
7509
+ * ```
7510
+ *
7511
+ * `on` is a plain boolean FIELD — never a getter — and it is the FIRST thing
7512
+ * read. Disarmed, a call site costs one load and one branch, and the `extras`
7513
+ * object literal is never constructed because it lives inside the branch. It
7514
+ * is the same shape already proven in production at `stream-broker.ts:1650`,
7515
+ * and the same discipline `LoggingGate.allowsDestination` uses for the
7516
+ * destination floor (measured at 1.93 ns/call when off).
7517
+ *
7518
+ * ## Why a channel emits at `info`
7519
+ *
7520
+ * `loki-logging.addon.ts` pins the destination default at `info` and
7521
+ * `loki-destination.ts` drops everything below it, so a line emitted at
7522
+ * `debug` never reaches Loki and the hub's in-memory ring only holds ~35
7523
+ * minutes. A diagnostic that cannot be read an hour later is worse than no
7524
+ * diagnostic, because it looks done. {@link LogChannelGate.log} therefore
7525
+ * emits at the channel's declared level, whose schema floor is `info`.
7526
+ */
7527
+ /**
7528
+ * The level a channel writes at once armed.
7529
+ *
7530
+ * `debug` is absent ON PURPOSE and not by omission: below `info` the line does
7531
+ * not leave the process for Loki, and the whole point of arming a channel is
7532
+ * to read it later.
7533
+ */
7534
+ var LogChannelLevelSchema = _enum([
7535
+ "info",
7536
+ "warn",
7537
+ "error"
7538
+ ]);
7539
+ /**
7540
+ * What an addon declares about one channel. No value, no state — a
7541
+ * declaration is inert.
7542
+ */
7543
+ var LogChannelDescriptorSchema = object({
7544
+ /**
7545
+ * Dotted `area.thing`, unique across the workspace. `area` is conventionally
7546
+ * the addon's short name so an operator reading a channel list can tell who
7547
+ * owns it without a second lookup.
7548
+ */
7549
+ name: string().min(3).regex(/^[a-z0-9-]+(\.[a-z0-9-]+)+$/, "a channel name is dotted lower-kebab, e.g. area.thing"),
7550
+ /** One sentence: what the operator will SEE after arming it. */
7551
+ description: string().min(1),
7552
+ /** The level its lines are emitted at. Never below `info`. */
7553
+ defaultLevel: LogChannelLevelSchema,
7554
+ /**
7555
+ * Whether this channel can be narrowed to a camera.
7556
+ *
7557
+ * `true` is a PROMISE with two halves, and both must hold: the gate is
7558
+ * consulted with the numeric device id, AND every line the channel admits
7559
+ * carries `tags: { deviceId }` with that same numeric id. The second half is
7560
+ * what makes `| json | deviceId="617"` work in Loki — `loki-payload.ts`
7561
+ * keeps `deviceId` out of the stream labels for cardinality, so the tag in
7562
+ * the body is the only way to filter.
7563
+ *
7564
+ * A channel whose lines carry the device only in `meta` (or not at all) is
7565
+ * declared `false`. Declaring it `true` anyway would be a lie the UI repeats:
7566
+ * the operator narrows to one camera, sees nothing, and concludes the code
7567
+ * path was never taken.
7568
+ */
7569
+ perDevice: boolean()
7570
+ });
7571
+ /**
7572
+ * An armed window over one channel, as the document hands it to a mirror.
7573
+ *
7574
+ * A window is a DEADLINE, never a flag (ADR-0244): a channel somebody forgot
7575
+ * expires by itself, which is the one failure a boolean cannot avoid.
7576
+ */
7577
+ var LogChannelWindowSchema = object({
7578
+ channel: string().min(1),
7579
+ /** Epoch ms the window closes at. */
7580
+ armedUntilMs: number(),
7581
+ /** `null` = every camera. A non-empty list narrows to those numeric ids. */
7582
+ deviceIds: array(number().int()).readonly().nullable()
7583
+ });
7584
+ /**
7480
7585
  * Ops-log — the durable, append-only operations audit shared by the
7481
7586
  * recordings and events management surfaces.
7482
7587
  *
@@ -11146,6 +11251,35 @@ var MutationFilterSchema = object({
11146
11251
  whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
11147
11252
  whereNot: record(string(), unknown()).optional()
11148
11253
  });
11254
+ /**
11255
+ * One scalar an {@link settingsStoreCapability.methods.aggregate} call asks for.
11256
+ *
11257
+ * `as` names the slot in the result, so the SAME column may be asked twice with
11258
+ * two operations (`MIN(startMs)` and `MAX(startMs)` in one round trip) — which
11259
+ * a `Record<column, op>` shape could not express.
11260
+ */
11261
+ var AggregateFieldSchema = object({
11262
+ /** Result key. */
11263
+ as: string().min(1),
11264
+ /** Column to aggregate. Must be a real column of a declared collection. */
11265
+ field: string().min(1),
11266
+ op: _enum([
11267
+ "sum",
11268
+ "min",
11269
+ "max"
11270
+ ])
11271
+ });
11272
+ /**
11273
+ * `COUNT(*)` plus one number per requested field.
11274
+ *
11275
+ * `null` means NO ROW MATCHED, never zero: a `SUM` over an empty set and a sum
11276
+ * that really is 0 are different facts, and an accounting caller that renders
11277
+ * "0 bytes, oldest = 0" for "nothing here" reports a lie about a disk.
11278
+ */
11279
+ var AggregateResultSchema = object({
11280
+ count: number().int(),
11281
+ values: record(string(), number().nullable())
11282
+ });
11149
11283
  /** A single stored record: `{ id, data }`. */
11150
11284
  var SettingsRecordSchema = object({
11151
11285
  id: string(),
@@ -11230,6 +11364,11 @@ method(object({
11230
11364
  collection: string(),
11231
11365
  filter: QueryFilterSchema.optional()
11232
11366
  }), number()), method(object({
11367
+ namespace: string().optional(),
11368
+ collection: string(),
11369
+ fields: array(AggregateFieldSchema).readonly(),
11370
+ filter: QueryFilterSchema.optional()
11371
+ }), AggregateResultSchema), method(object({
11233
11372
  namespace: string().optional(),
11234
11373
  collection: string(),
11235
11374
  field: string(),
@@ -11346,6 +11485,11 @@ method(_void(), EngineInfoSchema, { auth: "admin" }), method(object({
11346
11485
  collection: string(),
11347
11486
  filter: QueryFilterSchema.optional()
11348
11487
  }), number(), { auth: "admin" }), method(object({
11488
+ namespace: string().optional(),
11489
+ collection: string(),
11490
+ fields: array(AggregateFieldSchema).readonly(),
11491
+ filter: QueryFilterSchema.optional()
11492
+ }), AggregateResultSchema, { auth: "admin" }), method(object({
11349
11493
  namespace: string().optional(),
11350
11494
  collection: string(),
11351
11495
  field: string(),
@@ -12086,24 +12230,6 @@ var deviceProviderCapability = {
12086
12230
  })
12087
12231
  }
12088
12232
  };
12089
- /**
12090
- * Device Manager capability — hub-side singleton that unifies device persistence,
12091
- * live registry access, and all management operations into a single tRPC surface.
12092
- *
12093
- * Replaces:
12094
- * - `device-persistence` capability (persistence methods absorbed here)
12095
- * - `device-management.router.ts` (deleted in Phase 2)
12096
- * - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
12097
- *
12098
- * All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
12099
- * fork into separate processes but never run on remote cluster agents. Therefore:
12100
- * - No nodeId routing needed — this is a pure hub singleton.
12101
- * - The hub's DeviceRegistry is the single source of truth for all live devices.
12102
- * - No shadow registry or cross-node aggregation required.
12103
- *
12104
- * Forked workers register devices back to the hub via `ctx.devices`
12105
- * (DeviceManagerApi → ctx.api.deviceManager.registerDevice), same as today.
12106
- */
12107
12233
  /** One child-placement directive on a container's `childLayout`. Structurally
12108
12234
  * identical to `ChildLayoutEntry` in `device-management.ts` — the cap wire
12109
12235
  * shape for the same field. The child is identified by its re-sync-stable
@@ -12472,7 +12598,7 @@ method(object({
12472
12598
  * it answers today and the caller filters as it already does.
12473
12599
  */
12474
12600
  deviceIds: array(number()).optional()
12475
- }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
12601
+ }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ parentDeviceIds: array(number()).max(256) }), record(string(), array(DeviceInfoSchema))), method(object({ deviceId: number() }), object({
12476
12602
  mode: LinkedDevicesModeSchema,
12477
12603
  devices: array(LinkedDeviceSchema)
12478
12604
  })), method(object({ deviceIds: array(number()) }), array(LinkedDevicesForDeviceSchema)), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
@@ -13196,6 +13322,59 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13196
13322
  kind: "mutation",
13197
13323
  auth: "admin"
13198
13324
  });
13325
+ /**
13326
+ * `log-channels` — the capability an addon DECLARES its diagnostic channels
13327
+ * through. It stores nothing.
13328
+ *
13329
+ * ## Why a capability at all, and why this shape
13330
+ *
13331
+ * Which channels exist is knowledge only the addon has: `stream-broker` knows
13332
+ * webrtc/ICE/RTP, `provider-reolink` knows baichuan/handshake. A central list
13333
+ * maintained by hand rots at the first addition and rots INVISIBLY — nothing
13334
+ * fails, an operator just never sees the channel somebody added. So the list
13335
+ * is assembled from declarations at runtime.
13336
+ *
13337
+ * The shape is copied from `log-destination.cap.ts`, which already does
13338
+ * exactly this job: `mode: 'collection'`, `internal: true`,
13339
+ * `mount: { kind: 'skip' }` — no tRPC route, no generated hooks — while
13340
+ * `addons.listCapabilityProviders` still enumerates it, and the hub's
13341
+ * `CapabilityRegistry` still holds an RPC proxy per provider so a forked
13342
+ * runner's declarations reach hub-main over the transport that already exists.
13343
+ * No new UDS message, no second registry.
13344
+ *
13345
+ * ## What it deliberately does NOT own
13346
+ *
13347
+ * The VALUES — which channel is armed, for which cameras, until when — live in
13348
+ * ONE place: the logging settings document on the `system` cap
13349
+ * (`getLoggingSettings` / `setLoggingSettings`). Two authorities over the same
13350
+ * value is the defect the plan behind this work exists to remove, and
13351
+ * `setRequestCensus` was retired (D245) rather than allowed to be a second
13352
+ * one. {@link logChannelsCapability} therefore has no getter for a level, no
13353
+ * setter for a window and no persistence of any kind.
13354
+ *
13355
+ * ## Why `apply` is here even so
13356
+ *
13357
+ * The gate lives in the addon's PROCESS; the document lives in hub-main. Some
13358
+ * seam has to carry the value from the authority to the mirror, and a channel
13359
+ * that cannot be reached is precisely the dead knob this whole slice exists to
13360
+ * make impossible (D62 — `audioThresholdDbfs`, the HA entities with no source).
13361
+ * `apply` is that seam and nothing more: it writes an in-memory mirror, it
13362
+ * persists nothing, it is never the source of a value, and it is called only
13363
+ * with a set the hub actually read (D49 — a read that fails does not call it
13364
+ * at all, so no channel is silently disarmed by a bad read).
13365
+ */
13366
+ /** What `apply` reports back — enough to log, not enough to be a second state. */
13367
+ var LogChannelApplyResultSchema = object({
13368
+ /** How many declared channels are armed in this process after the call. */
13369
+ armed: number().int().min(0),
13370
+ /**
13371
+ * Names the document armed that this process does not declare. Reported
13372
+ * rather than swallowed: a name here is either a typo or an addon that has
13373
+ * not booted, and both deserve a line instead of silence.
13374
+ */
13375
+ unknown: array(string()).readonly()
13376
+ });
13377
+ method(_void(), array(LogChannelDescriptorSchema).readonly()), method(object({ windows: array(LogChannelWindowSchema).readonly() }), LogChannelApplyResultSchema, { kind: "mutation" });
13199
13378
  var LogLevelSchema = _enum([
13200
13379
  "debug",
13201
13380
  "info",
@@ -28478,17 +28657,60 @@ var SetSiteLocationInputSchema = object({
28478
28657
  longitude: number().min(-180).max(180)
28479
28658
  }).nullable();
28480
28659
  /**
28481
- * One `(procedure, user-agent, ip, principal)` tuple of the HTTP request
28660
+ * The TRANSPORT a call arrived on.
28661
+ *
28662
+ * Every counted call carries exactly one of these, and `unknown` is a PLANE
28663
+ * rather than a gap: a plane that cannot attribute a call declares it here, so
28664
+ * the call lands in a named bucket instead of vanishing. `planes` summing to
28665
+ * `procedureCalls` is what makes "the sum of the planes explains the total"
28666
+ * checkable rather than asserted.
28667
+ *
28668
+ * - `http` — the Fastify tRPC plugin (`/trpc/*`), one context per request.
28669
+ * - `ws` — `applyWSSHandler`, counted per OPERATION rather than per
28670
+ * connection; the viewer talks to the hub over `wsLink`
28671
+ * exclusively, so this is the plane the HTTP census could not see.
28672
+ * - `mesh` — the in-process `$core-caps` bridge (`createCaller`), which
28673
+ * never touches a socket and therefore never touched a census.
28674
+ * - `unknown` — counted, plane undecidable. No hook produces it today, and
28675
+ * that is exactly what its `0` asserts: every plane the hub has can name
28676
+ * itself. It is an output bucket, never a knob — a call that arrives on a
28677
+ * plane nobody instrumented lands here instead of vanishing from the total.
28678
+ */
28679
+ var TransportPlaneSchema = _enum([
28680
+ "http",
28681
+ "ws",
28682
+ "mesh",
28683
+ "unknown"
28684
+ ]);
28685
+ /**
28686
+ * Calls per plane. Every key is always present, `0` included — an absent plane
28687
+ * reads as "not instrumented", which is the one thing this census must never
28688
+ * make an operator wonder about.
28689
+ */
28690
+ var TransportPlaneCountsSchema = object({
28691
+ http: number(),
28692
+ ws: number(),
28693
+ mesh: number(),
28694
+ unknown: number()
28695
+ });
28696
+ /**
28697
+ * One `(plane, procedure, user-agent, ip, principal)` tuple of the transport
28482
28698
  * census. `principal` is the DERIVED identity (`apocaliss92 (admin)`,
28483
28699
  * `scoped:1a2b3c4d (scoped-token)`, `anonymous`) that the tRPC error log
28484
28700
  * already prints - never a token, never an `Authorization` header.
28701
+ *
28702
+ * `subscriptions` is counted APART from `calls`: a subscription is opened once
28703
+ * and lives for hours, so folding it into a call count makes one long-lived
28704
+ * stream look like a storm.
28485
28705
  */
28486
28706
  var RequestCensusGroupSchema = object({
28707
+ plane: TransportPlaneSchema,
28487
28708
  procedure: string(),
28488
28709
  userAgent: string(),
28489
28710
  ip: string(),
28490
28711
  principal: string(),
28491
28712
  calls: number(),
28713
+ subscriptions: number(),
28492
28714
  perMin: number()
28493
28715
  });
28494
28716
  /**
@@ -28501,6 +28723,14 @@ var RequestCensusGroupSchema = object({
28501
28723
  var RequestCensusProcedureSchema = object({
28502
28724
  procedure: string(),
28503
28725
  calls: number(),
28726
+ /**
28727
+ * The same total, split by transport. THIS is the row that answers the
28728
+ * question the census exists for: one look at `deviceManager.listAll` says
28729
+ * which plane carried the 4 960, without joining two log lines by eye.
28730
+ */
28731
+ planes: TransportPlaneCountsSchema,
28732
+ /** Subscription STARTS on this procedure. Never folded into `calls`. */
28733
+ subscriptions: number(),
28504
28734
  perMin: number()
28505
28735
  });
28506
28736
  /**
@@ -28528,14 +28758,45 @@ var RequestCensusStatusSchema = object({
28528
28758
  */
28529
28759
  procedureCalls: number(),
28530
28760
  /**
28761
+ * `procedureCalls` split by transport. The four keys sum to
28762
+ * `procedureCalls` by construction - {@link RequestCensusSnapshotSchema}'s
28763
+ * `planesExplainTotal` is that identity, checked rather than assumed.
28764
+ */
28765
+ planes: TransportPlaneCountsSchema,
28766
+ /**
28767
+ * True iff `planes` sums to `procedureCalls`. False means a call was counted
28768
+ * on no plane at all - which is a RESULT (a plane is missing from the
28769
+ * instrument), not a failure, and it has to be visible to be read as one.
28770
+ */
28771
+ planesExplainTotal: boolean(),
28772
+ /**
28531
28773
  * tRPC WebSocket connections opened during the window. NOT calls - the WS
28532
- * transport resolves one context per connection - but the number that says
28533
- * whether a plane this census cannot see was busy while HTTP was quiet.
28774
+ * adapter resolves one context per connection - kept because a plane's call
28775
+ * count of zero against 37 open connections says something different from a
28776
+ * plane with no connections at all.
28534
28777
  */
28535
28778
  wsConnections: number(),
28779
+ /**
28780
+ * Client frames the WS plane looked at. `wsMessages` far above
28781
+ * `planes.ws + subscriptions` means most traffic is not operations
28782
+ * (keepalives, connection params) - which is itself an answer.
28783
+ */
28784
+ wsMessages: number(),
28785
+ /**
28786
+ * Subscription STARTS across every plane, excluded from `procedureCalls` on
28787
+ * purpose: one live-events stream opened at boot and held for six hours is
28788
+ * one subscription, and counting it as a call would let a quiet plane
28789
+ * masquerade as the storm.
28790
+ */
28791
+ subscriptions: number(),
28792
+ /** `subscription.stop` frames. Starts minus stops is what is still open. */
28793
+ subscriptionStops: number(),
28536
28794
  distinctGroups: number(),
28537
- /** Calls counted in the totals whose group attribution was shed at the
28538
- * cardinality bound. */
28795
+ /**
28796
+ * Operations counted in the totals whose CALLER attribution was shed at the
28797
+ * cardinality bound. Unrelated to the `unknown` PLANE: these calls know
28798
+ * which transport they arrived on, they just lost their group row.
28799
+ */
28539
28800
  unattributedCalls: number(),
28540
28801
  procedures: array(RequestCensusProcedureSchema).readonly(),
28541
28802
  groups: array(RequestCensusGroupSchema).readonly()
@@ -28558,10 +28819,11 @@ var DiagnosticIdSchema = _enum(["request-census"]);
28558
28819
  * The layers of the level hierarchy, general → specific. The most specific
28559
28820
  * layer that carries an explicit value wins.
28560
28821
  *
28561
- * `component` is DECLARED and not yet resolvable: the per-component channels
28562
- * are a later slice of the same plan, and a `levelSource` enum that has to
28563
- * grow later would force every consumer of this document to change with it.
28564
- * Nothing returns `component` today.
28822
+ * `component` became RESOLVABLE on 2026-08-27: a component is a declared log
28823
+ * CHANNEL (`stream-broker.webrtc`, `provider-reolink.baichuan`), named by
28824
+ * `scopeComponent`. It was declared-but-dark in the first slice precisely so
28825
+ * that turning it on would not force every consumer of this document to widen
28826
+ * a `levelSource` enum — which is what has now not happened.
28565
28827
  */
28566
28828
  var LoggingScopeKindSchema = _enum([
28567
28829
  "cluster",
@@ -28588,6 +28850,14 @@ var LoggingLevelLayerSchema = object({
28588
28850
  scope: LoggingScopeKindSchema,
28589
28851
  /** The node this layer speaks for; `null` on the cluster layer. */
28590
28852
  nodeId: string().nullable(),
28853
+ /**
28854
+ * The declared channel this layer speaks for; `null` on every layer but
28855
+ * `component`. Never folded into `nodeId`: a component level is CLUSTER-WIDE
28856
+ * by design — the convention this repo settled on is one orchestrator-wide
28857
+ * setting, never per node (D52) — so a component layer that carried a node
28858
+ * would invite a per-node copy of a value that has no per-node meaning.
28859
+ */
28860
+ component: string().nullable(),
28591
28861
  /** Explicitly set here, or `null` when this layer inherits. */
28592
28862
  level: LogLevelSchema$1.nullable()
28593
28863
  });
@@ -28629,6 +28899,49 @@ var DiagnosticWindowPatchSchema = object({
28629
28899
  reportEveryMs: number().int().positive().optional()
28630
28900
  });
28631
28901
  /**
28902
+ * A channel ARMED, as the document reports it.
28903
+ *
28904
+ * `armMs` is not echoed back: what an operator needs to see is the deadline
28905
+ * and the time left, because a diagnostic left running is itself an incident
28906
+ * and "armed for 10 minutes" said an hour ago is not an answer.
28907
+ */
28908
+ var LogChannelWindowStateSchema = object({
28909
+ channel: string(),
28910
+ armed: boolean(),
28911
+ /** Epoch ms the window closes at. 0 when disarmed. */
28912
+ armedUntilMs: number(),
28913
+ /** Ms left before it expires on its own. 0 when disarmed. */
28914
+ remainingMs: number(),
28915
+ /**
28916
+ * The cameras it is narrowed to, or `null` for every camera.
28917
+ *
28918
+ * A channel declared `perDevice: false` can only ever report `null` here:
28919
+ * its lines do not carry `tags: { deviceId }`, so narrowing them would
28920
+ * produce a filter that silently matches nothing. The server REFUSES such a
28921
+ * patch rather than quietly widening it — ignoring the request would teach
28922
+ * the operator that per-camera filtering works on that channel when it does
28923
+ * not.
28924
+ */
28925
+ deviceIds: array(number().int()).readonly().nullable()
28926
+ });
28927
+ /**
28928
+ * `armMs: 0` DISARMS. Same grammar as {@link DiagnosticWindowPatchSchema}, and
28929
+ * for the same reason: a channel is a window with a deadline, never a switch.
28930
+ */
28931
+ var LogChannelWindowPatchSchema = object({
28932
+ channel: string().min(1),
28933
+ armMs: number().int().min(0),
28934
+ /**
28935
+ * Narrow to these numeric device ids. Absent or `null` = every camera.
28936
+ *
28937
+ * Numeric because the repo's own rule makes it possible: every log line
28938
+ * about a device carries `tags: { deviceId }` with the numeric id. That rule
28939
+ * was paid for with a 22% thumbnail gap and a 3-hour media blackout both
28940
+ * diagnosed by hand, and this is the first thing that collects on it.
28941
+ */
28942
+ deviceIds: array(number().int()).readonly().nullable().optional()
28943
+ });
28944
+ /**
28632
28945
  * A PATCH, and patches MERGE.
28633
28946
  *
28634
28947
  * A field absent from the patch is left exactly as it was — arming a
@@ -28647,7 +28960,14 @@ var LoggingSettingsPatchSchema = object({
28647
28960
  * Only the diagnostics NAMED here change. An armed window that is not listed
28648
28961
  * keeps running — a patch is never a full replacement.
28649
28962
  */
28650
- diagnostics: array(DiagnosticWindowPatchSchema).readonly().optional()
28963
+ diagnostics: array(DiagnosticWindowPatchSchema).readonly().optional(),
28964
+ /**
28965
+ * Only the channels NAMED here change. An armed channel that is not listed
28966
+ * keeps running — same rule as `diagnostics`, because a patch that silently
28967
+ * disarmed the channels it did not mention would make the Levels page and
28968
+ * the Diagnostics page fight over the same value.
28969
+ */
28970
+ channels: array(LogChannelWindowPatchSchema).readonly().optional()
28651
28971
  });
28652
28972
  /**
28653
28973
  * Which LAYER of the hierarchy is addressed. Absent = the cluster layer.
@@ -28660,9 +28980,22 @@ var LoggingSettingsPatchSchema = object({
28660
28980
  * authority over the whole hierarchy and answers for every layer, so the
28661
28981
  * layer selector needs a name the transport does not already own.
28662
28982
  */
28663
- var GetLoggingSettingsInputSchema = object({ scopeNodeId: string().optional() });
28983
+ var GetLoggingSettingsInputSchema = object({
28984
+ scopeNodeId: string().optional(),
28985
+ /**
28986
+ * The declared CHANNEL this document is addressed at, when the caller wants
28987
+ * the `component` layer. Absent = the node/cluster hierarchy only.
28988
+ *
28989
+ * Naming it separately rather than overloading `scopeNodeId` keeps the two
28990
+ * axes from collapsing: a component level is cluster-wide, a node level is
28991
+ * not, and one selector for both would make "which of these two did I just
28992
+ * set" unanswerable — the exact ambiguity `explicit` exists to remove.
28993
+ */
28994
+ scopeComponent: string().optional()
28995
+ });
28664
28996
  var SetLoggingSettingsInputSchema = object({
28665
28997
  scopeNodeId: string().optional(),
28998
+ scopeComponent: string().optional(),
28666
28999
  patch: LoggingSettingsPatchSchema
28667
29000
  });
28668
29001
  /**
@@ -28677,9 +29010,20 @@ var SetLoggingSettingsInputSchema = object({
28677
29010
  var LoggingSettingsStateSchema = object({
28678
29011
  /** The layer this document was read at. `null` = the cluster layer. */
28679
29012
  scopeNodeId: string().nullable(),
29013
+ /** The channel this document was read at. `null` = no component layer. */
29014
+ scopeComponent: string().nullable(),
28680
29015
  effective: LoggingEffectiveSchema,
28681
29016
  explicit: LoggingExplicitSchema,
28682
29017
  activeWindows: array(DiagnosticWindowSchema).readonly(),
29018
+ /**
29019
+ * Every channel the cluster's addons DECLARE, gathered from the
29020
+ * `log-channels` providers. Not stored anywhere: assembled per read, so a
29021
+ * channel added by a redeployed addon appears without anybody editing a
29022
+ * list, and a channel whose addon is gone stops being offered.
29023
+ */
29024
+ channels: array(LogChannelDescriptorSchema).readonly(),
29025
+ /** The channels ARMED right now, each with its deadline. */
29026
+ activeChannels: array(LogChannelWindowStateSchema).readonly(),
28683
29027
  persisted: boolean()
28684
29028
  });
28685
29029
  method(_void(), FeatureManifestSchema), method(_void(), HealthStatusSchema), method(_void(), FeatureManifestSchema), method(_void(), array(NetworkAddressSchema).readonly()), method(_void(), unknown().nullable(), { auth: "admin" }), method(record(string(), unknown()), _null(), {
@@ -31664,6 +32008,12 @@ Object.freeze({
31664
32008
  addonId: null,
31665
32009
  access: "view"
31666
32010
  },
32011
+ "dataStoreProvider.aggregate": {
32012
+ capName: "data-store-provider",
32013
+ capScope: "system",
32014
+ addonId: null,
32015
+ access: "view"
32016
+ },
31667
32017
  "dataStoreProvider.count": {
31668
32018
  capName: "data-store-provider",
31669
32019
  capScope: "system",
@@ -32078,6 +32428,12 @@ Object.freeze({
32078
32428
  addonId: null,
32079
32429
  access: "view"
32080
32430
  },
32431
+ "deviceManager.getChildrenBatch": {
32432
+ capName: "device-manager",
32433
+ capScope: "system",
32434
+ addonId: null,
32435
+ access: "view"
32436
+ },
32081
32437
  "deviceManager.getConfigSchema": {
32082
32438
  capName: "device-manager",
32083
32439
  capScope: "system",
@@ -33128,6 +33484,18 @@ Object.freeze({
33128
33484
  addonId: null,
33129
33485
  access: "create"
33130
33486
  },
33487
+ "logChannels.apply": {
33488
+ capName: "log-channels",
33489
+ capScope: "system",
33490
+ addonId: null,
33491
+ access: "create"
33492
+ },
33493
+ "logChannels.list": {
33494
+ capName: "log-channels",
33495
+ capScope: "system",
33496
+ addonId: null,
33497
+ access: "view"
33498
+ },
33131
33499
  "logDestination.query": {
33132
33500
  capName: "log-destination",
33133
33501
  capScope: "system",
@@ -35282,6 +35650,12 @@ Object.freeze({
35282
35650
  addonId: null,
35283
35651
  access: "create"
35284
35652
  },
35653
+ "settingsStore.aggregate": {
35654
+ capName: "settings-store",
35655
+ capScope: "system",
35656
+ addonId: null,
35657
+ access: "view"
35658
+ },
35285
35659
  "settingsStore.count": {
35286
35660
  capName: "settings-store",
35287
35661
  capScope: "system",
@@ -36861,6 +37235,11 @@ Object.freeze({
36861
37235
  form: "single",
36862
37236
  optional: false
36863
37237
  }],
37238
+ "deviceManager.getChildrenBatch": [{
37239
+ name: "parentDeviceIds",
37240
+ form: "array",
37241
+ optional: false
37242
+ }],
36864
37243
  "deviceManager.getConfigSchema": [{
36865
37244
  name: "deviceId",
36866
37245
  form: "single",
package/dist/addon.mjs CHANGED
@@ -7478,6 +7478,111 @@ var CameraSwitchGroupSchema = object({
7478
7478
  fetchedAt: number()
7479
7479
  });
7480
7480
  /**
7481
+ * Per-component log CHANNELS — the gate a hot path consults, and the registry
7482
+ * an addon declares its channels in.
7483
+ *
7484
+ * ## Two axes, deliberately separated
7485
+ *
7486
+ * - **DECLARATION** — which channels exist. Only the addon knows:
7487
+ * `stream-broker` knows webrtc/ICE/RTP, `provider-reolink` knows
7488
+ * baichuan/handshake. A hand-wired central list rots at the first addition,
7489
+ * and rots silently. So a channel is declared where it is consulted, and the
7490
+ * `log-channels` capability enumerates the declarations.
7491
+ * - **VALUE** — at which level, for which scope, until when. That stays ONE
7492
+ * thing: the logging settings document on the `system` cap. Two authorities
7493
+ * over the values is the exact defect
7494
+ * `docs/design/plans/2026-08-26-logging-per-componente.md` was written to
7495
+ * remove; re-introducing it from the cure side would be grotesque.
7496
+ *
7497
+ * Nothing in this file reads a clock, an env var or a store. The registry is
7498
+ * a MIRROR: it is moved only by {@link LogChannelRegistry.apply}, called off
7499
+ * the hot path with a value somebody actually read, and by
7500
+ * {@link LogChannelRegistry.tick}, called on a timer. A store read that fails
7501
+ * never reaches here, so it can neither disarm an armed channel nor arm a
7502
+ * disarmed one (D49).
7503
+ *
7504
+ * ## The canonical call shape
7505
+ *
7506
+ * ```ts
7507
+ * if (CH_RTP.on && CH_RTP.wants(deviceId)) {
7508
+ * CH_RTP.log(logger, 'rtp subscriber added', { tags: { deviceId }, meta: { ssrc } })
7509
+ * }
7510
+ * ```
7511
+ *
7512
+ * `on` is a plain boolean FIELD — never a getter — and it is the FIRST thing
7513
+ * read. Disarmed, a call site costs one load and one branch, and the `extras`
7514
+ * object literal is never constructed because it lives inside the branch. It
7515
+ * is the same shape already proven in production at `stream-broker.ts:1650`,
7516
+ * and the same discipline `LoggingGate.allowsDestination` uses for the
7517
+ * destination floor (measured at 1.93 ns/call when off).
7518
+ *
7519
+ * ## Why a channel emits at `info`
7520
+ *
7521
+ * `loki-logging.addon.ts` pins the destination default at `info` and
7522
+ * `loki-destination.ts` drops everything below it, so a line emitted at
7523
+ * `debug` never reaches Loki and the hub's in-memory ring only holds ~35
7524
+ * minutes. A diagnostic that cannot be read an hour later is worse than no
7525
+ * diagnostic, because it looks done. {@link LogChannelGate.log} therefore
7526
+ * emits at the channel's declared level, whose schema floor is `info`.
7527
+ */
7528
+ /**
7529
+ * The level a channel writes at once armed.
7530
+ *
7531
+ * `debug` is absent ON PURPOSE and not by omission: below `info` the line does
7532
+ * not leave the process for Loki, and the whole point of arming a channel is
7533
+ * to read it later.
7534
+ */
7535
+ var LogChannelLevelSchema = _enum([
7536
+ "info",
7537
+ "warn",
7538
+ "error"
7539
+ ]);
7540
+ /**
7541
+ * What an addon declares about one channel. No value, no state — a
7542
+ * declaration is inert.
7543
+ */
7544
+ var LogChannelDescriptorSchema = object({
7545
+ /**
7546
+ * Dotted `area.thing`, unique across the workspace. `area` is conventionally
7547
+ * the addon's short name so an operator reading a channel list can tell who
7548
+ * owns it without a second lookup.
7549
+ */
7550
+ name: string().min(3).regex(/^[a-z0-9-]+(\.[a-z0-9-]+)+$/, "a channel name is dotted lower-kebab, e.g. area.thing"),
7551
+ /** One sentence: what the operator will SEE after arming it. */
7552
+ description: string().min(1),
7553
+ /** The level its lines are emitted at. Never below `info`. */
7554
+ defaultLevel: LogChannelLevelSchema,
7555
+ /**
7556
+ * Whether this channel can be narrowed to a camera.
7557
+ *
7558
+ * `true` is a PROMISE with two halves, and both must hold: the gate is
7559
+ * consulted with the numeric device id, AND every line the channel admits
7560
+ * carries `tags: { deviceId }` with that same numeric id. The second half is
7561
+ * what makes `| json | deviceId="617"` work in Loki — `loki-payload.ts`
7562
+ * keeps `deviceId` out of the stream labels for cardinality, so the tag in
7563
+ * the body is the only way to filter.
7564
+ *
7565
+ * A channel whose lines carry the device only in `meta` (or not at all) is
7566
+ * declared `false`. Declaring it `true` anyway would be a lie the UI repeats:
7567
+ * the operator narrows to one camera, sees nothing, and concludes the code
7568
+ * path was never taken.
7569
+ */
7570
+ perDevice: boolean()
7571
+ });
7572
+ /**
7573
+ * An armed window over one channel, as the document hands it to a mirror.
7574
+ *
7575
+ * A window is a DEADLINE, never a flag (ADR-0244): a channel somebody forgot
7576
+ * expires by itself, which is the one failure a boolean cannot avoid.
7577
+ */
7578
+ var LogChannelWindowSchema = object({
7579
+ channel: string().min(1),
7580
+ /** Epoch ms the window closes at. */
7581
+ armedUntilMs: number(),
7582
+ /** `null` = every camera. A non-empty list narrows to those numeric ids. */
7583
+ deviceIds: array(number().int()).readonly().nullable()
7584
+ });
7585
+ /**
7481
7586
  * Ops-log — the durable, append-only operations audit shared by the
7482
7587
  * recordings and events management surfaces.
7483
7588
  *
@@ -11147,6 +11252,35 @@ var MutationFilterSchema = object({
11147
11252
  whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
11148
11253
  whereNot: record(string(), unknown()).optional()
11149
11254
  });
11255
+ /**
11256
+ * One scalar an {@link settingsStoreCapability.methods.aggregate} call asks for.
11257
+ *
11258
+ * `as` names the slot in the result, so the SAME column may be asked twice with
11259
+ * two operations (`MIN(startMs)` and `MAX(startMs)` in one round trip) — which
11260
+ * a `Record<column, op>` shape could not express.
11261
+ */
11262
+ var AggregateFieldSchema = object({
11263
+ /** Result key. */
11264
+ as: string().min(1),
11265
+ /** Column to aggregate. Must be a real column of a declared collection. */
11266
+ field: string().min(1),
11267
+ op: _enum([
11268
+ "sum",
11269
+ "min",
11270
+ "max"
11271
+ ])
11272
+ });
11273
+ /**
11274
+ * `COUNT(*)` plus one number per requested field.
11275
+ *
11276
+ * `null` means NO ROW MATCHED, never zero: a `SUM` over an empty set and a sum
11277
+ * that really is 0 are different facts, and an accounting caller that renders
11278
+ * "0 bytes, oldest = 0" for "nothing here" reports a lie about a disk.
11279
+ */
11280
+ var AggregateResultSchema = object({
11281
+ count: number().int(),
11282
+ values: record(string(), number().nullable())
11283
+ });
11150
11284
  /** A single stored record: `{ id, data }`. */
11151
11285
  var SettingsRecordSchema = object({
11152
11286
  id: string(),
@@ -11231,6 +11365,11 @@ method(object({
11231
11365
  collection: string(),
11232
11366
  filter: QueryFilterSchema.optional()
11233
11367
  }), number()), method(object({
11368
+ namespace: string().optional(),
11369
+ collection: string(),
11370
+ fields: array(AggregateFieldSchema).readonly(),
11371
+ filter: QueryFilterSchema.optional()
11372
+ }), AggregateResultSchema), method(object({
11234
11373
  namespace: string().optional(),
11235
11374
  collection: string(),
11236
11375
  field: string(),
@@ -11347,6 +11486,11 @@ method(_void(), EngineInfoSchema, { auth: "admin" }), method(object({
11347
11486
  collection: string(),
11348
11487
  filter: QueryFilterSchema.optional()
11349
11488
  }), number(), { auth: "admin" }), method(object({
11489
+ namespace: string().optional(),
11490
+ collection: string(),
11491
+ fields: array(AggregateFieldSchema).readonly(),
11492
+ filter: QueryFilterSchema.optional()
11493
+ }), AggregateResultSchema, { auth: "admin" }), method(object({
11350
11494
  namespace: string().optional(),
11351
11495
  collection: string(),
11352
11496
  field: string(),
@@ -12087,24 +12231,6 @@ var deviceProviderCapability = {
12087
12231
  })
12088
12232
  }
12089
12233
  };
12090
- /**
12091
- * Device Manager capability — hub-side singleton that unifies device persistence,
12092
- * live registry access, and all management operations into a single tRPC surface.
12093
- *
12094
- * Replaces:
12095
- * - `device-persistence` capability (persistence methods absorbed here)
12096
- * - `device-management.router.ts` (deleted in Phase 2)
12097
- * - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
12098
- *
12099
- * All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
12100
- * fork into separate processes but never run on remote cluster agents. Therefore:
12101
- * - No nodeId routing needed — this is a pure hub singleton.
12102
- * - The hub's DeviceRegistry is the single source of truth for all live devices.
12103
- * - No shadow registry or cross-node aggregation required.
12104
- *
12105
- * Forked workers register devices back to the hub via `ctx.devices`
12106
- * (DeviceManagerApi → ctx.api.deviceManager.registerDevice), same as today.
12107
- */
12108
12234
  /** One child-placement directive on a container's `childLayout`. Structurally
12109
12235
  * identical to `ChildLayoutEntry` in `device-management.ts` — the cap wire
12110
12236
  * shape for the same field. The child is identified by its re-sync-stable
@@ -12473,7 +12599,7 @@ method(object({
12473
12599
  * it answers today and the caller filters as it already does.
12474
12600
  */
12475
12601
  deviceIds: array(number()).optional()
12476
- }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
12602
+ }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ parentDeviceIds: array(number()).max(256) }), record(string(), array(DeviceInfoSchema))), method(object({ deviceId: number() }), object({
12477
12603
  mode: LinkedDevicesModeSchema,
12478
12604
  devices: array(LinkedDeviceSchema)
12479
12605
  })), method(object({ deviceIds: array(number()) }), array(LinkedDevicesForDeviceSchema)), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
@@ -13197,6 +13323,59 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }
13197
13323
  kind: "mutation",
13198
13324
  auth: "admin"
13199
13325
  });
13326
+ /**
13327
+ * `log-channels` — the capability an addon DECLARES its diagnostic channels
13328
+ * through. It stores nothing.
13329
+ *
13330
+ * ## Why a capability at all, and why this shape
13331
+ *
13332
+ * Which channels exist is knowledge only the addon has: `stream-broker` knows
13333
+ * webrtc/ICE/RTP, `provider-reolink` knows baichuan/handshake. A central list
13334
+ * maintained by hand rots at the first addition and rots INVISIBLY — nothing
13335
+ * fails, an operator just never sees the channel somebody added. So the list
13336
+ * is assembled from declarations at runtime.
13337
+ *
13338
+ * The shape is copied from `log-destination.cap.ts`, which already does
13339
+ * exactly this job: `mode: 'collection'`, `internal: true`,
13340
+ * `mount: { kind: 'skip' }` — no tRPC route, no generated hooks — while
13341
+ * `addons.listCapabilityProviders` still enumerates it, and the hub's
13342
+ * `CapabilityRegistry` still holds an RPC proxy per provider so a forked
13343
+ * runner's declarations reach hub-main over the transport that already exists.
13344
+ * No new UDS message, no second registry.
13345
+ *
13346
+ * ## What it deliberately does NOT own
13347
+ *
13348
+ * The VALUES — which channel is armed, for which cameras, until when — live in
13349
+ * ONE place: the logging settings document on the `system` cap
13350
+ * (`getLoggingSettings` / `setLoggingSettings`). Two authorities over the same
13351
+ * value is the defect the plan behind this work exists to remove, and
13352
+ * `setRequestCensus` was retired (D245) rather than allowed to be a second
13353
+ * one. {@link logChannelsCapability} therefore has no getter for a level, no
13354
+ * setter for a window and no persistence of any kind.
13355
+ *
13356
+ * ## Why `apply` is here even so
13357
+ *
13358
+ * The gate lives in the addon's PROCESS; the document lives in hub-main. Some
13359
+ * seam has to carry the value from the authority to the mirror, and a channel
13360
+ * that cannot be reached is precisely the dead knob this whole slice exists to
13361
+ * make impossible (D62 — `audioThresholdDbfs`, the HA entities with no source).
13362
+ * `apply` is that seam and nothing more: it writes an in-memory mirror, it
13363
+ * persists nothing, it is never the source of a value, and it is called only
13364
+ * with a set the hub actually read (D49 — a read that fails does not call it
13365
+ * at all, so no channel is silently disarmed by a bad read).
13366
+ */
13367
+ /** What `apply` reports back — enough to log, not enough to be a second state. */
13368
+ var LogChannelApplyResultSchema = object({
13369
+ /** How many declared channels are armed in this process after the call. */
13370
+ armed: number().int().min(0),
13371
+ /**
13372
+ * Names the document armed that this process does not declare. Reported
13373
+ * rather than swallowed: a name here is either a typo or an addon that has
13374
+ * not booted, and both deserve a line instead of silence.
13375
+ */
13376
+ unknown: array(string()).readonly()
13377
+ });
13378
+ method(_void(), array(LogChannelDescriptorSchema).readonly()), method(object({ windows: array(LogChannelWindowSchema).readonly() }), LogChannelApplyResultSchema, { kind: "mutation" });
13200
13379
  var LogLevelSchema = _enum([
13201
13380
  "debug",
13202
13381
  "info",
@@ -28479,17 +28658,60 @@ var SetSiteLocationInputSchema = object({
28479
28658
  longitude: number().min(-180).max(180)
28480
28659
  }).nullable();
28481
28660
  /**
28482
- * One `(procedure, user-agent, ip, principal)` tuple of the HTTP request
28661
+ * The TRANSPORT a call arrived on.
28662
+ *
28663
+ * Every counted call carries exactly one of these, and `unknown` is a PLANE
28664
+ * rather than a gap: a plane that cannot attribute a call declares it here, so
28665
+ * the call lands in a named bucket instead of vanishing. `planes` summing to
28666
+ * `procedureCalls` is what makes "the sum of the planes explains the total"
28667
+ * checkable rather than asserted.
28668
+ *
28669
+ * - `http` — the Fastify tRPC plugin (`/trpc/*`), one context per request.
28670
+ * - `ws` — `applyWSSHandler`, counted per OPERATION rather than per
28671
+ * connection; the viewer talks to the hub over `wsLink`
28672
+ * exclusively, so this is the plane the HTTP census could not see.
28673
+ * - `mesh` — the in-process `$core-caps` bridge (`createCaller`), which
28674
+ * never touches a socket and therefore never touched a census.
28675
+ * - `unknown` — counted, plane undecidable. No hook produces it today, and
28676
+ * that is exactly what its `0` asserts: every plane the hub has can name
28677
+ * itself. It is an output bucket, never a knob — a call that arrives on a
28678
+ * plane nobody instrumented lands here instead of vanishing from the total.
28679
+ */
28680
+ var TransportPlaneSchema = _enum([
28681
+ "http",
28682
+ "ws",
28683
+ "mesh",
28684
+ "unknown"
28685
+ ]);
28686
+ /**
28687
+ * Calls per plane. Every key is always present, `0` included — an absent plane
28688
+ * reads as "not instrumented", which is the one thing this census must never
28689
+ * make an operator wonder about.
28690
+ */
28691
+ var TransportPlaneCountsSchema = object({
28692
+ http: number(),
28693
+ ws: number(),
28694
+ mesh: number(),
28695
+ unknown: number()
28696
+ });
28697
+ /**
28698
+ * One `(plane, procedure, user-agent, ip, principal)` tuple of the transport
28483
28699
  * census. `principal` is the DERIVED identity (`apocaliss92 (admin)`,
28484
28700
  * `scoped:1a2b3c4d (scoped-token)`, `anonymous`) that the tRPC error log
28485
28701
  * already prints - never a token, never an `Authorization` header.
28702
+ *
28703
+ * `subscriptions` is counted APART from `calls`: a subscription is opened once
28704
+ * and lives for hours, so folding it into a call count makes one long-lived
28705
+ * stream look like a storm.
28486
28706
  */
28487
28707
  var RequestCensusGroupSchema = object({
28708
+ plane: TransportPlaneSchema,
28488
28709
  procedure: string(),
28489
28710
  userAgent: string(),
28490
28711
  ip: string(),
28491
28712
  principal: string(),
28492
28713
  calls: number(),
28714
+ subscriptions: number(),
28493
28715
  perMin: number()
28494
28716
  });
28495
28717
  /**
@@ -28502,6 +28724,14 @@ var RequestCensusGroupSchema = object({
28502
28724
  var RequestCensusProcedureSchema = object({
28503
28725
  procedure: string(),
28504
28726
  calls: number(),
28727
+ /**
28728
+ * The same total, split by transport. THIS is the row that answers the
28729
+ * question the census exists for: one look at `deviceManager.listAll` says
28730
+ * which plane carried the 4 960, without joining two log lines by eye.
28731
+ */
28732
+ planes: TransportPlaneCountsSchema,
28733
+ /** Subscription STARTS on this procedure. Never folded into `calls`. */
28734
+ subscriptions: number(),
28505
28735
  perMin: number()
28506
28736
  });
28507
28737
  /**
@@ -28529,14 +28759,45 @@ var RequestCensusStatusSchema = object({
28529
28759
  */
28530
28760
  procedureCalls: number(),
28531
28761
  /**
28762
+ * `procedureCalls` split by transport. The four keys sum to
28763
+ * `procedureCalls` by construction - {@link RequestCensusSnapshotSchema}'s
28764
+ * `planesExplainTotal` is that identity, checked rather than assumed.
28765
+ */
28766
+ planes: TransportPlaneCountsSchema,
28767
+ /**
28768
+ * True iff `planes` sums to `procedureCalls`. False means a call was counted
28769
+ * on no plane at all - which is a RESULT (a plane is missing from the
28770
+ * instrument), not a failure, and it has to be visible to be read as one.
28771
+ */
28772
+ planesExplainTotal: boolean(),
28773
+ /**
28532
28774
  * tRPC WebSocket connections opened during the window. NOT calls - the WS
28533
- * transport resolves one context per connection - but the number that says
28534
- * whether a plane this census cannot see was busy while HTTP was quiet.
28775
+ * adapter resolves one context per connection - kept because a plane's call
28776
+ * count of zero against 37 open connections says something different from a
28777
+ * plane with no connections at all.
28535
28778
  */
28536
28779
  wsConnections: number(),
28780
+ /**
28781
+ * Client frames the WS plane looked at. `wsMessages` far above
28782
+ * `planes.ws + subscriptions` means most traffic is not operations
28783
+ * (keepalives, connection params) - which is itself an answer.
28784
+ */
28785
+ wsMessages: number(),
28786
+ /**
28787
+ * Subscription STARTS across every plane, excluded from `procedureCalls` on
28788
+ * purpose: one live-events stream opened at boot and held for six hours is
28789
+ * one subscription, and counting it as a call would let a quiet plane
28790
+ * masquerade as the storm.
28791
+ */
28792
+ subscriptions: number(),
28793
+ /** `subscription.stop` frames. Starts minus stops is what is still open. */
28794
+ subscriptionStops: number(),
28537
28795
  distinctGroups: number(),
28538
- /** Calls counted in the totals whose group attribution was shed at the
28539
- * cardinality bound. */
28796
+ /**
28797
+ * Operations counted in the totals whose CALLER attribution was shed at the
28798
+ * cardinality bound. Unrelated to the `unknown` PLANE: these calls know
28799
+ * which transport they arrived on, they just lost their group row.
28800
+ */
28540
28801
  unattributedCalls: number(),
28541
28802
  procedures: array(RequestCensusProcedureSchema).readonly(),
28542
28803
  groups: array(RequestCensusGroupSchema).readonly()
@@ -28559,10 +28820,11 @@ var DiagnosticIdSchema = _enum(["request-census"]);
28559
28820
  * The layers of the level hierarchy, general → specific. The most specific
28560
28821
  * layer that carries an explicit value wins.
28561
28822
  *
28562
- * `component` is DECLARED and not yet resolvable: the per-component channels
28563
- * are a later slice of the same plan, and a `levelSource` enum that has to
28564
- * grow later would force every consumer of this document to change with it.
28565
- * Nothing returns `component` today.
28823
+ * `component` became RESOLVABLE on 2026-08-27: a component is a declared log
28824
+ * CHANNEL (`stream-broker.webrtc`, `provider-reolink.baichuan`), named by
28825
+ * `scopeComponent`. It was declared-but-dark in the first slice precisely so
28826
+ * that turning it on would not force every consumer of this document to widen
28827
+ * a `levelSource` enum — which is what has now not happened.
28566
28828
  */
28567
28829
  var LoggingScopeKindSchema = _enum([
28568
28830
  "cluster",
@@ -28589,6 +28851,14 @@ var LoggingLevelLayerSchema = object({
28589
28851
  scope: LoggingScopeKindSchema,
28590
28852
  /** The node this layer speaks for; `null` on the cluster layer. */
28591
28853
  nodeId: string().nullable(),
28854
+ /**
28855
+ * The declared channel this layer speaks for; `null` on every layer but
28856
+ * `component`. Never folded into `nodeId`: a component level is CLUSTER-WIDE
28857
+ * by design — the convention this repo settled on is one orchestrator-wide
28858
+ * setting, never per node (D52) — so a component layer that carried a node
28859
+ * would invite a per-node copy of a value that has no per-node meaning.
28860
+ */
28861
+ component: string().nullable(),
28592
28862
  /** Explicitly set here, or `null` when this layer inherits. */
28593
28863
  level: LogLevelSchema$1.nullable()
28594
28864
  });
@@ -28630,6 +28900,49 @@ var DiagnosticWindowPatchSchema = object({
28630
28900
  reportEveryMs: number().int().positive().optional()
28631
28901
  });
28632
28902
  /**
28903
+ * A channel ARMED, as the document reports it.
28904
+ *
28905
+ * `armMs` is not echoed back: what an operator needs to see is the deadline
28906
+ * and the time left, because a diagnostic left running is itself an incident
28907
+ * and "armed for 10 minutes" said an hour ago is not an answer.
28908
+ */
28909
+ var LogChannelWindowStateSchema = object({
28910
+ channel: string(),
28911
+ armed: boolean(),
28912
+ /** Epoch ms the window closes at. 0 when disarmed. */
28913
+ armedUntilMs: number(),
28914
+ /** Ms left before it expires on its own. 0 when disarmed. */
28915
+ remainingMs: number(),
28916
+ /**
28917
+ * The cameras it is narrowed to, or `null` for every camera.
28918
+ *
28919
+ * A channel declared `perDevice: false` can only ever report `null` here:
28920
+ * its lines do not carry `tags: { deviceId }`, so narrowing them would
28921
+ * produce a filter that silently matches nothing. The server REFUSES such a
28922
+ * patch rather than quietly widening it — ignoring the request would teach
28923
+ * the operator that per-camera filtering works on that channel when it does
28924
+ * not.
28925
+ */
28926
+ deviceIds: array(number().int()).readonly().nullable()
28927
+ });
28928
+ /**
28929
+ * `armMs: 0` DISARMS. Same grammar as {@link DiagnosticWindowPatchSchema}, and
28930
+ * for the same reason: a channel is a window with a deadline, never a switch.
28931
+ */
28932
+ var LogChannelWindowPatchSchema = object({
28933
+ channel: string().min(1),
28934
+ armMs: number().int().min(0),
28935
+ /**
28936
+ * Narrow to these numeric device ids. Absent or `null` = every camera.
28937
+ *
28938
+ * Numeric because the repo's own rule makes it possible: every log line
28939
+ * about a device carries `tags: { deviceId }` with the numeric id. That rule
28940
+ * was paid for with a 22% thumbnail gap and a 3-hour media blackout both
28941
+ * diagnosed by hand, and this is the first thing that collects on it.
28942
+ */
28943
+ deviceIds: array(number().int()).readonly().nullable().optional()
28944
+ });
28945
+ /**
28633
28946
  * A PATCH, and patches MERGE.
28634
28947
  *
28635
28948
  * A field absent from the patch is left exactly as it was — arming a
@@ -28648,7 +28961,14 @@ var LoggingSettingsPatchSchema = object({
28648
28961
  * Only the diagnostics NAMED here change. An armed window that is not listed
28649
28962
  * keeps running — a patch is never a full replacement.
28650
28963
  */
28651
- diagnostics: array(DiagnosticWindowPatchSchema).readonly().optional()
28964
+ diagnostics: array(DiagnosticWindowPatchSchema).readonly().optional(),
28965
+ /**
28966
+ * Only the channels NAMED here change. An armed channel that is not listed
28967
+ * keeps running — same rule as `diagnostics`, because a patch that silently
28968
+ * disarmed the channels it did not mention would make the Levels page and
28969
+ * the Diagnostics page fight over the same value.
28970
+ */
28971
+ channels: array(LogChannelWindowPatchSchema).readonly().optional()
28652
28972
  });
28653
28973
  /**
28654
28974
  * Which LAYER of the hierarchy is addressed. Absent = the cluster layer.
@@ -28661,9 +28981,22 @@ var LoggingSettingsPatchSchema = object({
28661
28981
  * authority over the whole hierarchy and answers for every layer, so the
28662
28982
  * layer selector needs a name the transport does not already own.
28663
28983
  */
28664
- var GetLoggingSettingsInputSchema = object({ scopeNodeId: string().optional() });
28984
+ var GetLoggingSettingsInputSchema = object({
28985
+ scopeNodeId: string().optional(),
28986
+ /**
28987
+ * The declared CHANNEL this document is addressed at, when the caller wants
28988
+ * the `component` layer. Absent = the node/cluster hierarchy only.
28989
+ *
28990
+ * Naming it separately rather than overloading `scopeNodeId` keeps the two
28991
+ * axes from collapsing: a component level is cluster-wide, a node level is
28992
+ * not, and one selector for both would make "which of these two did I just
28993
+ * set" unanswerable — the exact ambiguity `explicit` exists to remove.
28994
+ */
28995
+ scopeComponent: string().optional()
28996
+ });
28665
28997
  var SetLoggingSettingsInputSchema = object({
28666
28998
  scopeNodeId: string().optional(),
28999
+ scopeComponent: string().optional(),
28667
29000
  patch: LoggingSettingsPatchSchema
28668
29001
  });
28669
29002
  /**
@@ -28678,9 +29011,20 @@ var SetLoggingSettingsInputSchema = object({
28678
29011
  var LoggingSettingsStateSchema = object({
28679
29012
  /** The layer this document was read at. `null` = the cluster layer. */
28680
29013
  scopeNodeId: string().nullable(),
29014
+ /** The channel this document was read at. `null` = no component layer. */
29015
+ scopeComponent: string().nullable(),
28681
29016
  effective: LoggingEffectiveSchema,
28682
29017
  explicit: LoggingExplicitSchema,
28683
29018
  activeWindows: array(DiagnosticWindowSchema).readonly(),
29019
+ /**
29020
+ * Every channel the cluster's addons DECLARE, gathered from the
29021
+ * `log-channels` providers. Not stored anywhere: assembled per read, so a
29022
+ * channel added by a redeployed addon appears without anybody editing a
29023
+ * list, and a channel whose addon is gone stops being offered.
29024
+ */
29025
+ channels: array(LogChannelDescriptorSchema).readonly(),
29026
+ /** The channels ARMED right now, each with its deadline. */
29027
+ activeChannels: array(LogChannelWindowStateSchema).readonly(),
28684
29028
  persisted: boolean()
28685
29029
  });
28686
29030
  method(_void(), FeatureManifestSchema), method(_void(), HealthStatusSchema), method(_void(), FeatureManifestSchema), method(_void(), array(NetworkAddressSchema).readonly()), method(_void(), unknown().nullable(), { auth: "admin" }), method(record(string(), unknown()), _null(), {
@@ -31665,6 +32009,12 @@ Object.freeze({
31665
32009
  addonId: null,
31666
32010
  access: "view"
31667
32011
  },
32012
+ "dataStoreProvider.aggregate": {
32013
+ capName: "data-store-provider",
32014
+ capScope: "system",
32015
+ addonId: null,
32016
+ access: "view"
32017
+ },
31668
32018
  "dataStoreProvider.count": {
31669
32019
  capName: "data-store-provider",
31670
32020
  capScope: "system",
@@ -32079,6 +32429,12 @@ Object.freeze({
32079
32429
  addonId: null,
32080
32430
  access: "view"
32081
32431
  },
32432
+ "deviceManager.getChildrenBatch": {
32433
+ capName: "device-manager",
32434
+ capScope: "system",
32435
+ addonId: null,
32436
+ access: "view"
32437
+ },
32082
32438
  "deviceManager.getConfigSchema": {
32083
32439
  capName: "device-manager",
32084
32440
  capScope: "system",
@@ -33129,6 +33485,18 @@ Object.freeze({
33129
33485
  addonId: null,
33130
33486
  access: "create"
33131
33487
  },
33488
+ "logChannels.apply": {
33489
+ capName: "log-channels",
33490
+ capScope: "system",
33491
+ addonId: null,
33492
+ access: "create"
33493
+ },
33494
+ "logChannels.list": {
33495
+ capName: "log-channels",
33496
+ capScope: "system",
33497
+ addonId: null,
33498
+ access: "view"
33499
+ },
33132
33500
  "logDestination.query": {
33133
33501
  capName: "log-destination",
33134
33502
  capScope: "system",
@@ -35283,6 +35651,12 @@ Object.freeze({
35283
35651
  addonId: null,
35284
35652
  access: "create"
35285
35653
  },
35654
+ "settingsStore.aggregate": {
35655
+ capName: "settings-store",
35656
+ capScope: "system",
35657
+ addonId: null,
35658
+ access: "view"
35659
+ },
35286
35660
  "settingsStore.count": {
35287
35661
  capName: "settings-store",
35288
35662
  capScope: "system",
@@ -36862,6 +37236,11 @@ Object.freeze({
36862
37236
  form: "single",
36863
37237
  optional: false
36864
37238
  }],
37239
+ "deviceManager.getChildrenBatch": [{
37240
+ name: "parentDeviceIds",
37241
+ form: "array",
37242
+ optional: false
37243
+ }],
36865
37244
  "deviceManager.getConfigSchema": [{
36866
37245
  name: "deviceId",
36867
37246
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-homematic",
3
- "version": "1.2.33",
3
+ "version": "1.2.35",
4
4
  "description": "Homematic / HomematicIP (CCU3 / RaspberryMatic) device-provider addon for CamStack — wraps the nodehomematic library",
5
5
  "keywords": [
6
6
  "camstack",