@camstack/addon-provider-petkit 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
@@ -6508,6 +6508,12 @@ Object.fromEntries([
6508
6508
  icon: "move",
6509
6509
  order: 40
6510
6510
  },
6511
+ {
6512
+ id: "navigation",
6513
+ label: "Navigation",
6514
+ icon: "compass",
6515
+ order: 41
6516
+ },
6511
6517
  {
6512
6518
  id: "consumables",
6513
6519
  label: "Consumables",
@@ -30074,6 +30080,287 @@ var ptzAutotrackCapability = {
30074
30080
  */
30075
30081
  durability: "session"
30076
30082
  };
30083
+ /**
30084
+ * `navigation` — a device-scoped capability that natively expresses the FULL
30085
+ * navigation / action surface of a robot that DRIVES ITSELF and carries an
30086
+ * on-board camera (the Dreame robot-vacuum camera is the first provider).
30087
+ *
30088
+ * Why a NEW cap rather than overloading `ptz`:
30089
+ * - `ptz` moves a *gimbal* on a fixed camera (pan/tilt/zoom of the lens). A
30090
+ * robot vacuum has no gimbal — the whole chassis drives, turns and spins.
30091
+ * The two are different physical models: PTZ is absolute-position + presets,
30092
+ * navigation is momentary drive nudges + discrete robot ACTIONS
30093
+ * (dock / spot-clean / follow-pet / go-to-point / …).
30094
+ * - This cap is the SOURCE OF TRUTH. Two consumers adapt from it rather than
30095
+ * the reverse:
30096
+ * 1. a native CamStack navigation panel (data-driven from `listActions`
30097
+ * / `getOptions`), and
30098
+ * 2. the PTZ surface — a thin adapter mimics `ptz` from `navigation` so a
30099
+ * robot camera shows up in the existing PTZ control path without every
30100
+ * PTZ provider learning about robots. The mapping lives in the adapter,
30101
+ * not here (see the addon design note):
30102
+ * ptz.continuousMove({pan,tilt}) → navigation.move({pan,tilt})
30103
+ * ptz.stop() → navigation.stop()
30104
+ * ptz.goHome() → navigation.runAction('goHome')
30105
+ * ptz.getPresets() → navigation.listActions() (id→preset)
30106
+ * ptz.goToPreset(id) → navigation.runAction(id)
30107
+ *
30108
+ * ## Continuous drive
30109
+ *
30110
+ * `move` is MOMENTARY. Fluid navigation comes from the UI (or the PTZ adapter)
30111
+ * sending `move({pan,tilt})` REPEATEDLY at ~1 Hz while a direction is held, and
30112
+ * one `stop()` on release — exactly like the robot app's remote-drive joystick.
30113
+ * The provider forwards EACH `move` to one drive write; it must NOT debounce or
30114
+ * coalesce them. The UI owns the cadence.
30115
+ *
30116
+ * ## The action dictionary
30117
+ *
30118
+ * The discrete controls (dock / locate / spot-clean / follow / flash / sounds)
30119
+ * are a DATA-DRIVEN dictionary the cap exposes via `listActions()`. Each entry
30120
+ * carries `{ id, kind, label, icon }` (plus `soundId` for sound entries) so the
30121
+ * native panel AND the PTZ mimic render buttons WITHOUT hardcoding a
30122
+ * vendor-specific list. `kind: 'action'` entries are triggered with
30123
+ * `runAction({ actionId })`; `kind: 'sound'` entries with `playSound({ soundId })`
30124
+ * (the entry carries the `soundId` to pass). The general primitives — `move`,
30125
+ * `stop`, `goToPoint` — stay as first-class methods, not dictionary entries.
30126
+ *
30127
+ * NOTE (provider wiring): the first provider (Dreame) drives this with the RAW
30128
+ * `callAction(siid,aiid,in)` / `setProperty({siid,piid,value})` MIoT primitives
30129
+ * that the currently-published `@apocaliss92/nodedreame` already exposes on
30130
+ * every device handle. A future nodedreame publish adds a typed
30131
+ * `DreameCameraController` (drive / playPetSound / spotClean / findPet / …); the
30132
+ * provider can then swap the raw calls for the typed methods with no change to
30133
+ * THIS contract.
30134
+ */
30135
+ /**
30136
+ * A momentary drive nudge. The robot MOVES (no gimbal) for as long as the caller
30137
+ * keeps sending nudges (~1 Hz); an explicit `stop` (or letting the nudges lapse)
30138
+ * halts it.
30139
+ *
30140
+ * - `pan` — turn: negative = left, positive = right, 0 = straight.
30141
+ * - `tilt` — throttle: positive = forward, negative = spin / turn-around.
30142
+ * - `speed` — optional intensity hint [0, 1]; the provider may scale the drive
30143
+ * vector by it (drivers without proportional drive ignore it).
30144
+ *
30145
+ * `pan` / `tilt` are normalized [-1, 1]. Both optional so a caller can nudge one
30146
+ * axis alone; an all-undefined nudge is a no-op.
30147
+ */
30148
+ var NavigationMoveCommandSchema = object({
30149
+ pan: number().min(-1).max(1).optional(),
30150
+ tilt: number().min(-1).max(1).optional(),
30151
+ speed: number().min(0).max(1).optional()
30152
+ });
30153
+ /**
30154
+ * The enumerated discrete actions a navigation-capable robot can perform via
30155
+ * `runAction`. This is the CLOSED vocabulary; a given device advertises the
30156
+ * subset it supports through `listActions`. Sounds are NOT here — they go through
30157
+ * `playSound` (see the `sound` dictionary entries).
30158
+ */
30159
+ var NavigationActionIdSchema = _enum([
30160
+ "goHome",
30161
+ "locate",
30162
+ "spotClean",
30163
+ "findPet",
30164
+ "personFollow",
30165
+ "stop",
30166
+ "startClean",
30167
+ "pauseClean",
30168
+ "dockWash",
30169
+ "autoEmpty",
30170
+ "flashOn",
30171
+ "flashOff"
30172
+ ]);
30173
+ /** Whether a dictionary entry is a `runAction` action or a `playSound` sound. */
30174
+ var NavigationEntryKindSchema = _enum(["action", "sound"]);
30175
+ /**
30176
+ * One entry in the navigation action dictionary — the DATA-DRIVEN unit both the
30177
+ * native panel and the PTZ mimic render as a button.
30178
+ *
30179
+ * - `id` — stable id. For `kind:'action'` it is a {@link NavigationActionId}
30180
+ * (pass to `runAction`); for `kind:'sound'` it is a namespaced id
30181
+ * (`sound:meow`) whose `soundId` is passed to `playSound`.
30182
+ * - `icon` — icon HINT (lucide-style name; the UI maps it to its own set).
30183
+ * - `label` — operator-facing English label.
30184
+ * - `soundId` — wire sound id, present only on `kind:'sound'` entries.
30185
+ * - `enabled` — per-device FEATURE FLAG. `listActions` reports it so the UI /
30186
+ * PTZ render ONLY enabled entries. Data-driven: the provider
30187
+ * flips it from config, never by editing code.
30188
+ */
30189
+ var NavigationActionEntrySchema = object({
30190
+ id: string(),
30191
+ kind: NavigationEntryKindSchema,
30192
+ label: string(),
30193
+ icon: string(),
30194
+ /** Present only on `kind:'sound'` entries — the id to pass to `playSound`. */
30195
+ soundId: number().int().optional(),
30196
+ /** Per-device feature flag — render this entry only when true. */
30197
+ enabled: boolean()
30198
+ });
30199
+ /** Coordinates for `goToPoint` — a point on the robot's live map. */
30200
+ var NavigationPointSchema = object({
30201
+ x: number(),
30202
+ y: number()
30203
+ });
30204
+ /**
30205
+ * Per-device FEATURE-FLAG report for the general (non-dictionary) primitives.
30206
+ * The cap reports which are enabled so the UI / PTZ render only the controls
30207
+ * that are turned on for THIS device. Data-driven: the provider derives these
30208
+ * from config + probe, never hardcoded in the UI. The per-DICTIONARY-entry flags
30209
+ * live on {@link NavigationActionEntrySchema.enabled}; these gate the primitives
30210
+ * that are not dictionary entries.
30211
+ *
30212
+ * - `move` / `stop` — the momentary drive joystick.
30213
+ * - `goToPoint` — send-to-map-coordinate. Ships OFF on Dreame until the
30214
+ * map-coordinate plumbing is wired.
30215
+ * - `runAction` — the discrete action buttons (dictionary `kind:'action'`).
30216
+ * - `playSound` — the sound buttons (dictionary `kind:'sound'`).
30217
+ * - `light` — the on/off fill-light toggle (works anytime).
30218
+ * - `lightMode` — the auto/manual selector + manual level slider (a
30219
+ * camera-service control; needs an active stream).
30220
+ */
30221
+ var NavigationFeaturesSchema = object({
30222
+ move: boolean(),
30223
+ stop: boolean(),
30224
+ goToPoint: boolean(),
30225
+ runAction: boolean(),
30226
+ playSound: boolean(),
30227
+ light: boolean(),
30228
+ lightMode: boolean()
30229
+ });
30230
+ /** Light mode: `auto` lets the camera choose brightness; `manual` uses `level`. */
30231
+ var NavigationLightModeSchema = _enum(["auto", "manual"]);
30232
+ /**
30233
+ * Live navigation state so the UI can reflect what the robot is doing:
30234
+ * - `mode` — coarse activity (idle / cleaning / following / …).
30235
+ * - `following` — person/pet follow is currently armed.
30236
+ * - `flash` — the on-camera fill light is on.
30237
+ * - `lightMode` — auto vs manual fill-light mode.
30238
+ * - `lightLevel` — manual fill-light level (40..100); meaningful when
30239
+ * `lightMode === 'manual'`.
30240
+ */
30241
+ var NavigationStatusSchema = object({
30242
+ mode: _enum([
30243
+ "idle",
30244
+ "cleaning",
30245
+ "spot",
30246
+ "following",
30247
+ "goto",
30248
+ "returning",
30249
+ "paused",
30250
+ "unknown"
30251
+ ]),
30252
+ following: boolean(),
30253
+ flash: boolean(),
30254
+ lightMode: NavigationLightModeSchema,
30255
+ lightLevel: number().min(40).max(100),
30256
+ /** Ms epoch when the slice was last updated. */
30257
+ lastChangedAt: number()
30258
+ });
30259
+ /**
30260
+ * Runtime-state slice owned by this cap (kernel-managed: validated, mirrored,
30261
+ * observable). Adds `lastFetchedAt` on top of the status shape per the
30262
+ * convention.
30263
+ */
30264
+ var NavigationRuntimeStateSchema = NavigationStatusSchema.extend({ lastFetchedAt: number() });
30265
+ var navigationCapability = {
30266
+ name: "navigation",
30267
+ scope: "device",
30268
+ deviceNative: true,
30269
+ mode: "singleton",
30270
+ deviceTypes: [DeviceType.Camera],
30271
+ deviceConfig: { ui: {
30272
+ kind: "widget",
30273
+ widgetId: "host/navigation-panel",
30274
+ tab: "navigation",
30275
+ topTab: true,
30276
+ label: "Navigation",
30277
+ order: 0
30278
+ } },
30279
+ methods: {
30280
+ /**
30281
+ * Momentary drive nudge (the robot moves). `protected` — mirrors
30282
+ * `ptz.continuousMove` so the Viewer navigation panel (and the PTZ-mimic
30283
+ * path) works for any authenticated user, not admin-only. The UI sends
30284
+ * these at ~1 Hz while a control is held; the provider forwards each one to
30285
+ * a single drive write WITHOUT debouncing.
30286
+ */
30287
+ move: method(NavigationMoveCommandSchema.extend({ deviceId: number() }), _void(), { kind: "mutation" }),
30288
+ /** Halt all motion immediately (zero drive vector). */
30289
+ stop: method(object({ deviceId: number() }), _void(), { kind: "mutation" }),
30290
+ /** Send the robot to a point on its live map. */
30291
+ goToPoint: method(NavigationPointSchema.extend({ deviceId: number() }), _void(), { kind: "mutation" }),
30292
+ /**
30293
+ * Enumerate the discrete controls THIS device supports (data-driven UI +
30294
+ * PTZ mimic). Camera-probed subset of {@link NAVIGATION_ACTION_CATALOG}.
30295
+ */
30296
+ listActions: method(object({ deviceId: number() }), array(NavigationActionEntrySchema)),
30297
+ /**
30298
+ * Run one discrete action (a `kind:'action'` dictionary entry). Invalid /
30299
+ * unsupported action ids are rejected by the provider.
30300
+ */
30301
+ runAction: method(object({
30302
+ deviceId: number(),
30303
+ actionId: NavigationActionIdSchema
30304
+ }), _void(), { kind: "mutation" }),
30305
+ /** Play a sound by its wire id (the `soundId` of a `kind:'sound'` entry). */
30306
+ playSound: method(object({
30307
+ deviceId: number(),
30308
+ soundId: number().int()
30309
+ }), _void(), { kind: "mutation" }),
30310
+ /**
30311
+ * Turn the on-camera fill light on / off (the `OpenFullLight` control —
30312
+ * works anytime, no active stream required).
30313
+ */
30314
+ setLightOn: method(object({
30315
+ deviceId: number(),
30316
+ on: boolean()
30317
+ }), _void(), { kind: "mutation" }),
30318
+ /**
30319
+ * Set the fill-light mode (auto vs manual). `manual` optionally carries the
30320
+ * initial `level`. The auto/manual + level control is a CAMERA-service
30321
+ * action that generally needs an active camera stream/monitor session — the
30322
+ * UI shows the manual level slider ONLY when `mode === 'manual'`.
30323
+ */
30324
+ setLightMode: method(object({
30325
+ deviceId: number(),
30326
+ mode: NavigationLightModeSchema,
30327
+ level: number().min(40).max(100).optional()
30328
+ }), _void(), { kind: "mutation" }),
30329
+ /** Set the MANUAL fill-light level (40..100). Implies `manual` mode. */
30330
+ setLightLevel: method(object({
30331
+ deviceId: number(),
30332
+ level: number().min(40).max(100)
30333
+ }), _void(), { kind: "mutation" }),
30334
+ /**
30335
+ * Per-device FEATURE-FLAG report for the general primitives — drives which
30336
+ * controls the UI shows (the per-entry flags for the dictionary come back on
30337
+ * `listActions`).
30338
+ */
30339
+ getFeatures: method(object({ deviceId: number() }), NavigationFeaturesSchema)
30340
+ },
30341
+ events: { onStatusChanged: { data: object({
30342
+ deviceId: number(),
30343
+ status: NavigationStatusSchema
30344
+ }) } },
30345
+ status: {
30346
+ schema: NavigationStatusSchema,
30347
+ kind: "push"
30348
+ },
30349
+ /**
30350
+ * Runtime-state slice mirrored by the kernel. The navigation panel watches it
30351
+ * for live mode / follow / flash changes.
30352
+ */
30353
+ runtimeState: NavigationRuntimeStateSchema,
30354
+ /**
30355
+ * Runtime-state durability: **session** — like `vacuum-control`, a restored
30356
+ * `mode: cleaning` / `following: true` is a robot that is not actually doing
30357
+ * that. The live handle re-publishes on connect.
30358
+ *
30359
+ * See `RuntimeStateDurability`. Enforced by
30360
+ * `scripts/check-runtime-state-durability.ts`.
30361
+ */
30362
+ durability: "session"
30363
+ };
30077
30364
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, method(object({ deviceId: number().int().nonnegative() }), object({ success: literal(true) }), {
30078
30365
  kind: "mutation",
30079
30366
  auth: "admin"
@@ -33349,6 +33636,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
33349
33636
  motionTrigger: motionTriggerCapability,
33350
33637
  motionZones: motionZonesCapability,
33351
33638
  nativeObjectDetection: nativeObjectDetectionCapability,
33639
+ navigation: navigationCapability,
33352
33640
  networkLink: networkLinkCapability,
33353
33641
  notifier: notifierCapability,
33354
33642
  numericSensor: numericSensorCapability,
@@ -37203,6 +37491,66 @@ Object.freeze({
37203
37491
  addonId: null,
37204
37492
  access: "create"
37205
37493
  },
37494
+ "navigation.getFeatures": {
37495
+ capName: "navigation",
37496
+ capScope: "device",
37497
+ addonId: null,
37498
+ access: "view"
37499
+ },
37500
+ "navigation.goToPoint": {
37501
+ capName: "navigation",
37502
+ capScope: "device",
37503
+ addonId: null,
37504
+ access: "create"
37505
+ },
37506
+ "navigation.listActions": {
37507
+ capName: "navigation",
37508
+ capScope: "device",
37509
+ addonId: null,
37510
+ access: "view"
37511
+ },
37512
+ "navigation.move": {
37513
+ capName: "navigation",
37514
+ capScope: "device",
37515
+ addonId: null,
37516
+ access: "create"
37517
+ },
37518
+ "navigation.playSound": {
37519
+ capName: "navigation",
37520
+ capScope: "device",
37521
+ addonId: null,
37522
+ access: "create"
37523
+ },
37524
+ "navigation.runAction": {
37525
+ capName: "navigation",
37526
+ capScope: "device",
37527
+ addonId: null,
37528
+ access: "create"
37529
+ },
37530
+ "navigation.setLightLevel": {
37531
+ capName: "navigation",
37532
+ capScope: "device",
37533
+ addonId: null,
37534
+ access: "create"
37535
+ },
37536
+ "navigation.setLightMode": {
37537
+ capName: "navigation",
37538
+ capScope: "device",
37539
+ addonId: null,
37540
+ access: "create"
37541
+ },
37542
+ "navigation.setLightOn": {
37543
+ capName: "navigation",
37544
+ capScope: "device",
37545
+ addonId: null,
37546
+ access: "create"
37547
+ },
37548
+ "navigation.stop": {
37549
+ capName: "navigation",
37550
+ capScope: "device",
37551
+ addonId: null,
37552
+ access: "create"
37553
+ },
37206
37554
  "networkAccess.getEndpoint": {
37207
37555
  capName: "network-access",
37208
37556
  capScope: "system",
@@ -41298,6 +41646,56 @@ Object.freeze({
41298
41646
  form: "single",
41299
41647
  optional: false
41300
41648
  }],
41649
+ "navigation.getFeatures": [{
41650
+ name: "deviceId",
41651
+ form: "single",
41652
+ optional: false
41653
+ }],
41654
+ "navigation.goToPoint": [{
41655
+ name: "deviceId",
41656
+ form: "single",
41657
+ optional: false
41658
+ }],
41659
+ "navigation.listActions": [{
41660
+ name: "deviceId",
41661
+ form: "single",
41662
+ optional: false
41663
+ }],
41664
+ "navigation.move": [{
41665
+ name: "deviceId",
41666
+ form: "single",
41667
+ optional: false
41668
+ }],
41669
+ "navigation.playSound": [{
41670
+ name: "deviceId",
41671
+ form: "single",
41672
+ optional: false
41673
+ }],
41674
+ "navigation.runAction": [{
41675
+ name: "deviceId",
41676
+ form: "single",
41677
+ optional: false
41678
+ }],
41679
+ "navigation.setLightLevel": [{
41680
+ name: "deviceId",
41681
+ form: "single",
41682
+ optional: false
41683
+ }],
41684
+ "navigation.setLightMode": [{
41685
+ name: "deviceId",
41686
+ form: "single",
41687
+ optional: false
41688
+ }],
41689
+ "navigation.setLightOn": [{
41690
+ name: "deviceId",
41691
+ form: "single",
41692
+ optional: false
41693
+ }],
41694
+ "navigation.stop": [{
41695
+ name: "deviceId",
41696
+ form: "single",
41697
+ optional: false
41698
+ }],
41301
41699
  "networkQuality.getDeviceStats": [{
41302
41700
  name: "deviceId",
41303
41701
  form: "single",
package/dist/addon.mjs CHANGED
@@ -6507,6 +6507,12 @@ Object.fromEntries([
6507
6507
  icon: "move",
6508
6508
  order: 40
6509
6509
  },
6510
+ {
6511
+ id: "navigation",
6512
+ label: "Navigation",
6513
+ icon: "compass",
6514
+ order: 41
6515
+ },
6510
6516
  {
6511
6517
  id: "consumables",
6512
6518
  label: "Consumables",
@@ -30073,6 +30079,287 @@ var ptzAutotrackCapability = {
30073
30079
  */
30074
30080
  durability: "session"
30075
30081
  };
30082
+ /**
30083
+ * `navigation` — a device-scoped capability that natively expresses the FULL
30084
+ * navigation / action surface of a robot that DRIVES ITSELF and carries an
30085
+ * on-board camera (the Dreame robot-vacuum camera is the first provider).
30086
+ *
30087
+ * Why a NEW cap rather than overloading `ptz`:
30088
+ * - `ptz` moves a *gimbal* on a fixed camera (pan/tilt/zoom of the lens). A
30089
+ * robot vacuum has no gimbal — the whole chassis drives, turns and spins.
30090
+ * The two are different physical models: PTZ is absolute-position + presets,
30091
+ * navigation is momentary drive nudges + discrete robot ACTIONS
30092
+ * (dock / spot-clean / follow-pet / go-to-point / …).
30093
+ * - This cap is the SOURCE OF TRUTH. Two consumers adapt from it rather than
30094
+ * the reverse:
30095
+ * 1. a native CamStack navigation panel (data-driven from `listActions`
30096
+ * / `getOptions`), and
30097
+ * 2. the PTZ surface — a thin adapter mimics `ptz` from `navigation` so a
30098
+ * robot camera shows up in the existing PTZ control path without every
30099
+ * PTZ provider learning about robots. The mapping lives in the adapter,
30100
+ * not here (see the addon design note):
30101
+ * ptz.continuousMove({pan,tilt}) → navigation.move({pan,tilt})
30102
+ * ptz.stop() → navigation.stop()
30103
+ * ptz.goHome() → navigation.runAction('goHome')
30104
+ * ptz.getPresets() → navigation.listActions() (id→preset)
30105
+ * ptz.goToPreset(id) → navigation.runAction(id)
30106
+ *
30107
+ * ## Continuous drive
30108
+ *
30109
+ * `move` is MOMENTARY. Fluid navigation comes from the UI (or the PTZ adapter)
30110
+ * sending `move({pan,tilt})` REPEATEDLY at ~1 Hz while a direction is held, and
30111
+ * one `stop()` on release — exactly like the robot app's remote-drive joystick.
30112
+ * The provider forwards EACH `move` to one drive write; it must NOT debounce or
30113
+ * coalesce them. The UI owns the cadence.
30114
+ *
30115
+ * ## The action dictionary
30116
+ *
30117
+ * The discrete controls (dock / locate / spot-clean / follow / flash / sounds)
30118
+ * are a DATA-DRIVEN dictionary the cap exposes via `listActions()`. Each entry
30119
+ * carries `{ id, kind, label, icon }` (plus `soundId` for sound entries) so the
30120
+ * native panel AND the PTZ mimic render buttons WITHOUT hardcoding a
30121
+ * vendor-specific list. `kind: 'action'` entries are triggered with
30122
+ * `runAction({ actionId })`; `kind: 'sound'` entries with `playSound({ soundId })`
30123
+ * (the entry carries the `soundId` to pass). The general primitives — `move`,
30124
+ * `stop`, `goToPoint` — stay as first-class methods, not dictionary entries.
30125
+ *
30126
+ * NOTE (provider wiring): the first provider (Dreame) drives this with the RAW
30127
+ * `callAction(siid,aiid,in)` / `setProperty({siid,piid,value})` MIoT primitives
30128
+ * that the currently-published `@apocaliss92/nodedreame` already exposes on
30129
+ * every device handle. A future nodedreame publish adds a typed
30130
+ * `DreameCameraController` (drive / playPetSound / spotClean / findPet / …); the
30131
+ * provider can then swap the raw calls for the typed methods with no change to
30132
+ * THIS contract.
30133
+ */
30134
+ /**
30135
+ * A momentary drive nudge. The robot MOVES (no gimbal) for as long as the caller
30136
+ * keeps sending nudges (~1 Hz); an explicit `stop` (or letting the nudges lapse)
30137
+ * halts it.
30138
+ *
30139
+ * - `pan` — turn: negative = left, positive = right, 0 = straight.
30140
+ * - `tilt` — throttle: positive = forward, negative = spin / turn-around.
30141
+ * - `speed` — optional intensity hint [0, 1]; the provider may scale the drive
30142
+ * vector by it (drivers without proportional drive ignore it).
30143
+ *
30144
+ * `pan` / `tilt` are normalized [-1, 1]. Both optional so a caller can nudge one
30145
+ * axis alone; an all-undefined nudge is a no-op.
30146
+ */
30147
+ var NavigationMoveCommandSchema = object({
30148
+ pan: number().min(-1).max(1).optional(),
30149
+ tilt: number().min(-1).max(1).optional(),
30150
+ speed: number().min(0).max(1).optional()
30151
+ });
30152
+ /**
30153
+ * The enumerated discrete actions a navigation-capable robot can perform via
30154
+ * `runAction`. This is the CLOSED vocabulary; a given device advertises the
30155
+ * subset it supports through `listActions`. Sounds are NOT here — they go through
30156
+ * `playSound` (see the `sound` dictionary entries).
30157
+ */
30158
+ var NavigationActionIdSchema = _enum([
30159
+ "goHome",
30160
+ "locate",
30161
+ "spotClean",
30162
+ "findPet",
30163
+ "personFollow",
30164
+ "stop",
30165
+ "startClean",
30166
+ "pauseClean",
30167
+ "dockWash",
30168
+ "autoEmpty",
30169
+ "flashOn",
30170
+ "flashOff"
30171
+ ]);
30172
+ /** Whether a dictionary entry is a `runAction` action or a `playSound` sound. */
30173
+ var NavigationEntryKindSchema = _enum(["action", "sound"]);
30174
+ /**
30175
+ * One entry in the navigation action dictionary — the DATA-DRIVEN unit both the
30176
+ * native panel and the PTZ mimic render as a button.
30177
+ *
30178
+ * - `id` — stable id. For `kind:'action'` it is a {@link NavigationActionId}
30179
+ * (pass to `runAction`); for `kind:'sound'` it is a namespaced id
30180
+ * (`sound:meow`) whose `soundId` is passed to `playSound`.
30181
+ * - `icon` — icon HINT (lucide-style name; the UI maps it to its own set).
30182
+ * - `label` — operator-facing English label.
30183
+ * - `soundId` — wire sound id, present only on `kind:'sound'` entries.
30184
+ * - `enabled` — per-device FEATURE FLAG. `listActions` reports it so the UI /
30185
+ * PTZ render ONLY enabled entries. Data-driven: the provider
30186
+ * flips it from config, never by editing code.
30187
+ */
30188
+ var NavigationActionEntrySchema = object({
30189
+ id: string(),
30190
+ kind: NavigationEntryKindSchema,
30191
+ label: string(),
30192
+ icon: string(),
30193
+ /** Present only on `kind:'sound'` entries — the id to pass to `playSound`. */
30194
+ soundId: number().int().optional(),
30195
+ /** Per-device feature flag — render this entry only when true. */
30196
+ enabled: boolean()
30197
+ });
30198
+ /** Coordinates for `goToPoint` — a point on the robot's live map. */
30199
+ var NavigationPointSchema = object({
30200
+ x: number(),
30201
+ y: number()
30202
+ });
30203
+ /**
30204
+ * Per-device FEATURE-FLAG report for the general (non-dictionary) primitives.
30205
+ * The cap reports which are enabled so the UI / PTZ render only the controls
30206
+ * that are turned on for THIS device. Data-driven: the provider derives these
30207
+ * from config + probe, never hardcoded in the UI. The per-DICTIONARY-entry flags
30208
+ * live on {@link NavigationActionEntrySchema.enabled}; these gate the primitives
30209
+ * that are not dictionary entries.
30210
+ *
30211
+ * - `move` / `stop` — the momentary drive joystick.
30212
+ * - `goToPoint` — send-to-map-coordinate. Ships OFF on Dreame until the
30213
+ * map-coordinate plumbing is wired.
30214
+ * - `runAction` — the discrete action buttons (dictionary `kind:'action'`).
30215
+ * - `playSound` — the sound buttons (dictionary `kind:'sound'`).
30216
+ * - `light` — the on/off fill-light toggle (works anytime).
30217
+ * - `lightMode` — the auto/manual selector + manual level slider (a
30218
+ * camera-service control; needs an active stream).
30219
+ */
30220
+ var NavigationFeaturesSchema = object({
30221
+ move: boolean(),
30222
+ stop: boolean(),
30223
+ goToPoint: boolean(),
30224
+ runAction: boolean(),
30225
+ playSound: boolean(),
30226
+ light: boolean(),
30227
+ lightMode: boolean()
30228
+ });
30229
+ /** Light mode: `auto` lets the camera choose brightness; `manual` uses `level`. */
30230
+ var NavigationLightModeSchema = _enum(["auto", "manual"]);
30231
+ /**
30232
+ * Live navigation state so the UI can reflect what the robot is doing:
30233
+ * - `mode` — coarse activity (idle / cleaning / following / …).
30234
+ * - `following` — person/pet follow is currently armed.
30235
+ * - `flash` — the on-camera fill light is on.
30236
+ * - `lightMode` — auto vs manual fill-light mode.
30237
+ * - `lightLevel` — manual fill-light level (40..100); meaningful when
30238
+ * `lightMode === 'manual'`.
30239
+ */
30240
+ var NavigationStatusSchema = object({
30241
+ mode: _enum([
30242
+ "idle",
30243
+ "cleaning",
30244
+ "spot",
30245
+ "following",
30246
+ "goto",
30247
+ "returning",
30248
+ "paused",
30249
+ "unknown"
30250
+ ]),
30251
+ following: boolean(),
30252
+ flash: boolean(),
30253
+ lightMode: NavigationLightModeSchema,
30254
+ lightLevel: number().min(40).max(100),
30255
+ /** Ms epoch when the slice was last updated. */
30256
+ lastChangedAt: number()
30257
+ });
30258
+ /**
30259
+ * Runtime-state slice owned by this cap (kernel-managed: validated, mirrored,
30260
+ * observable). Adds `lastFetchedAt` on top of the status shape per the
30261
+ * convention.
30262
+ */
30263
+ var NavigationRuntimeStateSchema = NavigationStatusSchema.extend({ lastFetchedAt: number() });
30264
+ var navigationCapability = {
30265
+ name: "navigation",
30266
+ scope: "device",
30267
+ deviceNative: true,
30268
+ mode: "singleton",
30269
+ deviceTypes: [DeviceType.Camera],
30270
+ deviceConfig: { ui: {
30271
+ kind: "widget",
30272
+ widgetId: "host/navigation-panel",
30273
+ tab: "navigation",
30274
+ topTab: true,
30275
+ label: "Navigation",
30276
+ order: 0
30277
+ } },
30278
+ methods: {
30279
+ /**
30280
+ * Momentary drive nudge (the robot moves). `protected` — mirrors
30281
+ * `ptz.continuousMove` so the Viewer navigation panel (and the PTZ-mimic
30282
+ * path) works for any authenticated user, not admin-only. The UI sends
30283
+ * these at ~1 Hz while a control is held; the provider forwards each one to
30284
+ * a single drive write WITHOUT debouncing.
30285
+ */
30286
+ move: method(NavigationMoveCommandSchema.extend({ deviceId: number() }), _void(), { kind: "mutation" }),
30287
+ /** Halt all motion immediately (zero drive vector). */
30288
+ stop: method(object({ deviceId: number() }), _void(), { kind: "mutation" }),
30289
+ /** Send the robot to a point on its live map. */
30290
+ goToPoint: method(NavigationPointSchema.extend({ deviceId: number() }), _void(), { kind: "mutation" }),
30291
+ /**
30292
+ * Enumerate the discrete controls THIS device supports (data-driven UI +
30293
+ * PTZ mimic). Camera-probed subset of {@link NAVIGATION_ACTION_CATALOG}.
30294
+ */
30295
+ listActions: method(object({ deviceId: number() }), array(NavigationActionEntrySchema)),
30296
+ /**
30297
+ * Run one discrete action (a `kind:'action'` dictionary entry). Invalid /
30298
+ * unsupported action ids are rejected by the provider.
30299
+ */
30300
+ runAction: method(object({
30301
+ deviceId: number(),
30302
+ actionId: NavigationActionIdSchema
30303
+ }), _void(), { kind: "mutation" }),
30304
+ /** Play a sound by its wire id (the `soundId` of a `kind:'sound'` entry). */
30305
+ playSound: method(object({
30306
+ deviceId: number(),
30307
+ soundId: number().int()
30308
+ }), _void(), { kind: "mutation" }),
30309
+ /**
30310
+ * Turn the on-camera fill light on / off (the `OpenFullLight` control —
30311
+ * works anytime, no active stream required).
30312
+ */
30313
+ setLightOn: method(object({
30314
+ deviceId: number(),
30315
+ on: boolean()
30316
+ }), _void(), { kind: "mutation" }),
30317
+ /**
30318
+ * Set the fill-light mode (auto vs manual). `manual` optionally carries the
30319
+ * initial `level`. The auto/manual + level control is a CAMERA-service
30320
+ * action that generally needs an active camera stream/monitor session — the
30321
+ * UI shows the manual level slider ONLY when `mode === 'manual'`.
30322
+ */
30323
+ setLightMode: method(object({
30324
+ deviceId: number(),
30325
+ mode: NavigationLightModeSchema,
30326
+ level: number().min(40).max(100).optional()
30327
+ }), _void(), { kind: "mutation" }),
30328
+ /** Set the MANUAL fill-light level (40..100). Implies `manual` mode. */
30329
+ setLightLevel: method(object({
30330
+ deviceId: number(),
30331
+ level: number().min(40).max(100)
30332
+ }), _void(), { kind: "mutation" }),
30333
+ /**
30334
+ * Per-device FEATURE-FLAG report for the general primitives — drives which
30335
+ * controls the UI shows (the per-entry flags for the dictionary come back on
30336
+ * `listActions`).
30337
+ */
30338
+ getFeatures: method(object({ deviceId: number() }), NavigationFeaturesSchema)
30339
+ },
30340
+ events: { onStatusChanged: { data: object({
30341
+ deviceId: number(),
30342
+ status: NavigationStatusSchema
30343
+ }) } },
30344
+ status: {
30345
+ schema: NavigationStatusSchema,
30346
+ kind: "push"
30347
+ },
30348
+ /**
30349
+ * Runtime-state slice mirrored by the kernel. The navigation panel watches it
30350
+ * for live mode / follow / flash changes.
30351
+ */
30352
+ runtimeState: NavigationRuntimeStateSchema,
30353
+ /**
30354
+ * Runtime-state durability: **session** — like `vacuum-control`, a restored
30355
+ * `mode: cleaning` / `following: true` is a robot that is not actually doing
30356
+ * that. The live handle re-publishes on connect.
30357
+ *
30358
+ * See `RuntimeStateDurability`. Enforced by
30359
+ * `scripts/check-runtime-state-durability.ts`.
30360
+ */
30361
+ durability: "session"
30362
+ };
30076
30363
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, method(object({ deviceId: number().int().nonnegative() }), object({ success: literal(true) }), {
30077
30364
  kind: "mutation",
30078
30365
  auth: "admin"
@@ -33348,6 +33635,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
33348
33635
  motionTrigger: motionTriggerCapability,
33349
33636
  motionZones: motionZonesCapability,
33350
33637
  nativeObjectDetection: nativeObjectDetectionCapability,
33638
+ navigation: navigationCapability,
33351
33639
  networkLink: networkLinkCapability,
33352
33640
  notifier: notifierCapability,
33353
33641
  numericSensor: numericSensorCapability,
@@ -37202,6 +37490,66 @@ Object.freeze({
37202
37490
  addonId: null,
37203
37491
  access: "create"
37204
37492
  },
37493
+ "navigation.getFeatures": {
37494
+ capName: "navigation",
37495
+ capScope: "device",
37496
+ addonId: null,
37497
+ access: "view"
37498
+ },
37499
+ "navigation.goToPoint": {
37500
+ capName: "navigation",
37501
+ capScope: "device",
37502
+ addonId: null,
37503
+ access: "create"
37504
+ },
37505
+ "navigation.listActions": {
37506
+ capName: "navigation",
37507
+ capScope: "device",
37508
+ addonId: null,
37509
+ access: "view"
37510
+ },
37511
+ "navigation.move": {
37512
+ capName: "navigation",
37513
+ capScope: "device",
37514
+ addonId: null,
37515
+ access: "create"
37516
+ },
37517
+ "navigation.playSound": {
37518
+ capName: "navigation",
37519
+ capScope: "device",
37520
+ addonId: null,
37521
+ access: "create"
37522
+ },
37523
+ "navigation.runAction": {
37524
+ capName: "navigation",
37525
+ capScope: "device",
37526
+ addonId: null,
37527
+ access: "create"
37528
+ },
37529
+ "navigation.setLightLevel": {
37530
+ capName: "navigation",
37531
+ capScope: "device",
37532
+ addonId: null,
37533
+ access: "create"
37534
+ },
37535
+ "navigation.setLightMode": {
37536
+ capName: "navigation",
37537
+ capScope: "device",
37538
+ addonId: null,
37539
+ access: "create"
37540
+ },
37541
+ "navigation.setLightOn": {
37542
+ capName: "navigation",
37543
+ capScope: "device",
37544
+ addonId: null,
37545
+ access: "create"
37546
+ },
37547
+ "navigation.stop": {
37548
+ capName: "navigation",
37549
+ capScope: "device",
37550
+ addonId: null,
37551
+ access: "create"
37552
+ },
37205
37553
  "networkAccess.getEndpoint": {
37206
37554
  capName: "network-access",
37207
37555
  capScope: "system",
@@ -41297,6 +41645,56 @@ Object.freeze({
41297
41645
  form: "single",
41298
41646
  optional: false
41299
41647
  }],
41648
+ "navigation.getFeatures": [{
41649
+ name: "deviceId",
41650
+ form: "single",
41651
+ optional: false
41652
+ }],
41653
+ "navigation.goToPoint": [{
41654
+ name: "deviceId",
41655
+ form: "single",
41656
+ optional: false
41657
+ }],
41658
+ "navigation.listActions": [{
41659
+ name: "deviceId",
41660
+ form: "single",
41661
+ optional: false
41662
+ }],
41663
+ "navigation.move": [{
41664
+ name: "deviceId",
41665
+ form: "single",
41666
+ optional: false
41667
+ }],
41668
+ "navigation.playSound": [{
41669
+ name: "deviceId",
41670
+ form: "single",
41671
+ optional: false
41672
+ }],
41673
+ "navigation.runAction": [{
41674
+ name: "deviceId",
41675
+ form: "single",
41676
+ optional: false
41677
+ }],
41678
+ "navigation.setLightLevel": [{
41679
+ name: "deviceId",
41680
+ form: "single",
41681
+ optional: false
41682
+ }],
41683
+ "navigation.setLightMode": [{
41684
+ name: "deviceId",
41685
+ form: "single",
41686
+ optional: false
41687
+ }],
41688
+ "navigation.setLightOn": [{
41689
+ name: "deviceId",
41690
+ form: "single",
41691
+ optional: false
41692
+ }],
41693
+ "navigation.stop": [{
41694
+ name: "deviceId",
41695
+ form: "single",
41696
+ optional: false
41697
+ }],
41300
41698
  "networkQuality.getDeviceStats": [{
41301
41699
  name: "deviceId",
41302
41700
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-petkit",
3
- "version": "0.2.70",
3
+ "version": "0.2.72",
4
4
  "description": "PetKit smart-feeder device-provider addon for CamStack — wraps the @apocaliss92/nodepetkit PetKit cloud client",
5
5
  "keywords": [
6
6
  "camstack",