@camstack/system 1.1.11 → 1.1.13

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.
@@ -1,4 +1,4 @@
1
- import { $ as setWorkerNativeCapsChangeListener, A as createUdsLoggerWithControl, X as getWorkerNativeCapProvider, Z as getWorkerNativeCapSnapshot, i as createUdsAddonContext, j as LocalChildClient, mt as resolveAddonClass, pt as installManifestNativeDeps, t as installManifestPythonDeps, tt as validateProviderRegistrations } from "./manifest-python-deps-1zTy3vXe.mjs";
1
+ import { $ as setWorkerNativeCapsChangeListener, A as createUdsLoggerWithControl, X as getWorkerNativeCapProvider, Z as getWorkerNativeCapSnapshot, i as createUdsAddonContext, j as LocalChildClient, mt as resolveAddonClass, pt as installManifestNativeDeps, t as installManifestPythonDeps, tt as validateProviderRegistrations } from "./manifest-python-deps-BfJEdXhi.mjs";
2
2
  import { t as CustomActionRegistry } from "./custom-action-registry-BEXwC-oo.mjs";
3
3
  import { register } from "node:module";
4
4
  import * as fs from "node:fs";
@@ -0,0 +1,22 @@
1
+ import { ConfigUISchemaWithValues } from '@camstack/types';
2
+ import { ContributionShape } from './device-meta-types.js';
3
+ /**
4
+ * Walk the sections/fields of a contribution and inject `writerCapName` +
5
+ * `writerAddonId` + `source` on each editable field. Readonly fields and
6
+ * structural fields (separator/info/button) pass through untouched. The
7
+ * aggregator is the single place that knows provenance — provider schemas
8
+ * stay clean, UI-bound metadata is attached once at the boundary.
9
+ */
10
+ export declare function tagContribution(contribution: ContributionShape, capName: string, addonId: string, kind: 'settings' | 'live'): ContributionShape;
11
+ export declare function isFieldRecord(value: unknown): value is Record<string, unknown>;
12
+ /**
13
+ * Convert a strict `ConfigUISchemaWithValues` (readonly arrays, typed
14
+ * field union) into the cap wire shape `ContributionShape` (mutable
15
+ * arrays, opaque field records). Required because the cap method z.infer
16
+ * uses mutable arrays — readonly arrays are not assignable to mutable
17
+ * even when structurally identical, so a structural copy bridges the gap
18
+ * without disabling the type checker.
19
+ */
20
+ export declare function toWireShape(input: ConfigUISchemaWithValues): ContributionShape;
21
+ export declare function tagField(field: unknown, capName: string, addonId: string, source: 'settings' | 'live', kind: 'settings' | 'live'): unknown;
22
+ export declare function mergeAggregates(parts: readonly ContributionShape[]): ContributionShape;
@@ -0,0 +1,220 @@
1
+ import { deviceManagerCapability, InferProvider, AddonContext, CapabilityDefinition, DeviceBindingEntry, DeviceConfigSpec, DeviceSettingsContribution, ICapabilityRegistry, IDevice } from '@camstack/types';
2
+ import { ContributionShape, DriverConfigSchemaResult } from './device-meta-types.js';
3
+ import { BindingsDeps, RemoteNativeCaps } from './device-bindings-store.js';
4
+ /**
5
+ * Dependencies the aggregation + update routing functions consume. Assembled
6
+ * by the addon from its own state and passed in explicitly — no hidden `this`
7
+ * capture. `bindingsDeps` carries the lower-level context the bindings-store
8
+ * resolvers need so this module can call them directly.
9
+ */
10
+ export interface AggregationDeps {
11
+ readonly ctx: AddonContext;
12
+ readonly capabilityRegistry: ICapabilityRegistry | undefined;
13
+ readonly remoteNativeCaps: RemoteNativeCaps;
14
+ readonly bindingsDeps: BindingsDeps;
15
+ }
16
+ type IDeviceManagerProvider = InferProvider<typeof deviceManagerCapability>;
17
+ export declare function getDeviceAggregate(deps: AggregationDeps, deviceId: number, kind: 'settings' | 'live'): Promise<ContributionShape | null>;
18
+ /**
19
+ * System-scoped per-device contributions — the deliberate exception to
20
+ * binding-driven aggregation (D12).
21
+ *
22
+ * A `scope: 'system'` cap with `exposesDeviceSettings: true` holds per-device
23
+ * state but is NOT bound per-device: it is a system addon, not a BaseDevice,
24
+ * so it never calls `registerNativeCap` and never appears in
25
+ * `getBindings(deviceId)`. The binding-driven loop therefore skips it and its
26
+ * per-device panel never renders. Two such caps ship today:
27
+ * - `stream-broker` (singleton) — the RTSP-restream / pre-buffer settings
28
+ * tab, scoped to the cameras whose streams it serves.
29
+ * - `device-export` (collection) — the "Export" panel; a device can be
30
+ * exposed to MULTIPLE exporters at once (Alexa + HomeKit + HA-MQTT).
31
+ *
32
+ * D12 bans fanning a per-device call out to every same-named provider of a
33
+ * `scope: 'device'` cap, because vendor providers SHARE cap names
34
+ * (`stream-params`/`ptz` from reolink AND hikvision). System caps are NOT
35
+ * vendor-shared — a system cap name maps to one active provider (singleton)
36
+ * or one declared collection — so consulting the cap's own provider(s)
37
+ * directly is safe. Each provider gates internally by deviceId and returns
38
+ * null for devices it does not own.
39
+ *
40
+ * `seenCaps` skips any system cap that already contributed via a binding so
41
+ * its section is never duplicated.
42
+ */
43
+ export declare function collectSystemDeviceContributions(deps: AggregationDeps, deviceId: number, kind: 'settings' | 'live', seenCaps: ReadonlySet<string>): Promise<ContributionShape[]>;
44
+ /**
45
+ * D14: framework-derived device-config contribution.
46
+ *
47
+ * `kind === 'live'` — device-config caps contribute nothing to the live
48
+ * aggregate (they hold editable config, not live observables).
49
+ *
50
+ * `ui.kind === 'widget'` — emits a single structural `type:'widget'`
51
+ * section; the widget self-persists via the cap's own mutations.
52
+ *
53
+ * `ui.kind === 'derived-form'` — calls `getOptions`/`getStatus` on the
54
+ * bound provider, runs the registered pure builder, and returns the
55
+ * derived form sections. Returns null when the camera exposes nothing
56
+ * configurable or the provider is not yet registered.
57
+ */
58
+ export declare function deriveDeviceConfigContribution(deps: AggregationDeps, registry: ICapabilityRegistry, def: CapabilityDefinition & {
59
+ deviceConfig: DeviceConfigSpec;
60
+ }, entry: DeviceBindingEntry, deviceId: number, kind: 'settings' | 'live'): Promise<ContributionShape | null>;
61
+ /**
62
+ * Build the device-manager's own contribution to the aggregator — the
63
+ * device identity (id, stableId, addonId, type, online) + the
64
+ * driver-specific config exposed by the device class via
65
+ * `zodEntriesToConfigUI`.
66
+ *
67
+ * Two paths, deliberately symmetric with `getSettingsSchema`:
68
+ *
69
+ * - Hub-local: device's IDevice instance lives in this process'
70
+ * DeviceRegistry, we read config + schema directly by reference.
71
+ * - Cross-process: device lives in a forked worker (RtspCamera on
72
+ * provider-rtsp, ONVIF on provider-onvif, …). We ask the worker's
73
+ * `device-ops.getSettingsSchema` native provider for a wire-
74
+ * serializable ConfigUISchema and merge it in under the same
75
+ * "Driver Config" section, so the UI sees the same shape regardless
76
+ * of where the IDevice physically runs.
77
+ *
78
+ * Returns `null` only when the device genuinely doesn't exist anywhere
79
+ * (no hub-local, no persisted ownership, no device-ops native). The
80
+ * aggregator falls back to contributor sections only in that case.
81
+ */
82
+ export declare function buildBaseDeviceSection(deps: AggregationDeps, deviceId: number): Promise<ContributionShape | null>;
83
+ /**
84
+ * Lookup the native owner for `device-ops` on `deviceId` — the native-cap
85
+ * registry (hub-local and remote) is keyed by numeric id.
86
+ */
87
+ export declare function resolveNativeDeviceOwner(deps: AggregationDeps, deviceId: number): {
88
+ addonId: string;
89
+ nodeId: string;
90
+ } | null;
91
+ /**
92
+ * Aggregate `status` across every registered cap for a device.
93
+ *
94
+ * Walks the supplied cap list (or `CAP_NAMES_WITH_STATUS` when
95
+ * omitted), looks up a native provider per cap via the capability
96
+ * registry, calls `provider.getStatus({ deviceId })`, and validates
97
+ * the return against the cap's own `status.schema`. Validation
98
+ * failures log a warning and yield `null` for that cap so the
99
+ * overall aggregate stays usable — a single misbehaving provider
100
+ * must not blank out a device's entire status view.
101
+ *
102
+ * Returned shape is `Record<capName, unknown | null>`; the client-
103
+ * side hook tightens this to `CapStatusTypeMap` via the generated
104
+ * `cap-status-types.ts`.
105
+ */
106
+ export declare function getDeviceStatusAggregate(deps: AggregationDeps, input: {
107
+ readonly deviceId: number;
108
+ readonly caps?: readonly string[];
109
+ }): Promise<Record<string, unknown | null>>;
110
+ /**
111
+ * Return the driver-specific device-settings contribution. Hub-local
112
+ * devices call `getSettingsUISchema()` directly; forked-worker devices
113
+ * go through the `device-ops.getSettingsSchema` cap method on the
114
+ * numeric-id-keyed native registry.
115
+ *
116
+ * Returns a discriminated result so callers can distinguish three states:
117
+ * 'ok' – schema obtained successfully
118
+ * 'none' – driver genuinely has no settings schema
119
+ * 'unavailable' – worker was unreachable after retries (transient)
120
+ */
121
+ export declare function resolveDriverConfigSchema(deps: AggregationDeps, deviceId: number, hubLocal: {
122
+ addonId: string;
123
+ device: IDevice;
124
+ } | null): Promise<DriverConfigSchemaResult>;
125
+ export declare function updateDeviceField(deps: AggregationDeps, input: {
126
+ deviceId: number;
127
+ writerCapName: string;
128
+ writerAddonId: string;
129
+ key: string;
130
+ value: unknown;
131
+ }): Promise<{
132
+ success: true;
133
+ }>;
134
+ /**
135
+ * Dispatch a device custom action. Hub-local devices run it directly;
136
+ * forked/remote devices route through the `device-ops` native cap
137
+ * (`runAction`) — the same hub-local-then-device-ops fan-out that
138
+ * `updateDeviceField` uses for `applySettingsPatch`.
139
+ */
140
+ export declare function dispatchDeviceAction(deps: AggregationDeps, deviceId: number, action: string, input: unknown): Promise<unknown>;
141
+ /**
142
+ * Resolve the `DeviceSettingsContribution` provider that owns a tagged
143
+ * field. The `writerAddonId` on a tagged field equals `entry.providerAddonId`
144
+ * from the binding that produced it — for `kind:'wrapped'` entries, that is
145
+ * the wrapper addon id (e.g. 'snapshot-addon'); for native-only entries,
146
+ * the system addon id.
147
+ *
148
+ * Resolution order (Bug-3 fix):
149
+ * 1. `getProviderByAddon(capName, writerAddonId)` — resolves the
150
+ * system-registered provider by the addon id from the tagged field.
151
+ * For wrapper bindings (snapshot, motion-detection, etc.) this directly
152
+ * returns the wrapper provider, bypassing the native-first resolution
153
+ * order of `getProviderForDevice` that caused "method not found".
154
+ * 2. `getSingleton(capName)` — fallback for stale/mismatched writerAddonIds.
155
+ * The active singleton handles the contribution even if the addonId
156
+ * stored in the field is out of date (e.g. after an addon rename).
157
+ *
158
+ * `getProviderForDevice` is intentionally NOT used here: it returns the
159
+ * per-device native first when present (e.g. Reolink/ONVIF), and the native
160
+ * does NOT implement contribution methods — the root cause of Bug-3.
161
+ */
162
+ export declare function resolveContributionProvider(registry: ICapabilityRegistry, _def: CapabilityDefinition, writerCapName: string, writerAddonId: string, deviceId: number): DeviceSettingsContribution;
163
+ /**
164
+ * Batched counterpart of `updateDeviceField`. Groups changes by
165
+ * `(writerCapName, writerAddonId)` so each contributor receives a
166
+ * single `applyDeviceSettingsPatch` with all of its updates merged —
167
+ * avoids N round-trips for simultaneous edits in the same save.
168
+ *
169
+ * Per-provider failures are captured in the `failures[]` output so the
170
+ * admin UI can highlight which sections didn't persist; a failure on
171
+ * one provider does NOT abort the others.
172
+ */
173
+ export declare function updateDeviceFieldsBatch(deps: AggregationDeps, input: {
174
+ deviceId: number;
175
+ changes: ReadonlyArray<{
176
+ writerCapName: string;
177
+ writerAddonId: string;
178
+ key: string;
179
+ value: unknown;
180
+ }>;
181
+ }): Promise<{
182
+ success: true;
183
+ failures: {
184
+ writerCapName: string;
185
+ writerAddonId: string;
186
+ error: string;
187
+ }[];
188
+ }>;
189
+ /** Apply a single grouped patch to the appropriate provider. Mirrors
190
+ * `updateDeviceField` routing (special-case device-manager, else
191
+ * registry lookup). Used by `updateDeviceFieldsBatch`. */
192
+ export declare function applyGroupPatch(deps: AggregationDeps, deviceId: number, group: {
193
+ writerCapName: string;
194
+ writerAddonId: string;
195
+ patch: Record<string, unknown>;
196
+ }): Promise<void>;
197
+ export declare function listWrappersForCap(deps: AggregationDeps, input: {
198
+ capName: string;
199
+ }): Promise<string[]>;
200
+ export declare function listBindableCapsForDeviceType(deps: AggregationDeps, input: {
201
+ deviceType: string;
202
+ }): Promise<Array<{
203
+ capName: string;
204
+ wrappers: string[];
205
+ }>>;
206
+ export declare function setWrapperActive(deps: AggregationDeps, input: {
207
+ deviceId: number;
208
+ capName: string;
209
+ wrapperAddonId: string;
210
+ active: boolean;
211
+ }): Promise<void>;
212
+ /**
213
+ * Per-device wireable-field catalog — the domain status fields a device's
214
+ * bound caps expose, for the operator's cross-device wiring UI. Binding-driven:
215
+ * walks `getBindings(deviceId)`, skips wrapper caps (their status schemas are
216
+ * internal book-keeping, not wireable domain data), and enumerates each cap's
217
+ * status schema leaf fields. Behavior unchanged from the inline provider method.
218
+ */
219
+ export declare function getWireableFields(deps: AggregationDeps, input: Parameters<IDeviceManagerProvider['getWireableFields']>[0]): ReturnType<IDeviceManagerProvider['getWireableFields']>;
220
+ export {};
@@ -0,0 +1,100 @@
1
+ import { AddonContext, DeviceBindingEntry, ICapabilityRegistry } from '@camstack/types';
2
+ import { DeviceBindingsStore } from './device-meta-types.js';
3
+ /** Push-fed remote native-cap cache shape (numeric deviceId → capName → owner). */
4
+ export type RemoteNativeCaps = Map<number, Map<string, {
5
+ addonId: string;
6
+ nodeId: string;
7
+ }>>;
8
+ /**
9
+ * Low-level dependencies the binding resolver needs. Assembled by the addon
10
+ * from its own (mostly private) state and passed in explicitly — no hidden
11
+ * `this` capture.
12
+ */
13
+ export interface BindingsDeps {
14
+ readonly ctx: AddonContext;
15
+ readonly capabilityRegistry: ICapabilityRegistry | undefined;
16
+ readonly remoteNativeCaps: RemoteNativeCaps;
17
+ }
18
+ /**
19
+ * Wire the push-fed cross-process native-cap cache (`remoteNativeCaps`).
20
+ * Workers emit `DeviceBindingsChanged` on `ctx.registerNativeCap` / device
21
+ * removal; we mirror those into `remoteNativeCaps` so `getBindings` returns the
22
+ * full cluster view. Events from the local node are ignored: hub-local natives
23
+ * live in `capabilityRegistry` and are folded in directly by getBindings.
24
+ *
25
+ * Push events are accurate in the steady state but can be lost during the
26
+ * Moleculer transport handshake window (hub restart, crash-respawn,
27
+ * restartAddon). The reliable replacement for lost events is the D3 re-handshake
28
+ * (`listClusterNativeCaps()`), which `resolveNativeCapOwnerSync` /
29
+ * `getBindings` step 4 fall through to on a push miss. The `$node.disconnected`
30
+ * handler purges a gone node's entries; the worker re-handshakes (and re-emits
31
+ * `native-registered`) on its next boot.
32
+ */
33
+ export declare function wireRemoteNativeCapSync(ctx: AddonContext, remoteNativeCaps: RemoteNativeCaps): void;
34
+ export declare function readBindingsStore(deps: BindingsDeps): Promise<DeviceBindingsStore>;
35
+ export declare function writeBindingsStore(deps: BindingsDeps, next: DeviceBindingsStore): Promise<void>;
36
+ export declare function resolveWrapperNodeId(_wrapperAddonId: string): string;
37
+ /**
38
+ * Reduce a provider node id to the routable form `DeviceProxy` can pin.
39
+ *
40
+ * Every addon runs in its own `addon-runner` with the composite node id
41
+ * `${parentNodeId}/${runnerId}` (see `addon-runner.ts`) — e.g.
42
+ * `hub/provider-reolink` or `dev-agent-0/provider-reolink`. That composite is
43
+ * an IDENTITY, not a routable target: the `CapRouteResolver` reaches a child
44
+ * only THROUGH its parent (the hub resolves a hub-local-uds child by
45
+ * cap+device; an agent forwards to its own child). `DeviceProxy` pins
46
+ * `entry.providerNodeId` on every cap call, so a binding entry must expose the
47
+ * parent node id — otherwise the explicit pin classifies as `remote-moleculer`
48
+ * to an unknown node → `no-provider`, which surfaces as
49
+ * "this camera doesn't expose …" for client-proxy-driven widget caps
50
+ * (motion-zones, privacy-mask). Wrappers already report the parent via
51
+ * `resolveWrapperNodeId`; this aligns natives with the same contract.
52
+ *
53
+ * A flat node id (a genuine standalone node with no `/`) is returned
54
+ * unchanged.
55
+ */
56
+ export declare function toRoutableProviderNodeId(nodeId: string): string;
57
+ /**
58
+ * Resolve a remote native cap entry for a given `(capName, deviceId)` by
59
+ * consulting the handshake-fed `HubNodeRegistry` via
60
+ * `ctx.kernel.listClusterNativeCaps()`. Called when the push-based
61
+ * `remoteNativeCaps` cache misses — covers the Moleculer transport
62
+ * handshake window where `DeviceBindingsChanged` events were lost but the
63
+ * D3 re-handshake (post device restore) has already populated the registry.
64
+ *
65
+ * Returns `null` when the entry is genuinely not present in the cluster
66
+ * view (cap not registered on any worker for that device).
67
+ */
68
+ export declare function resolveRemoteNativeCapFromRegistry(deps: BindingsDeps, capName: string, deviceId: number): {
69
+ addonId: string;
70
+ nodeId: string;
71
+ } | null;
72
+ export declare function getBindings(deps: BindingsDeps, input: {
73
+ deviceId: number;
74
+ }): Promise<{
75
+ deviceId: number;
76
+ entries: DeviceBindingEntry[];
77
+ }>;
78
+ /**
79
+ * Whole-fleet binding dump. Iterates every device known to the
80
+ * deviceRegistry and reuses the per-device `getBindings` resolver
81
+ * for each — same routing rules, single round-trip. Used by
82
+ * `SystemManager.init()` for warm-boot.
83
+ *
84
+ * Bindings change rarely (wrapper toggle, device add/remove) so
85
+ * clients invalidate via the existing
86
+ * `capability.binding-changed` event rather than re-fetching this
87
+ * payload periodically.
88
+ */
89
+ export declare function getAllBindings(deps: BindingsDeps): Promise<Array<{
90
+ deviceId: number;
91
+ entries: DeviceBindingEntry[];
92
+ }>>;
93
+ /**
94
+ * Resolve a numeric deviceId to a stableId via persisted meta.
95
+ * Used only by the device-identity section of the device-details
96
+ * aggregator (see `buildBaseDeviceSection`) to surface the stableId as
97
+ * a readonly display field. All runtime/registry lookups are keyed by
98
+ * numeric deviceId; this helper is display-only.
99
+ */
100
+ export declare function lookupPersistedStableId(deps: BindingsDeps, deviceId: number): Promise<string | undefined>;