@camstack/addon-post-analysis 1.2.270 → 1.2.272

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.
@@ -5359,7 +5359,7 @@ var ZodIssueCode = {
5359
5359
  var ZodFirstPartyTypeKind;
5360
5360
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5361
5361
  //#endregion
5362
- //#region ../types/dist/sleep-CiR4z7r9.mjs
5362
+ //#region ../types/dist/sleep-CopaBJss.mjs
5363
5363
  /**
5364
5364
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5365
5365
  * window to float samples (D455).
@@ -5559,6 +5559,12 @@ Object.fromEntries([
5559
5559
  icon: "circle-dot",
5560
5560
  order: 40
5561
5561
  },
5562
+ {
5563
+ id: "clips",
5564
+ label: "Clips",
5565
+ icon: "clapperboard",
5566
+ order: 41
5567
+ },
5562
5568
  {
5563
5569
  id: "engine",
5564
5570
  label: "Inference Engine",
@@ -14904,6 +14910,8 @@ method(object({
14904
14910
  deviceId: number(),
14905
14911
  capName: string(),
14906
14912
  wrapperAddonId: string(),
14913
+ /** The `ClipSource.source` id to toggle. Absent = all of the addon's. */
14914
+ sourceId: string().optional(),
14907
14915
  active: boolean()
14908
14916
  }), _void(), {
14909
14917
  kind: "mutation",
@@ -24936,10 +24944,9 @@ var ServerBootModeSchema = _enum([
24936
24944
  * - `idle` / `checking` / `staging` — steady / in-flight registry work.
24937
24945
  * - `pending-restart` — a version is staged and the node has NOT yet
24938
24946
  * restarted onto it (still running the OLD version).
24939
- * - `awaiting-confirmation` — the node HAS restarted onto the staged version
24940
- * (it is the active probation boot) and is waiting to confirm boot-health.
24941
- * Apply/rollback are refused in this state and the node must NOT be
24942
- * manually restarted, or the probation boot auto-rolls-back.
24947
+ * - `awaiting-confirmation` — RETIRED with the probation boot (single-copy
24948
+ * collapse, 2026-07-18). Kept in the enum so an older client still parses;
24949
+ * no provider emits it.
24943
24950
  */
24944
24951
  var ServerUpdateStateSchema = _enum([
24945
24952
  "idle",
@@ -25028,12 +25035,35 @@ var ServerRollbackInfoSchema = object({
25028
25035
  var ServerPackageStatusSchema = object({
25029
25036
  /** Root package name (`@camstack/server` on the hub). */
25030
25037
  packageName: string(),
25031
- /** Version of the code the running process ACTUALLY loaded. */
25038
+ /**
25039
+ * Version of the code THIS PROCESS loaded — captured once, at boot, from the
25040
+ * package.json of the copy the code was loaded from, and never re-read. A
25041
+ * copy replaced on disk under a live process must not move this number;
25042
+ * that divergence is what `installedVersion` is for. Before 2026-09-20 this
25043
+ * was a lazy disk read that would have followed such a swap silently.
25044
+ */
25032
25045
  runningVersion: string().nullable(),
25033
25046
  /** Node.js runtime version the node's process runs on (`process.versions.node`). */
25034
25047
  nodeRuntimeVersion: string().nullable(),
25035
25048
  /** Active data-dir root version; null when booted from seed/workspace. */
25036
25049
  activeVersion: string().nullable(),
25050
+ /**
25051
+ * Version of the root package in the data-root `current/` copy RIGHT NOW —
25052
+ * what the next boot would load if nothing is staged. Normally equal to
25053
+ * `runningVersion`; differs only when the copy on disk changed under the
25054
+ * live process. `null` when there is no data-root copy (baked seed or
25055
+ * workspace boot). Optional for version skew: an older provider omits it.
25056
+ */
25057
+ installedVersion: string().nullable().optional(),
25058
+ /**
25059
+ * True when the code that would run after a restart is NOT the code running
25060
+ * now: a version is staged (`pendingVersion`), or `installedVersion` differs
25061
+ * from `runningVersion`. Derived on every read, never stored. This is the
25062
+ * one field that lets an operator or a script say "1.2.353 is installed and
25063
+ * will run after a restart" instead of reading it off a number that means
25064
+ * "installed". Optional for version skew.
25065
+ */
25066
+ restartRequired: boolean().optional(),
25037
25067
  /** N-1 version kept for rollback; null when no previous version exists. */
25038
25068
  previousVersion: string().nullable(),
25039
25069
  /** Version of the immutable baked seed closure (image fallback). */
@@ -25068,19 +25098,71 @@ var ServerPackageStatusSchema = object({
25068
25098
  });
25069
25099
  var ServerUpdateCheckResultSchema = object({
25070
25100
  packageName: string(),
25101
+ /** Same meaning as on the status payload: what THIS process loaded. */
25071
25102
  runningVersion: string().nullable(),
25072
25103
  latestVersion: string().nullable(),
25104
+ /**
25105
+ * `latestVersion` is newer than `runningVersion` AND is not the version
25106
+ * already staged on this node. A staged-and-waiting version is not "an
25107
+ * update available" — applying it again is refused (`already-staged`), and
25108
+ * a button that announced "updating…" on this boolean then got refused
25109
+ * would be lying. It is reported through `pendingVersion` instead.
25110
+ */
25073
25111
  updateAvailable: boolean(),
25074
25112
  checkedAtMs: number(),
25075
25113
  /** Non-null when the registry lookup failed (offline, bad registry, …). */
25076
- error: string().nullable()
25114
+ error: string().nullable(),
25115
+ /**
25116
+ * Version staged on this node and awaiting its restart, when one is. Before
25117
+ * 2026-09-20 a check answered `{runningVersion, updateAvailable:false}` for a
25118
+ * node whose staged closure had never booted, and the caller had no way to
25119
+ * see the difference. Optional for version skew.
25120
+ */
25121
+ pendingVersion: string().nullable().optional(),
25122
+ /** See the status payload: staged, or installed-on-disk ≠ running. Optional for skew. */
25123
+ restartRequired: boolean().optional()
25077
25124
  });
25125
+ /**
25126
+ * What an action DID — the discriminant `accepted` cannot carry. Before
25127
+ * 2026-09-20 `applyServerUpdate` answered `accepted:false` both for a refusal
25128
+ * where nothing was wrong (already current, already staged) and for a staging
25129
+ * install that died on `npm error code ECONNRESET`; the only difference was
25130
+ * prose in `message`, and an operator restarted a container on the strength of
25131
+ * it, booting the previous version.
25132
+ *
25133
+ * - `staged` — the closure is installed and validated; the node
25134
+ * restarts to apply it (`restarting:true`). NOTHING
25135
+ * runs the new code until that restart completes.
25136
+ * - `restart-scheduled` — a plain, version-preserving restart was accepted.
25137
+ * - `already-current` — the target IS the running version; nothing done.
25138
+ * - `refused` — dropped by a gate; `reason` names which.
25139
+ * - `failed` — attempted and did not land; `error` says why.
25140
+ */
25141
+ var ServerUpdateOutcomeSchema = _enum([
25142
+ "staged",
25143
+ "restart-scheduled",
25144
+ "already-current",
25145
+ "refused",
25146
+ "failed"
25147
+ ]);
25078
25148
  var ServerUpdateActionResultSchema = object({
25079
25149
  accepted: boolean(),
25080
25150
  targetVersion: string().nullable(),
25081
25151
  /** True when a graceful restart was scheduled to apply the change. */
25082
25152
  restarting: boolean(),
25083
- message: string()
25153
+ message: string(),
25154
+ /** See {@link ServerUpdateOutcomeSchema}. Optional for version skew. */
25155
+ outcome: ServerUpdateOutcomeSchema.optional(),
25156
+ /**
25157
+ * Stable, greppable cause for `refused` / `failed` (the engine's
25158
+ * `UpdateRefusalReason`, `staging-failed`, …). `null` on success.
25159
+ */
25160
+ reason: string().nullable().optional(),
25161
+ /**
25162
+ * The failure itself, verbatim enough to act on (the npm output for a
25163
+ * staging install). Non-null only for `failed`.
25164
+ */
25165
+ error: string().nullable().optional()
25084
25166
  });
25085
25167
  method(_void(), ServerPackageStatusSchema, { auth: "admin" }), method(_void(), ServerUpdateCheckResultSchema, {
25086
25168
  kind: "mutation",
@@ -26283,41 +26365,299 @@ var ClipSchema = object({
26283
26365
  holes: array(object({
26284
26366
  startMs: number(),
26285
26367
  endMs: number()
26286
- })).optional()
26368
+ })).optional(),
26369
+ /**
26370
+ * A URL that MAY produce this clip's still, asked only for rows actually on
26371
+ * screen. The opposite of {@link ClipSchema.thumbnail}: that one is VOUCHED
26372
+ * (a JPEG is already on disk), this one is an offer. The route answers 200
26373
+ * with the image, or 204 with `x-camstack-reason` when it could not mint one
26374
+ * — a surface latches that refusal to the instant rather than retrying.
26375
+ *
26376
+ * Never both: a clip with a vouched `thumbnail` needs no mint.
26377
+ */
26378
+ thumbnailMint: string().optional(),
26379
+ /**
26380
+ * Why no still will be produced for this clip RIGHT NOW — set when the
26381
+ * provider already knows, so the surface draws the glyph and the reason
26382
+ * instead of firing a mint that cannot succeed.
26383
+ *
26384
+ * `sleeping` — a standalone battery camera; a read would be a wake (D549 4).
26385
+ * `camera-refused` — the camera answered the CoverPreview with a refusal.
26386
+ * `no-keyframe` — the window holds no decodable I-frame (an event that sits
26387
+ * inside no file is the measured case).
26388
+ * `unsupported` — this source cannot mint stills at all.
26389
+ */
26390
+ thumbnailUnavailable: object({ reason: _enum([
26391
+ "sleeping",
26392
+ "camera-refused",
26393
+ "no-keyframe",
26394
+ "unsupported"
26395
+ ]) }).optional(),
26396
+ /**
26397
+ * When the catalog this row came from was last CONFIRMED against the device.
26398
+ * Absent means "this row was read live". A persisted catalog served while a
26399
+ * camera sleeps carries the age it really has — a cached list is never drawn
26400
+ * as current (D549 13).
26401
+ */
26402
+ catalogAsOf: number().optional(),
26403
+ /**
26404
+ * The camera's OWN type strings for this clip, all of them, unmapped
26405
+ * (`md`, `people`, `dog_cat`, `sched`, …). Kept beside {@link labels}
26406
+ * because a firmware inventing a type must not vanish: the mapping into our
26407
+ * filter vocabulary is lossy on purpose and this is the lossless copy
26408
+ * (D549 20).
26409
+ */
26410
+ nativeTypes: array(string()).optional(),
26411
+ /**
26412
+ * What the subject DID — `crossline`, `intrude`, `loitering`. A behaviour
26413
+ * travels BESIDE a class, never instead of one, and the class filter ignores
26414
+ * it: "a person crossed a line" is still a person (D549 20).
26415
+ */
26416
+ behaviours: array(string()).optional(),
26417
+ /**
26418
+ * The same recording as two files — the sub twin (what the row and its
26419
+ * thumbnail are) and its main twin, matched at listing time so a quality
26420
+ * change never re-searches the camera (D549 15). `getClipPlayback`'s
26421
+ * `profile` picks between them: `low | mid` → sub, `high` → main.
26422
+ */
26423
+ streams: object({
26424
+ sub: object({
26425
+ id: string(),
26426
+ bytes: number().optional()
26427
+ }).optional(),
26428
+ main: object({
26429
+ id: string(),
26430
+ bytes: number().optional()
26431
+ }).optional()
26432
+ }).optional(),
26433
+ /** The camera says it holds a sub-stream copy of this recording. */
26434
+ supportSub: boolean().optional(),
26435
+ /**
26436
+ * Whether this row has bytes behind it. ABSENT means yes — every clip that
26437
+ * IS a file is playable, and only a source that lists EVENTS can produce a
26438
+ * row with nothing to play (the measured case: 1 of 29 hub events on 3628
26439
+ * fell inside no file). Such a row is shown, never dropped and never offered
26440
+ * as playable-then-failing (D549 19).
26441
+ */
26442
+ playable: boolean().optional(),
26443
+ /** Why {@link playable} is false, verbatim (`no-file-for-window`). */
26444
+ unplayableReason: string().optional()
26287
26445
  });
26288
26446
  var ClipPlaybackSchema = object({
26289
- /** HLS master URL through the hub data-plane (Range + token in path). */
26447
+ /**
26448
+ * A media URL through the hub data-plane. {@link ClipPlaybackSchema.format}
26449
+ * says what kind — an HLS master playlist for a recording-derived clip, a
26450
+ * progressive MP4 for a native file the vendor addon muxed and serves with
26451
+ * `Range`. The URL is NOT a credential: both planes are registered
26452
+ * `access: 'authenticated'` and the hub's `/addon/<id>/<prefix>` proxy takes
26453
+ * the session cookie, so nothing is appended to it (D549 22).
26454
+ */
26290
26455
  playbackUrl: string(),
26456
+ /**
26457
+ * How to play {@link playbackUrl}. Absent means `hls` — the shape every
26458
+ * existing consumer already assumes. A player that branches on this is the
26459
+ * one change a native clip needs; a player that ignores it will hand an MP4
26460
+ * to hls.js and fail parsing it as a manifest.
26461
+ */
26462
+ format: _enum(["hls", "mp4"]).optional(),
26463
+ /**
26464
+ * Which twin was actually served. A clip that holds only one stream answers
26465
+ * with the one it has, and the surface SAYS so — a missing main twin is
26466
+ * never served silently as if it were the asked-for quality (D549 15).
26467
+ */
26468
+ served: CamProfileSchema.optional(),
26291
26469
  /** Optional LAN/remote alternates for the same clip. */
26292
26470
  playbackEndpoints: array(string()).optional(),
26293
26471
  token: string().optional()
26294
26472
  });
26473
+ /**
26474
+ * Why a source cannot answer right now — per SOURCE, never fleet-wide.
26475
+ *
26476
+ * `ok` is the only state whose clip list may be read as complete. The other
26477
+ * three exist because an empty list from a sleeping camera reads as "this
26478
+ * camera has no recordings", which is the defect this whole line of work is
26479
+ * about: a battery camera nobody woke (`sleeping`), a camera that could not be
26480
+ * reached at all (`unreachable`, also what a provider that THREW reports), a
26481
+ * camera whose SD card is not mounted (`no-storage`, measured on 640 —
26482
+ * `HddInfo mount 0`, honestly nothing to list rather than "no clips"), and a
26483
+ * camera whose OWN index disagrees with its OWN calendar (`index-empty`,
26484
+ * measured on 618: the calendar marks 3–20 September, days 6–20 list zero
26485
+ * files on both streams and both filters, with 177 GB free). That last one is
26486
+ * a defect ON THE CAMERA, and the only honest thing a provider can do is say
26487
+ * which days it asked for and got nothing — "no clips" would be a lie about a
26488
+ * card that is full of them.
26489
+ */
26490
+ var ClipSourceAvailabilitySchema = object({
26491
+ state: _enum([
26492
+ "ok",
26493
+ "sleeping",
26494
+ "unreachable",
26495
+ "no-storage",
26496
+ "index-empty"
26497
+ ]),
26498
+ /** Free text, shown verbatim. Names the camera's own refusal when there is one. */
26499
+ reason: string().optional(),
26500
+ /** When this source's catalog was last CONFIRMED. A cached list is never
26501
+ * drawn as current: the surface shows the age whenever it is older than the
26502
+ * refresh interval. */
26503
+ catalogAsOf: number().optional()
26504
+ });
26505
+ /**
26506
+ * One SOURCE of clips for a camera — a row of the picker, and the namespace
26507
+ * every clip id from it is prefixed with (`analytics`,
26508
+ * `native:reolink:onboard`, `hksv`, …).
26509
+ *
26510
+ * A provider lists the sources IT serves for that device, and answers for each
26511
+ * of them whether it can answer at all. A provider with nothing to offer on a
26512
+ * camera returns `[]` — it is not that camera's business.
26513
+ */
26514
+ var ClipSourceSchema = object({
26515
+ /** The value this source stamps on {@link ClipSchema.source}, and the prefix
26516
+ * of every clip id it mints. `getClipPlayback` routes on it. */
26517
+ source: string(),
26518
+ /** Operator-facing name of the source ("CamStack recordings", "SD card"). */
26519
+ label: string(),
26520
+ /**
26521
+ * The addon that SERVES this row.
26522
+ *
26523
+ * A surface cannot otherwise resolve a source to who answers it, and the
26524
+ * alternative — a `native:reolink:* → provider-reolink` table inside the
26525
+ * widget — is a second authority on provider identity living in the one
26526
+ * package with no business knowing it, wrong the day a third source appears
26527
+ * (D557). One addon may serve SEVERAL sources: `addon-provider-reolink`
26528
+ * answers a hub child with both `native:reolink:onboard` and
26529
+ * `native:reolink:hub`, which is why the per-device switch is keyed by
26530
+ * SOURCE and not by this (D555).
26531
+ *
26532
+ * Optional for version skew only. The collection dispatcher stamps it from
26533
+ * the registry, so a row that travelled through the fan-out carries the
26534
+ * authoritative id whatever the provider filled in.
26535
+ */
26536
+ addonId: string().optional(),
26537
+ /**
26538
+ * Which API this source resolved to FOR THIS CAMERA, when it has a choice.
26539
+ *
26540
+ * A source may cover one store through more than one surface — the Reolink
26541
+ * provider reads a hub child through the parent's event log and a standalone
26542
+ * through its own file list, because that is what each camera answers. The
26543
+ * CHOICE is the provider's, made from what the camera is, and is never a row
26544
+ * the operator has to understand; but it is REPORTED, because a source that
26545
+ * silently reads a different API on two cameras and then behaves differently
26546
+ * is the thing nobody can debug later. Absent when the source has only one
26547
+ * way to read its store.
26548
+ */
26549
+ via: string().optional(),
26550
+ availability: ClipSourceAvailabilitySchema,
26551
+ /**
26552
+ * The operator switched this source OFF for this camera.
26553
+ *
26554
+ * Deliberately NOT a member of {@link ClipSourceAvailabilitySchema}'s
26555
+ * vocabulary. That enum models what the source CAN do — a sleeping camera, an
26556
+ * unmounted card, an index that disagrees with its own calendar — and a
26557
+ * switched-off source could answer perfectly well; the operator decided it
26558
+ * should not. Folding the choice in is how `disabled` and `broken` stop being
26559
+ * distinguishable, which is the D62 rule this repo has already paid for twice:
26560
+ * an off switch is REPORTED off (`CameraStatus.switchedOff`, the same word),
26561
+ * and disabled must never look like broken. It is also what lets every
26562
+ * exhaustive consumer of the availability enum keep compiling.
26563
+ *
26564
+ * A switched-off source contributes NO clips (`listClips` never calls it) and
26565
+ * its row carries no `catalogAsOf`: nothing confirms a catalog it is not
26566
+ * allowed to serve, and a frozen age that can only grow draws a stalling
26567
+ * source rather than an off switch.
26568
+ *
26569
+ * The row survives BECAUSE it is the control the operator switches the source
26570
+ * back on from — D554 decision 4's rule ("a source that cannot answer
26571
+ * produces a ROW, not an absence") applied to the one case D556 carved out of
26572
+ * it, and D557's own kept property ("a source is never hidden, only its
26573
+ * rows"). Absent means on.
26574
+ *
26575
+ * A provider never sets this — like {@link ClipSourceSchema.addonId} it is
26576
+ * stamped by the collection dispatcher, which holds the registry's projection
26577
+ * of the persisted authority (D556) and is the only place that knows it.
26578
+ */
26579
+ switchedOff: boolean().optional()
26580
+ });
26295
26581
  var videoclipsCapability = {
26296
26582
  name: "videoclips",
26297
26583
  scope: "device",
26298
- mode: "singleton",
26584
+ mode: "collection",
26299
26585
  kind: "wrapper",
26300
26586
  defaultActive: true,
26301
26587
  /** A clip is a window over a camera's footage — the cap is meaningless on a
26302
26588
  * sensor, a button or an event emitter, and the `defaultActive` auto-bind
26303
26589
  * reads this to decide which devices it may claim. */
26304
26590
  deviceTypes: [DeviceType.Camera],
26591
+ /**
26592
+ * The Clips section of a camera's device details is FRAMEWORK-DERIVED (D14):
26593
+ * the aggregator turns this declaration into the `type:'widget'` section and
26594
+ * `DeviceDetail.tsx` is never edited. `videoclips` is the first WRAPPER cap
26595
+ * to declare one — every previous `host/` widget cap is `deviceNative` — so
26596
+ * `device-config-widget-wrapped-binding.spec.ts` pins that a `kind:'wrapped'`
26597
+ * binding entry derives the same section a native one does.
26598
+ *
26599
+ * `topTab` because the browser owns the whole pane (a day's clips, a source
26600
+ * list and a player), exactly as `ptz` does; the widget itself is
26601
+ * `host/clips-browser` in ui-library's `HOST_WIDGETS`.
26602
+ */
26603
+ deviceConfig: { ui: {
26604
+ kind: "widget",
26605
+ widgetId: "host/clips-browser",
26606
+ tab: "clips",
26607
+ topTab: true,
26608
+ label: "Clips",
26609
+ order: 0
26610
+ } },
26305
26611
  methods: {
26306
26612
  listClips: method(object({
26307
26613
  deviceId: number(),
26308
26614
  since: number(),
26309
26615
  until: number(),
26310
- limit: number().int().positive().optional()
26616
+ limit: number().int().positive().optional(),
26617
+ /**
26618
+ * View filter over {@link ClipSourceSchema.source} values — the
26619
+ * picker's selection, forwarded so a provider need not list what
26620
+ * nobody is looking at. ABSENT means every source this camera has,
26621
+ * which is the honest default for a surface whose whole point is that
26622
+ * nothing is hidden (D554 3). A provider with one source ignores it.
26623
+ */
26624
+ sources: array(string()).optional()
26311
26625
  }), array(ClipSchema).readonly(), {
26312
26626
  kind: "query",
26313
- auth: "admin"
26627
+ auth: "protected"
26628
+ }),
26629
+ /**
26630
+ * The sources this camera has, WITH the reason any of them cannot answer.
26631
+ *
26632
+ * Asked separately from `listClips` because an empty clip list is
26633
+ * ambiguous and this is the only place the ambiguity is resolved: every
26634
+ * bound provider contributes its own rows, and a provider that could not
26635
+ * be reached at all still produces one row saying so. A surface that draws
26636
+ * "no clips" without reading this is drawing a guess.
26637
+ */
26638
+ listSources: method(object({ deviceId: number() }), array(ClipSourceSchema).readonly(), {
26639
+ kind: "query",
26640
+ auth: "protected"
26314
26641
  }),
26315
26642
  getClipPlayback: method(object({
26316
26643
  deviceId: number(),
26317
- clipId: string()
26644
+ clipId: string(),
26645
+ /**
26646
+ * Which twin to serve, on the ONE quality scale the system already has
26647
+ * (`CamProfileSchema`). `low | mid` → the sub file, `high` → the main
26648
+ * twin; both ids are already on the row so this never re-searches the
26649
+ * camera. Absent means the provider's own default (the sub file, which
26650
+ * every source is measured to hold).
26651
+ *
26652
+ * `auto` is deliberately NOT accepted here: a stored file has no
26653
+ * broker session, so the adaptive tier cannot be resolved for it. The
26654
+ * viewer resolves `auto` to a profile the same way live does, before
26655
+ * it calls (D549 15).
26656
+ */
26657
+ profile: CamProfileSchema.optional()
26318
26658
  }), ClipPlaybackSchema, {
26319
26659
  kind: "query",
26320
- auth: "admin"
26660
+ auth: "protected"
26321
26661
  })
26322
26662
  }
26323
26663
  };
@@ -44217,6 +44557,12 @@ Object.freeze({
44217
44557
  addonId: null,
44218
44558
  access: "view"
44219
44559
  },
44560
+ "videoclips.listSources": {
44561
+ capName: "videoclips",
44562
+ capScope: "device",
44563
+ addonId: null,
44564
+ access: "view"
44565
+ },
44220
44566
  "viewerUi.getStaticDir": {
44221
44567
  capName: "viewer-ui",
44222
44568
  capScope: "system",
@@ -46225,6 +46571,11 @@ Object.freeze({
46225
46571
  form: "single",
46226
46572
  optional: false
46227
46573
  }],
46574
+ "videoclips.listSources": [{
46575
+ name: "deviceId",
46576
+ form: "single",
46577
+ optional: false
46578
+ }],
46228
46579
  "waterHeater.setAway": [{
46229
46580
  name: "deviceId",
46230
46581
  form: "single",