@camstack/addon-provider-tuya 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
@@ -6226,6 +6226,12 @@ Object.fromEntries([
6226
6226
  icon: "move",
6227
6227
  order: 40
6228
6228
  },
6229
+ {
6230
+ id: "navigation",
6231
+ label: "Navigation",
6232
+ icon: "compass",
6233
+ order: 41
6234
+ },
6229
6235
  {
6230
6236
  id: "consumables",
6231
6237
  label: "Consumables",
@@ -29792,6 +29798,287 @@ var ptzAutotrackCapability = {
29792
29798
  */
29793
29799
  durability: "session"
29794
29800
  };
29801
+ /**
29802
+ * `navigation` — a device-scoped capability that natively expresses the FULL
29803
+ * navigation / action surface of a robot that DRIVES ITSELF and carries an
29804
+ * on-board camera (the Dreame robot-vacuum camera is the first provider).
29805
+ *
29806
+ * Why a NEW cap rather than overloading `ptz`:
29807
+ * - `ptz` moves a *gimbal* on a fixed camera (pan/tilt/zoom of the lens). A
29808
+ * robot vacuum has no gimbal — the whole chassis drives, turns and spins.
29809
+ * The two are different physical models: PTZ is absolute-position + presets,
29810
+ * navigation is momentary drive nudges + discrete robot ACTIONS
29811
+ * (dock / spot-clean / follow-pet / go-to-point / …).
29812
+ * - This cap is the SOURCE OF TRUTH. Two consumers adapt from it rather than
29813
+ * the reverse:
29814
+ * 1. a native CamStack navigation panel (data-driven from `listActions`
29815
+ * / `getOptions`), and
29816
+ * 2. the PTZ surface — a thin adapter mimics `ptz` from `navigation` so a
29817
+ * robot camera shows up in the existing PTZ control path without every
29818
+ * PTZ provider learning about robots. The mapping lives in the adapter,
29819
+ * not here (see the addon design note):
29820
+ * ptz.continuousMove({pan,tilt}) → navigation.move({pan,tilt})
29821
+ * ptz.stop() → navigation.stop()
29822
+ * ptz.goHome() → navigation.runAction('goHome')
29823
+ * ptz.getPresets() → navigation.listActions() (id→preset)
29824
+ * ptz.goToPreset(id) → navigation.runAction(id)
29825
+ *
29826
+ * ## Continuous drive
29827
+ *
29828
+ * `move` is MOMENTARY. Fluid navigation comes from the UI (or the PTZ adapter)
29829
+ * sending `move({pan,tilt})` REPEATEDLY at ~1 Hz while a direction is held, and
29830
+ * one `stop()` on release — exactly like the robot app's remote-drive joystick.
29831
+ * The provider forwards EACH `move` to one drive write; it must NOT debounce or
29832
+ * coalesce them. The UI owns the cadence.
29833
+ *
29834
+ * ## The action dictionary
29835
+ *
29836
+ * The discrete controls (dock / locate / spot-clean / follow / flash / sounds)
29837
+ * are a DATA-DRIVEN dictionary the cap exposes via `listActions()`. Each entry
29838
+ * carries `{ id, kind, label, icon }` (plus `soundId` for sound entries) so the
29839
+ * native panel AND the PTZ mimic render buttons WITHOUT hardcoding a
29840
+ * vendor-specific list. `kind: 'action'` entries are triggered with
29841
+ * `runAction({ actionId })`; `kind: 'sound'` entries with `playSound({ soundId })`
29842
+ * (the entry carries the `soundId` to pass). The general primitives — `move`,
29843
+ * `stop`, `goToPoint` — stay as first-class methods, not dictionary entries.
29844
+ *
29845
+ * NOTE (provider wiring): the first provider (Dreame) drives this with the RAW
29846
+ * `callAction(siid,aiid,in)` / `setProperty({siid,piid,value})` MIoT primitives
29847
+ * that the currently-published `@apocaliss92/nodedreame` already exposes on
29848
+ * every device handle. A future nodedreame publish adds a typed
29849
+ * `DreameCameraController` (drive / playPetSound / spotClean / findPet / …); the
29850
+ * provider can then swap the raw calls for the typed methods with no change to
29851
+ * THIS contract.
29852
+ */
29853
+ /**
29854
+ * A momentary drive nudge. The robot MOVES (no gimbal) for as long as the caller
29855
+ * keeps sending nudges (~1 Hz); an explicit `stop` (or letting the nudges lapse)
29856
+ * halts it.
29857
+ *
29858
+ * - `pan` — turn: negative = left, positive = right, 0 = straight.
29859
+ * - `tilt` — throttle: positive = forward, negative = spin / turn-around.
29860
+ * - `speed` — optional intensity hint [0, 1]; the provider may scale the drive
29861
+ * vector by it (drivers without proportional drive ignore it).
29862
+ *
29863
+ * `pan` / `tilt` are normalized [-1, 1]. Both optional so a caller can nudge one
29864
+ * axis alone; an all-undefined nudge is a no-op.
29865
+ */
29866
+ var NavigationMoveCommandSchema = object({
29867
+ pan: number().min(-1).max(1).optional(),
29868
+ tilt: number().min(-1).max(1).optional(),
29869
+ speed: number().min(0).max(1).optional()
29870
+ });
29871
+ /**
29872
+ * The enumerated discrete actions a navigation-capable robot can perform via
29873
+ * `runAction`. This is the CLOSED vocabulary; a given device advertises the
29874
+ * subset it supports through `listActions`. Sounds are NOT here — they go through
29875
+ * `playSound` (see the `sound` dictionary entries).
29876
+ */
29877
+ var NavigationActionIdSchema = _enum([
29878
+ "goHome",
29879
+ "locate",
29880
+ "spotClean",
29881
+ "findPet",
29882
+ "personFollow",
29883
+ "stop",
29884
+ "startClean",
29885
+ "pauseClean",
29886
+ "dockWash",
29887
+ "autoEmpty",
29888
+ "flashOn",
29889
+ "flashOff"
29890
+ ]);
29891
+ /** Whether a dictionary entry is a `runAction` action or a `playSound` sound. */
29892
+ var NavigationEntryKindSchema = _enum(["action", "sound"]);
29893
+ /**
29894
+ * One entry in the navigation action dictionary — the DATA-DRIVEN unit both the
29895
+ * native panel and the PTZ mimic render as a button.
29896
+ *
29897
+ * - `id` — stable id. For `kind:'action'` it is a {@link NavigationActionId}
29898
+ * (pass to `runAction`); for `kind:'sound'` it is a namespaced id
29899
+ * (`sound:meow`) whose `soundId` is passed to `playSound`.
29900
+ * - `icon` — icon HINT (lucide-style name; the UI maps it to its own set).
29901
+ * - `label` — operator-facing English label.
29902
+ * - `soundId` — wire sound id, present only on `kind:'sound'` entries.
29903
+ * - `enabled` — per-device FEATURE FLAG. `listActions` reports it so the UI /
29904
+ * PTZ render ONLY enabled entries. Data-driven: the provider
29905
+ * flips it from config, never by editing code.
29906
+ */
29907
+ var NavigationActionEntrySchema = object({
29908
+ id: string(),
29909
+ kind: NavigationEntryKindSchema,
29910
+ label: string(),
29911
+ icon: string(),
29912
+ /** Present only on `kind:'sound'` entries — the id to pass to `playSound`. */
29913
+ soundId: number().int().optional(),
29914
+ /** Per-device feature flag — render this entry only when true. */
29915
+ enabled: boolean()
29916
+ });
29917
+ /** Coordinates for `goToPoint` — a point on the robot's live map. */
29918
+ var NavigationPointSchema = object({
29919
+ x: number(),
29920
+ y: number()
29921
+ });
29922
+ /**
29923
+ * Per-device FEATURE-FLAG report for the general (non-dictionary) primitives.
29924
+ * The cap reports which are enabled so the UI / PTZ render only the controls
29925
+ * that are turned on for THIS device. Data-driven: the provider derives these
29926
+ * from config + probe, never hardcoded in the UI. The per-DICTIONARY-entry flags
29927
+ * live on {@link NavigationActionEntrySchema.enabled}; these gate the primitives
29928
+ * that are not dictionary entries.
29929
+ *
29930
+ * - `move` / `stop` — the momentary drive joystick.
29931
+ * - `goToPoint` — send-to-map-coordinate. Ships OFF on Dreame until the
29932
+ * map-coordinate plumbing is wired.
29933
+ * - `runAction` — the discrete action buttons (dictionary `kind:'action'`).
29934
+ * - `playSound` — the sound buttons (dictionary `kind:'sound'`).
29935
+ * - `light` — the on/off fill-light toggle (works anytime).
29936
+ * - `lightMode` — the auto/manual selector + manual level slider (a
29937
+ * camera-service control; needs an active stream).
29938
+ */
29939
+ var NavigationFeaturesSchema = object({
29940
+ move: boolean(),
29941
+ stop: boolean(),
29942
+ goToPoint: boolean(),
29943
+ runAction: boolean(),
29944
+ playSound: boolean(),
29945
+ light: boolean(),
29946
+ lightMode: boolean()
29947
+ });
29948
+ /** Light mode: `auto` lets the camera choose brightness; `manual` uses `level`. */
29949
+ var NavigationLightModeSchema = _enum(["auto", "manual"]);
29950
+ /**
29951
+ * Live navigation state so the UI can reflect what the robot is doing:
29952
+ * - `mode` — coarse activity (idle / cleaning / following / …).
29953
+ * - `following` — person/pet follow is currently armed.
29954
+ * - `flash` — the on-camera fill light is on.
29955
+ * - `lightMode` — auto vs manual fill-light mode.
29956
+ * - `lightLevel` — manual fill-light level (40..100); meaningful when
29957
+ * `lightMode === 'manual'`.
29958
+ */
29959
+ var NavigationStatusSchema = object({
29960
+ mode: _enum([
29961
+ "idle",
29962
+ "cleaning",
29963
+ "spot",
29964
+ "following",
29965
+ "goto",
29966
+ "returning",
29967
+ "paused",
29968
+ "unknown"
29969
+ ]),
29970
+ following: boolean(),
29971
+ flash: boolean(),
29972
+ lightMode: NavigationLightModeSchema,
29973
+ lightLevel: number().min(40).max(100),
29974
+ /** Ms epoch when the slice was last updated. */
29975
+ lastChangedAt: number()
29976
+ });
29977
+ /**
29978
+ * Runtime-state slice owned by this cap (kernel-managed: validated, mirrored,
29979
+ * observable). Adds `lastFetchedAt` on top of the status shape per the
29980
+ * convention.
29981
+ */
29982
+ var NavigationRuntimeStateSchema = NavigationStatusSchema.extend({ lastFetchedAt: number() });
29983
+ var navigationCapability = {
29984
+ name: "navigation",
29985
+ scope: "device",
29986
+ deviceNative: true,
29987
+ mode: "singleton",
29988
+ deviceTypes: [DeviceType.Camera],
29989
+ deviceConfig: { ui: {
29990
+ kind: "widget",
29991
+ widgetId: "host/navigation-panel",
29992
+ tab: "navigation",
29993
+ topTab: true,
29994
+ label: "Navigation",
29995
+ order: 0
29996
+ } },
29997
+ methods: {
29998
+ /**
29999
+ * Momentary drive nudge (the robot moves). `protected` — mirrors
30000
+ * `ptz.continuousMove` so the Viewer navigation panel (and the PTZ-mimic
30001
+ * path) works for any authenticated user, not admin-only. The UI sends
30002
+ * these at ~1 Hz while a control is held; the provider forwards each one to
30003
+ * a single drive write WITHOUT debouncing.
30004
+ */
30005
+ move: method(NavigationMoveCommandSchema.extend({ deviceId: number() }), _void(), { kind: "mutation" }),
30006
+ /** Halt all motion immediately (zero drive vector). */
30007
+ stop: method(object({ deviceId: number() }), _void(), { kind: "mutation" }),
30008
+ /** Send the robot to a point on its live map. */
30009
+ goToPoint: method(NavigationPointSchema.extend({ deviceId: number() }), _void(), { kind: "mutation" }),
30010
+ /**
30011
+ * Enumerate the discrete controls THIS device supports (data-driven UI +
30012
+ * PTZ mimic). Camera-probed subset of {@link NAVIGATION_ACTION_CATALOG}.
30013
+ */
30014
+ listActions: method(object({ deviceId: number() }), array(NavigationActionEntrySchema)),
30015
+ /**
30016
+ * Run one discrete action (a `kind:'action'` dictionary entry). Invalid /
30017
+ * unsupported action ids are rejected by the provider.
30018
+ */
30019
+ runAction: method(object({
30020
+ deviceId: number(),
30021
+ actionId: NavigationActionIdSchema
30022
+ }), _void(), { kind: "mutation" }),
30023
+ /** Play a sound by its wire id (the `soundId` of a `kind:'sound'` entry). */
30024
+ playSound: method(object({
30025
+ deviceId: number(),
30026
+ soundId: number().int()
30027
+ }), _void(), { kind: "mutation" }),
30028
+ /**
30029
+ * Turn the on-camera fill light on / off (the `OpenFullLight` control —
30030
+ * works anytime, no active stream required).
30031
+ */
30032
+ setLightOn: method(object({
30033
+ deviceId: number(),
30034
+ on: boolean()
30035
+ }), _void(), { kind: "mutation" }),
30036
+ /**
30037
+ * Set the fill-light mode (auto vs manual). `manual` optionally carries the
30038
+ * initial `level`. The auto/manual + level control is a CAMERA-service
30039
+ * action that generally needs an active camera stream/monitor session — the
30040
+ * UI shows the manual level slider ONLY when `mode === 'manual'`.
30041
+ */
30042
+ setLightMode: method(object({
30043
+ deviceId: number(),
30044
+ mode: NavigationLightModeSchema,
30045
+ level: number().min(40).max(100).optional()
30046
+ }), _void(), { kind: "mutation" }),
30047
+ /** Set the MANUAL fill-light level (40..100). Implies `manual` mode. */
30048
+ setLightLevel: method(object({
30049
+ deviceId: number(),
30050
+ level: number().min(40).max(100)
30051
+ }), _void(), { kind: "mutation" }),
30052
+ /**
30053
+ * Per-device FEATURE-FLAG report for the general primitives — drives which
30054
+ * controls the UI shows (the per-entry flags for the dictionary come back on
30055
+ * `listActions`).
30056
+ */
30057
+ getFeatures: method(object({ deviceId: number() }), NavigationFeaturesSchema)
30058
+ },
30059
+ events: { onStatusChanged: { data: object({
30060
+ deviceId: number(),
30061
+ status: NavigationStatusSchema
30062
+ }) } },
30063
+ status: {
30064
+ schema: NavigationStatusSchema,
30065
+ kind: "push"
30066
+ },
30067
+ /**
30068
+ * Runtime-state slice mirrored by the kernel. The navigation panel watches it
30069
+ * for live mode / follow / flash changes.
30070
+ */
30071
+ runtimeState: NavigationRuntimeStateSchema,
30072
+ /**
30073
+ * Runtime-state durability: **session** — like `vacuum-control`, a restored
30074
+ * `mode: cleaning` / `following: true` is a robot that is not actually doing
30075
+ * that. The live handle re-publishes on connect.
30076
+ *
30077
+ * See `RuntimeStateDurability`. Enforced by
30078
+ * `scripts/check-runtime-state-durability.ts`.
30079
+ */
30080
+ durability: "session"
30081
+ };
29795
30082
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, method(object({ deviceId: number().int().nonnegative() }), object({ success: literal(true) }), {
29796
30083
  kind: "mutation",
29797
30084
  auth: "admin"
@@ -33067,6 +33354,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
33067
33354
  motionTrigger: motionTriggerCapability,
33068
33355
  motionZones: motionZonesCapability,
33069
33356
  nativeObjectDetection: nativeObjectDetectionCapability,
33357
+ navigation: navigationCapability,
33070
33358
  networkLink: networkLinkCapability,
33071
33359
  notifier: notifierCapability,
33072
33360
  numericSensor: numericSensorCapability,
@@ -36921,6 +37209,66 @@ Object.freeze({
36921
37209
  addonId: null,
36922
37210
  access: "create"
36923
37211
  },
37212
+ "navigation.getFeatures": {
37213
+ capName: "navigation",
37214
+ capScope: "device",
37215
+ addonId: null,
37216
+ access: "view"
37217
+ },
37218
+ "navigation.goToPoint": {
37219
+ capName: "navigation",
37220
+ capScope: "device",
37221
+ addonId: null,
37222
+ access: "create"
37223
+ },
37224
+ "navigation.listActions": {
37225
+ capName: "navigation",
37226
+ capScope: "device",
37227
+ addonId: null,
37228
+ access: "view"
37229
+ },
37230
+ "navigation.move": {
37231
+ capName: "navigation",
37232
+ capScope: "device",
37233
+ addonId: null,
37234
+ access: "create"
37235
+ },
37236
+ "navigation.playSound": {
37237
+ capName: "navigation",
37238
+ capScope: "device",
37239
+ addonId: null,
37240
+ access: "create"
37241
+ },
37242
+ "navigation.runAction": {
37243
+ capName: "navigation",
37244
+ capScope: "device",
37245
+ addonId: null,
37246
+ access: "create"
37247
+ },
37248
+ "navigation.setLightLevel": {
37249
+ capName: "navigation",
37250
+ capScope: "device",
37251
+ addonId: null,
37252
+ access: "create"
37253
+ },
37254
+ "navigation.setLightMode": {
37255
+ capName: "navigation",
37256
+ capScope: "device",
37257
+ addonId: null,
37258
+ access: "create"
37259
+ },
37260
+ "navigation.setLightOn": {
37261
+ capName: "navigation",
37262
+ capScope: "device",
37263
+ addonId: null,
37264
+ access: "create"
37265
+ },
37266
+ "navigation.stop": {
37267
+ capName: "navigation",
37268
+ capScope: "device",
37269
+ addonId: null,
37270
+ access: "create"
37271
+ },
36924
37272
  "networkAccess.getEndpoint": {
36925
37273
  capName: "network-access",
36926
37274
  capScope: "system",
@@ -41016,6 +41364,56 @@ Object.freeze({
41016
41364
  form: "single",
41017
41365
  optional: false
41018
41366
  }],
41367
+ "navigation.getFeatures": [{
41368
+ name: "deviceId",
41369
+ form: "single",
41370
+ optional: false
41371
+ }],
41372
+ "navigation.goToPoint": [{
41373
+ name: "deviceId",
41374
+ form: "single",
41375
+ optional: false
41376
+ }],
41377
+ "navigation.listActions": [{
41378
+ name: "deviceId",
41379
+ form: "single",
41380
+ optional: false
41381
+ }],
41382
+ "navigation.move": [{
41383
+ name: "deviceId",
41384
+ form: "single",
41385
+ optional: false
41386
+ }],
41387
+ "navigation.playSound": [{
41388
+ name: "deviceId",
41389
+ form: "single",
41390
+ optional: false
41391
+ }],
41392
+ "navigation.runAction": [{
41393
+ name: "deviceId",
41394
+ form: "single",
41395
+ optional: false
41396
+ }],
41397
+ "navigation.setLightLevel": [{
41398
+ name: "deviceId",
41399
+ form: "single",
41400
+ optional: false
41401
+ }],
41402
+ "navigation.setLightMode": [{
41403
+ name: "deviceId",
41404
+ form: "single",
41405
+ optional: false
41406
+ }],
41407
+ "navigation.setLightOn": [{
41408
+ name: "deviceId",
41409
+ form: "single",
41410
+ optional: false
41411
+ }],
41412
+ "navigation.stop": [{
41413
+ name: "deviceId",
41414
+ form: "single",
41415
+ optional: false
41416
+ }],
41019
41417
  "networkQuality.getDeviceStats": [{
41020
41418
  name: "deviceId",
41021
41419
  form: "single",
package/dist/addon.mjs CHANGED
@@ -6225,6 +6225,12 @@ Object.fromEntries([
6225
6225
  icon: "move",
6226
6226
  order: 40
6227
6227
  },
6228
+ {
6229
+ id: "navigation",
6230
+ label: "Navigation",
6231
+ icon: "compass",
6232
+ order: 41
6233
+ },
6228
6234
  {
6229
6235
  id: "consumables",
6230
6236
  label: "Consumables",
@@ -29791,6 +29797,287 @@ var ptzAutotrackCapability = {
29791
29797
  */
29792
29798
  durability: "session"
29793
29799
  };
29800
+ /**
29801
+ * `navigation` — a device-scoped capability that natively expresses the FULL
29802
+ * navigation / action surface of a robot that DRIVES ITSELF and carries an
29803
+ * on-board camera (the Dreame robot-vacuum camera is the first provider).
29804
+ *
29805
+ * Why a NEW cap rather than overloading `ptz`:
29806
+ * - `ptz` moves a *gimbal* on a fixed camera (pan/tilt/zoom of the lens). A
29807
+ * robot vacuum has no gimbal — the whole chassis drives, turns and spins.
29808
+ * The two are different physical models: PTZ is absolute-position + presets,
29809
+ * navigation is momentary drive nudges + discrete robot ACTIONS
29810
+ * (dock / spot-clean / follow-pet / go-to-point / …).
29811
+ * - This cap is the SOURCE OF TRUTH. Two consumers adapt from it rather than
29812
+ * the reverse:
29813
+ * 1. a native CamStack navigation panel (data-driven from `listActions`
29814
+ * / `getOptions`), and
29815
+ * 2. the PTZ surface — a thin adapter mimics `ptz` from `navigation` so a
29816
+ * robot camera shows up in the existing PTZ control path without every
29817
+ * PTZ provider learning about robots. The mapping lives in the adapter,
29818
+ * not here (see the addon design note):
29819
+ * ptz.continuousMove({pan,tilt}) → navigation.move({pan,tilt})
29820
+ * ptz.stop() → navigation.stop()
29821
+ * ptz.goHome() → navigation.runAction('goHome')
29822
+ * ptz.getPresets() → navigation.listActions() (id→preset)
29823
+ * ptz.goToPreset(id) → navigation.runAction(id)
29824
+ *
29825
+ * ## Continuous drive
29826
+ *
29827
+ * `move` is MOMENTARY. Fluid navigation comes from the UI (or the PTZ adapter)
29828
+ * sending `move({pan,tilt})` REPEATEDLY at ~1 Hz while a direction is held, and
29829
+ * one `stop()` on release — exactly like the robot app's remote-drive joystick.
29830
+ * The provider forwards EACH `move` to one drive write; it must NOT debounce or
29831
+ * coalesce them. The UI owns the cadence.
29832
+ *
29833
+ * ## The action dictionary
29834
+ *
29835
+ * The discrete controls (dock / locate / spot-clean / follow / flash / sounds)
29836
+ * are a DATA-DRIVEN dictionary the cap exposes via `listActions()`. Each entry
29837
+ * carries `{ id, kind, label, icon }` (plus `soundId` for sound entries) so the
29838
+ * native panel AND the PTZ mimic render buttons WITHOUT hardcoding a
29839
+ * vendor-specific list. `kind: 'action'` entries are triggered with
29840
+ * `runAction({ actionId })`; `kind: 'sound'` entries with `playSound({ soundId })`
29841
+ * (the entry carries the `soundId` to pass). The general primitives — `move`,
29842
+ * `stop`, `goToPoint` — stay as first-class methods, not dictionary entries.
29843
+ *
29844
+ * NOTE (provider wiring): the first provider (Dreame) drives this with the RAW
29845
+ * `callAction(siid,aiid,in)` / `setProperty({siid,piid,value})` MIoT primitives
29846
+ * that the currently-published `@apocaliss92/nodedreame` already exposes on
29847
+ * every device handle. A future nodedreame publish adds a typed
29848
+ * `DreameCameraController` (drive / playPetSound / spotClean / findPet / …); the
29849
+ * provider can then swap the raw calls for the typed methods with no change to
29850
+ * THIS contract.
29851
+ */
29852
+ /**
29853
+ * A momentary drive nudge. The robot MOVES (no gimbal) for as long as the caller
29854
+ * keeps sending nudges (~1 Hz); an explicit `stop` (or letting the nudges lapse)
29855
+ * halts it.
29856
+ *
29857
+ * - `pan` — turn: negative = left, positive = right, 0 = straight.
29858
+ * - `tilt` — throttle: positive = forward, negative = spin / turn-around.
29859
+ * - `speed` — optional intensity hint [0, 1]; the provider may scale the drive
29860
+ * vector by it (drivers without proportional drive ignore it).
29861
+ *
29862
+ * `pan` / `tilt` are normalized [-1, 1]. Both optional so a caller can nudge one
29863
+ * axis alone; an all-undefined nudge is a no-op.
29864
+ */
29865
+ var NavigationMoveCommandSchema = object({
29866
+ pan: number().min(-1).max(1).optional(),
29867
+ tilt: number().min(-1).max(1).optional(),
29868
+ speed: number().min(0).max(1).optional()
29869
+ });
29870
+ /**
29871
+ * The enumerated discrete actions a navigation-capable robot can perform via
29872
+ * `runAction`. This is the CLOSED vocabulary; a given device advertises the
29873
+ * subset it supports through `listActions`. Sounds are NOT here — they go through
29874
+ * `playSound` (see the `sound` dictionary entries).
29875
+ */
29876
+ var NavigationActionIdSchema = _enum([
29877
+ "goHome",
29878
+ "locate",
29879
+ "spotClean",
29880
+ "findPet",
29881
+ "personFollow",
29882
+ "stop",
29883
+ "startClean",
29884
+ "pauseClean",
29885
+ "dockWash",
29886
+ "autoEmpty",
29887
+ "flashOn",
29888
+ "flashOff"
29889
+ ]);
29890
+ /** Whether a dictionary entry is a `runAction` action or a `playSound` sound. */
29891
+ var NavigationEntryKindSchema = _enum(["action", "sound"]);
29892
+ /**
29893
+ * One entry in the navigation action dictionary — the DATA-DRIVEN unit both the
29894
+ * native panel and the PTZ mimic render as a button.
29895
+ *
29896
+ * - `id` — stable id. For `kind:'action'` it is a {@link NavigationActionId}
29897
+ * (pass to `runAction`); for `kind:'sound'` it is a namespaced id
29898
+ * (`sound:meow`) whose `soundId` is passed to `playSound`.
29899
+ * - `icon` — icon HINT (lucide-style name; the UI maps it to its own set).
29900
+ * - `label` — operator-facing English label.
29901
+ * - `soundId` — wire sound id, present only on `kind:'sound'` entries.
29902
+ * - `enabled` — per-device FEATURE FLAG. `listActions` reports it so the UI /
29903
+ * PTZ render ONLY enabled entries. Data-driven: the provider
29904
+ * flips it from config, never by editing code.
29905
+ */
29906
+ var NavigationActionEntrySchema = object({
29907
+ id: string(),
29908
+ kind: NavigationEntryKindSchema,
29909
+ label: string(),
29910
+ icon: string(),
29911
+ /** Present only on `kind:'sound'` entries — the id to pass to `playSound`. */
29912
+ soundId: number().int().optional(),
29913
+ /** Per-device feature flag — render this entry only when true. */
29914
+ enabled: boolean()
29915
+ });
29916
+ /** Coordinates for `goToPoint` — a point on the robot's live map. */
29917
+ var NavigationPointSchema = object({
29918
+ x: number(),
29919
+ y: number()
29920
+ });
29921
+ /**
29922
+ * Per-device FEATURE-FLAG report for the general (non-dictionary) primitives.
29923
+ * The cap reports which are enabled so the UI / PTZ render only the controls
29924
+ * that are turned on for THIS device. Data-driven: the provider derives these
29925
+ * from config + probe, never hardcoded in the UI. The per-DICTIONARY-entry flags
29926
+ * live on {@link NavigationActionEntrySchema.enabled}; these gate the primitives
29927
+ * that are not dictionary entries.
29928
+ *
29929
+ * - `move` / `stop` — the momentary drive joystick.
29930
+ * - `goToPoint` — send-to-map-coordinate. Ships OFF on Dreame until the
29931
+ * map-coordinate plumbing is wired.
29932
+ * - `runAction` — the discrete action buttons (dictionary `kind:'action'`).
29933
+ * - `playSound` — the sound buttons (dictionary `kind:'sound'`).
29934
+ * - `light` — the on/off fill-light toggle (works anytime).
29935
+ * - `lightMode` — the auto/manual selector + manual level slider (a
29936
+ * camera-service control; needs an active stream).
29937
+ */
29938
+ var NavigationFeaturesSchema = object({
29939
+ move: boolean(),
29940
+ stop: boolean(),
29941
+ goToPoint: boolean(),
29942
+ runAction: boolean(),
29943
+ playSound: boolean(),
29944
+ light: boolean(),
29945
+ lightMode: boolean()
29946
+ });
29947
+ /** Light mode: `auto` lets the camera choose brightness; `manual` uses `level`. */
29948
+ var NavigationLightModeSchema = _enum(["auto", "manual"]);
29949
+ /**
29950
+ * Live navigation state so the UI can reflect what the robot is doing:
29951
+ * - `mode` — coarse activity (idle / cleaning / following / …).
29952
+ * - `following` — person/pet follow is currently armed.
29953
+ * - `flash` — the on-camera fill light is on.
29954
+ * - `lightMode` — auto vs manual fill-light mode.
29955
+ * - `lightLevel` — manual fill-light level (40..100); meaningful when
29956
+ * `lightMode === 'manual'`.
29957
+ */
29958
+ var NavigationStatusSchema = object({
29959
+ mode: _enum([
29960
+ "idle",
29961
+ "cleaning",
29962
+ "spot",
29963
+ "following",
29964
+ "goto",
29965
+ "returning",
29966
+ "paused",
29967
+ "unknown"
29968
+ ]),
29969
+ following: boolean(),
29970
+ flash: boolean(),
29971
+ lightMode: NavigationLightModeSchema,
29972
+ lightLevel: number().min(40).max(100),
29973
+ /** Ms epoch when the slice was last updated. */
29974
+ lastChangedAt: number()
29975
+ });
29976
+ /**
29977
+ * Runtime-state slice owned by this cap (kernel-managed: validated, mirrored,
29978
+ * observable). Adds `lastFetchedAt` on top of the status shape per the
29979
+ * convention.
29980
+ */
29981
+ var NavigationRuntimeStateSchema = NavigationStatusSchema.extend({ lastFetchedAt: number() });
29982
+ var navigationCapability = {
29983
+ name: "navigation",
29984
+ scope: "device",
29985
+ deviceNative: true,
29986
+ mode: "singleton",
29987
+ deviceTypes: [DeviceType.Camera],
29988
+ deviceConfig: { ui: {
29989
+ kind: "widget",
29990
+ widgetId: "host/navigation-panel",
29991
+ tab: "navigation",
29992
+ topTab: true,
29993
+ label: "Navigation",
29994
+ order: 0
29995
+ } },
29996
+ methods: {
29997
+ /**
29998
+ * Momentary drive nudge (the robot moves). `protected` — mirrors
29999
+ * `ptz.continuousMove` so the Viewer navigation panel (and the PTZ-mimic
30000
+ * path) works for any authenticated user, not admin-only. The UI sends
30001
+ * these at ~1 Hz while a control is held; the provider forwards each one to
30002
+ * a single drive write WITHOUT debouncing.
30003
+ */
30004
+ move: method(NavigationMoveCommandSchema.extend({ deviceId: number() }), _void(), { kind: "mutation" }),
30005
+ /** Halt all motion immediately (zero drive vector). */
30006
+ stop: method(object({ deviceId: number() }), _void(), { kind: "mutation" }),
30007
+ /** Send the robot to a point on its live map. */
30008
+ goToPoint: method(NavigationPointSchema.extend({ deviceId: number() }), _void(), { kind: "mutation" }),
30009
+ /**
30010
+ * Enumerate the discrete controls THIS device supports (data-driven UI +
30011
+ * PTZ mimic). Camera-probed subset of {@link NAVIGATION_ACTION_CATALOG}.
30012
+ */
30013
+ listActions: method(object({ deviceId: number() }), array(NavigationActionEntrySchema)),
30014
+ /**
30015
+ * Run one discrete action (a `kind:'action'` dictionary entry). Invalid /
30016
+ * unsupported action ids are rejected by the provider.
30017
+ */
30018
+ runAction: method(object({
30019
+ deviceId: number(),
30020
+ actionId: NavigationActionIdSchema
30021
+ }), _void(), { kind: "mutation" }),
30022
+ /** Play a sound by its wire id (the `soundId` of a `kind:'sound'` entry). */
30023
+ playSound: method(object({
30024
+ deviceId: number(),
30025
+ soundId: number().int()
30026
+ }), _void(), { kind: "mutation" }),
30027
+ /**
30028
+ * Turn the on-camera fill light on / off (the `OpenFullLight` control —
30029
+ * works anytime, no active stream required).
30030
+ */
30031
+ setLightOn: method(object({
30032
+ deviceId: number(),
30033
+ on: boolean()
30034
+ }), _void(), { kind: "mutation" }),
30035
+ /**
30036
+ * Set the fill-light mode (auto vs manual). `manual` optionally carries the
30037
+ * initial `level`. The auto/manual + level control is a CAMERA-service
30038
+ * action that generally needs an active camera stream/monitor session — the
30039
+ * UI shows the manual level slider ONLY when `mode === 'manual'`.
30040
+ */
30041
+ setLightMode: method(object({
30042
+ deviceId: number(),
30043
+ mode: NavigationLightModeSchema,
30044
+ level: number().min(40).max(100).optional()
30045
+ }), _void(), { kind: "mutation" }),
30046
+ /** Set the MANUAL fill-light level (40..100). Implies `manual` mode. */
30047
+ setLightLevel: method(object({
30048
+ deviceId: number(),
30049
+ level: number().min(40).max(100)
30050
+ }), _void(), { kind: "mutation" }),
30051
+ /**
30052
+ * Per-device FEATURE-FLAG report for the general primitives — drives which
30053
+ * controls the UI shows (the per-entry flags for the dictionary come back on
30054
+ * `listActions`).
30055
+ */
30056
+ getFeatures: method(object({ deviceId: number() }), NavigationFeaturesSchema)
30057
+ },
30058
+ events: { onStatusChanged: { data: object({
30059
+ deviceId: number(),
30060
+ status: NavigationStatusSchema
30061
+ }) } },
30062
+ status: {
30063
+ schema: NavigationStatusSchema,
30064
+ kind: "push"
30065
+ },
30066
+ /**
30067
+ * Runtime-state slice mirrored by the kernel. The navigation panel watches it
30068
+ * for live mode / follow / flash changes.
30069
+ */
30070
+ runtimeState: NavigationRuntimeStateSchema,
30071
+ /**
30072
+ * Runtime-state durability: **session** — like `vacuum-control`, a restored
30073
+ * `mode: cleaning` / `following: true` is a robot that is not actually doing
30074
+ * that. The live handle re-publishes on connect.
30075
+ *
30076
+ * See `RuntimeStateDurability`. Enforced by
30077
+ * `scripts/check-runtime-state-durability.ts`.
30078
+ */
30079
+ durability: "session"
30080
+ };
29794
30081
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, method(object({ deviceId: number().int().nonnegative() }), object({ success: literal(true) }), {
29795
30082
  kind: "mutation",
29796
30083
  auth: "admin"
@@ -33066,6 +33353,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
33066
33353
  motionTrigger: motionTriggerCapability,
33067
33354
  motionZones: motionZonesCapability,
33068
33355
  nativeObjectDetection: nativeObjectDetectionCapability,
33356
+ navigation: navigationCapability,
33069
33357
  networkLink: networkLinkCapability,
33070
33358
  notifier: notifierCapability,
33071
33359
  numericSensor: numericSensorCapability,
@@ -36920,6 +37208,66 @@ Object.freeze({
36920
37208
  addonId: null,
36921
37209
  access: "create"
36922
37210
  },
37211
+ "navigation.getFeatures": {
37212
+ capName: "navigation",
37213
+ capScope: "device",
37214
+ addonId: null,
37215
+ access: "view"
37216
+ },
37217
+ "navigation.goToPoint": {
37218
+ capName: "navigation",
37219
+ capScope: "device",
37220
+ addonId: null,
37221
+ access: "create"
37222
+ },
37223
+ "navigation.listActions": {
37224
+ capName: "navigation",
37225
+ capScope: "device",
37226
+ addonId: null,
37227
+ access: "view"
37228
+ },
37229
+ "navigation.move": {
37230
+ capName: "navigation",
37231
+ capScope: "device",
37232
+ addonId: null,
37233
+ access: "create"
37234
+ },
37235
+ "navigation.playSound": {
37236
+ capName: "navigation",
37237
+ capScope: "device",
37238
+ addonId: null,
37239
+ access: "create"
37240
+ },
37241
+ "navigation.runAction": {
37242
+ capName: "navigation",
37243
+ capScope: "device",
37244
+ addonId: null,
37245
+ access: "create"
37246
+ },
37247
+ "navigation.setLightLevel": {
37248
+ capName: "navigation",
37249
+ capScope: "device",
37250
+ addonId: null,
37251
+ access: "create"
37252
+ },
37253
+ "navigation.setLightMode": {
37254
+ capName: "navigation",
37255
+ capScope: "device",
37256
+ addonId: null,
37257
+ access: "create"
37258
+ },
37259
+ "navigation.setLightOn": {
37260
+ capName: "navigation",
37261
+ capScope: "device",
37262
+ addonId: null,
37263
+ access: "create"
37264
+ },
37265
+ "navigation.stop": {
37266
+ capName: "navigation",
37267
+ capScope: "device",
37268
+ addonId: null,
37269
+ access: "create"
37270
+ },
36923
37271
  "networkAccess.getEndpoint": {
36924
37272
  capName: "network-access",
36925
37273
  capScope: "system",
@@ -41015,6 +41363,56 @@ Object.freeze({
41015
41363
  form: "single",
41016
41364
  optional: false
41017
41365
  }],
41366
+ "navigation.getFeatures": [{
41367
+ name: "deviceId",
41368
+ form: "single",
41369
+ optional: false
41370
+ }],
41371
+ "navigation.goToPoint": [{
41372
+ name: "deviceId",
41373
+ form: "single",
41374
+ optional: false
41375
+ }],
41376
+ "navigation.listActions": [{
41377
+ name: "deviceId",
41378
+ form: "single",
41379
+ optional: false
41380
+ }],
41381
+ "navigation.move": [{
41382
+ name: "deviceId",
41383
+ form: "single",
41384
+ optional: false
41385
+ }],
41386
+ "navigation.playSound": [{
41387
+ name: "deviceId",
41388
+ form: "single",
41389
+ optional: false
41390
+ }],
41391
+ "navigation.runAction": [{
41392
+ name: "deviceId",
41393
+ form: "single",
41394
+ optional: false
41395
+ }],
41396
+ "navigation.setLightLevel": [{
41397
+ name: "deviceId",
41398
+ form: "single",
41399
+ optional: false
41400
+ }],
41401
+ "navigation.setLightMode": [{
41402
+ name: "deviceId",
41403
+ form: "single",
41404
+ optional: false
41405
+ }],
41406
+ "navigation.setLightOn": [{
41407
+ name: "deviceId",
41408
+ form: "single",
41409
+ optional: false
41410
+ }],
41411
+ "navigation.stop": [{
41412
+ name: "deviceId",
41413
+ form: "single",
41414
+ optional: false
41415
+ }],
41018
41416
  "networkQuality.getDeviceStats": [{
41019
41417
  name: "deviceId",
41020
41418
  form: "single",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-tuya",
3
- "version": "0.2.70",
3
+ "version": "0.2.72",
4
4
  "description": "Tuya / Smart Life device-provider addon for CamStack — account-onboarded (Tuya IoT cloud fetch of device localKeys) + LOCAL DP control via the @apocaliss92/nodetuya encrypted-LAN client, exposing switch / water-heater-family kettle entities",
5
5
  "keywords": [
6
6
  "camstack",