@camstack/addon-ai 0.4.71 → 0.4.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.js CHANGED
@@ -12,7 +12,7 @@ let _camstack_system_addon_utils = require("@camstack/system/addon-utils");
12
12
  let node_fs_promises = require("node:fs/promises");
13
13
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
14
14
  let node_child_process = require("node:child_process");
15
- //#region ../types/dist/event-category-BZL-fdNj.mjs
15
+ //#region ../types/dist/event-category-zAv7pMUz.mjs
16
16
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
17
17
  EventCategory["SystemBoot"] = "system.boot";
18
18
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -384,6 +384,13 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
384
384
  */
385
385
  EventCategory["BatteryOnStatusChanged"] = "battery.onStatusChanged";
386
386
  /**
387
+ * Cap event fired by every device that registers the `network-link`
388
+ * capability. Mirrors the cap definition's `onStatusChanged`. Carries
389
+ * `{ deviceId, status: NetworkLinkStatus }` — a link switch or a signal
390
+ * reading that moved.
391
+ */
392
+ EventCategory["NetworkLinkOnStatusChanged"] = "network-link.onStatusChanged";
393
+ /**
387
394
  * Emitted by the battery cap provider WHEN `wakeForStream` enters the
388
395
  * "wake in progress" window — between the Baichuan wake-up issue and
389
396
  * the camera's first dialed-back RTP packet. The stream-broker
@@ -23478,6 +23485,39 @@ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, meth
23478
23485
  deviceId: number$1(),
23479
23486
  status: BatteryStatusSchema
23480
23487
  });
23488
+ /**
23489
+ * Network-link snapshot. Same shape for every provider (a Reolink wifi
23490
+ * camera, a Home Assistant device with a signal-strength sensor, a Tapo
23491
+ * plug): one slice under `device.runtimeState['network-link']`, one badge,
23492
+ * one Home Assistant projection.
23493
+ */
23494
+ var NetworkLinkStatusSchema = object({
23495
+ /** The link the device is on. `'unknown'` = not read yet, not "no link". */
23496
+ type: _enum([
23497
+ "wifi",
23498
+ "ethernet",
23499
+ "cellular",
23500
+ "unknown"
23501
+ ]),
23502
+ /**
23503
+ * Link quality, 0..100 inclusive, normalised by the provider from whatever
23504
+ * the firmware reports (bars, RSSI, a vendor scale). **`null` means NOT
23505
+ * KNOWN or NOT APPLICABLE** — a wired link has no signal, and a wireless
23506
+ * one whose reading has not landed must not be drawn at 0 %. Consumers
23507
+ * SKIP a null rather than coerce it.
23508
+ */
23509
+ signalPercent: number$1().min(0).max(100).nullable(),
23510
+ /** Raw received signal strength in dBm, when the firmware reports one. */
23511
+ rssiDbm: number$1().optional(),
23512
+ /** Network name of a wireless link, when the firmware reports it. */
23513
+ ssid: string().optional(),
23514
+ /** Ms epoch of the last observation. Lets consumers reason about freshness. */
23515
+ lastUpdated: number$1()
23516
+ });
23517
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.Light, DeviceType.Lock, DeviceType.Siren, object({
23518
+ deviceId: number$1(),
23519
+ status: NetworkLinkStatusSchema
23520
+ });
23481
23521
  object({
23482
23522
  on: boolean(),
23483
23523
  /** Ms epoch of the last transition. 0 if never observed. */
@@ -27072,6 +27112,203 @@ DeviceType.Camera, method(object({ deviceId: number$1() }), PtzAutotrackStatusSc
27072
27112
  deviceId: number$1(),
27073
27113
  status: PtzAutotrackStatusSchema
27074
27114
  });
27115
+ /**
27116
+ * `navigation` — a device-scoped capability that natively expresses the FULL
27117
+ * navigation / action surface of a robot that DRIVES ITSELF and carries an
27118
+ * on-board camera (the Dreame robot-vacuum camera is the first provider).
27119
+ *
27120
+ * Why a NEW cap rather than overloading `ptz`:
27121
+ * - `ptz` moves a *gimbal* on a fixed camera (pan/tilt/zoom of the lens). A
27122
+ * robot vacuum has no gimbal — the whole chassis drives, turns and spins.
27123
+ * The two are different physical models: PTZ is absolute-position + presets,
27124
+ * navigation is momentary drive nudges + discrete robot ACTIONS
27125
+ * (dock / spot-clean / follow-pet / go-to-point / …).
27126
+ * - This cap is the SOURCE OF TRUTH. Two consumers adapt from it rather than
27127
+ * the reverse:
27128
+ * 1. a native CamStack navigation panel (data-driven from `listActions`
27129
+ * / `getOptions`), and
27130
+ * 2. the PTZ surface — a thin adapter mimics `ptz` from `navigation` so a
27131
+ * robot camera shows up in the existing PTZ control path without every
27132
+ * PTZ provider learning about robots. The mapping lives in the adapter,
27133
+ * not here (see the addon design note):
27134
+ * ptz.continuousMove({pan,tilt}) → navigation.move({pan,tilt})
27135
+ * ptz.stop() → navigation.stop()
27136
+ * ptz.goHome() → navigation.runAction('goHome')
27137
+ * ptz.getPresets() → navigation.listActions() (id→preset)
27138
+ * ptz.goToPreset(id) → navigation.runAction(id)
27139
+ *
27140
+ * ## Continuous drive
27141
+ *
27142
+ * `move` is MOMENTARY. Fluid navigation comes from the UI (or the PTZ adapter)
27143
+ * sending `move({pan,tilt})` REPEATEDLY at ~1 Hz while a direction is held, and
27144
+ * one `stop()` on release — exactly like the robot app's remote-drive joystick.
27145
+ * The provider forwards EACH `move` to one drive write; it must NOT debounce or
27146
+ * coalesce them. The UI owns the cadence.
27147
+ *
27148
+ * ## The action dictionary
27149
+ *
27150
+ * The discrete controls (dock / locate / spot-clean / follow / flash / sounds)
27151
+ * are a DATA-DRIVEN dictionary the cap exposes via `listActions()`. Each entry
27152
+ * carries `{ id, kind, label, icon }` (plus `soundId` for sound entries) so the
27153
+ * native panel AND the PTZ mimic render buttons WITHOUT hardcoding a
27154
+ * vendor-specific list. `kind: 'action'` entries are triggered with
27155
+ * `runAction({ actionId })`; `kind: 'sound'` entries with `playSound({ soundId })`
27156
+ * (the entry carries the `soundId` to pass). The general primitives — `move`,
27157
+ * `stop`, `goToPoint` — stay as first-class methods, not dictionary entries.
27158
+ *
27159
+ * NOTE (provider wiring): the first provider (Dreame) drives this with the RAW
27160
+ * `callAction(siid,aiid,in)` / `setProperty({siid,piid,value})` MIoT primitives
27161
+ * that the currently-published `@apocaliss92/nodedreame` already exposes on
27162
+ * every device handle. A future nodedreame publish adds a typed
27163
+ * `DreameCameraController` (drive / playPetSound / spotClean / findPet / …); the
27164
+ * provider can then swap the raw calls for the typed methods with no change to
27165
+ * THIS contract.
27166
+ */
27167
+ /**
27168
+ * A momentary drive nudge. The robot MOVES (no gimbal) for as long as the caller
27169
+ * keeps sending nudges (~1 Hz); an explicit `stop` (or letting the nudges lapse)
27170
+ * halts it.
27171
+ *
27172
+ * - `pan` — turn: negative = left, positive = right, 0 = straight.
27173
+ * - `tilt` — throttle: positive = forward, negative = spin / turn-around.
27174
+ * - `speed` — optional intensity hint [0, 1]; the provider may scale the drive
27175
+ * vector by it (drivers without proportional drive ignore it).
27176
+ *
27177
+ * `pan` / `tilt` are normalized [-1, 1]. Both optional so a caller can nudge one
27178
+ * axis alone; an all-undefined nudge is a no-op.
27179
+ */
27180
+ var NavigationMoveCommandSchema = object({
27181
+ pan: number$1().min(-1).max(1).optional(),
27182
+ tilt: number$1().min(-1).max(1).optional(),
27183
+ speed: number$1().min(0).max(1).optional()
27184
+ });
27185
+ /**
27186
+ * The enumerated discrete actions a navigation-capable robot can perform via
27187
+ * `runAction`. This is the CLOSED vocabulary; a given device advertises the
27188
+ * subset it supports through `listActions`. Sounds are NOT here — they go through
27189
+ * `playSound` (see the `sound` dictionary entries).
27190
+ */
27191
+ var NavigationActionIdSchema = _enum([
27192
+ "goHome",
27193
+ "locate",
27194
+ "spotClean",
27195
+ "findPet",
27196
+ "personFollow",
27197
+ "stop",
27198
+ "startClean",
27199
+ "pauseClean",
27200
+ "dockWash",
27201
+ "autoEmpty",
27202
+ "flashOn",
27203
+ "flashOff"
27204
+ ]);
27205
+ /** Whether a dictionary entry is a `runAction` action or a `playSound` sound. */
27206
+ var NavigationEntryKindSchema = _enum(["action", "sound"]);
27207
+ /**
27208
+ * One entry in the navigation action dictionary — the DATA-DRIVEN unit both the
27209
+ * native panel and the PTZ mimic render as a button.
27210
+ *
27211
+ * - `id` — stable id. For `kind:'action'` it is a {@link NavigationActionId}
27212
+ * (pass to `runAction`); for `kind:'sound'` it is a namespaced id
27213
+ * (`sound:meow`) whose `soundId` is passed to `playSound`.
27214
+ * - `icon` — icon HINT (lucide-style name; the UI maps it to its own set).
27215
+ * - `label` — operator-facing English label.
27216
+ * - `soundId` — wire sound id, present only on `kind:'sound'` entries.
27217
+ * - `enabled` — per-device FEATURE FLAG. `listActions` reports it so the UI /
27218
+ * PTZ render ONLY enabled entries. Data-driven: the provider
27219
+ * flips it from config, never by editing code.
27220
+ */
27221
+ var NavigationActionEntrySchema = object({
27222
+ id: string(),
27223
+ kind: NavigationEntryKindSchema,
27224
+ label: string(),
27225
+ icon: string(),
27226
+ /** Present only on `kind:'sound'` entries — the id to pass to `playSound`. */
27227
+ soundId: number$1().int().optional(),
27228
+ /** Per-device feature flag — render this entry only when true. */
27229
+ enabled: boolean()
27230
+ });
27231
+ /** Coordinates for `goToPoint` — a point on the robot's live map. */
27232
+ var NavigationPointSchema = object({
27233
+ x: number$1(),
27234
+ y: number$1()
27235
+ });
27236
+ /**
27237
+ * Per-device FEATURE-FLAG report for the general (non-dictionary) primitives.
27238
+ * The cap reports which are enabled so the UI / PTZ render only the controls
27239
+ * that are turned on for THIS device. Data-driven: the provider derives these
27240
+ * from config + probe, never hardcoded in the UI. The per-DICTIONARY-entry flags
27241
+ * live on {@link NavigationActionEntrySchema.enabled}; these gate the primitives
27242
+ * that are not dictionary entries.
27243
+ *
27244
+ * - `move` / `stop` — the momentary drive joystick.
27245
+ * - `goToPoint` — send-to-map-coordinate. Ships OFF on Dreame until the
27246
+ * map-coordinate plumbing is wired.
27247
+ * - `runAction` — the discrete action buttons (dictionary `kind:'action'`).
27248
+ * - `playSound` — the sound buttons (dictionary `kind:'sound'`).
27249
+ * - `light` — the on/off fill-light toggle (works anytime).
27250
+ * - `lightMode` — the auto/manual selector + manual level slider (a
27251
+ * camera-service control; needs an active stream).
27252
+ */
27253
+ var NavigationFeaturesSchema = object({
27254
+ move: boolean(),
27255
+ stop: boolean(),
27256
+ goToPoint: boolean(),
27257
+ runAction: boolean(),
27258
+ playSound: boolean(),
27259
+ light: boolean(),
27260
+ lightMode: boolean()
27261
+ });
27262
+ /** Light mode: `auto` lets the camera choose brightness; `manual` uses `level`. */
27263
+ var NavigationLightModeSchema = _enum(["auto", "manual"]);
27264
+ /**
27265
+ * Live navigation state so the UI can reflect what the robot is doing:
27266
+ * - `mode` — coarse activity (idle / cleaning / following / …).
27267
+ * - `following` — person/pet follow is currently armed.
27268
+ * - `flash` — the on-camera fill light is on.
27269
+ * - `lightMode` — auto vs manual fill-light mode.
27270
+ * - `lightLevel` — manual fill-light level (40..100); meaningful when
27271
+ * `lightMode === 'manual'`.
27272
+ */
27273
+ var NavigationStatusSchema = object({
27274
+ mode: _enum([
27275
+ "idle",
27276
+ "cleaning",
27277
+ "spot",
27278
+ "following",
27279
+ "goto",
27280
+ "returning",
27281
+ "paused",
27282
+ "unknown"
27283
+ ]),
27284
+ following: boolean(),
27285
+ flash: boolean(),
27286
+ lightMode: NavigationLightModeSchema,
27287
+ lightLevel: number$1().min(40).max(100),
27288
+ /** Ms epoch when the slice was last updated. */
27289
+ lastChangedAt: number$1()
27290
+ });
27291
+ NavigationStatusSchema.extend({ lastFetchedAt: number$1() });
27292
+ DeviceType.Camera, method(NavigationMoveCommandSchema.extend({ deviceId: number$1() }), _void(), { kind: "mutation" }), method(object({ deviceId: number$1() }), _void(), { kind: "mutation" }), method(NavigationPointSchema.extend({ deviceId: number$1() }), _void(), { kind: "mutation" }), method(object({ deviceId: number$1() }), array(NavigationActionEntrySchema)), method(object({
27293
+ deviceId: number$1(),
27294
+ actionId: NavigationActionIdSchema
27295
+ }), _void(), { kind: "mutation" }), method(object({
27296
+ deviceId: number$1(),
27297
+ soundId: number$1().int()
27298
+ }), _void(), { kind: "mutation" }), method(object({
27299
+ deviceId: number$1(),
27300
+ on: boolean()
27301
+ }), _void(), { kind: "mutation" }), method(object({
27302
+ deviceId: number$1(),
27303
+ mode: NavigationLightModeSchema,
27304
+ level: number$1().min(40).max(100).optional()
27305
+ }), _void(), { kind: "mutation" }), method(object({
27306
+ deviceId: number$1(),
27307
+ level: number$1().min(40).max(100)
27308
+ }), _void(), { kind: "mutation" }), method(object({ deviceId: number$1() }), NavigationFeaturesSchema), object({
27309
+ deviceId: number$1(),
27310
+ status: NavigationStatusSchema
27311
+ });
27075
27312
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, method(object({ deviceId: number$1().int().nonnegative() }), object({ success: literal(true) }), {
27076
27313
  kind: "mutation",
27077
27314
  auth: "admin"
@@ -32136,6 +32373,66 @@ Object.freeze({
32136
32373
  addonId: null,
32137
32374
  access: "create"
32138
32375
  },
32376
+ "navigation.getFeatures": {
32377
+ capName: "navigation",
32378
+ capScope: "device",
32379
+ addonId: null,
32380
+ access: "view"
32381
+ },
32382
+ "navigation.goToPoint": {
32383
+ capName: "navigation",
32384
+ capScope: "device",
32385
+ addonId: null,
32386
+ access: "create"
32387
+ },
32388
+ "navigation.listActions": {
32389
+ capName: "navigation",
32390
+ capScope: "device",
32391
+ addonId: null,
32392
+ access: "view"
32393
+ },
32394
+ "navigation.move": {
32395
+ capName: "navigation",
32396
+ capScope: "device",
32397
+ addonId: null,
32398
+ access: "create"
32399
+ },
32400
+ "navigation.playSound": {
32401
+ capName: "navigation",
32402
+ capScope: "device",
32403
+ addonId: null,
32404
+ access: "create"
32405
+ },
32406
+ "navigation.runAction": {
32407
+ capName: "navigation",
32408
+ capScope: "device",
32409
+ addonId: null,
32410
+ access: "create"
32411
+ },
32412
+ "navigation.setLightLevel": {
32413
+ capName: "navigation",
32414
+ capScope: "device",
32415
+ addonId: null,
32416
+ access: "create"
32417
+ },
32418
+ "navigation.setLightMode": {
32419
+ capName: "navigation",
32420
+ capScope: "device",
32421
+ addonId: null,
32422
+ access: "create"
32423
+ },
32424
+ "navigation.setLightOn": {
32425
+ capName: "navigation",
32426
+ capScope: "device",
32427
+ addonId: null,
32428
+ access: "create"
32429
+ },
32430
+ "navigation.stop": {
32431
+ capName: "navigation",
32432
+ capScope: "device",
32433
+ addonId: null,
32434
+ access: "create"
32435
+ },
32139
32436
  "networkAccess.getEndpoint": {
32140
32437
  capName: "network-access",
32141
32438
  capScope: "system",
@@ -36231,6 +36528,56 @@ Object.freeze({
36231
36528
  form: "single",
36232
36529
  optional: false
36233
36530
  }],
36531
+ "navigation.getFeatures": [{
36532
+ name: "deviceId",
36533
+ form: "single",
36534
+ optional: false
36535
+ }],
36536
+ "navigation.goToPoint": [{
36537
+ name: "deviceId",
36538
+ form: "single",
36539
+ optional: false
36540
+ }],
36541
+ "navigation.listActions": [{
36542
+ name: "deviceId",
36543
+ form: "single",
36544
+ optional: false
36545
+ }],
36546
+ "navigation.move": [{
36547
+ name: "deviceId",
36548
+ form: "single",
36549
+ optional: false
36550
+ }],
36551
+ "navigation.playSound": [{
36552
+ name: "deviceId",
36553
+ form: "single",
36554
+ optional: false
36555
+ }],
36556
+ "navigation.runAction": [{
36557
+ name: "deviceId",
36558
+ form: "single",
36559
+ optional: false
36560
+ }],
36561
+ "navigation.setLightLevel": [{
36562
+ name: "deviceId",
36563
+ form: "single",
36564
+ optional: false
36565
+ }],
36566
+ "navigation.setLightMode": [{
36567
+ name: "deviceId",
36568
+ form: "single",
36569
+ optional: false
36570
+ }],
36571
+ "navigation.setLightOn": [{
36572
+ name: "deviceId",
36573
+ form: "single",
36574
+ optional: false
36575
+ }],
36576
+ "navigation.stop": [{
36577
+ name: "deviceId",
36578
+ form: "single",
36579
+ optional: false
36580
+ }],
36234
36581
  "networkQuality.getDeviceStats": [{
36235
36582
  name: "deviceId",
36236
36583
  form: "single",
package/dist/addon.mjs CHANGED
@@ -39,7 +39,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
39
39
  }) : target, mod));
40
40
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
41
41
  //#endregion
42
- //#region ../types/dist/event-category-BZL-fdNj.mjs
42
+ //#region ../types/dist/event-category-zAv7pMUz.mjs
43
43
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
44
44
  EventCategory["SystemBoot"] = "system.boot";
45
45
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -411,6 +411,13 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
411
411
  */
412
412
  EventCategory["BatteryOnStatusChanged"] = "battery.onStatusChanged";
413
413
  /**
414
+ * Cap event fired by every device that registers the `network-link`
415
+ * capability. Mirrors the cap definition's `onStatusChanged`. Carries
416
+ * `{ deviceId, status: NetworkLinkStatus }` — a link switch or a signal
417
+ * reading that moved.
418
+ */
419
+ EventCategory["NetworkLinkOnStatusChanged"] = "network-link.onStatusChanged";
420
+ /**
414
421
  * Emitted by the battery cap provider WHEN `wakeForStream` enters the
415
422
  * "wake in progress" window — between the Baichuan wake-up issue and
416
423
  * the camera's first dialed-back RTP packet. The stream-broker
@@ -23505,6 +23512,39 @@ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, meth
23505
23512
  deviceId: number$1(),
23506
23513
  status: BatteryStatusSchema
23507
23514
  });
23515
+ /**
23516
+ * Network-link snapshot. Same shape for every provider (a Reolink wifi
23517
+ * camera, a Home Assistant device with a signal-strength sensor, a Tapo
23518
+ * plug): one slice under `device.runtimeState['network-link']`, one badge,
23519
+ * one Home Assistant projection.
23520
+ */
23521
+ var NetworkLinkStatusSchema = object({
23522
+ /** The link the device is on. `'unknown'` = not read yet, not "no link". */
23523
+ type: _enum([
23524
+ "wifi",
23525
+ "ethernet",
23526
+ "cellular",
23527
+ "unknown"
23528
+ ]),
23529
+ /**
23530
+ * Link quality, 0..100 inclusive, normalised by the provider from whatever
23531
+ * the firmware reports (bars, RSSI, a vendor scale). **`null` means NOT
23532
+ * KNOWN or NOT APPLICABLE** — a wired link has no signal, and a wireless
23533
+ * one whose reading has not landed must not be drawn at 0 %. Consumers
23534
+ * SKIP a null rather than coerce it.
23535
+ */
23536
+ signalPercent: number$1().min(0).max(100).nullable(),
23537
+ /** Raw received signal strength in dBm, when the firmware reports one. */
23538
+ rssiDbm: number$1().optional(),
23539
+ /** Network name of a wireless link, when the firmware reports it. */
23540
+ ssid: string().optional(),
23541
+ /** Ms epoch of the last observation. Lets consumers reason about freshness. */
23542
+ lastUpdated: number$1()
23543
+ });
23544
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.Light, DeviceType.Lock, DeviceType.Siren, object({
23545
+ deviceId: number$1(),
23546
+ status: NetworkLinkStatusSchema
23547
+ });
23508
23548
  object({
23509
23549
  on: boolean(),
23510
23550
  /** Ms epoch of the last transition. 0 if never observed. */
@@ -27099,6 +27139,203 @@ DeviceType.Camera, method(object({ deviceId: number$1() }), PtzAutotrackStatusSc
27099
27139
  deviceId: number$1(),
27100
27140
  status: PtzAutotrackStatusSchema
27101
27141
  });
27142
+ /**
27143
+ * `navigation` — a device-scoped capability that natively expresses the FULL
27144
+ * navigation / action surface of a robot that DRIVES ITSELF and carries an
27145
+ * on-board camera (the Dreame robot-vacuum camera is the first provider).
27146
+ *
27147
+ * Why a NEW cap rather than overloading `ptz`:
27148
+ * - `ptz` moves a *gimbal* on a fixed camera (pan/tilt/zoom of the lens). A
27149
+ * robot vacuum has no gimbal — the whole chassis drives, turns and spins.
27150
+ * The two are different physical models: PTZ is absolute-position + presets,
27151
+ * navigation is momentary drive nudges + discrete robot ACTIONS
27152
+ * (dock / spot-clean / follow-pet / go-to-point / …).
27153
+ * - This cap is the SOURCE OF TRUTH. Two consumers adapt from it rather than
27154
+ * the reverse:
27155
+ * 1. a native CamStack navigation panel (data-driven from `listActions`
27156
+ * / `getOptions`), and
27157
+ * 2. the PTZ surface — a thin adapter mimics `ptz` from `navigation` so a
27158
+ * robot camera shows up in the existing PTZ control path without every
27159
+ * PTZ provider learning about robots. The mapping lives in the adapter,
27160
+ * not here (see the addon design note):
27161
+ * ptz.continuousMove({pan,tilt}) → navigation.move({pan,tilt})
27162
+ * ptz.stop() → navigation.stop()
27163
+ * ptz.goHome() → navigation.runAction('goHome')
27164
+ * ptz.getPresets() → navigation.listActions() (id→preset)
27165
+ * ptz.goToPreset(id) → navigation.runAction(id)
27166
+ *
27167
+ * ## Continuous drive
27168
+ *
27169
+ * `move` is MOMENTARY. Fluid navigation comes from the UI (or the PTZ adapter)
27170
+ * sending `move({pan,tilt})` REPEATEDLY at ~1 Hz while a direction is held, and
27171
+ * one `stop()` on release — exactly like the robot app's remote-drive joystick.
27172
+ * The provider forwards EACH `move` to one drive write; it must NOT debounce or
27173
+ * coalesce them. The UI owns the cadence.
27174
+ *
27175
+ * ## The action dictionary
27176
+ *
27177
+ * The discrete controls (dock / locate / spot-clean / follow / flash / sounds)
27178
+ * are a DATA-DRIVEN dictionary the cap exposes via `listActions()`. Each entry
27179
+ * carries `{ id, kind, label, icon }` (plus `soundId` for sound entries) so the
27180
+ * native panel AND the PTZ mimic render buttons WITHOUT hardcoding a
27181
+ * vendor-specific list. `kind: 'action'` entries are triggered with
27182
+ * `runAction({ actionId })`; `kind: 'sound'` entries with `playSound({ soundId })`
27183
+ * (the entry carries the `soundId` to pass). The general primitives — `move`,
27184
+ * `stop`, `goToPoint` — stay as first-class methods, not dictionary entries.
27185
+ *
27186
+ * NOTE (provider wiring): the first provider (Dreame) drives this with the RAW
27187
+ * `callAction(siid,aiid,in)` / `setProperty({siid,piid,value})` MIoT primitives
27188
+ * that the currently-published `@apocaliss92/nodedreame` already exposes on
27189
+ * every device handle. A future nodedreame publish adds a typed
27190
+ * `DreameCameraController` (drive / playPetSound / spotClean / findPet / …); the
27191
+ * provider can then swap the raw calls for the typed methods with no change to
27192
+ * THIS contract.
27193
+ */
27194
+ /**
27195
+ * A momentary drive nudge. The robot MOVES (no gimbal) for as long as the caller
27196
+ * keeps sending nudges (~1 Hz); an explicit `stop` (or letting the nudges lapse)
27197
+ * halts it.
27198
+ *
27199
+ * - `pan` — turn: negative = left, positive = right, 0 = straight.
27200
+ * - `tilt` — throttle: positive = forward, negative = spin / turn-around.
27201
+ * - `speed` — optional intensity hint [0, 1]; the provider may scale the drive
27202
+ * vector by it (drivers without proportional drive ignore it).
27203
+ *
27204
+ * `pan` / `tilt` are normalized [-1, 1]. Both optional so a caller can nudge one
27205
+ * axis alone; an all-undefined nudge is a no-op.
27206
+ */
27207
+ var NavigationMoveCommandSchema = object({
27208
+ pan: number$1().min(-1).max(1).optional(),
27209
+ tilt: number$1().min(-1).max(1).optional(),
27210
+ speed: number$1().min(0).max(1).optional()
27211
+ });
27212
+ /**
27213
+ * The enumerated discrete actions a navigation-capable robot can perform via
27214
+ * `runAction`. This is the CLOSED vocabulary; a given device advertises the
27215
+ * subset it supports through `listActions`. Sounds are NOT here — they go through
27216
+ * `playSound` (see the `sound` dictionary entries).
27217
+ */
27218
+ var NavigationActionIdSchema = _enum([
27219
+ "goHome",
27220
+ "locate",
27221
+ "spotClean",
27222
+ "findPet",
27223
+ "personFollow",
27224
+ "stop",
27225
+ "startClean",
27226
+ "pauseClean",
27227
+ "dockWash",
27228
+ "autoEmpty",
27229
+ "flashOn",
27230
+ "flashOff"
27231
+ ]);
27232
+ /** Whether a dictionary entry is a `runAction` action or a `playSound` sound. */
27233
+ var NavigationEntryKindSchema = _enum(["action", "sound"]);
27234
+ /**
27235
+ * One entry in the navigation action dictionary — the DATA-DRIVEN unit both the
27236
+ * native panel and the PTZ mimic render as a button.
27237
+ *
27238
+ * - `id` — stable id. For `kind:'action'` it is a {@link NavigationActionId}
27239
+ * (pass to `runAction`); for `kind:'sound'` it is a namespaced id
27240
+ * (`sound:meow`) whose `soundId` is passed to `playSound`.
27241
+ * - `icon` — icon HINT (lucide-style name; the UI maps it to its own set).
27242
+ * - `label` — operator-facing English label.
27243
+ * - `soundId` — wire sound id, present only on `kind:'sound'` entries.
27244
+ * - `enabled` — per-device FEATURE FLAG. `listActions` reports it so the UI /
27245
+ * PTZ render ONLY enabled entries. Data-driven: the provider
27246
+ * flips it from config, never by editing code.
27247
+ */
27248
+ var NavigationActionEntrySchema = object({
27249
+ id: string(),
27250
+ kind: NavigationEntryKindSchema,
27251
+ label: string(),
27252
+ icon: string(),
27253
+ /** Present only on `kind:'sound'` entries — the id to pass to `playSound`. */
27254
+ soundId: number$1().int().optional(),
27255
+ /** Per-device feature flag — render this entry only when true. */
27256
+ enabled: boolean()
27257
+ });
27258
+ /** Coordinates for `goToPoint` — a point on the robot's live map. */
27259
+ var NavigationPointSchema = object({
27260
+ x: number$1(),
27261
+ y: number$1()
27262
+ });
27263
+ /**
27264
+ * Per-device FEATURE-FLAG report for the general (non-dictionary) primitives.
27265
+ * The cap reports which are enabled so the UI / PTZ render only the controls
27266
+ * that are turned on for THIS device. Data-driven: the provider derives these
27267
+ * from config + probe, never hardcoded in the UI. The per-DICTIONARY-entry flags
27268
+ * live on {@link NavigationActionEntrySchema.enabled}; these gate the primitives
27269
+ * that are not dictionary entries.
27270
+ *
27271
+ * - `move` / `stop` — the momentary drive joystick.
27272
+ * - `goToPoint` — send-to-map-coordinate. Ships OFF on Dreame until the
27273
+ * map-coordinate plumbing is wired.
27274
+ * - `runAction` — the discrete action buttons (dictionary `kind:'action'`).
27275
+ * - `playSound` — the sound buttons (dictionary `kind:'sound'`).
27276
+ * - `light` — the on/off fill-light toggle (works anytime).
27277
+ * - `lightMode` — the auto/manual selector + manual level slider (a
27278
+ * camera-service control; needs an active stream).
27279
+ */
27280
+ var NavigationFeaturesSchema = object({
27281
+ move: boolean(),
27282
+ stop: boolean(),
27283
+ goToPoint: boolean(),
27284
+ runAction: boolean(),
27285
+ playSound: boolean(),
27286
+ light: boolean(),
27287
+ lightMode: boolean()
27288
+ });
27289
+ /** Light mode: `auto` lets the camera choose brightness; `manual` uses `level`. */
27290
+ var NavigationLightModeSchema = _enum(["auto", "manual"]);
27291
+ /**
27292
+ * Live navigation state so the UI can reflect what the robot is doing:
27293
+ * - `mode` — coarse activity (idle / cleaning / following / …).
27294
+ * - `following` — person/pet follow is currently armed.
27295
+ * - `flash` — the on-camera fill light is on.
27296
+ * - `lightMode` — auto vs manual fill-light mode.
27297
+ * - `lightLevel` — manual fill-light level (40..100); meaningful when
27298
+ * `lightMode === 'manual'`.
27299
+ */
27300
+ var NavigationStatusSchema = object({
27301
+ mode: _enum([
27302
+ "idle",
27303
+ "cleaning",
27304
+ "spot",
27305
+ "following",
27306
+ "goto",
27307
+ "returning",
27308
+ "paused",
27309
+ "unknown"
27310
+ ]),
27311
+ following: boolean(),
27312
+ flash: boolean(),
27313
+ lightMode: NavigationLightModeSchema,
27314
+ lightLevel: number$1().min(40).max(100),
27315
+ /** Ms epoch when the slice was last updated. */
27316
+ lastChangedAt: number$1()
27317
+ });
27318
+ NavigationStatusSchema.extend({ lastFetchedAt: number$1() });
27319
+ DeviceType.Camera, method(NavigationMoveCommandSchema.extend({ deviceId: number$1() }), _void(), { kind: "mutation" }), method(object({ deviceId: number$1() }), _void(), { kind: "mutation" }), method(NavigationPointSchema.extend({ deviceId: number$1() }), _void(), { kind: "mutation" }), method(object({ deviceId: number$1() }), array(NavigationActionEntrySchema)), method(object({
27320
+ deviceId: number$1(),
27321
+ actionId: NavigationActionIdSchema
27322
+ }), _void(), { kind: "mutation" }), method(object({
27323
+ deviceId: number$1(),
27324
+ soundId: number$1().int()
27325
+ }), _void(), { kind: "mutation" }), method(object({
27326
+ deviceId: number$1(),
27327
+ on: boolean()
27328
+ }), _void(), { kind: "mutation" }), method(object({
27329
+ deviceId: number$1(),
27330
+ mode: NavigationLightModeSchema,
27331
+ level: number$1().min(40).max(100).optional()
27332
+ }), _void(), { kind: "mutation" }), method(object({
27333
+ deviceId: number$1(),
27334
+ level: number$1().min(40).max(100)
27335
+ }), _void(), { kind: "mutation" }), method(object({ deviceId: number$1() }), NavigationFeaturesSchema), object({
27336
+ deviceId: number$1(),
27337
+ status: NavigationStatusSchema
27338
+ });
27102
27339
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, method(object({ deviceId: number$1().int().nonnegative() }), object({ success: literal(true) }), {
27103
27340
  kind: "mutation",
27104
27341
  auth: "admin"
@@ -32163,6 +32400,66 @@ Object.freeze({
32163
32400
  addonId: null,
32164
32401
  access: "create"
32165
32402
  },
32403
+ "navigation.getFeatures": {
32404
+ capName: "navigation",
32405
+ capScope: "device",
32406
+ addonId: null,
32407
+ access: "view"
32408
+ },
32409
+ "navigation.goToPoint": {
32410
+ capName: "navigation",
32411
+ capScope: "device",
32412
+ addonId: null,
32413
+ access: "create"
32414
+ },
32415
+ "navigation.listActions": {
32416
+ capName: "navigation",
32417
+ capScope: "device",
32418
+ addonId: null,
32419
+ access: "view"
32420
+ },
32421
+ "navigation.move": {
32422
+ capName: "navigation",
32423
+ capScope: "device",
32424
+ addonId: null,
32425
+ access: "create"
32426
+ },
32427
+ "navigation.playSound": {
32428
+ capName: "navigation",
32429
+ capScope: "device",
32430
+ addonId: null,
32431
+ access: "create"
32432
+ },
32433
+ "navigation.runAction": {
32434
+ capName: "navigation",
32435
+ capScope: "device",
32436
+ addonId: null,
32437
+ access: "create"
32438
+ },
32439
+ "navigation.setLightLevel": {
32440
+ capName: "navigation",
32441
+ capScope: "device",
32442
+ addonId: null,
32443
+ access: "create"
32444
+ },
32445
+ "navigation.setLightMode": {
32446
+ capName: "navigation",
32447
+ capScope: "device",
32448
+ addonId: null,
32449
+ access: "create"
32450
+ },
32451
+ "navigation.setLightOn": {
32452
+ capName: "navigation",
32453
+ capScope: "device",
32454
+ addonId: null,
32455
+ access: "create"
32456
+ },
32457
+ "navigation.stop": {
32458
+ capName: "navigation",
32459
+ capScope: "device",
32460
+ addonId: null,
32461
+ access: "create"
32462
+ },
32166
32463
  "networkAccess.getEndpoint": {
32167
32464
  capName: "network-access",
32168
32465
  capScope: "system",
@@ -36258,6 +36555,56 @@ Object.freeze({
36258
36555
  form: "single",
36259
36556
  optional: false
36260
36557
  }],
36558
+ "navigation.getFeatures": [{
36559
+ name: "deviceId",
36560
+ form: "single",
36561
+ optional: false
36562
+ }],
36563
+ "navigation.goToPoint": [{
36564
+ name: "deviceId",
36565
+ form: "single",
36566
+ optional: false
36567
+ }],
36568
+ "navigation.listActions": [{
36569
+ name: "deviceId",
36570
+ form: "single",
36571
+ optional: false
36572
+ }],
36573
+ "navigation.move": [{
36574
+ name: "deviceId",
36575
+ form: "single",
36576
+ optional: false
36577
+ }],
36578
+ "navigation.playSound": [{
36579
+ name: "deviceId",
36580
+ form: "single",
36581
+ optional: false
36582
+ }],
36583
+ "navigation.runAction": [{
36584
+ name: "deviceId",
36585
+ form: "single",
36586
+ optional: false
36587
+ }],
36588
+ "navigation.setLightLevel": [{
36589
+ name: "deviceId",
36590
+ form: "single",
36591
+ optional: false
36592
+ }],
36593
+ "navigation.setLightMode": [{
36594
+ name: "deviceId",
36595
+ form: "single",
36596
+ optional: false
36597
+ }],
36598
+ "navigation.setLightOn": [{
36599
+ name: "deviceId",
36600
+ form: "single",
36601
+ optional: false
36602
+ }],
36603
+ "navigation.stop": [{
36604
+ name: "deviceId",
36605
+ form: "single",
36606
+ optional: false
36607
+ }],
36261
36608
  "networkQuality.getDeviceStats": [{
36262
36609
  name: "deviceId",
36263
36610
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-ai",
3
- "version": "0.4.71",
3
+ "version": "0.4.73",
4
4
  "description": "AI addon for CamStack — the `llm` collection provider (cloud, LAN, and camstack-managed local llama.cpp profiles) plus the per-node `llm-runtime` managed executor.",
5
5
  "keywords": [
6
6
  "camstack",