@camstack/addon-provider-dreo 0.2.70 → 0.2.72

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
@@ -5442,6 +5442,12 @@ Object.fromEntries([
5442
5442
  icon: "move",
5443
5443
  order: 40
5444
5444
  },
5445
+ {
5446
+ id: "navigation",
5447
+ label: "Navigation",
5448
+ icon: "compass",
5449
+ order: 41
5450
+ },
5445
5451
  {
5446
5452
  id: "consumables",
5447
5453
  label: "Consumables",
@@ -29008,6 +29014,287 @@ var ptzAutotrackCapability = {
29008
29014
  */
29009
29015
  durability: "session"
29010
29016
  };
29017
+ /**
29018
+ * `navigation` — a device-scoped capability that natively expresses the FULL
29019
+ * navigation / action surface of a robot that DRIVES ITSELF and carries an
29020
+ * on-board camera (the Dreame robot-vacuum camera is the first provider).
29021
+ *
29022
+ * Why a NEW cap rather than overloading `ptz`:
29023
+ * - `ptz` moves a *gimbal* on a fixed camera (pan/tilt/zoom of the lens). A
29024
+ * robot vacuum has no gimbal — the whole chassis drives, turns and spins.
29025
+ * The two are different physical models: PTZ is absolute-position + presets,
29026
+ * navigation is momentary drive nudges + discrete robot ACTIONS
29027
+ * (dock / spot-clean / follow-pet / go-to-point / …).
29028
+ * - This cap is the SOURCE OF TRUTH. Two consumers adapt from it rather than
29029
+ * the reverse:
29030
+ * 1. a native CamStack navigation panel (data-driven from `listActions`
29031
+ * / `getOptions`), and
29032
+ * 2. the PTZ surface — a thin adapter mimics `ptz` from `navigation` so a
29033
+ * robot camera shows up in the existing PTZ control path without every
29034
+ * PTZ provider learning about robots. The mapping lives in the adapter,
29035
+ * not here (see the addon design note):
29036
+ * ptz.continuousMove({pan,tilt}) → navigation.move({pan,tilt})
29037
+ * ptz.stop() → navigation.stop()
29038
+ * ptz.goHome() → navigation.runAction('goHome')
29039
+ * ptz.getPresets() → navigation.listActions() (id→preset)
29040
+ * ptz.goToPreset(id) → navigation.runAction(id)
29041
+ *
29042
+ * ## Continuous drive
29043
+ *
29044
+ * `move` is MOMENTARY. Fluid navigation comes from the UI (or the PTZ adapter)
29045
+ * sending `move({pan,tilt})` REPEATEDLY at ~1 Hz while a direction is held, and
29046
+ * one `stop()` on release — exactly like the robot app's remote-drive joystick.
29047
+ * The provider forwards EACH `move` to one drive write; it must NOT debounce or
29048
+ * coalesce them. The UI owns the cadence.
29049
+ *
29050
+ * ## The action dictionary
29051
+ *
29052
+ * The discrete controls (dock / locate / spot-clean / follow / flash / sounds)
29053
+ * are a DATA-DRIVEN dictionary the cap exposes via `listActions()`. Each entry
29054
+ * carries `{ id, kind, label, icon }` (plus `soundId` for sound entries) so the
29055
+ * native panel AND the PTZ mimic render buttons WITHOUT hardcoding a
29056
+ * vendor-specific list. `kind: 'action'` entries are triggered with
29057
+ * `runAction({ actionId })`; `kind: 'sound'` entries with `playSound({ soundId })`
29058
+ * (the entry carries the `soundId` to pass). The general primitives — `move`,
29059
+ * `stop`, `goToPoint` — stay as first-class methods, not dictionary entries.
29060
+ *
29061
+ * NOTE (provider wiring): the first provider (Dreame) drives this with the RAW
29062
+ * `callAction(siid,aiid,in)` / `setProperty({siid,piid,value})` MIoT primitives
29063
+ * that the currently-published `@apocaliss92/nodedreame` already exposes on
29064
+ * every device handle. A future nodedreame publish adds a typed
29065
+ * `DreameCameraController` (drive / playPetSound / spotClean / findPet / …); the
29066
+ * provider can then swap the raw calls for the typed methods with no change to
29067
+ * THIS contract.
29068
+ */
29069
+ /**
29070
+ * A momentary drive nudge. The robot MOVES (no gimbal) for as long as the caller
29071
+ * keeps sending nudges (~1 Hz); an explicit `stop` (or letting the nudges lapse)
29072
+ * halts it.
29073
+ *
29074
+ * - `pan` — turn: negative = left, positive = right, 0 = straight.
29075
+ * - `tilt` — throttle: positive = forward, negative = spin / turn-around.
29076
+ * - `speed` — optional intensity hint [0, 1]; the provider may scale the drive
29077
+ * vector by it (drivers without proportional drive ignore it).
29078
+ *
29079
+ * `pan` / `tilt` are normalized [-1, 1]. Both optional so a caller can nudge one
29080
+ * axis alone; an all-undefined nudge is a no-op.
29081
+ */
29082
+ var NavigationMoveCommandSchema = object({
29083
+ pan: number().min(-1).max(1).optional(),
29084
+ tilt: number().min(-1).max(1).optional(),
29085
+ speed: number().min(0).max(1).optional()
29086
+ });
29087
+ /**
29088
+ * The enumerated discrete actions a navigation-capable robot can perform via
29089
+ * `runAction`. This is the CLOSED vocabulary; a given device advertises the
29090
+ * subset it supports through `listActions`. Sounds are NOT here — they go through
29091
+ * `playSound` (see the `sound` dictionary entries).
29092
+ */
29093
+ var NavigationActionIdSchema = _enum([
29094
+ "goHome",
29095
+ "locate",
29096
+ "spotClean",
29097
+ "findPet",
29098
+ "personFollow",
29099
+ "stop",
29100
+ "startClean",
29101
+ "pauseClean",
29102
+ "dockWash",
29103
+ "autoEmpty",
29104
+ "flashOn",
29105
+ "flashOff"
29106
+ ]);
29107
+ /** Whether a dictionary entry is a `runAction` action or a `playSound` sound. */
29108
+ var NavigationEntryKindSchema = _enum(["action", "sound"]);
29109
+ /**
29110
+ * One entry in the navigation action dictionary — the DATA-DRIVEN unit both the
29111
+ * native panel and the PTZ mimic render as a button.
29112
+ *
29113
+ * - `id` — stable id. For `kind:'action'` it is a {@link NavigationActionId}
29114
+ * (pass to `runAction`); for `kind:'sound'` it is a namespaced id
29115
+ * (`sound:meow`) whose `soundId` is passed to `playSound`.
29116
+ * - `icon` — icon HINT (lucide-style name; the UI maps it to its own set).
29117
+ * - `label` — operator-facing English label.
29118
+ * - `soundId` — wire sound id, present only on `kind:'sound'` entries.
29119
+ * - `enabled` — per-device FEATURE FLAG. `listActions` reports it so the UI /
29120
+ * PTZ render ONLY enabled entries. Data-driven: the provider
29121
+ * flips it from config, never by editing code.
29122
+ */
29123
+ var NavigationActionEntrySchema = object({
29124
+ id: string(),
29125
+ kind: NavigationEntryKindSchema,
29126
+ label: string(),
29127
+ icon: string(),
29128
+ /** Present only on `kind:'sound'` entries — the id to pass to `playSound`. */
29129
+ soundId: number().int().optional(),
29130
+ /** Per-device feature flag — render this entry only when true. */
29131
+ enabled: boolean()
29132
+ });
29133
+ /** Coordinates for `goToPoint` — a point on the robot's live map. */
29134
+ var NavigationPointSchema = object({
29135
+ x: number(),
29136
+ y: number()
29137
+ });
29138
+ /**
29139
+ * Per-device FEATURE-FLAG report for the general (non-dictionary) primitives.
29140
+ * The cap reports which are enabled so the UI / PTZ render only the controls
29141
+ * that are turned on for THIS device. Data-driven: the provider derives these
29142
+ * from config + probe, never hardcoded in the UI. The per-DICTIONARY-entry flags
29143
+ * live on {@link NavigationActionEntrySchema.enabled}; these gate the primitives
29144
+ * that are not dictionary entries.
29145
+ *
29146
+ * - `move` / `stop` — the momentary drive joystick.
29147
+ * - `goToPoint` — send-to-map-coordinate. Ships OFF on Dreame until the
29148
+ * map-coordinate plumbing is wired.
29149
+ * - `runAction` — the discrete action buttons (dictionary `kind:'action'`).
29150
+ * - `playSound` — the sound buttons (dictionary `kind:'sound'`).
29151
+ * - `light` — the on/off fill-light toggle (works anytime).
29152
+ * - `lightMode` — the auto/manual selector + manual level slider (a
29153
+ * camera-service control; needs an active stream).
29154
+ */
29155
+ var NavigationFeaturesSchema = object({
29156
+ move: boolean(),
29157
+ stop: boolean(),
29158
+ goToPoint: boolean(),
29159
+ runAction: boolean(),
29160
+ playSound: boolean(),
29161
+ light: boolean(),
29162
+ lightMode: boolean()
29163
+ });
29164
+ /** Light mode: `auto` lets the camera choose brightness; `manual` uses `level`. */
29165
+ var NavigationLightModeSchema = _enum(["auto", "manual"]);
29166
+ /**
29167
+ * Live navigation state so the UI can reflect what the robot is doing:
29168
+ * - `mode` — coarse activity (idle / cleaning / following / …).
29169
+ * - `following` — person/pet follow is currently armed.
29170
+ * - `flash` — the on-camera fill light is on.
29171
+ * - `lightMode` — auto vs manual fill-light mode.
29172
+ * - `lightLevel` — manual fill-light level (40..100); meaningful when
29173
+ * `lightMode === 'manual'`.
29174
+ */
29175
+ var NavigationStatusSchema = object({
29176
+ mode: _enum([
29177
+ "idle",
29178
+ "cleaning",
29179
+ "spot",
29180
+ "following",
29181
+ "goto",
29182
+ "returning",
29183
+ "paused",
29184
+ "unknown"
29185
+ ]),
29186
+ following: boolean(),
29187
+ flash: boolean(),
29188
+ lightMode: NavigationLightModeSchema,
29189
+ lightLevel: number().min(40).max(100),
29190
+ /** Ms epoch when the slice was last updated. */
29191
+ lastChangedAt: number()
29192
+ });
29193
+ /**
29194
+ * Runtime-state slice owned by this cap (kernel-managed: validated, mirrored,
29195
+ * observable). Adds `lastFetchedAt` on top of the status shape per the
29196
+ * convention.
29197
+ */
29198
+ var NavigationRuntimeStateSchema = NavigationStatusSchema.extend({ lastFetchedAt: number() });
29199
+ var navigationCapability = {
29200
+ name: "navigation",
29201
+ scope: "device",
29202
+ deviceNative: true,
29203
+ mode: "singleton",
29204
+ deviceTypes: [DeviceType.Camera],
29205
+ deviceConfig: { ui: {
29206
+ kind: "widget",
29207
+ widgetId: "host/navigation-panel",
29208
+ tab: "navigation",
29209
+ topTab: true,
29210
+ label: "Navigation",
29211
+ order: 0
29212
+ } },
29213
+ methods: {
29214
+ /**
29215
+ * Momentary drive nudge (the robot moves). `protected` — mirrors
29216
+ * `ptz.continuousMove` so the Viewer navigation panel (and the PTZ-mimic
29217
+ * path) works for any authenticated user, not admin-only. The UI sends
29218
+ * these at ~1 Hz while a control is held; the provider forwards each one to
29219
+ * a single drive write WITHOUT debouncing.
29220
+ */
29221
+ move: method(NavigationMoveCommandSchema.extend({ deviceId: number() }), _void(), { kind: "mutation" }),
29222
+ /** Halt all motion immediately (zero drive vector). */
29223
+ stop: method(object({ deviceId: number() }), _void(), { kind: "mutation" }),
29224
+ /** Send the robot to a point on its live map. */
29225
+ goToPoint: method(NavigationPointSchema.extend({ deviceId: number() }), _void(), { kind: "mutation" }),
29226
+ /**
29227
+ * Enumerate the discrete controls THIS device supports (data-driven UI +
29228
+ * PTZ mimic). Camera-probed subset of {@link NAVIGATION_ACTION_CATALOG}.
29229
+ */
29230
+ listActions: method(object({ deviceId: number() }), array(NavigationActionEntrySchema)),
29231
+ /**
29232
+ * Run one discrete action (a `kind:'action'` dictionary entry). Invalid /
29233
+ * unsupported action ids are rejected by the provider.
29234
+ */
29235
+ runAction: method(object({
29236
+ deviceId: number(),
29237
+ actionId: NavigationActionIdSchema
29238
+ }), _void(), { kind: "mutation" }),
29239
+ /** Play a sound by its wire id (the `soundId` of a `kind:'sound'` entry). */
29240
+ playSound: method(object({
29241
+ deviceId: number(),
29242
+ soundId: number().int()
29243
+ }), _void(), { kind: "mutation" }),
29244
+ /**
29245
+ * Turn the on-camera fill light on / off (the `OpenFullLight` control —
29246
+ * works anytime, no active stream required).
29247
+ */
29248
+ setLightOn: method(object({
29249
+ deviceId: number(),
29250
+ on: boolean()
29251
+ }), _void(), { kind: "mutation" }),
29252
+ /**
29253
+ * Set the fill-light mode (auto vs manual). `manual` optionally carries the
29254
+ * initial `level`. The auto/manual + level control is a CAMERA-service
29255
+ * action that generally needs an active camera stream/monitor session — the
29256
+ * UI shows the manual level slider ONLY when `mode === 'manual'`.
29257
+ */
29258
+ setLightMode: method(object({
29259
+ deviceId: number(),
29260
+ mode: NavigationLightModeSchema,
29261
+ level: number().min(40).max(100).optional()
29262
+ }), _void(), { kind: "mutation" }),
29263
+ /** Set the MANUAL fill-light level (40..100). Implies `manual` mode. */
29264
+ setLightLevel: method(object({
29265
+ deviceId: number(),
29266
+ level: number().min(40).max(100)
29267
+ }), _void(), { kind: "mutation" }),
29268
+ /**
29269
+ * Per-device FEATURE-FLAG report for the general primitives — drives which
29270
+ * controls the UI shows (the per-entry flags for the dictionary come back on
29271
+ * `listActions`).
29272
+ */
29273
+ getFeatures: method(object({ deviceId: number() }), NavigationFeaturesSchema)
29274
+ },
29275
+ events: { onStatusChanged: { data: object({
29276
+ deviceId: number(),
29277
+ status: NavigationStatusSchema
29278
+ }) } },
29279
+ status: {
29280
+ schema: NavigationStatusSchema,
29281
+ kind: "push"
29282
+ },
29283
+ /**
29284
+ * Runtime-state slice mirrored by the kernel. The navigation panel watches it
29285
+ * for live mode / follow / flash changes.
29286
+ */
29287
+ runtimeState: NavigationRuntimeStateSchema,
29288
+ /**
29289
+ * Runtime-state durability: **session** — like `vacuum-control`, a restored
29290
+ * `mode: cleaning` / `following: true` is a robot that is not actually doing
29291
+ * that. The live handle re-publishes on connect.
29292
+ *
29293
+ * See `RuntimeStateDurability`. Enforced by
29294
+ * `scripts/check-runtime-state-durability.ts`.
29295
+ */
29296
+ durability: "session"
29297
+ };
29011
29298
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, method(object({ deviceId: number().int().nonnegative() }), object({ success: literal(true) }), {
29012
29299
  kind: "mutation",
29013
29300
  auth: "admin"
@@ -32283,6 +32570,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
32283
32570
  motionTrigger: motionTriggerCapability,
32284
32571
  motionZones: motionZonesCapability,
32285
32572
  nativeObjectDetection: nativeObjectDetectionCapability,
32573
+ navigation: navigationCapability,
32286
32574
  networkLink: networkLinkCapability,
32287
32575
  notifier: notifierCapability,
32288
32576
  numericSensor: numericSensorCapability,
@@ -36137,6 +36425,66 @@ Object.freeze({
36137
36425
  addonId: null,
36138
36426
  access: "create"
36139
36427
  },
36428
+ "navigation.getFeatures": {
36429
+ capName: "navigation",
36430
+ capScope: "device",
36431
+ addonId: null,
36432
+ access: "view"
36433
+ },
36434
+ "navigation.goToPoint": {
36435
+ capName: "navigation",
36436
+ capScope: "device",
36437
+ addonId: null,
36438
+ access: "create"
36439
+ },
36440
+ "navigation.listActions": {
36441
+ capName: "navigation",
36442
+ capScope: "device",
36443
+ addonId: null,
36444
+ access: "view"
36445
+ },
36446
+ "navigation.move": {
36447
+ capName: "navigation",
36448
+ capScope: "device",
36449
+ addonId: null,
36450
+ access: "create"
36451
+ },
36452
+ "navigation.playSound": {
36453
+ capName: "navigation",
36454
+ capScope: "device",
36455
+ addonId: null,
36456
+ access: "create"
36457
+ },
36458
+ "navigation.runAction": {
36459
+ capName: "navigation",
36460
+ capScope: "device",
36461
+ addonId: null,
36462
+ access: "create"
36463
+ },
36464
+ "navigation.setLightLevel": {
36465
+ capName: "navigation",
36466
+ capScope: "device",
36467
+ addonId: null,
36468
+ access: "create"
36469
+ },
36470
+ "navigation.setLightMode": {
36471
+ capName: "navigation",
36472
+ capScope: "device",
36473
+ addonId: null,
36474
+ access: "create"
36475
+ },
36476
+ "navigation.setLightOn": {
36477
+ capName: "navigation",
36478
+ capScope: "device",
36479
+ addonId: null,
36480
+ access: "create"
36481
+ },
36482
+ "navigation.stop": {
36483
+ capName: "navigation",
36484
+ capScope: "device",
36485
+ addonId: null,
36486
+ access: "create"
36487
+ },
36140
36488
  "networkAccess.getEndpoint": {
36141
36489
  capName: "network-access",
36142
36490
  capScope: "system",
@@ -40232,6 +40580,56 @@ Object.freeze({
40232
40580
  form: "single",
40233
40581
  optional: false
40234
40582
  }],
40583
+ "navigation.getFeatures": [{
40584
+ name: "deviceId",
40585
+ form: "single",
40586
+ optional: false
40587
+ }],
40588
+ "navigation.goToPoint": [{
40589
+ name: "deviceId",
40590
+ form: "single",
40591
+ optional: false
40592
+ }],
40593
+ "navigation.listActions": [{
40594
+ name: "deviceId",
40595
+ form: "single",
40596
+ optional: false
40597
+ }],
40598
+ "navigation.move": [{
40599
+ name: "deviceId",
40600
+ form: "single",
40601
+ optional: false
40602
+ }],
40603
+ "navigation.playSound": [{
40604
+ name: "deviceId",
40605
+ form: "single",
40606
+ optional: false
40607
+ }],
40608
+ "navigation.runAction": [{
40609
+ name: "deviceId",
40610
+ form: "single",
40611
+ optional: false
40612
+ }],
40613
+ "navigation.setLightLevel": [{
40614
+ name: "deviceId",
40615
+ form: "single",
40616
+ optional: false
40617
+ }],
40618
+ "navigation.setLightMode": [{
40619
+ name: "deviceId",
40620
+ form: "single",
40621
+ optional: false
40622
+ }],
40623
+ "navigation.setLightOn": [{
40624
+ name: "deviceId",
40625
+ form: "single",
40626
+ optional: false
40627
+ }],
40628
+ "navigation.stop": [{
40629
+ name: "deviceId",
40630
+ form: "single",
40631
+ optional: false
40632
+ }],
40235
40633
  "networkQuality.getDeviceStats": [{
40236
40634
  name: "deviceId",
40237
40635
  form: "single",
package/dist/addon.mjs CHANGED
@@ -5443,6 +5443,12 @@ Object.fromEntries([
5443
5443
  icon: "move",
5444
5444
  order: 40
5445
5445
  },
5446
+ {
5447
+ id: "navigation",
5448
+ label: "Navigation",
5449
+ icon: "compass",
5450
+ order: 41
5451
+ },
5446
5452
  {
5447
5453
  id: "consumables",
5448
5454
  label: "Consumables",
@@ -29009,6 +29015,287 @@ var ptzAutotrackCapability = {
29009
29015
  */
29010
29016
  durability: "session"
29011
29017
  };
29018
+ /**
29019
+ * `navigation` — a device-scoped capability that natively expresses the FULL
29020
+ * navigation / action surface of a robot that DRIVES ITSELF and carries an
29021
+ * on-board camera (the Dreame robot-vacuum camera is the first provider).
29022
+ *
29023
+ * Why a NEW cap rather than overloading `ptz`:
29024
+ * - `ptz` moves a *gimbal* on a fixed camera (pan/tilt/zoom of the lens). A
29025
+ * robot vacuum has no gimbal — the whole chassis drives, turns and spins.
29026
+ * The two are different physical models: PTZ is absolute-position + presets,
29027
+ * navigation is momentary drive nudges + discrete robot ACTIONS
29028
+ * (dock / spot-clean / follow-pet / go-to-point / …).
29029
+ * - This cap is the SOURCE OF TRUTH. Two consumers adapt from it rather than
29030
+ * the reverse:
29031
+ * 1. a native CamStack navigation panel (data-driven from `listActions`
29032
+ * / `getOptions`), and
29033
+ * 2. the PTZ surface — a thin adapter mimics `ptz` from `navigation` so a
29034
+ * robot camera shows up in the existing PTZ control path without every
29035
+ * PTZ provider learning about robots. The mapping lives in the adapter,
29036
+ * not here (see the addon design note):
29037
+ * ptz.continuousMove({pan,tilt}) → navigation.move({pan,tilt})
29038
+ * ptz.stop() → navigation.stop()
29039
+ * ptz.goHome() → navigation.runAction('goHome')
29040
+ * ptz.getPresets() → navigation.listActions() (id→preset)
29041
+ * ptz.goToPreset(id) → navigation.runAction(id)
29042
+ *
29043
+ * ## Continuous drive
29044
+ *
29045
+ * `move` is MOMENTARY. Fluid navigation comes from the UI (or the PTZ adapter)
29046
+ * sending `move({pan,tilt})` REPEATEDLY at ~1 Hz while a direction is held, and
29047
+ * one `stop()` on release — exactly like the robot app's remote-drive joystick.
29048
+ * The provider forwards EACH `move` to one drive write; it must NOT debounce or
29049
+ * coalesce them. The UI owns the cadence.
29050
+ *
29051
+ * ## The action dictionary
29052
+ *
29053
+ * The discrete controls (dock / locate / spot-clean / follow / flash / sounds)
29054
+ * are a DATA-DRIVEN dictionary the cap exposes via `listActions()`. Each entry
29055
+ * carries `{ id, kind, label, icon }` (plus `soundId` for sound entries) so the
29056
+ * native panel AND the PTZ mimic render buttons WITHOUT hardcoding a
29057
+ * vendor-specific list. `kind: 'action'` entries are triggered with
29058
+ * `runAction({ actionId })`; `kind: 'sound'` entries with `playSound({ soundId })`
29059
+ * (the entry carries the `soundId` to pass). The general primitives — `move`,
29060
+ * `stop`, `goToPoint` — stay as first-class methods, not dictionary entries.
29061
+ *
29062
+ * NOTE (provider wiring): the first provider (Dreame) drives this with the RAW
29063
+ * `callAction(siid,aiid,in)` / `setProperty({siid,piid,value})` MIoT primitives
29064
+ * that the currently-published `@apocaliss92/nodedreame` already exposes on
29065
+ * every device handle. A future nodedreame publish adds a typed
29066
+ * `DreameCameraController` (drive / playPetSound / spotClean / findPet / …); the
29067
+ * provider can then swap the raw calls for the typed methods with no change to
29068
+ * THIS contract.
29069
+ */
29070
+ /**
29071
+ * A momentary drive nudge. The robot MOVES (no gimbal) for as long as the caller
29072
+ * keeps sending nudges (~1 Hz); an explicit `stop` (or letting the nudges lapse)
29073
+ * halts it.
29074
+ *
29075
+ * - `pan` — turn: negative = left, positive = right, 0 = straight.
29076
+ * - `tilt` — throttle: positive = forward, negative = spin / turn-around.
29077
+ * - `speed` — optional intensity hint [0, 1]; the provider may scale the drive
29078
+ * vector by it (drivers without proportional drive ignore it).
29079
+ *
29080
+ * `pan` / `tilt` are normalized [-1, 1]. Both optional so a caller can nudge one
29081
+ * axis alone; an all-undefined nudge is a no-op.
29082
+ */
29083
+ var NavigationMoveCommandSchema = object({
29084
+ pan: number().min(-1).max(1).optional(),
29085
+ tilt: number().min(-1).max(1).optional(),
29086
+ speed: number().min(0).max(1).optional()
29087
+ });
29088
+ /**
29089
+ * The enumerated discrete actions a navigation-capable robot can perform via
29090
+ * `runAction`. This is the CLOSED vocabulary; a given device advertises the
29091
+ * subset it supports through `listActions`. Sounds are NOT here — they go through
29092
+ * `playSound` (see the `sound` dictionary entries).
29093
+ */
29094
+ var NavigationActionIdSchema = _enum([
29095
+ "goHome",
29096
+ "locate",
29097
+ "spotClean",
29098
+ "findPet",
29099
+ "personFollow",
29100
+ "stop",
29101
+ "startClean",
29102
+ "pauseClean",
29103
+ "dockWash",
29104
+ "autoEmpty",
29105
+ "flashOn",
29106
+ "flashOff"
29107
+ ]);
29108
+ /** Whether a dictionary entry is a `runAction` action or a `playSound` sound. */
29109
+ var NavigationEntryKindSchema = _enum(["action", "sound"]);
29110
+ /**
29111
+ * One entry in the navigation action dictionary — the DATA-DRIVEN unit both the
29112
+ * native panel and the PTZ mimic render as a button.
29113
+ *
29114
+ * - `id` — stable id. For `kind:'action'` it is a {@link NavigationActionId}
29115
+ * (pass to `runAction`); for `kind:'sound'` it is a namespaced id
29116
+ * (`sound:meow`) whose `soundId` is passed to `playSound`.
29117
+ * - `icon` — icon HINT (lucide-style name; the UI maps it to its own set).
29118
+ * - `label` — operator-facing English label.
29119
+ * - `soundId` — wire sound id, present only on `kind:'sound'` entries.
29120
+ * - `enabled` — per-device FEATURE FLAG. `listActions` reports it so the UI /
29121
+ * PTZ render ONLY enabled entries. Data-driven: the provider
29122
+ * flips it from config, never by editing code.
29123
+ */
29124
+ var NavigationActionEntrySchema = object({
29125
+ id: string(),
29126
+ kind: NavigationEntryKindSchema,
29127
+ label: string(),
29128
+ icon: string(),
29129
+ /** Present only on `kind:'sound'` entries — the id to pass to `playSound`. */
29130
+ soundId: number().int().optional(),
29131
+ /** Per-device feature flag — render this entry only when true. */
29132
+ enabled: boolean()
29133
+ });
29134
+ /** Coordinates for `goToPoint` — a point on the robot's live map. */
29135
+ var NavigationPointSchema = object({
29136
+ x: number(),
29137
+ y: number()
29138
+ });
29139
+ /**
29140
+ * Per-device FEATURE-FLAG report for the general (non-dictionary) primitives.
29141
+ * The cap reports which are enabled so the UI / PTZ render only the controls
29142
+ * that are turned on for THIS device. Data-driven: the provider derives these
29143
+ * from config + probe, never hardcoded in the UI. The per-DICTIONARY-entry flags
29144
+ * live on {@link NavigationActionEntrySchema.enabled}; these gate the primitives
29145
+ * that are not dictionary entries.
29146
+ *
29147
+ * - `move` / `stop` — the momentary drive joystick.
29148
+ * - `goToPoint` — send-to-map-coordinate. Ships OFF on Dreame until the
29149
+ * map-coordinate plumbing is wired.
29150
+ * - `runAction` — the discrete action buttons (dictionary `kind:'action'`).
29151
+ * - `playSound` — the sound buttons (dictionary `kind:'sound'`).
29152
+ * - `light` — the on/off fill-light toggle (works anytime).
29153
+ * - `lightMode` — the auto/manual selector + manual level slider (a
29154
+ * camera-service control; needs an active stream).
29155
+ */
29156
+ var NavigationFeaturesSchema = object({
29157
+ move: boolean(),
29158
+ stop: boolean(),
29159
+ goToPoint: boolean(),
29160
+ runAction: boolean(),
29161
+ playSound: boolean(),
29162
+ light: boolean(),
29163
+ lightMode: boolean()
29164
+ });
29165
+ /** Light mode: `auto` lets the camera choose brightness; `manual` uses `level`. */
29166
+ var NavigationLightModeSchema = _enum(["auto", "manual"]);
29167
+ /**
29168
+ * Live navigation state so the UI can reflect what the robot is doing:
29169
+ * - `mode` — coarse activity (idle / cleaning / following / …).
29170
+ * - `following` — person/pet follow is currently armed.
29171
+ * - `flash` — the on-camera fill light is on.
29172
+ * - `lightMode` — auto vs manual fill-light mode.
29173
+ * - `lightLevel` — manual fill-light level (40..100); meaningful when
29174
+ * `lightMode === 'manual'`.
29175
+ */
29176
+ var NavigationStatusSchema = object({
29177
+ mode: _enum([
29178
+ "idle",
29179
+ "cleaning",
29180
+ "spot",
29181
+ "following",
29182
+ "goto",
29183
+ "returning",
29184
+ "paused",
29185
+ "unknown"
29186
+ ]),
29187
+ following: boolean(),
29188
+ flash: boolean(),
29189
+ lightMode: NavigationLightModeSchema,
29190
+ lightLevel: number().min(40).max(100),
29191
+ /** Ms epoch when the slice was last updated. */
29192
+ lastChangedAt: number()
29193
+ });
29194
+ /**
29195
+ * Runtime-state slice owned by this cap (kernel-managed: validated, mirrored,
29196
+ * observable). Adds `lastFetchedAt` on top of the status shape per the
29197
+ * convention.
29198
+ */
29199
+ var NavigationRuntimeStateSchema = NavigationStatusSchema.extend({ lastFetchedAt: number() });
29200
+ var navigationCapability = {
29201
+ name: "navigation",
29202
+ scope: "device",
29203
+ deviceNative: true,
29204
+ mode: "singleton",
29205
+ deviceTypes: [DeviceType.Camera],
29206
+ deviceConfig: { ui: {
29207
+ kind: "widget",
29208
+ widgetId: "host/navigation-panel",
29209
+ tab: "navigation",
29210
+ topTab: true,
29211
+ label: "Navigation",
29212
+ order: 0
29213
+ } },
29214
+ methods: {
29215
+ /**
29216
+ * Momentary drive nudge (the robot moves). `protected` — mirrors
29217
+ * `ptz.continuousMove` so the Viewer navigation panel (and the PTZ-mimic
29218
+ * path) works for any authenticated user, not admin-only. The UI sends
29219
+ * these at ~1 Hz while a control is held; the provider forwards each one to
29220
+ * a single drive write WITHOUT debouncing.
29221
+ */
29222
+ move: method(NavigationMoveCommandSchema.extend({ deviceId: number() }), _void(), { kind: "mutation" }),
29223
+ /** Halt all motion immediately (zero drive vector). */
29224
+ stop: method(object({ deviceId: number() }), _void(), { kind: "mutation" }),
29225
+ /** Send the robot to a point on its live map. */
29226
+ goToPoint: method(NavigationPointSchema.extend({ deviceId: number() }), _void(), { kind: "mutation" }),
29227
+ /**
29228
+ * Enumerate the discrete controls THIS device supports (data-driven UI +
29229
+ * PTZ mimic). Camera-probed subset of {@link NAVIGATION_ACTION_CATALOG}.
29230
+ */
29231
+ listActions: method(object({ deviceId: number() }), array(NavigationActionEntrySchema)),
29232
+ /**
29233
+ * Run one discrete action (a `kind:'action'` dictionary entry). Invalid /
29234
+ * unsupported action ids are rejected by the provider.
29235
+ */
29236
+ runAction: method(object({
29237
+ deviceId: number(),
29238
+ actionId: NavigationActionIdSchema
29239
+ }), _void(), { kind: "mutation" }),
29240
+ /** Play a sound by its wire id (the `soundId` of a `kind:'sound'` entry). */
29241
+ playSound: method(object({
29242
+ deviceId: number(),
29243
+ soundId: number().int()
29244
+ }), _void(), { kind: "mutation" }),
29245
+ /**
29246
+ * Turn the on-camera fill light on / off (the `OpenFullLight` control —
29247
+ * works anytime, no active stream required).
29248
+ */
29249
+ setLightOn: method(object({
29250
+ deviceId: number(),
29251
+ on: boolean()
29252
+ }), _void(), { kind: "mutation" }),
29253
+ /**
29254
+ * Set the fill-light mode (auto vs manual). `manual` optionally carries the
29255
+ * initial `level`. The auto/manual + level control is a CAMERA-service
29256
+ * action that generally needs an active camera stream/monitor session — the
29257
+ * UI shows the manual level slider ONLY when `mode === 'manual'`.
29258
+ */
29259
+ setLightMode: method(object({
29260
+ deviceId: number(),
29261
+ mode: NavigationLightModeSchema,
29262
+ level: number().min(40).max(100).optional()
29263
+ }), _void(), { kind: "mutation" }),
29264
+ /** Set the MANUAL fill-light level (40..100). Implies `manual` mode. */
29265
+ setLightLevel: method(object({
29266
+ deviceId: number(),
29267
+ level: number().min(40).max(100)
29268
+ }), _void(), { kind: "mutation" }),
29269
+ /**
29270
+ * Per-device FEATURE-FLAG report for the general primitives — drives which
29271
+ * controls the UI shows (the per-entry flags for the dictionary come back on
29272
+ * `listActions`).
29273
+ */
29274
+ getFeatures: method(object({ deviceId: number() }), NavigationFeaturesSchema)
29275
+ },
29276
+ events: { onStatusChanged: { data: object({
29277
+ deviceId: number(),
29278
+ status: NavigationStatusSchema
29279
+ }) } },
29280
+ status: {
29281
+ schema: NavigationStatusSchema,
29282
+ kind: "push"
29283
+ },
29284
+ /**
29285
+ * Runtime-state slice mirrored by the kernel. The navigation panel watches it
29286
+ * for live mode / follow / flash changes.
29287
+ */
29288
+ runtimeState: NavigationRuntimeStateSchema,
29289
+ /**
29290
+ * Runtime-state durability: **session** — like `vacuum-control`, a restored
29291
+ * `mode: cleaning` / `following: true` is a robot that is not actually doing
29292
+ * that. The live handle re-publishes on connect.
29293
+ *
29294
+ * See `RuntimeStateDurability`. Enforced by
29295
+ * `scripts/check-runtime-state-durability.ts`.
29296
+ */
29297
+ durability: "session"
29298
+ };
29012
29299
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, method(object({ deviceId: number().int().nonnegative() }), object({ success: literal(true) }), {
29013
29300
  kind: "mutation",
29014
29301
  auth: "admin"
@@ -32284,6 +32571,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
32284
32571
  motionTrigger: motionTriggerCapability,
32285
32572
  motionZones: motionZonesCapability,
32286
32573
  nativeObjectDetection: nativeObjectDetectionCapability,
32574
+ navigation: navigationCapability,
32287
32575
  networkLink: networkLinkCapability,
32288
32576
  notifier: notifierCapability,
32289
32577
  numericSensor: numericSensorCapability,
@@ -36138,6 +36426,66 @@ Object.freeze({
36138
36426
  addonId: null,
36139
36427
  access: "create"
36140
36428
  },
36429
+ "navigation.getFeatures": {
36430
+ capName: "navigation",
36431
+ capScope: "device",
36432
+ addonId: null,
36433
+ access: "view"
36434
+ },
36435
+ "navigation.goToPoint": {
36436
+ capName: "navigation",
36437
+ capScope: "device",
36438
+ addonId: null,
36439
+ access: "create"
36440
+ },
36441
+ "navigation.listActions": {
36442
+ capName: "navigation",
36443
+ capScope: "device",
36444
+ addonId: null,
36445
+ access: "view"
36446
+ },
36447
+ "navigation.move": {
36448
+ capName: "navigation",
36449
+ capScope: "device",
36450
+ addonId: null,
36451
+ access: "create"
36452
+ },
36453
+ "navigation.playSound": {
36454
+ capName: "navigation",
36455
+ capScope: "device",
36456
+ addonId: null,
36457
+ access: "create"
36458
+ },
36459
+ "navigation.runAction": {
36460
+ capName: "navigation",
36461
+ capScope: "device",
36462
+ addonId: null,
36463
+ access: "create"
36464
+ },
36465
+ "navigation.setLightLevel": {
36466
+ capName: "navigation",
36467
+ capScope: "device",
36468
+ addonId: null,
36469
+ access: "create"
36470
+ },
36471
+ "navigation.setLightMode": {
36472
+ capName: "navigation",
36473
+ capScope: "device",
36474
+ addonId: null,
36475
+ access: "create"
36476
+ },
36477
+ "navigation.setLightOn": {
36478
+ capName: "navigation",
36479
+ capScope: "device",
36480
+ addonId: null,
36481
+ access: "create"
36482
+ },
36483
+ "navigation.stop": {
36484
+ capName: "navigation",
36485
+ capScope: "device",
36486
+ addonId: null,
36487
+ access: "create"
36488
+ },
36141
36489
  "networkAccess.getEndpoint": {
36142
36490
  capName: "network-access",
36143
36491
  capScope: "system",
@@ -40233,6 +40581,56 @@ Object.freeze({
40233
40581
  form: "single",
40234
40582
  optional: false
40235
40583
  }],
40584
+ "navigation.getFeatures": [{
40585
+ name: "deviceId",
40586
+ form: "single",
40587
+ optional: false
40588
+ }],
40589
+ "navigation.goToPoint": [{
40590
+ name: "deviceId",
40591
+ form: "single",
40592
+ optional: false
40593
+ }],
40594
+ "navigation.listActions": [{
40595
+ name: "deviceId",
40596
+ form: "single",
40597
+ optional: false
40598
+ }],
40599
+ "navigation.move": [{
40600
+ name: "deviceId",
40601
+ form: "single",
40602
+ optional: false
40603
+ }],
40604
+ "navigation.playSound": [{
40605
+ name: "deviceId",
40606
+ form: "single",
40607
+ optional: false
40608
+ }],
40609
+ "navigation.runAction": [{
40610
+ name: "deviceId",
40611
+ form: "single",
40612
+ optional: false
40613
+ }],
40614
+ "navigation.setLightLevel": [{
40615
+ name: "deviceId",
40616
+ form: "single",
40617
+ optional: false
40618
+ }],
40619
+ "navigation.setLightMode": [{
40620
+ name: "deviceId",
40621
+ form: "single",
40622
+ optional: false
40623
+ }],
40624
+ "navigation.setLightOn": [{
40625
+ name: "deviceId",
40626
+ form: "single",
40627
+ optional: false
40628
+ }],
40629
+ "navigation.stop": [{
40630
+ name: "deviceId",
40631
+ form: "single",
40632
+ optional: false
40633
+ }],
40236
40634
  "networkQuality.getDeviceStats": [{
40237
40635
  name: "deviceId",
40238
40636
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-dreo",
3
- "version": "0.2.70",
3
+ "version": "0.2.72",
4
4
  "description": "Dreo smart-device (fan / air-circulator / purifier / heater / humidifier) device-provider addon for CamStack — wraps the @apocaliss92/nodedreo Dreo cloud client (REST + WebSocket)",
5
5
  "keywords": [
6
6
  "camstack",