@camstack/system 1.1.11 → 1.1.12

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.
@@ -0,0 +1,159 @@
1
+ import { InferProvider, deviceManagerCapability, AddonContext } from '@camstack/types';
2
+ import { DeviceManagerSettings, DeviceMetaStore } from './device-meta-store.js';
3
+ import { ProviderContext } from './device-provider-context.js';
4
+ type IDeviceManagerProvider = InferProvider<typeof deviceManagerCapability>;
5
+ /**
6
+ * Stamp (or update) the owning integration id on a device's meta row + emit
7
+ * `DeviceMetaChanged`. The device-manager chokepoint shared by the
8
+ * `setIntegrationId` cap handler AND the create/adopt forwarders (queries)
9
+ * that stamp an integration's ownership onto every device it materializes.
10
+ * Idempotent. Takes its low-level deps directly (not the full `ProviderContext`)
11
+ * because `ProviderContext.stampIntegrationId` delegates HERE — passing the
12
+ * context would be a capture cycle.
13
+ */
14
+ export declare function stampIntegrationId(metaStore: DeviceMetaStore, settings: DeviceManagerSettings, ctx: AddonContext, deviceId: number, integrationId: string): Promise<void>;
15
+ export declare function allocateDeviceId(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['allocateDeviceId']>[0]): ReturnType<IDeviceManagerProvider['allocateDeviceId']>;
16
+ export declare function registerDevice(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['registerDevice']>[0]): ReturnType<IDeviceManagerProvider['registerDevice']>;
17
+ export declare function removeDevice(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['removeDevice']>[0]): ReturnType<IDeviceManagerProvider['removeDevice']>;
18
+ export declare function persistConfig(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['persistConfig']>[0]): ReturnType<IDeviceManagerProvider['persistConfig']>;
19
+ export declare function loadConfig(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['loadConfig']>[0]): ReturnType<IDeviceManagerProvider['loadConfig']>;
20
+ /**
21
+ * Load the operator-organisational meta surface for one device
22
+ * (`name` / `location` / `disabled` / `type` / `parentDeviceId`
23
+ * / `addonId` + `id` / `stableId`). Used by the kernel proxy's
24
+ * device-context factory to populate `ctx.deviceMeta` before
25
+ * the device class constructor runs. Returns `null` when no
26
+ * persisted row exists for the id.
27
+ *
28
+ * Reads default `location` to `null` and `disabled` to `false`
29
+ * for legacy rows that predate the field — production code
30
+ * relies on the IDevice type contract that both are present.
31
+ */
32
+ export declare function loadMeta(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['loadMeta']>[0]): ReturnType<IDeviceManagerProvider['loadMeta']>;
33
+ /**
34
+ * Update the operator-edited display name. Writes the meta
35
+ * row, emits a `DeviceMetaChanged` event so live consumers
36
+ * (UI device list, alert center) see the rename without
37
+ * polling. The live `IDevice.name` mirror is updated by the
38
+ * kernel proxy on its side (`device-cap-proxy.ts`).
39
+ */
40
+ export declare function setName(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['setName']>[0]): ReturnType<IDeviceManagerProvider['setName']>;
41
+ /**
42
+ * Update the operator-organisational location label. `null`
43
+ * clears it. Mirrors the same persist-then-emit shape as
44
+ * `setName`; consumers subscribe to `DeviceMetaChanged` and
45
+ * filter on `field: 'location'`.
46
+ */
47
+ export declare function setLocation(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['setLocation']>[0]): ReturnType<IDeviceManagerProvider['setLocation']>;
48
+ /**
49
+ * Update the device type. Writes the meta row, emits a
50
+ * `DeviceMetaChanged` event. Used by the kernel to apply
51
+ * `initialMeta.type` before device construction so the device
52
+ * constructs with its real type instead of the `allocateDeviceId`
53
+ * placeholder `'generic'`.
54
+ */
55
+ export declare function setType(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['setType']>[0]): ReturnType<IDeviceManagerProvider['setType']>;
56
+ /**
57
+ * Stamp (or update) the owning integration id on the device's
58
+ * meta row. Called by the kernel's `create()` pre-seed path
59
+ * when `initialMeta.integrationId` is set (analogous to
60
+ * `setName` / `setType`). Idempotent.
61
+ */
62
+ export declare function setIntegrationId(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['setIntegrationId']>[0]): ReturnType<IDeviceManagerProvider['setIntegrationId']>;
63
+ /**
64
+ * Stamp (or update) the linked device id on the device's meta
65
+ * row. Called by the kernel's `create()` pre-seed path when
66
+ * `initialMeta.linkDeviceId` is set (analogous to `setIntegrationId`).
67
+ * Idempotent. `null` explicitly clears a previous link.
68
+ */
69
+ export declare function setLinkDeviceId(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['setLinkDeviceId']>[0]): ReturnType<IDeviceManagerProvider['setLinkDeviceId']>;
70
+ /**
71
+ * Set (or clear) the durable primary-child override on a CONTAINER
72
+ * device's meta row, keyed on the chosen child's re-sync/rename-stable
73
+ * `entityId`. Mirrors `setLinkDeviceId` but persists a string entityId
74
+ * (survives a re-sync that reallocates the child's numeric id) instead
75
+ * of the numeric child id. `null` clears the override → priority default.
76
+ */
77
+ export declare function setPrimaryChildEntityId(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['setPrimaryChildEntityId']>[0]): ReturnType<IDeviceManagerProvider['setPrimaryChildEntityId']>;
78
+ /**
79
+ * Set (or replace) the container-level `childLayout` on a device's
80
+ * meta row. Mirrors `setPrimaryChildEntityId` but persists the
81
+ * accordion-section layout array (parent-only). Persisted, projected,
82
+ * and preserved across re-register/restore.
83
+ */
84
+ export declare function setChildLayout(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['setChildLayout']>[0]): ReturnType<IDeviceManagerProvider['setChildLayout']>;
85
+ /**
86
+ * Set (or replace) the operator-authored cross-device field wirings on
87
+ * a device's meta row. Mirrors `setChildLayout` but persists the
88
+ * `deviceLinks` array. Persisted, projected, and preserved across
89
+ * re-register/restore. Also maintains the in-memory `devicesWithLinks`
90
+ * Set used as an O(1) gate by `resolveLinkedStatus`.
91
+ */
92
+ export declare function setDeviceLinks(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['setDeviceLinks']>[0]): ReturnType<IDeviceManagerProvider['setDeviceLinks']>;
93
+ /**
94
+ * Stamp (or update) the semantic role on the device's meta row.
95
+ * Called by the kernel's `create()` / `spawnAccessoryChild`
96
+ * pre-seed when `initialMeta.role` is set (analogous to
97
+ * `setIntegrationId`). Idempotent. `null` clears a previous role.
98
+ */
99
+ export declare function setRole(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['setRole']>[0]): ReturnType<IDeviceManagerProvider['setRole']>;
100
+ /**
101
+ * Batched meta pre-seed. Applies every provided field to the
102
+ * device's meta row in ONE read-modify-write under a single
103
+ * `withMetaWriteLock` acquisition (one `deviceMeta` blob write),
104
+ * then emits one `DeviceMetaChanged` event per field that was
105
+ * supplied — preserving the exact semantics of the individual
106
+ * setters (`setName` / `setLocation` / `setType` /
107
+ * `setIntegrationId` / `setLinkDeviceId` / `setRole`). Omitted
108
+ * fields are left untouched; `null` clears `location` /
109
+ * `linkDeviceId` / `role`. Idempotent.
110
+ *
111
+ * Collapses the per-child meta pre-seed (up to 6 individual
112
+ * setter round-trips, each its own lock + write) into one — the
113
+ * dominant cost when the kernel's `spawnAccessoryChild` reconciles
114
+ * a many-entity container.
115
+ */
116
+ export declare function applyInitialMeta(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['applyInitialMeta']>[0]): ReturnType<IDeviceManagerProvider['applyInitialMeta']>;
117
+ /**
118
+ * Patch the device's hardware-identity metadata blob. Shallow
119
+ * merge — `null` removes a key, anything else overwrites.
120
+ * Drivers populate factual fields on first probe; operators
121
+ * augment via the Device Info tab. Idempotent: a no-op patch
122
+ * (every key already present with the same value) doesn't emit
123
+ * the meta-changed event.
124
+ */
125
+ export declare function setMetadata(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['setMetadata']>[0]): ReturnType<IDeviceManagerProvider['setMetadata']>;
126
+ /**
127
+ * Soft-disable the device. Persisted on the meta row;
128
+ * lifecycle gating is the driver's responsibility (BaseDevice
129
+ * exposes `this.disabled` for the driver to consult at the top
130
+ * of its lifecycle methods).
131
+ */
132
+ export declare function setDisabled(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['setDisabled']>[0]): ReturnType<IDeviceManagerProvider['setDisabled']>;
133
+ export declare function loadRuntimeState(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['loadRuntimeState']>[0]): ReturnType<IDeviceManagerProvider['loadRuntimeState']>;
134
+ /**
135
+ * Union of (1) operator-curated location registry and (2) labels
136
+ * currently in use on persisted devices. Case-insensitive
137
+ * dedupe (preserves the first-seen casing). Sorted
138
+ * case-insensitively for stable UI. Drives the Device Info
139
+ * location autocomplete.
140
+ */
141
+ export declare function listLocations(pctx: ProviderContext): ReturnType<IDeviceManagerProvider['listLocations']>;
142
+ /**
143
+ * Add a label to the curated location registry. Idempotent:
144
+ * existing entries (case-insensitive match) are silently kept.
145
+ * Empty / whitespace-only inputs throw — operators must supply a
146
+ * meaningful label.
147
+ */
148
+ export declare function addLocation(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['addLocation']>[0]): ReturnType<IDeviceManagerProvider['addLocation']>;
149
+ /**
150
+ * Remove a label from the curated registry. Match is
151
+ * case-insensitive. Devices that still reference this label keep
152
+ * their `meta.location` value (the registry is a suggestion
153
+ * list, not a foreign key) — pass `cascade: true` to also clear
154
+ * `setLocation` on every device that referenced this exact
155
+ * label. Cascade only matches case-insensitively + trimmed, same
156
+ * as the registry equality check.
157
+ */
158
+ export declare function removeLocation(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['removeLocation']>[0]): ReturnType<IDeviceManagerProvider['removeLocation']>;
159
+ export {};
@@ -0,0 +1,72 @@
1
+ import { DependentLink, ResolvedTargetLink } from './device-link-overlay.js';
2
+ import { AddonContext, IDeviceRegistry } from '@camstack/types';
3
+ import { AddonStore, PersistedDeviceMeta } from './device-meta-types.js';
4
+ /** The settings surface the meta store uses (`ctx.settings`, narrowed non-null). */
5
+ export type DeviceManagerSettings = NonNullable<AddonContext['settings']>;
6
+ /**
7
+ * Narrow view of the addon's cross-device link state the meta store mutates.
8
+ * The addon remains the SOLE owner of these maps; the store only reads
9
+ * `devicesWithLinks` and writes the rebuilt indexes back via `setLinkIndexes`.
10
+ */
11
+ export interface LinkIndexHost {
12
+ readonly devicesWithLinks: ReadonlySet<number>;
13
+ setLinkIndexes(targets: Map<string, ResolvedTargetLink[]>, dependents: Map<string, DependentLink[]>, expectedSourceStableIds: Set<string>): void;
14
+ }
15
+ export declare class DeviceMetaStore {
16
+ private readonly settings;
17
+ /** Hub-process device registry (null when this node owns no registry). */
18
+ readonly registry: IDeviceRegistry | null;
19
+ private readonly linkHost;
20
+ /** Synchronous ownership cache, keyed by NUMERIC deviceId → owning addonId.
21
+ * The persisted meta store is authoritative but reads are async; hub-side
22
+ * callers (e.g. `CapabilityRegistry.getNativeProvider` fallback) need
23
+ * ownership without awaiting. Kept in sync with every register/remove and
24
+ * warmed from persistence on boot. */
25
+ readonly idToAddonId: Map<number, string>;
26
+ /** Serialises every read-modify-write of the deviceMeta / deviceIndex blob
27
+ * through one promise chain (see `withMetaWriteLock`). Per-instance state —
28
+ * identical to the former `onInitialize` closure variable. */
29
+ private metaWriteChain;
30
+ constructor(settings: DeviceManagerSettings,
31
+ /** Hub-process device registry (null when this node owns no registry). */
32
+ registry: IDeviceRegistry | null, linkHost: LinkIndexHost);
33
+ readStore: () => Promise<AddonStore>;
34
+ readIndex: () => Promise<Record<string, string[]>>;
35
+ readMeta: () => Promise<Record<string, PersistedDeviceMeta>>;
36
+ /** Hardware-identity metadata map. Lives in a sibling key on the
37
+ * device-manager addon store so its writers (`setMetadata`) never
38
+ * collide with the lifecycle writers on `deviceMeta`
39
+ * (`registerDevice` / `setName` / `setLocation` / `setDisabled`).
40
+ * Single-writer per row eliminates the "writer X clobbers writer
41
+ * Y's field" bug class — `setMetadata` is the only producer. */
42
+ readMetadataMap: () => Promise<Record<string, Record<string, unknown>>>;
43
+ withMetaWriteLock: <T>(fn: () => Promise<T>) => Promise<T>;
44
+ /**
45
+ * Resolve a numeric deviceId to the owning `(addonId, stableId)` pair.
46
+ * Scans persisted meta — live IDevice lookup (hub registry) is handled
47
+ * separately per call site so callers can decide whether to route to
48
+ * an in-process driver or to the cross-process `device-ops` bridge.
49
+ * Returns null when no device with that id is known to the hub.
50
+ */
51
+ resolvePersistedById: (deviceId: number) => Promise<{
52
+ addonId: string;
53
+ stableId: string;
54
+ meta: PersistedDeviceMeta;
55
+ } | null>;
56
+ /** Direct children of a device: the union of the live registry's children
57
+ * and the persisted-meta scan (`parentDeviceId === parentId`), deduplicated
58
+ * and excluding self. Shared by the `remove` cascade and the `resetToSource`
59
+ * resync purge (#19). */
60
+ directChildIds: (parentId: number) => Promise<readonly number[]>;
61
+ /** Resolve a link's `sourceKey` to a live device id: the sibling accessory
62
+ * whose stableId is `${parentStableId}-${sourceKey}`. Null when absent.
63
+ * Pure over a pre-read meta map so a multi-link resolve reads the store once. */
64
+ resolveSourceDeviceId: (parentStableId: string, sourceKey: string, meta: Record<string, PersistedDeviceMeta>) => number | null;
65
+ allocateNextDeviceId: () => Promise<number>;
66
+ /** Rebuild the `linkTargets` / `linkDependents` reverse-index maps from the
67
+ * current persisted meta. Called at boot (once the `devicesWithLinks` seed
68
+ * has run) and whenever the link topology can change: `setDeviceLinks`,
69
+ * `registerDevice` (a new sibling source may resolve previously-dangling
70
+ * links), and `removeDevice` (its entries must be evicted). */
71
+ rebuildLinkDependents: () => Promise<void>;
72
+ }
@@ -0,0 +1,169 @@
1
+ import { ChildLayout, DeviceLinks } from '@camstack/types';
2
+ /**
3
+ * Wire shape matching `z.infer<typeof SettingsSchemaWithValuesSchema>` —
4
+ * duplicated as a plain interface because importing the Zod schema across
5
+ * package boundaries confuses `tsc` when the types package pins a
6
+ * different `zod` minor than the core package. Keeping the shape local
7
+ * keeps the addon decoupled from the schema's internal type encoding.
8
+ */
9
+ export interface ContributionShape {
10
+ tabs?: Array<{
11
+ id: string;
12
+ label: string;
13
+ icon: string;
14
+ order?: number;
15
+ }>;
16
+ sections: Array<{
17
+ id: string;
18
+ title: string;
19
+ description?: string;
20
+ style?: 'card' | 'accordion';
21
+ defaultCollapsed?: boolean;
22
+ columns?: 1 | 2 | 3 | 4;
23
+ tab?: string;
24
+ /** Where the section renders. Default 'settings' (Config tab); 'top-tab' hoists into the device-detail tab bar via DeviceDetail discovery. */
25
+ location?: 'settings' | 'top-tab';
26
+ order?: number;
27
+ fields: unknown[];
28
+ }>;
29
+ }
30
+ export type AggregateTabDecl = NonNullable<ContributionShape['tabs']>[number];
31
+ export type AggregateSection = ContributionShape['sections'][number];
32
+ /**
33
+ * Discriminated result from `resolveDriverConfigSchema`:
34
+ * 'ok' – schema obtained; `schema` field is present.
35
+ * 'none' – driver has no settings schema (legitimate empty).
36
+ * 'unavailable' – cross-process call failed after retries (transient).
37
+ */
38
+ export type DriverConfigSchemaResult = {
39
+ status: 'ok';
40
+ schema: ContributionShape;
41
+ } | {
42
+ status: 'none';
43
+ } | {
44
+ status: 'unavailable';
45
+ };
46
+ export interface DeviceBindingsStore {
47
+ readonly deviceBindings: Record<string, Record<string, {
48
+ wrapperAddonId: string | null;
49
+ }>>;
50
+ }
51
+ export interface PersistedDeviceMeta {
52
+ type: string;
53
+ name: string;
54
+ /** The owning addon's own identifier for this device (`stableId`) and the
55
+ * owning addon id (`addonId`). Both are now stored as FIELDS on the record
56
+ * because the `deviceMeta` map is keyed by the numeric device `id` (not the
57
+ * former composite `${addonId}:${stableId}` key). They make each row
58
+ * self-describing — meta lookups resolve `(addonId, stableId)` directly from
59
+ * the record instead of parsing a composite key. */
60
+ addonId: string;
61
+ stableId: string;
62
+ /** True once an operator explicitly renamed the device via `setName` (#17).
63
+ * Drives the `registerDevice` reconcile name-precedence: when set, the
64
+ * operator's name is PRESERVED; when unset, the fresh provider/construction
65
+ * name wins so an improved auto-name propagates instead of freezing. Absent on
66
+ * legacy rows — the reconcile treats a non-placeholder name as user-named for
67
+ * safety and stamps the resolved decision forward. */
68
+ userNamed?: boolean;
69
+ /**
70
+ * Operator-organisational label (room / area / zone). `null` when
71
+ * unset. Mutable through `setLocation` action; surfaced on
72
+ * `IDevice.location` via the kernel-built `DeviceContext.deviceMeta`
73
+ * snapshot. Free-text — providers don't interpret it.
74
+ */
75
+ location: string | null;
76
+ /**
77
+ * Soft-disabled flag. When `true`, the device class is still
78
+ * instantiated and visible in the UI (so the operator can flip
79
+ * back on without re-adding) but lifecycle hooks are gated by
80
+ * `BaseDevice.disabled`. Mutable through `setDisabled` action.
81
+ */
82
+ disabled: boolean;
83
+ /** Numeric parent id (or null for standalone). The hub resolves the
84
+ * parent's stableId to its numeric id at registration time, and from
85
+ * there everything internal references the parent by `parentDeviceId`
86
+ * — `stableId` only crosses the external boundary (autodiscovery and
87
+ * `(addonId, stableId)` row lookups). */
88
+ parentDeviceId: number | null;
89
+ /** Progressive, system-wide unique id. Allocated through
90
+ * `allocateDeviceId` before `registerDevice` runs; backfilled at boot
91
+ * for any legacy meta entry missing it (see init migration below).
92
+ * Post-init invariant: every persisted device has a numeric id. */
93
+ id: number;
94
+ /** Snapshot of `device.features` from the most recent `registerDevice`
95
+ * call. Used by `getDevice` when the live `IDevice` instance lives
96
+ * on a forked-worker / remote agent and isn't reachable from the
97
+ * hub registry. Optional for legacy rows that predate the field —
98
+ * callers fall back to an empty array. */
99
+ features?: readonly string[];
100
+ /** Stable hash of the device's EXPORT-relevant shape
101
+ * (`canonicalDeviceFingerprint({ deviceType, features })`). Recomputed on
102
+ * every `registerDevice`; when it changes, a `DeviceProvisioned` event is
103
+ * emitted so export adapters reconcile with ONE delta instead of reacting
104
+ * to the chatty per-cap `DeviceBindingsChanged`. Optional for legacy rows
105
+ * that predate the field. Spec:
106
+ * docs/superpowers/specs/2026-06-01-alexa-hap-export-reconciler-design.md */
107
+ exportFingerprint?: string;
108
+ /** Owning integration id, stamped at create via `setIntegrationId`. Optional:
109
+ * only present when the device was adopted through an integration (e.g. HA).
110
+ * Used by `removeByIntegration` to cascade-delete an integration's fleet. */
111
+ integrationId?: string;
112
+ /** Linked device id, stamped at create via `setLinkDeviceId`. Optional:
113
+ * only present when the device is linked to another device (e.g. HA entity
114
+ * linked to a parent camera). `null` explicitly clears a previous link. */
115
+ linkDeviceId?: number | null;
116
+ /** Durable primary-child override for a CONTAINER device, set via
117
+ * `setPrimaryChildEntityId`. Keyed on the chosen child's
118
+ * re-sync/rename-stable `entityId` (its `sourceInfo.id`, falling back to
119
+ * `stableId`) so it survives a re-sync that reallocates the child's numeric
120
+ * id — unlike the numeric `linkDeviceId`. `null` clears the override. */
121
+ primaryChildEntityId?: string | null;
122
+ /** Container-level layout hint: assigns specific child/accessory devices to
123
+ * named accordion sections (with optional order). Keyed on each child's
124
+ * re-sync-stable accessory `stableIdSuffix`. Set at create, preserved across
125
+ * re-register/restore — same lifecycle as `primaryChildEntityId`. */
126
+ childLayout?: ChildLayout;
127
+ /** Operator-authored cross-device field wirings (source field → this device's
128
+ * cap field). Same create/persist/project/restore lifecycle as `childLayout`.
129
+ * Absent ⇒ no links. Overlaid onto the target cap's `getStatus` at read time. */
130
+ deviceLinks?: DeviceLinks;
131
+ /** Semantic role string (`DeviceRole`), stamped at create via `setRole`.
132
+ * Optional: only present for accessory children that carry a known role
133
+ * (e.g. 'numeric-sensor', 'binary-sensor'). `null` clears a previous role. */
134
+ role?: string | null;
135
+ }
136
+ export interface AddonStore {
137
+ deviceIndex?: Record<string, string[]>;
138
+ /** Device meta records keyed by the numeric device `id` (`String(id)`).
139
+ * `addonId` and `stableId` are FIELDS on each `PersistedDeviceMeta` record
140
+ * — the map key is the numeric id, not the former composite
141
+ * `${addonId}:${stableId}` string. */
142
+ deviceMeta?: Record<string, PersistedDeviceMeta>;
143
+ /** Hardware-identity metadata blob keyed by the numeric device `id`
144
+ * (`String(id)`), the same primary key as `deviceMeta`.
145
+ * Stored in a separate key from `deviceMeta` so the lifecycle
146
+ * writers (registerDevice / setName / setLocation / setDisabled)
147
+ * never need to read or preserve the metadata field — single-writer
148
+ * per row eliminates the "writer X clobbers writer Y's field" bug
149
+ * class. The `metadata` field on `PersistedDeviceMeta` is a legacy
150
+ * fallback consulted only at read time during the lazy migration —
151
+ * new writes always go to `deviceMetadata`. */
152
+ deviceMetadata?: Record<string, Record<string, unknown>>;
153
+ /** Monotonic counter that feeds `PersistedDeviceMeta.id` for every
154
+ * new device. Incremented exactly once per registerDevice; never
155
+ * decremented. */
156
+ nextDeviceId?: number;
157
+ /**
158
+ * Operator-curated location registry. Each entry is a free-form room/
159
+ * area label (e.g. "Cucina", "Garage") that drives the
160
+ * autocomplete in the Device Info location editor. Devices reference
161
+ * these as plain strings via `PersistedDeviceMeta.location` — the
162
+ * registry is purely a suggestion list, NOT a foreign key. Removing a
163
+ * location from the registry leaves devices that still reference it
164
+ * intact; `listLocations` returns the union of registered labels +
165
+ * labels currently in use, so nothing disappears from the UI even if
166
+ * the operator forgot to register a label before assigning it.
167
+ */
168
+ locations?: readonly string[];
169
+ }
@@ -0,0 +1,74 @@
1
+ import { DeviceFeature, DeviceRole, IDevice, ICameraDevice, IDeviceRegistry, SourceInfo } from '@camstack/types';
2
+ import { PersistedDeviceMeta } from './device-meta-types.js';
3
+ /**
4
+ * Return true when `err` is a transient Moleculer error that is worth
5
+ * retrying — specifically any `MoleculerRetryableError` subclass
6
+ * (ServiceNotAvailableError, ServiceNotFoundError, BrokerDisconnectedError,
7
+ * RequestTimeoutError, …). Moleculer sets `retryable: true` on all of them.
8
+ *
9
+ * Falls back to a message-substring check for serialised errors that arrive
10
+ * across the Moleculer transport as plain objects rather than real instances.
11
+ */
12
+ export declare function isTransientMoleculerError(err: unknown): boolean;
13
+ export declare function shallowEqual(a: Record<string, unknown>, b: Record<string, unknown>): boolean;
14
+ /** Returns true when `x` is a non-null, non-array plain object. */
15
+ export declare function isRecord(x: unknown): x is Record<string, unknown>;
16
+ export declare function isCameraDevice(device: IDevice): device is ICameraDevice;
17
+ /**
18
+ * Validate persisted feature strings against the `DeviceFeature` enum
19
+ * — workers serialise the live `device.features` array (so every entry
20
+ * is a valid enum value at write time) but the persisted blob is loose
21
+ * `string[]` on the wire. The narrow keeps unknown values out of the
22
+ * `getDevice` response without losing the enum-typed contract.
23
+ */
24
+ export declare function persistedFeatures(features: readonly string[] | undefined): DeviceFeature[];
25
+ /**
26
+ * Build an identity-only `SourceInfo` from the persisted device config blob.
27
+ *
28
+ * Forked-worker accessory children (e.g. HA sensor entities) persist
29
+ * `entityId` and `system` in their config blob at spawn time. The hub has no
30
+ * live `IDevice` instance for these devices, so the persisted-fallback paths
31
+ * in `listAll` / `getDevice` / `getChildren` must reconstruct the identity
32
+ * `SourceInfo` from the config so dispatch routing keeps working.
33
+ *
34
+ * Rendering metadata (unit, precision) flows live through the cap STATUS SLICE
35
+ * and must NOT be derived here. Only `id` + `system` (+ `uniqueId` when
36
+ * present) are projected — purely identity, never rendering hints.
37
+ *
38
+ * Returns `undefined` when no identity anchor is resolvable (pure identity
39
+ * devices like cameras/hubs that don't carry `entityId`/`system` in their
40
+ * config blob) — the hub synthetic fallback applies in that case.
41
+ */
42
+ export declare function buildSourceInfoFromConfig(persistedConfig: Record<string, unknown>, stableId: string, addonId: string): SourceInfo | undefined;
43
+ /** Type guard: a string is a known `DeviceRole` enum member. */
44
+ export declare function isDeviceRole(value: string): value is DeviceRole;
45
+ /** Narrow a persisted role string (sqlite TEXT column) to a `DeviceRole`.
46
+ * Unknown / null values resolve to `null` so a stale or unrecognised role
47
+ * never leaks an off-enum string onto the wire shape. */
48
+ export declare function toDeviceRole(value: string | null | undefined): DeviceRole | null;
49
+ export declare function toDeviceInfo(addonId: string, device: IDevice, metadata?: Record<string, unknown> | null, metaRow?: PersistedDeviceMeta | null): {
50
+ deviceLinks?: import('@camstack/types').DeviceLinks | undefined;
51
+ childLayout?: import('@camstack/types').ChildLayout | undefined;
52
+ primaryChildEntityId?: string | null | undefined;
53
+ linkDeviceId?: number | null | undefined;
54
+ integrationId?: string | undefined;
55
+ id: number;
56
+ stableId: string;
57
+ addonId: string;
58
+ type: import('@camstack/types').DeviceType;
59
+ name: string;
60
+ location: string | null;
61
+ disabled: boolean;
62
+ parentDeviceId: number | null;
63
+ role: DeviceRole | null;
64
+ online: boolean;
65
+ probed: boolean;
66
+ features: DeviceFeature[];
67
+ isCamera: boolean;
68
+ config: Record<string, unknown>;
69
+ metadata: Record<string, unknown> | null;
70
+ };
71
+ export declare function resolveDeviceById(registry: IDeviceRegistry, deviceId: number): {
72
+ addonId: string;
73
+ device: IDevice;
74
+ } | null;
@@ -0,0 +1,65 @@
1
+ import { AddonContext, IDeviceAdoptionProvider, IDeviceManagerProvider, IDeviceOpsProvider, IDeviceRegistry, ICapabilityRegistry, InferProvider, deviceProviderCapability } from '@camstack/types';
2
+ import { DeviceManagerSettings, DeviceMetaStore } from './device-meta-store.js';
3
+ import { BindingsDeps } from './device-bindings-store.js';
4
+ type IDeviceProviderCap = InferProvider<typeof deviceProviderCapability>;
5
+ /**
6
+ * The addon-instance surface the extracted provider functions need. Exposed by
7
+ * the addon as a host object so the functions never capture `this` directly and
8
+ * the addon stays the sole owner of its private state.
9
+ */
10
+ export interface ProviderHost {
11
+ readonly ctx: AddonContext;
12
+ readonly capabilityRegistry: ICapabilityRegistry | undefined;
13
+ /** Device ids that currently carry at least one `deviceLinks` entry. */
14
+ readonly devicesWithLinks: Set<number>;
15
+ /** Expected link-source stableIds, gating the `registerDevice` rebuild. */
16
+ readonly expectedSourceStableIds: Set<string>;
17
+ /** Drop a removed device's overlay-emit guard entries (state-mirror keyed
18
+ * `${deviceId}:${cap}`). Called from `removeDevice`. */
19
+ dropDeviceOverlays(deviceId: number): void;
20
+ /** Push-fed remote native-cap cache (numeric deviceId → capName → owner). */
21
+ readonly remoteNativeCaps: Map<number, Map<string, {
22
+ addonId: string;
23
+ nodeId: string;
24
+ }>>;
25
+ /** Seed the runtime-state mirror for a device (boot path, no events). */
26
+ seedMirror(deviceId: number, blob: Record<string, unknown>): void;
27
+ /** Reset a stale per-session `feature-probe` timestamp before mirror seeding. */
28
+ withResetSessionProbe(blob: Record<string, unknown>): Record<string, unknown>;
29
+ /** Resolve a device's real `online` flag from the mirrored device-status slice. */
30
+ resolveDeviceOnline(deviceId: number, fallbackOnline: boolean): boolean;
31
+ /** Resolve a device's `probed` flag from the mirrored feature-probe slice. */
32
+ resolveDeviceProbed(deviceId: number): boolean;
33
+ /** Wait for a device-provider by addonId, returning null on timeout. */
34
+ waitDeviceProvider(addonId: string, timeoutMs?: number): Promise<IDeviceProviderCap | null>;
35
+ /** Require a device-provider by addonId — throws if not found. */
36
+ requireDeviceProvider(addonId: string): Promise<IDeviceProviderCap>;
37
+ /** Require a device-adoption provider by addonId — throws if not found. */
38
+ requireDeviceAdoptionProvider(addonId: string): Promise<IDeviceAdoptionProvider>;
39
+ }
40
+ /**
41
+ * Full context the provider-method modules consume. `provider` is a lazy getter
42
+ * for the assembled `IDeviceManagerProvider` so the few methods that self-call
43
+ * sibling cap methods (e.g. `remove` → `removeDevice`, `removeByIntegration` →
44
+ * `remove`, `enable`/`disable` → `setDisabled`) resolve the live implementation
45
+ * without a capture cycle at construction time.
46
+ */
47
+ export interface ProviderContext {
48
+ readonly host: ProviderHost;
49
+ readonly metaStore: DeviceMetaStore;
50
+ readonly settings: DeviceManagerSettings;
51
+ /** Hub-process device registry (null when this node owns no registry). */
52
+ readonly registry: IDeviceRegistry | null;
53
+ readonly bindingsDeps: BindingsDeps;
54
+ /** Resolve the per-device `device-ops` native provider (throws when absent). */
55
+ requireDeviceOps(deviceId: number): IDeviceOpsProvider;
56
+ /** Stamp an integration id onto a device's meta row + emit DeviceMetaChanged. */
57
+ stampIntegrationId(deviceId: number, integrationId: string): Promise<void>;
58
+ /** The assembled `IDeviceManagerProvider`, resolved lazily so the few methods
59
+ * that self-call sibling cap methods (`remove` → `removeDevice`,
60
+ * `removeByIntegration` → `remove`, `enable`/`disable` → `setDisabled`,
61
+ * `probeStreams`/`getStreamProfileMap` → `getStreamSources`) reach the live
62
+ * implementation without a construction-time capture cycle. */
63
+ readonly provider: IDeviceManagerProvider;
64
+ }
65
+ export {};
@@ -0,0 +1,38 @@
1
+ import { InferProvider, deviceManagerCapability } from '@camstack/types';
2
+ import { ProviderContext } from './device-provider-context.js';
3
+ type IDeviceManagerProvider = InferProvider<typeof deviceManagerCapability>;
4
+ export declare function listPersistedByAddon(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['listPersistedByAddon']>[0]): ReturnType<IDeviceManagerProvider['listPersistedByAddon']>;
5
+ export declare function listAll(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['listAll']>[0]): ReturnType<IDeviceManagerProvider['listAll']>;
6
+ export declare function getDevice(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['getDevice']>[0]): ReturnType<IDeviceManagerProvider['getDevice']>;
7
+ export declare function getChildren(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['getChildren']>[0]): ReturnType<IDeviceManagerProvider['getChildren']>;
8
+ export declare function getStreamSources(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['getStreamSources']>[0]): ReturnType<IDeviceManagerProvider['getStreamSources']>;
9
+ export declare function getConfigSchema(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['getConfigSchema']>[0]): ReturnType<IDeviceManagerProvider['getConfigSchema']>;
10
+ export declare function getSettingsSchema(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['getSettingsSchema']>[0]): ReturnType<IDeviceManagerProvider['getSettingsSchema']>;
11
+ export declare function updateConfig(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['updateConfig']>[0]): ReturnType<IDeviceManagerProvider['updateConfig']>;
12
+ export declare function enable(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['enable']>[0]): ReturnType<IDeviceManagerProvider['enable']>;
13
+ export declare function disable(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['disable']>[0]): ReturnType<IDeviceManagerProvider['disable']>;
14
+ export declare function remove(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['remove']>[0]): ReturnType<IDeviceManagerProvider['remove']>;
15
+ /**
16
+ * Cascade-delete every top-level device whose `integrationId`
17
+ * matches. Enumerates a SNAPSHOT of the meta map so concurrent
18
+ * removals don't clobber each other. Only top-level parents are
19
+ * enumerated — children cascade via the per-parent `removeCascade`
20
+ * inside the delegated `remove` call. Idempotent: devices with no
21
+ * `integrationId` never match.
22
+ */
23
+ export declare function removeByIntegration(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['removeByIntegration']>[0]): ReturnType<IDeviceManagerProvider['removeByIntegration']>;
24
+ export declare function getStreamProfileMap(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['getStreamProfileMap']>[0]): ReturnType<IDeviceManagerProvider['getStreamProfileMap']>;
25
+ export declare function setStreamProfileMap(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['setStreamProfileMap']>[0]): ReturnType<IDeviceManagerProvider['setStreamProfileMap']>;
26
+ export declare function probeStreams(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['probeStreams']>[0]): ReturnType<IDeviceManagerProvider['probeStreams']>;
27
+ export declare function discoverDevices(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['discoverDevices']>[0]): ReturnType<IDeviceManagerProvider['discoverDevices']>;
28
+ export declare function adoptDevice(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['adoptDevice']>[0]): ReturnType<IDeviceManagerProvider['adoptDevice']>;
29
+ export declare function getCreationSchema(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['getCreationSchema']>[0]): ReturnType<IDeviceManagerProvider['getCreationSchema']>;
30
+ export declare function createDevice(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['createDevice']>[0]): ReturnType<IDeviceManagerProvider['createDevice']>;
31
+ export declare function testCreationField(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['testCreationField']>[0]): ReturnType<IDeviceManagerProvider['testCreationField']>;
32
+ export declare function adoptionListCandidates(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['adoptionListCandidates']>[0]): ReturnType<IDeviceManagerProvider['adoptionListCandidates']>;
33
+ export declare function adoptionRefresh(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['adoptionRefresh']>[0]): ReturnType<IDeviceManagerProvider['adoptionRefresh']>;
34
+ export declare function adoptionAdopt(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['adoptionAdopt']>[0]): ReturnType<IDeviceManagerProvider['adoptionAdopt']>;
35
+ export declare function adoptionRelease(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['adoptionRelease']>[0]): ReturnType<IDeviceManagerProvider['adoptionRelease']>;
36
+ export declare function adoptionResync(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['adoptionResync']>[0]): ReturnType<IDeviceManagerProvider['adoptionResync']>;
37
+ export declare function testField(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['testField']>[0]): ReturnType<IDeviceManagerProvider['testField']>;
38
+ export {};