@camstack/addon-export-hap 1.2.26 → 1.2.28

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.
@@ -3082,6 +3082,9 @@ function handlePipeResult(left, next, ctx) {
3082
3082
  fallback: left.fallback
3083
3083
  }, ctx);
3084
3084
  }
3085
+ var $ZodPreprocess = /*@__PURE__*/ $constructor("$ZodPreprocess", (inst, def) => {
3086
+ $ZodPipe.init(inst, def);
3087
+ });
3085
3088
  var $ZodReadonly = /*@__PURE__*/ $constructor("$ZodReadonly", (inst, def) => {
3086
3089
  $ZodType.init(inst, def);
3087
3090
  defineLazy(inst._zod, "propValues", () => def.innerType._zod.propValues);
@@ -5267,6 +5270,10 @@ function pipe(in_, out) {
5267
5270
  out
5268
5271
  });
5269
5272
  }
5273
+ var ZodPreprocess = /*@__PURE__*/ $constructor("ZodPreprocess", (inst, def) => {
5274
+ ZodPipe.init(inst, def);
5275
+ $ZodPreprocess.init(inst, def);
5276
+ });
5270
5277
  var ZodReadonly = /*@__PURE__*/ $constructor("ZodReadonly", (inst, def) => {
5271
5278
  $ZodReadonly.init(inst, def);
5272
5279
  ZodType.init(inst, def);
@@ -5325,6 +5332,13 @@ function _instanceof(cls, params = {}) {
5325
5332
  };
5326
5333
  return inst;
5327
5334
  }
5335
+ function preprocess(fn, schema) {
5336
+ return new ZodPreprocess({
5337
+ type: "pipe",
5338
+ in: transform(fn),
5339
+ out: schema
5340
+ });
5341
+ }
5328
5342
  //#endregion
5329
5343
  //#region ../../node_modules/zod/v4/classic/compat.js
5330
5344
  /** @deprecated Use the raw string literal codes instead, e.g. "invalid_type". */
@@ -7601,7 +7615,7 @@ import { errMsg } from '@camstack/types'
7601
7615
  * Extract a human-readable message from an unknown error value.
7602
7616
  * Replaces the ubiquitous `errMsg(err)` pattern.
7603
7617
  */
7604
- function errMsg$12(err) {
7618
+ function errMsg$15(err) {
7605
7619
  if (err instanceof Error) return err.message;
7606
7620
  if (typeof err === "string") return err;
7607
7621
  return String(err);
@@ -13738,11 +13752,33 @@ var NotificationFormatSchema = _enum([
13738
13752
  * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
13739
13753
  * renderer's icon set; "acknowledge" survives an adapter that draws it
13740
13754
  * differently.
13755
+ *
13756
+ * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
13757
+ *
13758
+ * These names are for US. **No adapter may forward one verbatim.** Each maps
13759
+ * the whole set onto its own renderer's vocabulary through a
13760
+ * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
13761
+ * fallback, so adding a member here fails every adapter's build until someone
13762
+ * decides its glyph, which is the only place that decision can be made
13763
+ * honestly.
13764
+ *
13765
+ * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
13766
+ * `disarm` straight through; iOS feeds that string to
13767
+ * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
13768
+ * and every snooze and alarm button arrived BLANK. A pass-through is not a
13769
+ * mapping, and "the field is documented" is not "the value renders".
13770
+ *
13771
+ * Adding a member is TRAIN-BOUND. The enum lives in the published
13772
+ * `@camstack/server` closure and the cap seam validates against the HUB's copy,
13773
+ * so an addon that emits a token the running hub does not know does not lose an
13774
+ * icon — its whole `send` fails Zod validation and the notification never
13775
+ * arrives. Never emit a new token from an addon before the train carrying it.
13741
13776
  */
13742
13777
  var NotificationActionIconSchema = _enum([
13743
13778
  "acknowledge",
13744
13779
  "dismiss",
13745
13780
  "silence",
13781
+ "snooze",
13746
13782
  "view",
13747
13783
  "play",
13748
13784
  "open",
@@ -13750,9 +13786,13 @@ var NotificationActionIconSchema = _enum([
13750
13786
  "lock",
13751
13787
  "unlock",
13752
13788
  "arm",
13789
+ "arm-home",
13790
+ "arm-away",
13791
+ "arm-night",
13753
13792
  "disarm",
13754
13793
  "light",
13755
- "alert"
13794
+ "alert",
13795
+ "camera"
13756
13796
  ]);
13757
13797
  /** A single tap-through action button. */
13758
13798
  var NotificationActionSchema = object({
@@ -13952,6 +13992,24 @@ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSche
13952
13992
  targetId: string(),
13953
13993
  enabled: boolean()
13954
13994
  }), _void(), { kind: "mutation" });
13995
+ new Set([
13996
+ {
13997
+ id: "person",
13998
+ name: "Person"
13999
+ },
14000
+ {
14001
+ id: "vehicle",
14002
+ name: "Vehicle"
14003
+ },
14004
+ {
14005
+ id: "animal",
14006
+ name: "Animal"
14007
+ },
14008
+ {
14009
+ id: "package",
14010
+ name: "Package"
14011
+ }
14012
+ ].map((l) => l.id));
13955
14013
  var COCO_TO_MACRO = {
13956
14014
  mapping: {
13957
14015
  person: "person",
@@ -14749,6 +14807,8 @@ var NcSystemEventKindSchema = _enum([
14749
14807
  "alarm-triggered",
14750
14808
  "alarm-armed",
14751
14809
  "alarm-disarmed",
14810
+ "alarm-arming",
14811
+ "alarm-arm-refused",
14752
14812
  "camera-online",
14753
14813
  "camera-offline",
14754
14814
  "camera-disabled",
@@ -15957,7 +16017,87 @@ var MethodAccessSchema = _enum([
15957
16017
  var AllowedProviderSchema = union([literal("*"), array(string())]);
15958
16018
  var AllowedDevicesSchema = record(string(), union([literal("*"), array(string())]));
15959
16019
  var CapScopeSchema = _enum(["device", "system"]);
15960
- var TokenScopeSchema = discriminatedUnion("type", [
16020
+ /**
16021
+ * DeviceSelector (scope model v3 — 2026-08-12).
16022
+ *
16023
+ * A `device` grant no longer carries a frozen list of deviceIds. It carries
16024
+ * a SELECTOR the matcher resolves against the live fleet, so the grant can be
16025
+ * DYNAMIC: a `types:['camera']` selector automatically covers a camera added
16026
+ * AFTER the grant was minted — no re-grant, no re-login.
16027
+ *
16028
+ * - `all` — every device in the deployment. The broad viewer/operator
16029
+ * lever without a `category` grant (a `category` grant also covers device
16030
+ * caps that carry no deviceId; `all` is specifically the device set).
16031
+ * - `ids` — an explicit deviceId list. This is what a v2 `device:[…]`
16032
+ * grant migrates to (see {@link TokenScopeSchema}); STATIC — a new camera
16033
+ * is NOT covered until the grant is edited.
16034
+ * - `types` — every device of a `DeviceType` (e.g. every `camera`).
16035
+ * DYNAMIC. A device that changes type, or a new device of the type,
16036
+ * re-resolves on the next request.
16037
+ * - `locations` — every device whose operator-assigned `location` label is
16038
+ * in the set (e.g. "Garden", "Front door"). DYNAMIC. A device with a
16039
+ * null/unset location matches NO `locations` selector.
16040
+ */
16041
+ var DeviceSelectorSchema = discriminatedUnion("kind", [
16042
+ object({ kind: literal("all") }),
16043
+ object({
16044
+ kind: literal("ids"),
16045
+ ids: array(number().int()).min(1)
16046
+ }),
16047
+ object({
16048
+ kind: literal("types"),
16049
+ types: array(_enum(DeviceType)).min(1)
16050
+ }),
16051
+ object({
16052
+ kind: literal("locations"),
16053
+ locations: array(string().min(1)).min(1)
16054
+ })
16055
+ ]);
16056
+ var DeviceTokenScopeSchema = object({
16057
+ type: literal("device"),
16058
+ /** The device SET this grant covers — resolved against the live fleet. */
16059
+ selector: DeviceSelectorSchema,
16060
+ access: array(MethodAccessSchema).min(1),
16061
+ /**
16062
+ * Whether a grant on a PARENT device transparently covers its accessory
16063
+ * CHILDREN (siren / floodlight / PIR) via the persisted-parentage walk.
16064
+ * Direction is parent → children ONLY.
16065
+ *
16066
+ * Absent → the matcher DERIVES it from the access flavour: `view`
16067
+ * inherits (a camera viewer sees the camera's accessories), `create` /
16068
+ * `delete` do NOT (actuating/removing a child is an explicit act the
16069
+ * operator must grant on the child, not inherit from the parent). Set it
16070
+ * explicitly to override that default per grant.
16071
+ */
16072
+ includeLinked: boolean().optional()
16073
+ });
16074
+ /**
16075
+ * v2 → v3 lazy migration. A pre-v3 `device` grant carried
16076
+ * `targets: string[]` (stringified deviceIds); it rewrites to the equivalent
16077
+ * `selector: {kind:'ids', ids}`. Applied as a `preprocess` so it runs on
16078
+ * EVERY parse path — stored records AND the JWT-carried scope arrays
16079
+ * normalised at the request boundary ({@link normalizeTokenScopes} in
16080
+ * `device-selector.ts`). Chosen over a one-time DB migration because a
16081
+ * migration cannot reach a JWT already in a client's hands; parse-time
16082
+ * migration covers both without a flag day. No cast — the raw object is read
16083
+ * through `Reflect.get` (its static type is `unknown`).
16084
+ */
16085
+ function migrateLegacyTokenScope(raw) {
16086
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return raw;
16087
+ if (Reflect.get(raw, "type") !== "device") return raw;
16088
+ if (Reflect.get(raw, "selector") !== void 0) return raw;
16089
+ const targets = Reflect.get(raw, "targets");
16090
+ if (!Array.isArray(targets)) return raw;
16091
+ return {
16092
+ type: "device",
16093
+ selector: {
16094
+ kind: "ids",
16095
+ ids: targets.map((t) => typeof t === "string" ? Number(t) : t).filter((n) => typeof n === "number" && Number.isInteger(n))
16096
+ },
16097
+ access: Reflect.get(raw, "access")
16098
+ };
16099
+ }
16100
+ var TokenScopeSchema = preprocess(migrateLegacyTokenScope, discriminatedUnion("type", [
15961
16101
  object({
15962
16102
  type: literal("category"),
15963
16103
  target: CapScopeSchema,
@@ -15973,18 +16113,8 @@ var TokenScopeSchema = discriminatedUnion("type", [
15973
16113
  target: string(),
15974
16114
  access: array(MethodAccessSchema).min(1)
15975
16115
  }),
15976
- object({
15977
- type: literal("device"),
15978
- /**
15979
- * One or more deviceIds (serialised as strings for wire-format
15980
- * consistency with the rest of the union). Matcher accepts if
15981
- * `input.deviceId` ∈ `targets`. Array shape avoids the row-explosion
15982
- * of one scope-per-device when granting access to a set of cameras.
15983
- */
15984
- targets: array(string()).min(1),
15985
- access: array(MethodAccessSchema).min(1)
15986
- })
15987
- ]);
16116
+ DeviceTokenScopeSchema
16117
+ ]));
15988
16118
  object({
15989
16119
  id: string(),
15990
16120
  username: string(),
@@ -17981,10 +18111,26 @@ var detectionFpsField = {
17981
18111
  default: 10,
17982
18112
  step: 1
17983
18113
  };
18114
+ /**
18115
+ * The occupancy re-check interval. DEFAULT 300 s (2026-08-13 — was 30 s).
18116
+ *
18117
+ * The recheck is now on by default (a parked car is invisible to occupancy
18118
+ * rules until the stationary registry has been rebuilt by motion, which after a
18119
+ * restart may be never on a quiet camera). Each cycle re-subscribes a detection
18120
+ * session — an RTSP re-dial — so the switch is only affordable at a WIDE
18121
+ * interval: 300 s is ~12 re-dials an hour per camera, against 120 at the old
18122
+ * 30 s. A parked car is therefore counted within 5 minutes of a restart.
18123
+ *
18124
+ * Why not wider: `max` is 300 and raising it is TRAIN-BOUND, not addon-bound —
18125
+ * the host validates `attachCamera` against ITS copy of this schema, so a
18126
+ * runner asked for 600 would be rejected by the hub until a `@camstack/server`
18127
+ * carrying the wider bound is installed everywhere. 300 is the widest value
18128
+ * that ships with an addon deploy.
18129
+ */
17984
18130
  var occupancyRecheckSecField = {
17985
18131
  min: 0,
17986
18132
  max: 300,
17987
- default: 30,
18133
+ default: 300,
17988
18134
  step: 5
17989
18135
  };
17990
18136
  var occupancyRecheckFramesField = {
@@ -18182,15 +18328,21 @@ var RunnerCameraConfigSchema = object({
18182
18328
  */
18183
18329
  onboardMotionDrivesAnalyzer: boolean().default(true),
18184
18330
  /**
18185
- * Master toggle for the occupancy re-check. When `false` (DEFAULT) the runner
18186
- * never arms the periodic recheck timer, regardless of `occupancyRecheckSec`
18187
- * this is off by default because the recheck re-subscribes a detection session
18188
- * every N seconds while `watching`, a major source of pull-decoder re-dial
18189
- * churn (each cycle creates+tears a session → RTSP re-dial → latency). The
18331
+ * Master toggle for the occupancy re-check. When `false` the runner never arms
18332
+ * the periodic recheck timer, regardless of `occupancyRecheckSec`; the
18190
18333
  * `occupancyRecheckSec` / `occupancyRecheckFrames` sliders only take effect
18191
18334
  * (and only render) when this is enabled.
18192
- */
18193
- occupancyRecheckEnabled: boolean().default(false),
18335
+ *
18336
+ * DEFAULT `true` since 2026-08-13 (was `false`). It was off because the
18337
+ * recheck re-subscribes a detection session every N seconds while `watching`
18338
+ * — each cycle creates+tears a session ⇒ an RTSP re-dial ⇒ latency, a major
18339
+ * pull-decoder churn source. What that bought was a blind spot: a STATIONARY
18340
+ * object is counted only while the stationary registry holds it, and the
18341
+ * registry rebuilds from motion, so after a restart a parked car was invisible
18342
+ * to every occupancy rule until something moved in front of it. The churn is
18343
+ * now paid on the interval instead — see `occupancyRecheckSecField`.
18344
+ */
18345
+ occupancyRecheckEnabled: boolean().default(true),
18194
18346
  occupancyRecheckSec: number().min(occupancyRecheckSecField.min).max(occupancyRecheckSecField.max).default(occupancyRecheckSecField.default),
18195
18347
  occupancyRecheckFrames: number().min(occupancyRecheckFramesField.min).max(occupancyRecheckFramesField.max).default(occupancyRecheckFramesField.default),
18196
18348
  /**
@@ -20591,7 +20743,11 @@ object({
20591
20743
  precision: number().int().min(0).max(10).optional()
20592
20744
  });
20593
20745
  DeviceType.Sensor;
20594
- object({
20746
+ /**
20747
+ * Ambient illuminance reading in lux. Drives Home Assistant `sensor`
20748
+ * entries with `device_class: illuminance`.
20749
+ */
20750
+ var AmbientLightSensorStatusSchema = object({
20595
20751
  /** Current illuminance in lux (lx). */
20596
20752
  lux: number().min(0),
20597
20753
  /** Ms epoch when the slice was last updated. */
@@ -20864,7 +21020,11 @@ DeviceType.Camera, method(object({ deviceId: number() }), CameraCredentialsSchem
20864
21020
  kind: "query",
20865
21021
  auth: "admin"
20866
21022
  });
20867
- object({
21023
+ /**
21024
+ * Carbon-monoxide alarm sensor. Drives Home Assistant `binary_sensor`
21025
+ * entries with `device_class: carbon_monoxide`. Push-driven.
21026
+ */
21027
+ var CarbonMonoxideStatusSchema = object({
20868
21028
  detected: boolean(),
20869
21029
  /** Ms epoch of the last transition. 0 if never observed. */
20870
21030
  lastChangedAt: number()
@@ -21152,7 +21312,19 @@ Object.values(DeviceType), method(object({
21152
21312
  kind: "mutation",
21153
21313
  auth: "admin"
21154
21314
  }), ConsumablesStatusSchema.extend({ lastFetchedAt: number() });
21155
- object({
21315
+ /**
21316
+ * Door / window / opening / garage / valve contact sensor. Boolean
21317
+ * "is the entry currently open" with the timestamp of the last
21318
+ * transition. Drives Home Assistant `binary_sensor` entries whose
21319
+ * `device_class` is `door`, `window`, `opening`, `garage`, or
21320
+ * `garage_door` — and any future native integration that needs
21321
+ * the same semantics.
21322
+ *
21323
+ * Push-driven: providers update the slice on transition events from
21324
+ * the upstream source (HA WebSocket `state_changed`, ZWave
21325
+ * `notification` …). Consumers read the slice; no polling.
21326
+ */
21327
+ var ContactStatusSchema = object({
21156
21328
  /** True when the entry is open; false when closed. */
21157
21329
  entryOpen: boolean(),
21158
21330
  /** Ms epoch of the last open↔closed transition. 0 if never observed. */
@@ -21751,7 +21923,15 @@ object({
21751
21923
  deviceId: number(),
21752
21924
  status: FeatureProbeStatusSchema
21753
21925
  });
21754
- object({
21926
+ /**
21927
+ * Water leak / moisture sensor. Boolean "is liquid currently
21928
+ * detected" with the timestamp of the last transition. Drives Home
21929
+ * Assistant `binary_sensor` entries with `device_class: moisture`,
21930
+ * and any future native flood sensor.
21931
+ *
21932
+ * Push-driven from the upstream source.
21933
+ */
21934
+ var FloodStatusSchema = object({
21755
21935
  /** True when leak is currently detected. */
21756
21936
  flooded: boolean(),
21757
21937
  /** Ms epoch of the last flooded↔dry transition. 0 if never observed. */
@@ -21805,7 +21985,15 @@ DeviceType.Humidifier, method(object({
21805
21985
  kind: "mutation",
21806
21986
  auth: "admin"
21807
21987
  });
21808
- object({
21988
+ /**
21989
+ * Single-metric humidity reading. Drives Home Assistant `sensor`
21990
+ * entries with `device_class: humidity`.
21991
+ *
21992
+ * Unit normalisation: percent. The canonical display unit (`%`) is a
21993
+ * descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
21994
+ * `sourceInfo`.
21995
+ */
21996
+ var HumiditySensorStatusSchema = object({
21809
21997
  /** Current relative humidity, 0..100. */
21810
21998
  percent: number().min(0).max(100),
21811
21999
  /** Ms epoch when the slice was last updated. */
@@ -22427,7 +22615,7 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
22427
22615
  * tunnel always emits `https://` regardless. */
22428
22616
  scheme: _enum(["http", "https"]).optional()
22429
22617
  }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
22430
- object({
22618
+ var LockControlStatusSchema = object({
22431
22619
  /** Lifecycle state of the lock. `jammed` means the motor reported
22432
22620
  * failure to reach the target — operator intervention required. */
22433
22621
  state: _enum([
@@ -22765,7 +22953,19 @@ authKey: string().optional() }), object({
22765
22953
  /** Human-readable error when `ok: false`. */
22766
22954
  error: string().optional()
22767
22955
  }), { kind: "mutation" });
22768
- object({
22956
+ /**
22957
+ * Hardware / firmware motion sensor cap — binary detected state plus
22958
+ * a timestamp of the last observation. Distinct from
22959
+ * `motion-detection.cap.ts` which owns the LOCAL ML motion pipeline;
22960
+ * `motion` is the lightweight readout from on-camera motion (Reolink
22961
+ * `GetMdState`, Baichuan push `type: motion`, ONVIF analytics).
22962
+ *
22963
+ * Native-motion providers also fan out to `detection.camera-native`
22964
+ * with `source: 'onboard'` so cross-cutting system services
22965
+ * (alert-center, advanced-notifier) can subscribe once and receive
22966
+ * motion from every camera.
22967
+ */
22968
+ var MotionStatusSchema = object({
22769
22969
  detected: boolean(),
22770
22970
  /** Ms epoch of the last detected-true observation. Null if never detected. */
22771
22971
  lastDetectedAt: number().nullable(),
@@ -23877,7 +24077,7 @@ var GpsLocationSchema = object({
23877
24077
  /** Reported accuracy in meters (lower = better). */
23878
24078
  accuracyMeters: number().nonnegative()
23879
24079
  });
23880
- object({
24080
+ var PresenceStatusSchema = object({
23881
24081
  /** `home` / `not_home` / any user-defined zone name. */
23882
24082
  state: string(),
23883
24083
  /** Optional textual location label (zone name, city, address). Null
@@ -24293,7 +24493,7 @@ method(object({
24293
24493
  toMs: number()
24294
24494
  }), RecordingAvailabilitySchema, {
24295
24495
  kind: "query",
24296
- auth: "admin"
24496
+ auth: "protected"
24297
24497
  }), method(object({
24298
24498
  deviceId: number(),
24299
24499
  fromMs: number(),
@@ -24301,14 +24501,14 @@ method(object({
24301
24501
  tzOffsetMinutes: number()
24302
24502
  }), RecordingDaysSchema, {
24303
24503
  kind: "query",
24304
- auth: "admin"
24504
+ auth: "protected"
24305
24505
  }), method(object({
24306
24506
  deviceId: number(),
24307
24507
  fromMs: number(),
24308
24508
  toMs: number()
24309
24509
  }), RecordingManifestSchema, {
24310
24510
  kind: "query",
24311
- auth: "admin"
24511
+ auth: "protected"
24312
24512
  }), method(object({}), RecordingStorageUsageSchema, {
24313
24513
  kind: "query",
24314
24514
  auth: "admin"
@@ -24837,7 +25037,16 @@ DeviceType.Script, method(object({
24837
25037
  kind: "mutation",
24838
25038
  auth: "admin"
24839
25039
  });
24840
- object({
25040
+ /**
25041
+ * Smoke alarm sensor — boolean "is smoke currently detected" with
25042
+ * timestamp of the last transition. Drives Home Assistant
25043
+ * `binary_sensor` entries with `device_class: smoke`.
25044
+ *
25045
+ * Push-driven: a smoke event is critical, so the slice updates
25046
+ * immediately on the upstream signal. Auto-clearing back to false is
25047
+ * provider-controlled (some alarms latch until manually reset).
25048
+ */
25049
+ var SmokeStatusSchema = object({
24841
25050
  detected: boolean(),
24842
25051
  /** Ms epoch of the last transition. 0 if never observed. */
24843
25052
  lastChangedAt: number()
@@ -25046,7 +25255,23 @@ object({
25046
25255
  lastChangedAt: number()
25047
25256
  });
25048
25257
  DeviceType.Sensor;
25049
- object({
25258
+ /**
25259
+ * Single-metric temperature reading. Drives Home Assistant `sensor`
25260
+ * entries with `device_class: temperature` and any future native
25261
+ * thermometer.
25262
+ *
25263
+ * Unit normalisation: providers convert to Celsius before storing.
25264
+ * The slice value is always Celsius so cross-cap aggregators
25265
+ * (climate-control's `currentTemp`, energy analytics) can compose
25266
+ * without per-source unit fixups. The canonical display unit (`°C`) is
25267
+ * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
25268
+ * `sourceInfo`.
25269
+ *
25270
+ * Status `lastFetchedAt` lets staleness-aware consumers detect a
25271
+ * frozen feed (provider hung) distinct from a "temperature hasn't
25272
+ * changed" steady state.
25273
+ */
25274
+ var TemperatureSensorStatusSchema = object({
25050
25275
  /** Current temperature in Celsius. */
25051
25276
  celsius: number(),
25052
25277
  /** Ms epoch when the slice was last updated (push or poll). */
@@ -31307,6 +31532,1678 @@ Object.freeze({
31307
31532
  access: "create"
31308
31533
  }
31309
31534
  });
31535
+ Object.freeze({
31536
+ "accessories.setChildHidden": [{
31537
+ name: "childDeviceId",
31538
+ form: "single",
31539
+ optional: false
31540
+ }, {
31541
+ name: "deviceId",
31542
+ form: "single",
31543
+ optional: false
31544
+ }],
31545
+ "addonSettings.getDeviceSettings": [{
31546
+ name: "deviceId",
31547
+ form: "single",
31548
+ optional: false
31549
+ }],
31550
+ "addonSettings.updateDeviceSettings": [{
31551
+ name: "deviceId",
31552
+ form: "single",
31553
+ optional: false
31554
+ }],
31555
+ "alarmPanel.arm": [{
31556
+ name: "deviceId",
31557
+ form: "single",
31558
+ optional: false
31559
+ }],
31560
+ "alarmPanel.disarm": [{
31561
+ name: "deviceId",
31562
+ form: "single",
31563
+ optional: false
31564
+ }],
31565
+ "alarmPanel.trigger": [{
31566
+ name: "deviceId",
31567
+ form: "single",
31568
+ optional: false
31569
+ }],
31570
+ "audioAnalysis.resolveDeviceSettings": [{
31571
+ name: "deviceId",
31572
+ form: "single",
31573
+ optional: false
31574
+ }],
31575
+ "audioAnalyzer.classify": [{
31576
+ name: "deviceId",
31577
+ form: "single",
31578
+ optional: true
31579
+ }],
31580
+ "audioMetrics.getCurrentSnapshot": [{
31581
+ name: "deviceId",
31582
+ form: "single",
31583
+ optional: false
31584
+ }],
31585
+ "audioMetrics.getHistory": [{
31586
+ name: "deviceId",
31587
+ form: "single",
31588
+ optional: false
31589
+ }],
31590
+ "automationControl.disable": [{
31591
+ name: "deviceId",
31592
+ form: "single",
31593
+ optional: false
31594
+ }],
31595
+ "automationControl.enable": [{
31596
+ name: "deviceId",
31597
+ form: "single",
31598
+ optional: false
31599
+ }],
31600
+ "automationControl.trigger": [{
31601
+ name: "deviceId",
31602
+ form: "single",
31603
+ optional: false
31604
+ }],
31605
+ "battery.wakeForStream": [{
31606
+ name: "deviceId",
31607
+ form: "single",
31608
+ optional: false
31609
+ }],
31610
+ "brightness.setBrightness": [{
31611
+ name: "deviceId",
31612
+ form: "single",
31613
+ optional: false
31614
+ }],
31615
+ "button.press": [{
31616
+ name: "deviceId",
31617
+ form: "single",
31618
+ optional: false
31619
+ }],
31620
+ "cameraCredentials.getCredentials": [{
31621
+ name: "deviceId",
31622
+ form: "single",
31623
+ optional: false
31624
+ }],
31625
+ "cameraStreams.getBrokerStreams": [{
31626
+ name: "deviceId",
31627
+ form: "single",
31628
+ optional: false
31629
+ }],
31630
+ "cameraStreams.getCameraStreams": [{
31631
+ name: "deviceId",
31632
+ form: "single",
31633
+ optional: false
31634
+ }],
31635
+ "cameraStreams.getProfileRtspEntries": [{
31636
+ name: "deviceId",
31637
+ form: "single",
31638
+ optional: false
31639
+ }],
31640
+ "cameraStreams.getRtspEntries": [{
31641
+ name: "deviceId",
31642
+ form: "single",
31643
+ optional: false
31644
+ }],
31645
+ "cameraStreams.pickStream": [{
31646
+ name: "deviceId",
31647
+ form: "single",
31648
+ optional: false
31649
+ }],
31650
+ "climateControl.setFanMode": [{
31651
+ name: "deviceId",
31652
+ form: "single",
31653
+ optional: false
31654
+ }],
31655
+ "climateControl.setMode": [{
31656
+ name: "deviceId",
31657
+ form: "single",
31658
+ optional: false
31659
+ }],
31660
+ "climateControl.setPreset": [{
31661
+ name: "deviceId",
31662
+ form: "single",
31663
+ optional: false
31664
+ }],
31665
+ "climateControl.setSwingHorizontal": [{
31666
+ name: "deviceId",
31667
+ form: "single",
31668
+ optional: false
31669
+ }],
31670
+ "climateControl.setSwingVertical": [{
31671
+ name: "deviceId",
31672
+ form: "single",
31673
+ optional: false
31674
+ }],
31675
+ "climateControl.setTarget": [{
31676
+ name: "deviceId",
31677
+ form: "single",
31678
+ optional: false
31679
+ }],
31680
+ "climateControl.setTargetHumidity": [{
31681
+ name: "deviceId",
31682
+ form: "single",
31683
+ optional: false
31684
+ }],
31685
+ "climateControl.setTargetRange": [{
31686
+ name: "deviceId",
31687
+ form: "single",
31688
+ optional: false
31689
+ }],
31690
+ "color.setColor": [{
31691
+ name: "deviceId",
31692
+ form: "single",
31693
+ optional: false
31694
+ }],
31695
+ "consumables.reset": [{
31696
+ name: "deviceId",
31697
+ form: "single",
31698
+ optional: false
31699
+ }],
31700
+ "control.setValue": [{
31701
+ name: "deviceId",
31702
+ form: "single",
31703
+ optional: false
31704
+ }],
31705
+ "cover.close": [{
31706
+ name: "deviceId",
31707
+ form: "single",
31708
+ optional: false
31709
+ }],
31710
+ "cover.open": [{
31711
+ name: "deviceId",
31712
+ form: "single",
31713
+ optional: false
31714
+ }],
31715
+ "cover.setPosition": [{
31716
+ name: "deviceId",
31717
+ form: "single",
31718
+ optional: false
31719
+ }],
31720
+ "cover.setTiltPosition": [{
31721
+ name: "deviceId",
31722
+ form: "single",
31723
+ optional: false
31724
+ }],
31725
+ "cover.stop": [{
31726
+ name: "deviceId",
31727
+ form: "single",
31728
+ optional: false
31729
+ }],
31730
+ "dayNight.getOptions": [{
31731
+ name: "deviceId",
31732
+ form: "single",
31733
+ optional: false
31734
+ }],
31735
+ "dayNight.setSettings": [{
31736
+ name: "deviceId",
31737
+ form: "single",
31738
+ optional: false
31739
+ }],
31740
+ "decoder.createSession": [{
31741
+ name: "deviceId",
31742
+ form: "single",
31743
+ optional: true
31744
+ }],
31745
+ "deviceAdoption.release": [{
31746
+ name: "camDeviceId",
31747
+ form: "single",
31748
+ optional: false
31749
+ }],
31750
+ "deviceAdoption.resync": [{
31751
+ name: "camDeviceId",
31752
+ form: "single",
31753
+ optional: false
31754
+ }],
31755
+ "deviceDiscovery.adoptDevice": [{
31756
+ name: "deviceId",
31757
+ form: "single",
31758
+ optional: false
31759
+ }],
31760
+ "deviceDiscovery.listDiscovered": [{
31761
+ name: "deviceId",
31762
+ form: "single",
31763
+ optional: false
31764
+ }],
31765
+ "deviceDiscovery.refreshDiscovery": [{
31766
+ name: "deviceId",
31767
+ form: "single",
31768
+ optional: false
31769
+ }],
31770
+ "deviceDiscovery.releaseDevice": [{
31771
+ name: "childDeviceId",
31772
+ form: "single",
31773
+ optional: false
31774
+ }, {
31775
+ name: "deviceId",
31776
+ form: "single",
31777
+ optional: false
31778
+ }],
31779
+ "deviceManager.adoptionRelease": [{
31780
+ name: "camDeviceId",
31781
+ form: "single",
31782
+ optional: false
31783
+ }],
31784
+ "deviceManager.adoptionResync": [{
31785
+ name: "camDeviceId",
31786
+ form: "single",
31787
+ optional: false
31788
+ }],
31789
+ "deviceManager.applyInitialMeta": [{
31790
+ name: "deviceId",
31791
+ form: "single",
31792
+ optional: false
31793
+ }, {
31794
+ name: "linkDeviceId",
31795
+ form: "single",
31796
+ optional: true
31797
+ }],
31798
+ "deviceManager.disable": [{
31799
+ name: "deviceId",
31800
+ form: "single",
31801
+ optional: false
31802
+ }],
31803
+ "deviceManager.enable": [{
31804
+ name: "deviceId",
31805
+ form: "single",
31806
+ optional: false
31807
+ }],
31808
+ "deviceManager.getBindings": [{
31809
+ name: "deviceId",
31810
+ form: "single",
31811
+ optional: false
31812
+ }],
31813
+ "deviceManager.getChildren": [{
31814
+ name: "parentDeviceId",
31815
+ form: "single",
31816
+ optional: false
31817
+ }],
31818
+ "deviceManager.getConfigSchema": [{
31819
+ name: "deviceId",
31820
+ form: "single",
31821
+ optional: false
31822
+ }],
31823
+ "deviceManager.getDevice": [{
31824
+ name: "deviceId",
31825
+ form: "single",
31826
+ optional: false
31827
+ }],
31828
+ "deviceManager.getDeviceAggregate": [{
31829
+ name: "deviceId",
31830
+ form: "single",
31831
+ optional: false
31832
+ }],
31833
+ "deviceManager.getDeviceLiveInfoAggregate": [{
31834
+ name: "deviceId",
31835
+ form: "single",
31836
+ optional: false
31837
+ }],
31838
+ "deviceManager.getDeviceSettingsAggregate": [{
31839
+ name: "deviceId",
31840
+ form: "single",
31841
+ optional: false
31842
+ }],
31843
+ "deviceManager.getDeviceStatusAggregate": [{
31844
+ name: "deviceId",
31845
+ form: "single",
31846
+ optional: false
31847
+ }],
31848
+ "deviceManager.getDeviceStatusAggregateBatch": [{
31849
+ name: "deviceIds",
31850
+ form: "array",
31851
+ optional: false
31852
+ }],
31853
+ "deviceManager.getLinkedDevices": [{
31854
+ name: "deviceId",
31855
+ form: "single",
31856
+ optional: false
31857
+ }],
31858
+ "deviceManager.getSettingsSchema": [{
31859
+ name: "deviceId",
31860
+ form: "single",
31861
+ optional: false
31862
+ }],
31863
+ "deviceManager.getStreamProfileMap": [{
31864
+ name: "deviceId",
31865
+ form: "single",
31866
+ optional: false
31867
+ }],
31868
+ "deviceManager.getStreamSources": [{
31869
+ name: "deviceId",
31870
+ form: "single",
31871
+ optional: false
31872
+ }],
31873
+ "deviceManager.getWireableFields": [{
31874
+ name: "deviceId",
31875
+ form: "single",
31876
+ optional: false
31877
+ }],
31878
+ "deviceManager.loadConfig": [{
31879
+ name: "deviceId",
31880
+ form: "single",
31881
+ optional: false
31882
+ }],
31883
+ "deviceManager.loadMeta": [{
31884
+ name: "deviceId",
31885
+ form: "single",
31886
+ optional: false
31887
+ }],
31888
+ "deviceManager.loadRuntimeState": [{
31889
+ name: "deviceId",
31890
+ form: "single",
31891
+ optional: false
31892
+ }],
31893
+ "deviceManager.persistConfig": [{
31894
+ name: "deviceId",
31895
+ form: "single",
31896
+ optional: false
31897
+ }],
31898
+ "deviceManager.probeStreams": [{
31899
+ name: "deviceId",
31900
+ form: "single",
31901
+ optional: false
31902
+ }],
31903
+ "deviceManager.registerDevice": [{
31904
+ name: "parentDeviceId",
31905
+ form: "single",
31906
+ optional: true
31907
+ }],
31908
+ "deviceManager.remove": [{
31909
+ name: "deviceId",
31910
+ form: "single",
31911
+ optional: false
31912
+ }],
31913
+ "deviceManager.removeDevice": [{
31914
+ name: "deviceId",
31915
+ form: "single",
31916
+ optional: false
31917
+ }],
31918
+ "deviceManager.runDeviceAction": [{
31919
+ name: "deviceId",
31920
+ form: "single",
31921
+ optional: false
31922
+ }],
31923
+ "deviceManager.setChildLayout": [{
31924
+ name: "deviceId",
31925
+ form: "single",
31926
+ optional: false
31927
+ }],
31928
+ "deviceManager.setDisabled": [{
31929
+ name: "deviceId",
31930
+ form: "single",
31931
+ optional: false
31932
+ }],
31933
+ "deviceManager.setDisplay": [{
31934
+ name: "deviceId",
31935
+ form: "single",
31936
+ optional: false
31937
+ }],
31938
+ "deviceManager.setIntegrationId": [{
31939
+ name: "deviceId",
31940
+ form: "single",
31941
+ optional: false
31942
+ }],
31943
+ "deviceManager.setLinkDeviceId": [{
31944
+ name: "deviceId",
31945
+ form: "single",
31946
+ optional: false
31947
+ }, {
31948
+ name: "linkDeviceId",
31949
+ form: "single",
31950
+ optional: true
31951
+ }],
31952
+ "deviceManager.setLocation": [{
31953
+ name: "deviceId",
31954
+ form: "single",
31955
+ optional: false
31956
+ }],
31957
+ "deviceManager.setMetadata": [{
31958
+ name: "deviceId",
31959
+ form: "single",
31960
+ optional: false
31961
+ }],
31962
+ "deviceManager.setName": [{
31963
+ name: "deviceId",
31964
+ form: "single",
31965
+ optional: false
31966
+ }],
31967
+ "deviceManager.setPrimaryChildEntityId": [{
31968
+ name: "deviceId",
31969
+ form: "single",
31970
+ optional: false
31971
+ }],
31972
+ "deviceManager.setRole": [{
31973
+ name: "deviceId",
31974
+ form: "single",
31975
+ optional: false
31976
+ }],
31977
+ "deviceManager.setStreamProfileMap": [{
31978
+ name: "deviceId",
31979
+ form: "single",
31980
+ optional: false
31981
+ }],
31982
+ "deviceManager.setType": [{
31983
+ name: "deviceId",
31984
+ form: "single",
31985
+ optional: false
31986
+ }],
31987
+ "deviceManager.setWrapperActive": [{
31988
+ name: "deviceId",
31989
+ form: "single",
31990
+ optional: false
31991
+ }],
31992
+ "deviceManager.testField": [{
31993
+ name: "deviceId",
31994
+ form: "single",
31995
+ optional: false
31996
+ }],
31997
+ "deviceManager.updateConfig": [{
31998
+ name: "deviceId",
31999
+ form: "single",
32000
+ optional: false
32001
+ }],
32002
+ "deviceManager.updateDeviceField": [{
32003
+ name: "deviceId",
32004
+ form: "single",
32005
+ optional: false
32006
+ }],
32007
+ "deviceManager.updateDeviceFieldsBatch": [{
32008
+ name: "deviceId",
32009
+ form: "single",
32010
+ optional: false
32011
+ }],
32012
+ "deviceOps.getConfigEntries": [{
32013
+ name: "deviceId",
32014
+ form: "single",
32015
+ optional: false
32016
+ }],
32017
+ "deviceOps.getRawState": [{
32018
+ name: "deviceId",
32019
+ form: "single",
32020
+ optional: false
32021
+ }],
32022
+ "deviceOps.getSettingsSchema": [{
32023
+ name: "deviceId",
32024
+ form: "single",
32025
+ optional: false
32026
+ }],
32027
+ "deviceOps.getStreamSources": [{
32028
+ name: "deviceId",
32029
+ form: "single",
32030
+ optional: false
32031
+ }],
32032
+ "deviceOps.removeDevice": [{
32033
+ name: "deviceId",
32034
+ form: "single",
32035
+ optional: false
32036
+ }],
32037
+ "deviceOps.runAction": [{
32038
+ name: "deviceId",
32039
+ form: "single",
32040
+ optional: false
32041
+ }],
32042
+ "deviceOps.setConfig": [{
32043
+ name: "deviceId",
32044
+ form: "single",
32045
+ optional: false
32046
+ }],
32047
+ "deviceState.getCapSlice": [{
32048
+ name: "deviceId",
32049
+ form: "single",
32050
+ optional: false
32051
+ }],
32052
+ "deviceState.getSnapshot": [{
32053
+ name: "deviceId",
32054
+ form: "single",
32055
+ optional: false
32056
+ }],
32057
+ "deviceState.setCapSlice": [{
32058
+ name: "deviceId",
32059
+ form: "single",
32060
+ optional: false
32061
+ }],
32062
+ "events.getEventClipUrl": [{
32063
+ name: "deviceId",
32064
+ form: "single",
32065
+ optional: false
32066
+ }],
32067
+ "events.getEvents": [{
32068
+ name: "deviceId",
32069
+ form: "single",
32070
+ optional: false
32071
+ }],
32072
+ "events.getEventThumbnail": [{
32073
+ name: "deviceId",
32074
+ form: "single",
32075
+ optional: false
32076
+ }],
32077
+ "faceGallery.getFaceByTrack": [{
32078
+ name: "deviceId",
32079
+ form: "single",
32080
+ optional: false
32081
+ }],
32082
+ "faceGallery.listRecentFaces": [{
32083
+ name: "deviceId",
32084
+ form: "single",
32085
+ optional: true
32086
+ }],
32087
+ "fanControl.setDirection": [{
32088
+ name: "deviceId",
32089
+ form: "single",
32090
+ optional: false
32091
+ }],
32092
+ "fanControl.setOscillating": [{
32093
+ name: "deviceId",
32094
+ form: "single",
32095
+ optional: false
32096
+ }],
32097
+ "fanControl.setPercentage": [{
32098
+ name: "deviceId",
32099
+ form: "single",
32100
+ optional: false
32101
+ }],
32102
+ "fanControl.setPreset": [{
32103
+ name: "deviceId",
32104
+ form: "single",
32105
+ optional: false
32106
+ }],
32107
+ "humidifier.setMode": [{
32108
+ name: "deviceId",
32109
+ form: "single",
32110
+ optional: false
32111
+ }],
32112
+ "humidifier.setOn": [{
32113
+ name: "deviceId",
32114
+ form: "single",
32115
+ optional: false
32116
+ }],
32117
+ "humidifier.setTargetHumidity": [{
32118
+ name: "deviceId",
32119
+ form: "single",
32120
+ optional: false
32121
+ }],
32122
+ "imageSettings.getOptions": [{
32123
+ name: "deviceId",
32124
+ form: "single",
32125
+ optional: false
32126
+ }],
32127
+ "imageSettings.setSettings": [{
32128
+ name: "deviceId",
32129
+ form: "single",
32130
+ optional: false
32131
+ }],
32132
+ "intercom.endTalkSession": [{
32133
+ name: "deviceId",
32134
+ form: "single",
32135
+ optional: false
32136
+ }],
32137
+ "intercom.handleAnswer": [{
32138
+ name: "deviceId",
32139
+ form: "single",
32140
+ optional: false
32141
+ }],
32142
+ "intercom.pushTalkAudio": [{
32143
+ name: "deviceId",
32144
+ form: "single",
32145
+ optional: false
32146
+ }],
32147
+ "intercom.startSession": [{
32148
+ name: "deviceId",
32149
+ form: "single",
32150
+ optional: false
32151
+ }],
32152
+ "intercom.startTalkSession": [{
32153
+ name: "deviceId",
32154
+ form: "single",
32155
+ optional: false
32156
+ }],
32157
+ "intercom.stopSession": [{
32158
+ name: "deviceId",
32159
+ form: "single",
32160
+ optional: false
32161
+ }],
32162
+ "lawnMowerControl.dock": [{
32163
+ name: "deviceId",
32164
+ form: "single",
32165
+ optional: false
32166
+ }],
32167
+ "lawnMowerControl.pause": [{
32168
+ name: "deviceId",
32169
+ form: "single",
32170
+ optional: false
32171
+ }],
32172
+ "lawnMowerControl.startMowing": [{
32173
+ name: "deviceId",
32174
+ form: "single",
32175
+ optional: false
32176
+ }],
32177
+ "lockControl.lock": [{
32178
+ name: "deviceId",
32179
+ form: "single",
32180
+ optional: false
32181
+ }],
32182
+ "lockControl.open": [{
32183
+ name: "deviceId",
32184
+ form: "single",
32185
+ optional: false
32186
+ }],
32187
+ "lockControl.unlock": [{
32188
+ name: "deviceId",
32189
+ form: "single",
32190
+ optional: false
32191
+ }],
32192
+ "mediaPlayer.next": [{
32193
+ name: "deviceId",
32194
+ form: "single",
32195
+ optional: false
32196
+ }],
32197
+ "mediaPlayer.pause": [{
32198
+ name: "deviceId",
32199
+ form: "single",
32200
+ optional: false
32201
+ }],
32202
+ "mediaPlayer.play": [{
32203
+ name: "deviceId",
32204
+ form: "single",
32205
+ optional: false
32206
+ }],
32207
+ "mediaPlayer.playMedia": [{
32208
+ name: "deviceId",
32209
+ form: "single",
32210
+ optional: false
32211
+ }],
32212
+ "mediaPlayer.previous": [{
32213
+ name: "deviceId",
32214
+ form: "single",
32215
+ optional: false
32216
+ }],
32217
+ "mediaPlayer.seek": [{
32218
+ name: "deviceId",
32219
+ form: "single",
32220
+ optional: false
32221
+ }],
32222
+ "mediaPlayer.selectSource": [{
32223
+ name: "deviceId",
32224
+ form: "single",
32225
+ optional: false
32226
+ }],
32227
+ "mediaPlayer.setMute": [{
32228
+ name: "deviceId",
32229
+ form: "single",
32230
+ optional: false
32231
+ }],
32232
+ "mediaPlayer.setRepeat": [{
32233
+ name: "deviceId",
32234
+ form: "single",
32235
+ optional: false
32236
+ }],
32237
+ "mediaPlayer.setShuffle": [{
32238
+ name: "deviceId",
32239
+ form: "single",
32240
+ optional: false
32241
+ }],
32242
+ "mediaPlayer.setVolume": [{
32243
+ name: "deviceId",
32244
+ form: "single",
32245
+ optional: false
32246
+ }],
32247
+ "mediaPlayer.stop": [{
32248
+ name: "deviceId",
32249
+ form: "single",
32250
+ optional: false
32251
+ }],
32252
+ "motion.isDetected": [{
32253
+ name: "deviceId",
32254
+ form: "single",
32255
+ optional: false
32256
+ }],
32257
+ "motionDetection.analyze": [{
32258
+ name: "deviceId",
32259
+ form: "single",
32260
+ optional: false
32261
+ }],
32262
+ "motionDetection.removeCamera": [{
32263
+ name: "deviceId",
32264
+ form: "single",
32265
+ optional: false
32266
+ }],
32267
+ "motionTrigger.setMotionTrigger": [{
32268
+ name: "deviceId",
32269
+ form: "single",
32270
+ optional: false
32271
+ }],
32272
+ "motionZones.getOptions": [{
32273
+ name: "deviceId",
32274
+ form: "single",
32275
+ optional: false
32276
+ }],
32277
+ "motionZones.setZone": [{
32278
+ name: "deviceId",
32279
+ form: "single",
32280
+ optional: false
32281
+ }],
32282
+ "nativeObjectDetection.setEnabled": [{
32283
+ name: "deviceId",
32284
+ form: "single",
32285
+ optional: false
32286
+ }],
32287
+ "networkQuality.getDeviceStats": [{
32288
+ name: "deviceId",
32289
+ form: "single",
32290
+ optional: false
32291
+ }],
32292
+ "networkQuality.reportClientStats": [{
32293
+ name: "deviceId",
32294
+ form: "single",
32295
+ optional: false
32296
+ }],
32297
+ "notificationRules.setDeviceMuted": [{
32298
+ name: "deviceId",
32299
+ form: "single",
32300
+ optional: false
32301
+ }],
32302
+ "notifier.cancel": [{
32303
+ name: "deviceId",
32304
+ form: "single",
32305
+ optional: false
32306
+ }],
32307
+ "notifier.send": [{
32308
+ name: "deviceId",
32309
+ form: "single",
32310
+ optional: false
32311
+ }],
32312
+ "osd.setOverlay": [{
32313
+ name: "deviceId",
32314
+ form: "single",
32315
+ optional: false
32316
+ }],
32317
+ "osdManager.clearSlotBinding": [{
32318
+ name: "deviceId",
32319
+ form: "single",
32320
+ optional: false
32321
+ }],
32322
+ "osdManager.copyDeviceConfiguration": [{
32323
+ name: "sourceDeviceId",
32324
+ form: "single",
32325
+ optional: false
32326
+ }, {
32327
+ name: "targetDeviceId",
32328
+ form: "single",
32329
+ optional: false
32330
+ }],
32331
+ "osdManager.getDeviceOsd": [{
32332
+ name: "deviceId",
32333
+ form: "single",
32334
+ optional: false
32335
+ }],
32336
+ "osdManager.getSourceCatalog": [{
32337
+ name: "deviceId",
32338
+ form: "single",
32339
+ optional: false
32340
+ }],
32341
+ "osdManager.previewSlot": [{
32342
+ name: "deviceId",
32343
+ form: "single",
32344
+ optional: false
32345
+ }],
32346
+ "osdManager.renderDevice": [{
32347
+ name: "deviceId",
32348
+ form: "single",
32349
+ optional: false
32350
+ }],
32351
+ "osdManager.setSlotBinding": [{
32352
+ name: "deviceId",
32353
+ form: "single",
32354
+ optional: false
32355
+ }],
32356
+ "petFeeder.callPet": [{
32357
+ name: "deviceId",
32358
+ form: "single",
32359
+ optional: false
32360
+ }],
32361
+ "petFeeder.cancelFeed": [{
32362
+ name: "deviceId",
32363
+ form: "single",
32364
+ optional: false
32365
+ }],
32366
+ "petFeeder.feed": [{
32367
+ name: "deviceId",
32368
+ form: "single",
32369
+ optional: false
32370
+ }],
32371
+ "petFeeder.markFoodReplenished": [{
32372
+ name: "deviceId",
32373
+ form: "single",
32374
+ optional: false
32375
+ }],
32376
+ "petFeeder.playSound": [{
32377
+ name: "deviceId",
32378
+ form: "single",
32379
+ optional: false
32380
+ }],
32381
+ "petFeeder.resetDesiccant": [{
32382
+ name: "deviceId",
32383
+ form: "single",
32384
+ optional: false
32385
+ }],
32386
+ "petFeeder.setChildLock": [{
32387
+ name: "deviceId",
32388
+ form: "single",
32389
+ optional: false
32390
+ }],
32391
+ "petFeeder.setFeedSound": [{
32392
+ name: "deviceId",
32393
+ form: "single",
32394
+ optional: false
32395
+ }],
32396
+ "petFeeder.setIndicatorLight": [{
32397
+ name: "deviceId",
32398
+ form: "single",
32399
+ optional: false
32400
+ }],
32401
+ "petFeeder.setVolume": [{
32402
+ name: "deviceId",
32403
+ form: "single",
32404
+ optional: false
32405
+ }],
32406
+ "pipelineAnalytics.clearTracks": [{
32407
+ name: "deviceId",
32408
+ form: "single",
32409
+ optional: false
32410
+ }],
32411
+ "pipelineAnalytics.completeRetrainTrack": [{
32412
+ name: "deviceId",
32413
+ form: "single",
32414
+ optional: false
32415
+ }],
32416
+ "pipelineAnalytics.deleteDeviceEvents": [{
32417
+ name: "deviceId",
32418
+ form: "single",
32419
+ optional: false
32420
+ }],
32421
+ "pipelineAnalytics.deleteTracks": [{
32422
+ name: "deviceId",
32423
+ form: "single",
32424
+ optional: false
32425
+ }],
32426
+ "pipelineAnalytics.deselectRetrainFrame": [{
32427
+ name: "deviceId",
32428
+ form: "single",
32429
+ optional: false
32430
+ }],
32431
+ "pipelineAnalytics.getActiveTracks": [{
32432
+ name: "deviceId",
32433
+ form: "single",
32434
+ optional: false
32435
+ }],
32436
+ "pipelineAnalytics.getAudioEvents": [{
32437
+ name: "deviceId",
32438
+ form: "single",
32439
+ optional: false
32440
+ }],
32441
+ "pipelineAnalytics.getEventDensity": [{
32442
+ name: "deviceId",
32443
+ form: "single",
32444
+ optional: false
32445
+ }],
32446
+ "pipelineAnalytics.getKeyEvents": [{
32447
+ name: "deviceId",
32448
+ form: "single",
32449
+ optional: false
32450
+ }],
32451
+ "pipelineAnalytics.getMotionEvents": [{
32452
+ name: "deviceId",
32453
+ form: "single",
32454
+ optional: false
32455
+ }],
32456
+ "pipelineAnalytics.getObjectEvents": [{
32457
+ name: "deviceId",
32458
+ form: "single",
32459
+ optional: false
32460
+ }],
32461
+ "pipelineAnalytics.getRetrainExportUrl": [{
32462
+ name: "deviceIds",
32463
+ form: "array",
32464
+ optional: true
32465
+ }],
32466
+ "pipelineAnalytics.getSensorEvents": [{
32467
+ name: "deviceId",
32468
+ form: "single",
32469
+ optional: false
32470
+ }],
32471
+ "pipelineAnalytics.getTrack": [{
32472
+ name: "deviceId",
32473
+ form: "single",
32474
+ optional: false
32475
+ }],
32476
+ "pipelineAnalytics.getTrainingExportSummary": [{
32477
+ name: "deviceIds",
32478
+ form: "array",
32479
+ optional: true
32480
+ }],
32481
+ "pipelineAnalytics.getTrainingExportUrl": [{
32482
+ name: "deviceIds",
32483
+ form: "array",
32484
+ optional: true
32485
+ }],
32486
+ "pipelineAnalytics.listEventKinds": [{
32487
+ name: "deviceId",
32488
+ form: "single",
32489
+ optional: false
32490
+ }],
32491
+ "pipelineAnalytics.listEventKindsBatch": [{
32492
+ name: "deviceIds",
32493
+ form: "array",
32494
+ optional: false
32495
+ }],
32496
+ "pipelineAnalytics.listOpsLog": [{
32497
+ name: "deviceId",
32498
+ form: "single",
32499
+ optional: true
32500
+ }],
32501
+ "pipelineAnalytics.listRecentTracks": [{
32502
+ name: "deviceIds",
32503
+ form: "array",
32504
+ optional: false
32505
+ }],
32506
+ "pipelineAnalytics.listRetrainStaging": [{
32507
+ name: "deviceIds",
32508
+ form: "array",
32509
+ optional: true
32510
+ }],
32511
+ "pipelineAnalytics.listTracks": [{
32512
+ name: "deviceId",
32513
+ form: "single",
32514
+ optional: false
32515
+ }],
32516
+ "pipelineAnalytics.proposeRetrainAnnotations": [{
32517
+ name: "deviceId",
32518
+ form: "single",
32519
+ optional: false
32520
+ }],
32521
+ "pipelineAnalytics.pruneEventsBefore": [{
32522
+ name: "deviceId",
32523
+ form: "single",
32524
+ optional: false
32525
+ }],
32526
+ "pipelineAnalytics.pruneTracksBefore": [{
32527
+ name: "deviceId",
32528
+ form: "single",
32529
+ optional: false
32530
+ }],
32531
+ "pipelineAnalytics.rebuildObjectEmbeddings": [{
32532
+ name: "deviceId",
32533
+ form: "single",
32534
+ optional: true
32535
+ }],
32536
+ "pipelineAnalytics.restageRetrainTrack": [{
32537
+ name: "deviceId",
32538
+ form: "single",
32539
+ optional: false
32540
+ }],
32541
+ "pipelineAnalytics.saveRetrainAnnotations": [{
32542
+ name: "deviceId",
32543
+ form: "single",
32544
+ optional: false
32545
+ }],
32546
+ "pipelineAnalytics.searchObjectEvents": [{
32547
+ name: "deviceId",
32548
+ form: "single",
32549
+ optional: true
32550
+ }],
32551
+ "pipelineAnalytics.selectRetrainFrames": [{
32552
+ name: "deviceId",
32553
+ form: "single",
32554
+ optional: false
32555
+ }],
32556
+ "pipelineAnalytics.setTrackFlags": [{
32557
+ name: "deviceId",
32558
+ form: "single",
32559
+ optional: false
32560
+ }],
32561
+ "pipelineAnalytics.wipeAllAnalytics": [{
32562
+ name: "deviceId",
32563
+ form: "single",
32564
+ optional: false
32565
+ }],
32566
+ "pipelineExecutor.runPipeline": [{
32567
+ name: "deviceId",
32568
+ form: "single",
32569
+ optional: true
32570
+ }],
32571
+ "pipelineExecutor.runPipelineBatch": [{
32572
+ name: "deviceId",
32573
+ form: "single",
32574
+ optional: true
32575
+ }],
32576
+ "pipelineOrchestrator.assignAudio": [{
32577
+ name: "deviceId",
32578
+ form: "single",
32579
+ optional: false
32580
+ }],
32581
+ "pipelineOrchestrator.assignPipeline": [{
32582
+ name: "deviceId",
32583
+ form: "single",
32584
+ optional: false
32585
+ }],
32586
+ "pipelineOrchestrator.getAudioAssignment": [{
32587
+ name: "deviceId",
32588
+ form: "single",
32589
+ optional: false
32590
+ }],
32591
+ "pipelineOrchestrator.getCameraMetrics": [{
32592
+ name: "deviceId",
32593
+ form: "single",
32594
+ optional: false
32595
+ }],
32596
+ "pipelineOrchestrator.getCameraSettings": [{
32597
+ name: "deviceId",
32598
+ form: "single",
32599
+ optional: false
32600
+ }],
32601
+ "pipelineOrchestrator.getCameraStatus": [{
32602
+ name: "deviceId",
32603
+ form: "single",
32604
+ optional: false
32605
+ }],
32606
+ "pipelineOrchestrator.getCameraStatuses": [{
32607
+ name: "deviceIds",
32608
+ form: "array",
32609
+ optional: true
32610
+ }],
32611
+ "pipelineOrchestrator.getCameraStepOverrides": [{
32612
+ name: "deviceId",
32613
+ form: "single",
32614
+ optional: false
32615
+ }],
32616
+ "pipelineOrchestrator.getCameraSwitches": [{
32617
+ name: "deviceId",
32618
+ form: "single",
32619
+ optional: false
32620
+ }],
32621
+ "pipelineOrchestrator.getPipelineAssignment": [{
32622
+ name: "deviceId",
32623
+ form: "single",
32624
+ optional: false
32625
+ }],
32626
+ "pipelineOrchestrator.getPipelineDevicePin": [{
32627
+ name: "deviceId",
32628
+ form: "single",
32629
+ optional: false
32630
+ }],
32631
+ "pipelineOrchestrator.resolvePipeline": [{
32632
+ name: "deviceId",
32633
+ form: "single",
32634
+ optional: false
32635
+ }],
32636
+ "pipelineOrchestrator.setCameraPipelineForAgent": [{
32637
+ name: "deviceId",
32638
+ form: "single",
32639
+ optional: false
32640
+ }],
32641
+ "pipelineOrchestrator.setCameraStepOverride": [{
32642
+ name: "deviceId",
32643
+ form: "single",
32644
+ optional: false
32645
+ }],
32646
+ "pipelineOrchestrator.setCameraStepToggle": [{
32647
+ name: "deviceId",
32648
+ form: "single",
32649
+ optional: false
32650
+ }],
32651
+ "pipelineOrchestrator.setCameraSwitch": [{
32652
+ name: "deviceId",
32653
+ form: "single",
32654
+ optional: false
32655
+ }],
32656
+ "pipelineOrchestrator.setPipelineDevicePin": [{
32657
+ name: "deviceId",
32658
+ form: "single",
32659
+ optional: false
32660
+ }],
32661
+ "pipelineOrchestrator.unassignAudio": [{
32662
+ name: "deviceId",
32663
+ form: "single",
32664
+ optional: false
32665
+ }],
32666
+ "pipelineOrchestrator.unassignPipeline": [{
32667
+ name: "deviceId",
32668
+ form: "single",
32669
+ optional: false
32670
+ }],
32671
+ "pipelineRunner.attachCamera": [{
32672
+ name: "deviceId",
32673
+ form: "single",
32674
+ optional: false
32675
+ }],
32676
+ "pipelineRunner.detachCamera": [{
32677
+ name: "deviceId",
32678
+ form: "single",
32679
+ optional: false
32680
+ }],
32681
+ "pipelineRunner.getCameraMetrics": [{
32682
+ name: "deviceId",
32683
+ form: "single",
32684
+ optional: false
32685
+ }],
32686
+ "pipelineRunner.reportMotion": [{
32687
+ name: "deviceId",
32688
+ form: "single",
32689
+ optional: false
32690
+ }],
32691
+ "pipelineRunner.runDetailSubtree": [{
32692
+ name: "deviceId",
32693
+ form: "single",
32694
+ optional: false
32695
+ }],
32696
+ "pipelineRunner.runStatelessStep": [{
32697
+ name: "sourceDeviceId",
32698
+ form: "single",
32699
+ optional: false
32700
+ }],
32701
+ "plateGallery.getPlateByTrack": [{
32702
+ name: "deviceId",
32703
+ form: "single",
32704
+ optional: false
32705
+ }],
32706
+ "plateGallery.listPlates": [{
32707
+ name: "deviceId",
32708
+ form: "single",
32709
+ optional: true
32710
+ }],
32711
+ "privacyMask.getOptions": [{
32712
+ name: "deviceId",
32713
+ form: "single",
32714
+ optional: false
32715
+ }],
32716
+ "privacyMask.setAudioEnabled": [{
32717
+ name: "deviceId",
32718
+ form: "single",
32719
+ optional: false
32720
+ }],
32721
+ "privacyMask.setMask": [{
32722
+ name: "deviceId",
32723
+ form: "single",
32724
+ optional: false
32725
+ }],
32726
+ "ptz.continuousMove": [{
32727
+ name: "deviceId",
32728
+ form: "single",
32729
+ optional: false
32730
+ }],
32731
+ "ptz.deletePreset": [{
32732
+ name: "deviceId",
32733
+ form: "single",
32734
+ optional: false
32735
+ }],
32736
+ "ptz.getOptions": [{
32737
+ name: "deviceId",
32738
+ form: "single",
32739
+ optional: false
32740
+ }],
32741
+ "ptz.getPosition": [{
32742
+ name: "deviceId",
32743
+ form: "single",
32744
+ optional: false
32745
+ }],
32746
+ "ptz.getPresets": [{
32747
+ name: "deviceId",
32748
+ form: "single",
32749
+ optional: false
32750
+ }],
32751
+ "ptz.goHome": [{
32752
+ name: "deviceId",
32753
+ form: "single",
32754
+ optional: false
32755
+ }],
32756
+ "ptz.goToPreset": [{
32757
+ name: "deviceId",
32758
+ form: "single",
32759
+ optional: false
32760
+ }],
32761
+ "ptz.move": [{
32762
+ name: "deviceId",
32763
+ form: "single",
32764
+ optional: false
32765
+ }],
32766
+ "ptz.savePreset": [{
32767
+ name: "deviceId",
32768
+ form: "single",
32769
+ optional: false
32770
+ }],
32771
+ "ptz.setAutofocus": [{
32772
+ name: "deviceId",
32773
+ form: "single",
32774
+ optional: false
32775
+ }],
32776
+ "ptz.stop": [{
32777
+ name: "deviceId",
32778
+ form: "single",
32779
+ optional: false
32780
+ }],
32781
+ "ptzAutotrack.getSettings": [{
32782
+ name: "deviceId",
32783
+ form: "single",
32784
+ optional: false
32785
+ }],
32786
+ "ptzAutotrack.getStatus": [{
32787
+ name: "deviceId",
32788
+ form: "single",
32789
+ optional: false
32790
+ }],
32791
+ "ptzAutotrack.setEnabled": [{
32792
+ name: "deviceId",
32793
+ form: "single",
32794
+ optional: false
32795
+ }],
32796
+ "ptzAutotrack.setSettings": [{
32797
+ name: "deviceId",
32798
+ form: "single",
32799
+ optional: false
32800
+ }],
32801
+ "reboot.reboot": [{
32802
+ name: "deviceId",
32803
+ form: "single",
32804
+ optional: false
32805
+ }],
32806
+ "recording.deleteFootprint": [{
32807
+ name: "deviceId",
32808
+ form: "single",
32809
+ optional: false
32810
+ }],
32811
+ "recording.getAvailability": [{
32812
+ name: "deviceId",
32813
+ form: "single",
32814
+ optional: false
32815
+ }],
32816
+ "recording.getDaysWithRecordings": [{
32817
+ name: "deviceId",
32818
+ form: "single",
32819
+ optional: false
32820
+ }],
32821
+ "recording.getDeviceConfig": [{
32822
+ name: "deviceId",
32823
+ form: "single",
32824
+ optional: false
32825
+ }],
32826
+ "recording.getPlaybackManifest": [{
32827
+ name: "deviceId",
32828
+ form: "single",
32829
+ optional: false
32830
+ }],
32831
+ "recording.listOpsLog": [{
32832
+ name: "deviceId",
32833
+ form: "single",
32834
+ optional: true
32835
+ }],
32836
+ "recording.locateSegment": [{
32837
+ name: "deviceId",
32838
+ form: "single",
32839
+ optional: false
32840
+ }],
32841
+ "recording.pruneFootage": [{
32842
+ name: "deviceId",
32843
+ form: "single",
32844
+ optional: false
32845
+ }],
32846
+ "recording.readGopBytes": [{
32847
+ name: "deviceId",
32848
+ form: "single",
32849
+ optional: false
32850
+ }],
32851
+ "recording.readSegmentBytes": [{
32852
+ name: "deviceId",
32853
+ form: "single",
32854
+ optional: false
32855
+ }],
32856
+ "recording.relocateFootage": [{
32857
+ name: "deviceId",
32858
+ form: "single",
32859
+ optional: true
32860
+ }],
32861
+ "recording.renderClip": [{
32862
+ name: "deviceId",
32863
+ form: "single",
32864
+ optional: false
32865
+ }],
32866
+ "recording.renderGif": [{
32867
+ name: "deviceId",
32868
+ form: "single",
32869
+ optional: false
32870
+ }],
32871
+ "recording.rescanStorage": [{
32872
+ name: "deviceId",
32873
+ form: "single",
32874
+ optional: false
32875
+ }],
32876
+ "recording.setDeviceConfig": [{
32877
+ name: "deviceId",
32878
+ form: "single",
32879
+ optional: false
32880
+ }],
32881
+ "recording.startStorageMigrationMove": [{
32882
+ name: "deviceId",
32883
+ form: "single",
32884
+ optional: true
32885
+ }],
32886
+ "recordingExport.createExport": [{
32887
+ name: "deviceId",
32888
+ form: "single",
32889
+ optional: false
32890
+ }],
32891
+ "recordingExport.listExports": [{
32892
+ name: "deviceId",
32893
+ form: "single",
32894
+ optional: true
32895
+ }],
32896
+ "sceneMonitor.captureReference": [{
32897
+ name: "deviceId",
32898
+ form: "single",
32899
+ optional: false
32900
+ }],
32901
+ "sceneMonitor.createScene": [{
32902
+ name: "deviceId",
32903
+ form: "single",
32904
+ optional: false
32905
+ }],
32906
+ "sceneMonitor.deleteReference": [{
32907
+ name: "deviceId",
32908
+ form: "single",
32909
+ optional: false
32910
+ }],
32911
+ "sceneMonitor.deleteScene": [{
32912
+ name: "deviceId",
32913
+ form: "single",
32914
+ optional: false
32915
+ }],
32916
+ "sceneMonitor.listScenes": [{
32917
+ name: "deviceId",
32918
+ form: "single",
32919
+ optional: false
32920
+ }],
32921
+ "sceneMonitor.recheckNow": [{
32922
+ name: "deviceId",
32923
+ form: "single",
32924
+ optional: false
32925
+ }],
32926
+ "sceneMonitor.updateScene": [{
32927
+ name: "deviceId",
32928
+ form: "single",
32929
+ optional: false
32930
+ }],
32931
+ "scriptRunner.run": [{
32932
+ name: "deviceId",
32933
+ form: "single",
32934
+ optional: false
32935
+ }],
32936
+ "scriptRunner.stop": [{
32937
+ name: "deviceId",
32938
+ form: "single",
32939
+ optional: false
32940
+ }],
32941
+ "snapshot.getSnapshot": [{
32942
+ name: "deviceId",
32943
+ form: "single",
32944
+ optional: false
32945
+ }],
32946
+ "snapshot.getSnapshotOverview": [{
32947
+ name: "deviceIds",
32948
+ form: "array",
32949
+ optional: false
32950
+ }],
32951
+ "snapshot.invalidateCache": [{
32952
+ name: "deviceId",
32953
+ form: "single",
32954
+ optional: false
32955
+ }],
32956
+ "streamBroker.acquireEgressTranscode": [{
32957
+ name: "deviceId",
32958
+ form: "single",
32959
+ optional: false
32960
+ }],
32961
+ "streamBroker.assignProfile": [{
32962
+ name: "deviceId",
32963
+ form: "single",
32964
+ optional: false
32965
+ }],
32966
+ "streamBroker.getDeviceAudioMute": [{
32967
+ name: "deviceId",
32968
+ form: "single",
32969
+ optional: false
32970
+ }],
32971
+ "streamBroker.getStreamWithCodec": [{
32972
+ name: "deviceId",
32973
+ form: "single",
32974
+ optional: false
32975
+ }],
32976
+ "streamBroker.produceEventMedia": [{
32977
+ name: "deviceId",
32978
+ form: "single",
32979
+ optional: false
32980
+ }],
32981
+ "streamBroker.publishCameraStream": [{
32982
+ name: "deviceId",
32983
+ form: "single",
32984
+ optional: false
32985
+ }],
32986
+ "streamBroker.renderPreBufferClip": [{
32987
+ name: "deviceId",
32988
+ form: "single",
32989
+ optional: false
32990
+ }],
32991
+ "streamBroker.restartProfile": [{
32992
+ name: "deviceId",
32993
+ form: "single",
32994
+ optional: false
32995
+ }],
32996
+ "streamBroker.retractCameraStream": [{
32997
+ name: "deviceId",
32998
+ form: "single",
32999
+ optional: false
33000
+ }],
33001
+ "streamBroker.setDeviceAudioMute": [{
33002
+ name: "deviceId",
33003
+ form: "single",
33004
+ optional: false
33005
+ }],
33006
+ "streamBroker.unassignProfile": [{
33007
+ name: "deviceId",
33008
+ form: "single",
33009
+ optional: false
33010
+ }],
33011
+ "streamCatalog.getCatalog": [{
33012
+ name: "deviceId",
33013
+ form: "single",
33014
+ optional: false
33015
+ }],
33016
+ "streamParams.getConfigSchema": [{
33017
+ name: "deviceId",
33018
+ form: "single",
33019
+ optional: false
33020
+ }],
33021
+ "streamParams.getOptions": [{
33022
+ name: "deviceId",
33023
+ form: "single",
33024
+ optional: false
33025
+ }],
33026
+ "streamParams.setProfile": [{
33027
+ name: "deviceId",
33028
+ form: "single",
33029
+ optional: false
33030
+ }],
33031
+ "switch.setState": [{
33032
+ name: "deviceId",
33033
+ form: "single",
33034
+ optional: false
33035
+ }],
33036
+ "vacuumControl.locate": [{
33037
+ name: "deviceId",
33038
+ form: "single",
33039
+ optional: false
33040
+ }],
33041
+ "vacuumControl.pause": [{
33042
+ name: "deviceId",
33043
+ form: "single",
33044
+ optional: false
33045
+ }],
33046
+ "vacuumControl.returnToBase": [{
33047
+ name: "deviceId",
33048
+ form: "single",
33049
+ optional: false
33050
+ }],
33051
+ "vacuumControl.setFanSpeed": [{
33052
+ name: "deviceId",
33053
+ form: "single",
33054
+ optional: false
33055
+ }],
33056
+ "vacuumControl.start": [{
33057
+ name: "deviceId",
33058
+ form: "single",
33059
+ optional: false
33060
+ }],
33061
+ "vacuumControl.stop": [{
33062
+ name: "deviceId",
33063
+ form: "single",
33064
+ optional: false
33065
+ }],
33066
+ "valve.close": [{
33067
+ name: "deviceId",
33068
+ form: "single",
33069
+ optional: false
33070
+ }],
33071
+ "valve.open": [{
33072
+ name: "deviceId",
33073
+ form: "single",
33074
+ optional: false
33075
+ }],
33076
+ "valve.setPosition": [{
33077
+ name: "deviceId",
33078
+ form: "single",
33079
+ optional: false
33080
+ }],
33081
+ "valve.stop": [{
33082
+ name: "deviceId",
33083
+ form: "single",
33084
+ optional: false
33085
+ }],
33086
+ "videoclips.getClipPlayback": [{
33087
+ name: "deviceId",
33088
+ form: "single",
33089
+ optional: false
33090
+ }],
33091
+ "videoclips.listClips": [{
33092
+ name: "deviceId",
33093
+ form: "single",
33094
+ optional: false
33095
+ }],
33096
+ "waterHeater.setAway": [{
33097
+ name: "deviceId",
33098
+ form: "single",
33099
+ optional: false
33100
+ }],
33101
+ "waterHeater.setOperationMode": [{
33102
+ name: "deviceId",
33103
+ form: "single",
33104
+ optional: false
33105
+ }],
33106
+ "waterHeater.setTargetTemp": [{
33107
+ name: "deviceId",
33108
+ form: "single",
33109
+ optional: false
33110
+ }],
33111
+ "webrtcSession.addIceCandidate": [{
33112
+ name: "deviceId",
33113
+ form: "single",
33114
+ optional: false
33115
+ }],
33116
+ "webrtcSession.closeSession": [{
33117
+ name: "deviceId",
33118
+ form: "single",
33119
+ optional: false
33120
+ }],
33121
+ "webrtcSession.createSession": [{
33122
+ name: "deviceId",
33123
+ form: "single",
33124
+ optional: false
33125
+ }],
33126
+ "webrtcSession.getIceCandidates": [{
33127
+ name: "deviceId",
33128
+ form: "single",
33129
+ optional: false
33130
+ }],
33131
+ "webrtcSession.getSessionState": [{
33132
+ name: "deviceId",
33133
+ form: "single",
33134
+ optional: false
33135
+ }],
33136
+ "webrtcSession.handleAnswer": [{
33137
+ name: "deviceId",
33138
+ form: "single",
33139
+ optional: false
33140
+ }],
33141
+ "webrtcSession.handleOffer": [{
33142
+ name: "deviceId",
33143
+ form: "single",
33144
+ optional: false
33145
+ }],
33146
+ "webrtcSession.hasAdaptiveBitrate": [{
33147
+ name: "deviceId",
33148
+ form: "single",
33149
+ optional: false
33150
+ }],
33151
+ "webrtcSession.listStreams": [{
33152
+ name: "deviceId",
33153
+ form: "single",
33154
+ optional: false
33155
+ }],
33156
+ "zoneAnalytics.getCameraHistory": [{
33157
+ name: "deviceId",
33158
+ form: "single",
33159
+ optional: false
33160
+ }],
33161
+ "zoneAnalytics.getCurrentSnapshot": [{
33162
+ name: "deviceId",
33163
+ form: "single",
33164
+ optional: false
33165
+ }],
33166
+ "zoneAnalytics.getUnzonedHistory": [{
33167
+ name: "deviceId",
33168
+ form: "single",
33169
+ optional: false
33170
+ }],
33171
+ "zoneAnalytics.getZoneHistory": [{
33172
+ name: "deviceId",
33173
+ form: "single",
33174
+ optional: false
33175
+ }],
33176
+ "zoneRules.listRules": [{
33177
+ name: "deviceId",
33178
+ form: "single",
33179
+ optional: false
33180
+ }],
33181
+ "zoneRules.setRules": [{
33182
+ name: "deviceId",
33183
+ form: "single",
33184
+ optional: false
33185
+ }],
33186
+ "zones.addZone": [{
33187
+ name: "deviceId",
33188
+ form: "single",
33189
+ optional: false
33190
+ }],
33191
+ "zones.listZones": [{
33192
+ name: "deviceId",
33193
+ form: "single",
33194
+ optional: false
33195
+ }],
33196
+ "zones.removeZone": [{
33197
+ name: "deviceId",
33198
+ form: "single",
33199
+ optional: false
33200
+ }],
33201
+ "zones.updateZone": [{
33202
+ name: "deviceId",
33203
+ form: "single",
33204
+ optional: false
33205
+ }]
33206
+ });
31310
33207
  Object.freeze({
31311
33208
  "broker": "broker",
31312
33209
  "device-export": "device-export",
@@ -32026,7 +33923,7 @@ var Fmp4FragmentChild = class {
32026
33923
  meta: {
32027
33924
  sourceId: this.args.sourceId,
32028
33925
  decodeHwAccel: requested,
32029
- error: errMsg$12(err)
33926
+ error: errMsg$15(err)
32030
33927
  }
32031
33928
  });
32032
33929
  this.killChild();
@@ -32205,7 +34102,7 @@ var Fmp4FragmentChild = class {
32205
34102
  tags: { deviceId: this.args.deviceId },
32206
34103
  meta: {
32207
34104
  sourceId: this.args.sourceId,
32208
- error: errMsg$12(err)
34105
+ error: errMsg$15(err)
32209
34106
  }
32210
34107
  });
32211
34108
  }
@@ -78550,7 +80447,7 @@ function clearPairingFiles(accessoryUuid, logger) {
78550
80447
  }
78551
80448
  //#endregion
78552
80449
  //#region src/hap-setup-uri.ts
78553
- function errMsg$11(e) {
80450
+ function errMsg$14(e) {
78554
80451
  return e instanceof Error ? e.message : String(e);
78555
80452
  }
78556
80453
  /**
@@ -78577,11 +80474,79 @@ function firstExposedAccessorySetupUri(exposed, logger) {
78577
80474
  try {
78578
80475
  return first.setupURI();
78579
80476
  } catch (err) {
78580
- logger.debug("export-hap: setupURI failed on first exposed accessory", { meta: { error: errMsg$11(err) } });
80477
+ logger.debug("export-hap: setupURI failed on first exposed accessory", { meta: { error: errMsg$14(err) } });
78581
80478
  return;
78582
80479
  }
78583
80480
  }
78584
80481
  }
80482
+ //#endregion
80483
+ //#region src/mappers/builders/accessory-info.ts
80484
+ /**
80485
+ * `Service.AccessoryInformation` — the manufacturer / model / firmware /
80486
+ * serial block every HomeKit accessory carries.
80487
+ *
80488
+ * One implementation for both accessory shapes (camera and generic): the
80489
+ * fields come from the device's own metadata either way, and a second copy
80490
+ * would be a second answer to "what serial does this device publish".
80491
+ * Metadata is best-effort — a device that cannot answer still publishes.
80492
+ */
80493
+ async function populateAccessoryInfo(accessory, proxy, displayName, modelFallback) {
80494
+ const info = accessory.getService(import_dist.Service.AccessoryInformation);
80495
+ if (!info) return;
80496
+ try {
80497
+ const device = await proxy.deviceManager?.getDevice({});
80498
+ const metadata = readMetadata(device);
80499
+ info.setCharacteristic(import_dist.Characteristic.Name, device?.name ?? displayName);
80500
+ info.setCharacteristic(import_dist.Characteristic.Manufacturer, stringOr(metadata?.manufacturer, "CamStack"));
80501
+ info.setCharacteristic(import_dist.Characteristic.Model, stringOr(metadata?.model, modelFallback));
80502
+ info.setCharacteristic(import_dist.Characteristic.FirmwareRevision, stringOr(metadata?.firmware, "0.0.0"));
80503
+ info.setCharacteristic(import_dist.Characteristic.SerialNumber, stringOr(metadata?.sn, `camstack-${proxy.deviceId}`));
80504
+ } catch {}
80505
+ }
80506
+ /** The four fields this module reads, or `null` — the device record's
80507
+ * `metadata` is an open bag and nothing else here depends on its shape. */
80508
+ function readMetadata(device) {
80509
+ const metadata = device?.metadata;
80510
+ if (metadata === null || typeof metadata !== "object") return {};
80511
+ const entries = new Map(Object.entries(metadata));
80512
+ return {
80513
+ model: stringOrNull(entries.get("model")),
80514
+ manufacturer: stringOrNull(entries.get("manufacturer")),
80515
+ firmware: stringOrNull(entries.get("firmware")),
80516
+ sn: stringOrNull(entries.get("sn"))
80517
+ };
80518
+ }
80519
+ function stringOrNull(value) {
80520
+ return typeof value === "string" ? value : null;
80521
+ }
80522
+ function stringOr(value, fallback) {
80523
+ return typeof value === "string" && value.length > 0 ? value : fallback;
80524
+ }
80525
+ //#endregion
80526
+ //#region src/mappers/builders/generic/characteristic-update.ts
80527
+ /**
80528
+ * Parse `status` with the capability's OWN Zod schema and turn it into
80529
+ * characteristic writes.
80530
+ *
80531
+ * Duck-typing the payload here would be the second source of truth about what a
80532
+ * cap reports; the schema is the first and only one. A payload that does not
80533
+ * match yields NO updates — never a partial or invented value — and the caller
80534
+ * logs the drop, because a sensor that silently stops moving is
80535
+ * indistinguishable from a sensor that never changed.
80536
+ *
80537
+ * Rows `.pick()` only the fields they read, so a provider omitting a timestamp
80538
+ * cannot silence a sensor.
80539
+ */
80540
+ function reader(schema, toUpdates) {
80541
+ return (status) => {
80542
+ const parsed = schema.safeParse(status);
80543
+ return parsed.success ? toUpdates(parsed.data) : [];
80544
+ };
80545
+ }
80546
+ /** Push every update onto `service`. */
80547
+ function applyUpdates(service, updates) {
80548
+ for (const update of updates) service.updateCharacteristic(update.characteristic, update.value);
80549
+ }
78585
80550
  /**
78586
80551
  * hap-nodejs' `checkName` regex, verbatim.
78587
80552
  *
@@ -78718,6 +80683,92 @@ function titleCase(raw) {
78718
80683
  return raw.split(/[-_\s]+/u).filter((part) => part.length > 0).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
78719
80684
  }
78720
80685
  //#endregion
80686
+ //#region src/mappers/builders/service-label.ts
80687
+ /**
80688
+ * The ONE place a secondary service on the camera accessory gets its label.
80689
+ *
80690
+ * A "secondary service" here is a Switch or Lightbulb published alongside the
80691
+ * camera on the same accessory — the privacy switch, each accessory child
80692
+ * (siren, floodlight), each PTZ action. iOS Home renders these as their own
80693
+ * controls, and the operator has seen them as "Interruttore 1", "Interruttore
80694
+ * 2" through three separate rounds of fixes.
80695
+ *
80696
+ * ## Why `Name` alone cannot rename anything
80697
+ *
80698
+ * Two facts about hap-nodejs 2.1.7, both measured against the installed copy
80699
+ * rather than reasoned about:
80700
+ *
80701
+ * 1. `accessory.addService(Type, displayName, subtype)` ALREADY writes
80702
+ * `displayName` to `Characteristic.Name` (`Service` constructor). So every
80703
+ * round of this bug — including the one that moved the label onto
80704
+ * `ConfiguredName` — shipped with `Name` correctly set. "iOS had no name
80705
+ * to render" was never true.
80706
+ * 2. The mDNS configuration number (`c#`) is a sha1 over
80707
+ * `internalHAPRepresentation(false)`, which OMITS characteristic VALUES.
80708
+ * Changing the string in `Name` therefore does not bump `c#`, a paired
80709
+ * controller gets no signal to re-read `/accessories`, and the name it
80710
+ * cached at first enumeration stands forever.
80711
+ *
80712
+ * `Name` is also declared `pr` only — paired read, no write, no notify. It is
80713
+ * the seed a controller seeds its database from once; it is not a channel.
80714
+ *
80715
+ * ## Why `ConfiguredName`
80716
+ *
80717
+ * `ConfiguredName` (`000000E3`) is declared `pr | pw | ev` — the only name
80718
+ * characteristic a controller may write and may subscribe to. It is what iOS
80719
+ * 16+ reads for a service the user can rename, and adding it CHANGES the
80720
+ * accessory structure, so `c#` does bump and the controller re-reads.
80721
+ *
80722
+ * It was removed once because hap-nodejs logged
80723
+ *
80724
+ * ```
80725
+ * Characteristic not in required or optional characteristic section for
80726
+ * service Switch. Adding anyway.
80727
+ * ```
80728
+ *
80729
+ * That line is a WARNING, not a rejection: `Service.getCharacteristic` calls
80730
+ * `addCharacteristic` unconditionally and only then emits the warning. The
80731
+ * characteristic was always present and always published. hap-nodejs'
80732
+ * per-service optional lists simply predate `ConfiguredName` being valid on
80733
+ * any service.
80734
+ *
80735
+ * Registering it with {@link Service.addOptionalCharacteristic} first takes
80736
+ * the branch above the warning, so the accessory still builds with ZERO
80737
+ * characteristic warnings — which is what `service-naming.spec.ts` asserts.
80738
+ *
80739
+ * ## Scope: EVERY service, including the sensors
80740
+ *
80741
+ * An earlier round applied this to Switch- and Lightbulb-shaped services only,
80742
+ * on the theory that `Service.MotionSensor` and `Service.Battery` are not
80743
+ * separately named tiles in iOS Home and that naming them would be a guess.
80744
+ *
80745
+ * That theory was never measured, and it had a cost: it left services on the
80746
+ * accessory whose name a paired controller could never be told about, and it
80747
+ * made "did `ConfiguredName` fix the operator's 'Interruttore N'?" unanswerable
80748
+ * — a negative result on a partial application proves nothing about the
80749
+ * mechanism. Every service this addon publishes now carries both
80750
+ * characteristics, on the camera accessory and on the generic one.
80751
+ *
80752
+ * The reasoning above is mechanism, not measurement: it says why `Name` alone
80753
+ * CANNOT work and why `ConfiguredName` is the only characteristic that can. It
80754
+ * does not prove iOS renders it on every service shape. That is an observation
80755
+ * only a re-paired controller can make — and after a change like this one, the
80756
+ * controller must be re-paired, because a cached accessory database is exactly
80757
+ * what the whole mechanism is about.
80758
+ */
80759
+ /**
80760
+ * Publish `name` as both the immutable `Name` and the controller-visible
80761
+ * `ConfiguredName` of `service`.
80762
+ *
80763
+ * `name` must already be HAP-valid — build it with `service-names.ts`, which
80764
+ * cannot return a string hap-nodejs' `checkName` would warn about.
80765
+ */
80766
+ function applyServiceLabel(service, name) {
80767
+ service.setCharacteristic(import_dist.Characteristic.Name, name);
80768
+ if (!service.optionalCharacteristics.some((characteristic) => characteristic.UUID === import_dist.Characteristic.ConfiguredName.UUID)) service.addOptionalCharacteristic(import_dist.Characteristic.ConfiguredName);
80769
+ service.setCharacteristic(import_dist.Characteristic.ConfiguredName, name);
80770
+ }
80771
+ //#endregion
78721
80772
  //#region src/mappers/builders/battery.ts
78722
80773
  /**
78723
80774
  * Battery builder — surfaces a battery-operated camera's power state
@@ -78727,34 +80778,56 @@ function titleCase(raw) {
78727
80778
  * subscribes to the runtime-state slice so iOS Home reflects level /
78728
80779
  * charging changes pushed by the firmware without a poll loop.
78729
80780
  *
80781
+ * The status→characteristic mapping is {@link batteryCharacteristicUpdates},
80782
+ * exported because the generic (non-camera) export path publishes the same
80783
+ * `Service.Battery` from the same cap — one derivation, two accessory shapes.
80784
+ *
80785
+ * Skipped silently when the `battery` cap is not bound — caller checks
80786
+ * cap presence before invoking this builder (see `camera-accessory.ts`).
80787
+ */
80788
+ var LOW_BATTERY_THRESHOLD_PCT = 20;
80789
+ /**
78730
80790
  * Mapping:
78731
80791
  * - `BatteryStatus.percentage` (0..100) → `Characteristic.BatteryLevel`
78732
80792
  * - `BatteryStatus.charging`:
78733
80793
  * `'none'` → `ChargingState.NOT_CHARGING`
78734
80794
  * `'dc' | 'solar'` → `ChargingState.CHARGING`
78735
80795
  * - `percentage <= LOW_BATTERY_THRESHOLD_PCT` → `StatusLowBattery.LOW`
78736
- *
78737
- * Skipped silently when the `battery` cap is not bound — caller checks
78738
- * cap presence before invoking this builder (see `camera-accessory.ts`).
78739
80796
  */
78740
- var LOW_BATTERY_THRESHOLD_PCT = 20;
80797
+ var batteryCharacteristicUpdates = reader(BatteryStatusSchema.pick({ percentage: true }).extend({ charging: BatteryStatusSchema.shape.charging.optional() }), (status) => {
80798
+ const pct = Math.max(0, Math.min(100, Math.round(status.percentage)));
80799
+ return [
80800
+ {
80801
+ characteristic: import_dist.Characteristic.BatteryLevel,
80802
+ value: pct
80803
+ },
80804
+ ...status.charging === void 0 ? [] : [{
80805
+ characteristic: import_dist.Characteristic.ChargingState,
80806
+ value: status.charging === "none" ? import_dist.Characteristic.ChargingState.NOT_CHARGING : import_dist.Characteristic.ChargingState.CHARGING
80807
+ }],
80808
+ {
80809
+ characteristic: import_dist.Characteristic.StatusLowBattery,
80810
+ value: pct <= LOW_BATTERY_THRESHOLD_PCT ? import_dist.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : import_dist.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL
80811
+ }
80812
+ ];
80813
+ });
78741
80814
  async function buildBattery(bctx) {
78742
80815
  const { ctx, accessory, proxy, numericDeviceId, displayName } = bctx;
78743
80816
  const log = ctx.logger.withTags({ deviceId: numericDeviceId });
78744
- const service = accessory.addService(import_dist.Service.Battery, hapServiceName([displayName], `Camera ${numericDeviceId}`));
80817
+ const label = hapServiceName([displayName], `Camera ${numericDeviceId}`);
80818
+ const service = accessory.addService(import_dist.Service.Battery, label);
80819
+ applyServiceLabel(service, label);
78745
80820
  try {
78746
80821
  const status = await proxy.battery?.getStatus({});
78747
- if (status) applyToService(service, status);
80822
+ if (status !== void 0 && status !== null) applyToService(service, status);
78748
80823
  } catch (err) {
78749
- log.debug("export-hap: battery getStatus hydrate failed (non-fatal)", { meta: { error: errMsg$10(err) } });
80824
+ log.debug("export-hap: battery getStatus hydrate failed (non-fatal)", { meta: { error: errMsg$13(err) } });
78750
80825
  }
78751
80826
  const unsubscribes = [];
78752
80827
  if (proxy.state.battery) {
78753
80828
  const unsub = proxy.state.battery.subscribe((value) => {
78754
80829
  if (!value) return;
78755
- const status = value;
78756
- if (typeof status.percentage !== "number") return;
78757
- applyToService(service, status);
80830
+ applyToService(service, value);
78758
80831
  });
78759
80832
  unsubscribes.push(unsub);
78760
80833
  }
@@ -78765,14 +80838,9 @@ async function buildBattery(bctx) {
78765
80838
  } };
78766
80839
  }
78767
80840
  function applyToService(service, status) {
78768
- const pct = Math.max(0, Math.min(100, Math.round(status.percentage)));
78769
- service.updateCharacteristic(import_dist.Characteristic.BatteryLevel, pct);
78770
- const chargingState = status.charging === "none" ? import_dist.Characteristic.ChargingState.NOT_CHARGING : import_dist.Characteristic.ChargingState.CHARGING;
78771
- service.updateCharacteristic(import_dist.Characteristic.ChargingState, chargingState);
78772
- const lowBattery = pct <= LOW_BATTERY_THRESHOLD_PCT ? import_dist.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : import_dist.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL;
78773
- service.updateCharacteristic(import_dist.Characteristic.StatusLowBattery, lowBattery);
80841
+ applyUpdates(service, batteryCharacteristicUpdates(status));
78774
80842
  }
78775
- function errMsg$10(err) {
80843
+ function errMsg$13(err) {
78776
80844
  return err instanceof Error ? err.message : String(err);
78777
80845
  }
78778
80846
  //#endregion
@@ -89524,7 +91592,7 @@ var VIDEO_LOOPBACK_RCVBUF_BYTES = 8 * 1024 * 1024;
89524
91592
  * not burst size.
89525
91593
  */
89526
91594
  var AUDIO_LOOPBACK_RCVBUF_BYTES = 1024 * 1024;
89527
- function errMsg$9(err) {
91595
+ function errMsg$12(err) {
89528
91596
  return err instanceof Error ? err.message : String(err);
89529
91597
  }
89530
91598
  /**
@@ -89539,13 +91607,13 @@ function applyReceiveBuffer(socket, requestedBytes) {
89539
91607
  try {
89540
91608
  socket.setRecvBufferSize(requestedBytes);
89541
91609
  } catch (err) {
89542
- error = errMsg$9(err);
91610
+ error = errMsg$12(err);
89543
91611
  }
89544
91612
  let effectiveBytes = null;
89545
91613
  try {
89546
91614
  effectiveBytes = socket.getRecvBufferSize();
89547
91615
  } catch (err) {
89548
- if (error === null) error = errMsg$9(err);
91616
+ if (error === null) error = errMsg$12(err);
89549
91617
  }
89550
91618
  return {
89551
91619
  requestedBytes,
@@ -89805,20 +91873,20 @@ function buildCameraStreamingDelegate(bctx, advertised) {
89805
91873
  delegate: {
89806
91874
  handleSnapshotRequest(request, callback) {
89807
91875
  handleSnapshot(bctx, request).then((buf) => callback(void 0, buf)).catch((err) => {
89808
- log.warn("export-hap: snapshot failed", { meta: { error: errMsg$8(err) } });
89809
- callback(err instanceof Error ? err : new Error(errMsg$8(err)));
91876
+ log.warn("export-hap: snapshot failed", { meta: { error: errMsg$11(err) } });
91877
+ callback(err instanceof Error ? err : new Error(errMsg$11(err)));
89810
91878
  });
89811
91879
  },
89812
91880
  prepareStream(request, callback) {
89813
91881
  prepareStream(request, sessions, bctx).then((resp) => callback(void 0, resp)).catch((err) => {
89814
- log.warn("export-hap: prepareStream failed", { meta: { error: errMsg$8(err) } });
89815
- callback(err instanceof Error ? err : new Error(errMsg$8(err)));
91882
+ log.warn("export-hap: prepareStream failed", { meta: { error: errMsg$11(err) } });
91883
+ callback(err instanceof Error ? err : new Error(errMsg$11(err)));
89816
91884
  });
89817
91885
  },
89818
91886
  handleStreamRequest(request, callback) {
89819
91887
  handleStreamRequest(request, sessions, bctx, advertised).then(() => callback()).catch((err) => {
89820
- log.warn("export-hap: handleStreamRequest failed", { meta: { error: errMsg$8(err) } });
89821
- callback(err instanceof Error ? err : new Error(errMsg$8(err)));
91888
+ log.warn("export-hap: handleStreamRequest failed", { meta: { error: errMsg$11(err) } });
91889
+ callback(err instanceof Error ? err : new Error(errMsg$11(err)));
89822
91890
  });
89823
91891
  }
89824
91892
  },
@@ -89934,7 +92002,7 @@ async function prepareStream(request, sessions, bctx) {
89934
92002
  closeSocket(audioUdp);
89935
92003
  closeSocket(videoLoopUdp);
89936
92004
  closeSocket(audioLoopUdp);
89937
- throw new Error(`export-hap: outbound SrtpSession init failed: ${errMsg$8(err)}`, { cause: err });
92005
+ throw new Error(`export-hap: outbound SrtpSession init failed: ${errMsg$11(err)}`, { cause: err });
89938
92006
  }
89939
92007
  let upstreamAudioSrtp = null;
89940
92008
  try {
@@ -89948,7 +92016,7 @@ async function prepareStream(request, sessions, bctx) {
89948
92016
  profile: import_src.ProtectionProfileAes128CmHmacSha1_80
89949
92017
  });
89950
92018
  } catch (err) {
89951
- bctx.ctx.logger.withTags({ deviceId: bctx.numericDeviceId }).warn("export-hap: SrtpSession init failed (upstream audio decrypt disabled)", { meta: { error: errMsg$8(err) } });
92019
+ bctx.ctx.logger.withTags({ deviceId: bctx.numericDeviceId }).warn("export-hap: SrtpSession init failed (upstream audio decrypt disabled)", { meta: { error: errMsg$11(err) } });
89952
92020
  }
89953
92021
  const videoSsrc = randomSsrc();
89954
92022
  const audioSsrc = randomSsrc();
@@ -90065,7 +92133,7 @@ async function prepareStream(request, sessions, bctx) {
90065
92133
  });
90066
92134
  audioUdp.on("message", (packet, rinfo) => {
90067
92135
  handleIncomingAudioRtp(session, packet, rinfo.address, bctx).catch((err) => {
90068
- bctx.ctx.logger.withTags({ deviceId: bctx.numericDeviceId }).debug("export-hap: incoming-audio handler error (dropped)", { meta: { error: errMsg$8(err) } });
92136
+ bctx.ctx.logger.withTags({ deviceId: bctx.numericDeviceId }).debug("export-hap: incoming-audio handler error (dropped)", { meta: { error: errMsg$11(err) } });
90069
92137
  });
90070
92138
  });
90071
92139
  logLoopbackBuffer(tagLog, request.sessionID, "video", videoLoop.buffer);
@@ -90261,7 +92329,7 @@ function readControllerRtcp(session, leg, packet, log) {
90261
92329
  } catch (err) {
90262
92330
  drop(session, "inbound-rtcp-decrypt-failed");
90263
92331
  storeReceiverReports(session, leg, recordUnreadableRtcp(tally));
90264
- logUnreadableRtcp(session, leg, `decrypt: ${errMsg$8(err)}`, log);
92332
+ logUnreadableRtcp(session, leg, `decrypt: ${errMsg$11(err)}`, log);
90265
92333
  return;
90266
92334
  }
90267
92335
  const outcome = ingestDecryptedRtcp(plaintext, tally);
@@ -91027,7 +93095,7 @@ async function handleIncomingAudioRtp(session, packet, sourceAddress, bctx) {
91027
93095
  session.upstreamRtpDecryptFailures += 1;
91028
93096
  if (session.upstreamRtpDecryptFailures % 100 === 1) log.debug("export-hap: SRTP decrypt failed (rate-limited)", { meta: {
91029
93097
  failures: session.upstreamRtpDecryptFailures,
91030
- error: errMsg$8(err)
93098
+ error: errMsg$11(err)
91031
93099
  } });
91032
93100
  return;
91033
93101
  }
@@ -91041,7 +93109,7 @@ async function handleIncomingAudioRtp(session, packet, sourceAddress, bctx) {
91041
93109
  rtpPayloadType = parsedRtp.header.payloadType;
91042
93110
  } catch (err) {
91043
93111
  drop(session, "upstream-parse-failed");
91044
- log.debug("export-hap: RTP parse failed after decrypt", { meta: { error: errMsg$8(err) } });
93112
+ log.debug("export-hap: RTP parse failed after decrypt", { meta: { error: errMsg$11(err) } });
91045
93113
  return;
91046
93114
  }
91047
93115
  const negotiatedAudioPt = session.lastStartParams?.audioPt;
@@ -91065,7 +93133,7 @@ async function handleIncomingAudioRtp(session, packet, sourceAddress, bctx) {
91065
93133
  if (session.intercomTalkSessionId === null) {
91066
93134
  session.intercomTalkSessionId = "";
91067
93135
  const opened = await openIntercomTalkSession(bctx).catch((err) => {
91068
- log.warn("export-hap: intercom.startTalkSession failed", { meta: { error: errMsg$8(err) } });
93136
+ log.warn("export-hap: intercom.startTalkSession failed", { meta: { error: errMsg$11(err) } });
91069
93137
  return null;
91070
93138
  });
91071
93139
  if (opened) {
@@ -91092,7 +93160,7 @@ async function handleIncomingAudioRtp(session, packet, sourceAddress, bctx) {
91092
93160
  drop(session, "upstream-push-failed");
91093
93161
  log.debug("export-hap: intercom.pushTalkAudio failed (will re-open on next frame)", { meta: {
91094
93162
  sequenceNumber: session.intercomPcmSequence,
91095
- error: errMsg$8(err)
93163
+ error: errMsg$11(err)
91096
93164
  } });
91097
93165
  session.intercomTalkSessionId = null;
91098
93166
  }
@@ -91124,7 +93192,7 @@ async function closeIntercomTalkSession(session, bctx) {
91124
93192
  } catch (err) {
91125
93193
  log.debug("export-hap: intercom.endTalkSession failed (continuing)", { meta: {
91126
93194
  sessionId,
91127
- error: errMsg$8(err)
93195
+ error: errMsg$11(err)
91128
93196
  } });
91129
93197
  }
91130
93198
  }
@@ -91149,7 +93217,7 @@ function closeSocket(udp) {
91149
93217
  function randomSsrc() {
91150
93218
  return Math.floor(Math.random() * 2147483646) + 1 | 0;
91151
93219
  }
91152
- function errMsg$8(err) {
93220
+ function errMsg$11(err) {
91153
93221
  return err instanceof Error ? err.message : String(err);
91154
93222
  }
91155
93223
  //#endregion
@@ -91233,14 +93301,14 @@ async function buildDoorbell(input) {
91233
93301
  }
91234
93302
  log.info("export-hap: doorbell SINGLE_PRESS pushed to HomeKit", { meta: { ...delivery } });
91235
93303
  } catch (err) {
91236
- log.warn("export-hap: ringDoorbell() failed", { meta: { error: errMsg$7(err) } });
93304
+ log.warn("export-hap: ringDoorbell() failed", { meta: { error: errMsg$10(err) } });
91237
93305
  }
91238
93306
  });
91239
93307
  return { async dispose() {
91240
93308
  unsubscribe();
91241
93309
  } };
91242
93310
  }
91243
- function errMsg$7(err) {
93311
+ function errMsg$10(err) {
91244
93312
  return err instanceof Error ? err.message : String(err);
91245
93313
  }
91246
93314
  //#endregion
@@ -91280,13 +93348,15 @@ var RESET_DEBOUNCE_MS = 5e3;
91280
93348
  */
91281
93349
  async function buildMotionSensor(bctx, existing = null) {
91282
93350
  const { ctx, accessory, proxy, numericDeviceId, displayName } = bctx;
91283
- const motionService = existing ?? accessory.addService(import_dist.Service.MotionSensor, hapServiceName([displayName], `Camera ${numericDeviceId}`));
93351
+ const label = hapServiceName([displayName], `Camera ${numericDeviceId}`);
93352
+ const motionService = existing ?? accessory.addService(import_dist.Service.MotionSensor, label);
93353
+ applyServiceLabel(motionService, label);
91284
93354
  motionService.setCharacteristic(import_dist.Characteristic.MotionDetected, false);
91285
93355
  try {
91286
93356
  const detected = await proxy.motion?.isDetected({});
91287
93357
  if (typeof detected === "boolean") motionService.updateCharacteristic(import_dist.Characteristic.MotionDetected, detected);
91288
93358
  } catch (err) {
91289
- ctx.logger.withTags({ deviceId: numericDeviceId }).debug("export-hap: initial motion hydrate failed (non-fatal)", { meta: { error: errMsg$6(err) } });
93359
+ ctx.logger.withTags({ deviceId: numericDeviceId }).debug("export-hap: initial motion hydrate failed (non-fatal)", { meta: { error: errMsg$9(err) } });
91290
93360
  }
91291
93361
  let resetTimer = null;
91292
93362
  const armReset = () => {
@@ -91320,82 +93390,10 @@ async function buildMotionSensor(bctx, existing = null) {
91320
93390
  }
91321
93391
  } };
91322
93392
  }
91323
- function errMsg$6(err) {
93393
+ function errMsg$9(err) {
91324
93394
  return err instanceof Error ? err.message : String(err);
91325
93395
  }
91326
93396
  //#endregion
91327
- //#region src/mappers/builders/service-label.ts
91328
- /**
91329
- * The ONE place a secondary service on the camera accessory gets its label.
91330
- *
91331
- * A "secondary service" here is a Switch or Lightbulb published alongside the
91332
- * camera on the same accessory — the privacy switch, each accessory child
91333
- * (siren, floodlight), each PTZ action. iOS Home renders these as their own
91334
- * controls, and the operator has seen them as "Interruttore 1", "Interruttore
91335
- * 2" through three separate rounds of fixes.
91336
- *
91337
- * ## Why `Name` alone cannot rename anything
91338
- *
91339
- * Two facts about hap-nodejs 2.1.7, both measured against the installed copy
91340
- * rather than reasoned about:
91341
- *
91342
- * 1. `accessory.addService(Type, displayName, subtype)` ALREADY writes
91343
- * `displayName` to `Characteristic.Name` (`Service` constructor). So every
91344
- * round of this bug — including the one that moved the label onto
91345
- * `ConfiguredName` — shipped with `Name` correctly set. "iOS had no name
91346
- * to render" was never true.
91347
- * 2. The mDNS configuration number (`c#`) is a sha1 over
91348
- * `internalHAPRepresentation(false)`, which OMITS characteristic VALUES.
91349
- * Changing the string in `Name` therefore does not bump `c#`, a paired
91350
- * controller gets no signal to re-read `/accessories`, and the name it
91351
- * cached at first enumeration stands forever.
91352
- *
91353
- * `Name` is also declared `pr` only — paired read, no write, no notify. It is
91354
- * the seed a controller seeds its database from once; it is not a channel.
91355
- *
91356
- * ## Why `ConfiguredName`
91357
- *
91358
- * `ConfiguredName` (`000000E3`) is declared `pr | pw | ev` — the only name
91359
- * characteristic a controller may write and may subscribe to. It is what iOS
91360
- * 16+ reads for a service the user can rename, and adding it CHANGES the
91361
- * accessory structure, so `c#` does bump and the controller re-reads.
91362
- *
91363
- * It was removed once because hap-nodejs logged
91364
- *
91365
- * ```
91366
- * Characteristic not in required or optional characteristic section for
91367
- * service Switch. Adding anyway.
91368
- * ```
91369
- *
91370
- * That line is a WARNING, not a rejection: `Service.getCharacteristic` calls
91371
- * `addCharacteristic` unconditionally and only then emits the warning. The
91372
- * characteristic was always present and always published. hap-nodejs'
91373
- * per-service optional lists simply predate `ConfiguredName` being valid on
91374
- * any service.
91375
- *
91376
- * Registering it with {@link Service.addOptionalCharacteristic} first takes
91377
- * the branch above the warning, so the accessory still builds with ZERO
91378
- * characteristic warnings — which is what `service-naming.spec.ts` asserts.
91379
- *
91380
- * ## Scope
91381
- *
91382
- * Switch- and Lightbulb-shaped services only. `Service.MotionSensor` on a
91383
- * camera accessory is not a separately named tile in iOS Home, so giving it a
91384
- * writable name would be a guess, and this module does not guess.
91385
- */
91386
- /**
91387
- * Publish `name` as both the immutable `Name` and the controller-visible
91388
- * `ConfiguredName` of `service`.
91389
- *
91390
- * `name` must already be HAP-valid — build it with `service-names.ts`, which
91391
- * cannot return a string hap-nodejs' `checkName` would warn about.
91392
- */
91393
- function applyServiceLabel(service, name) {
91394
- service.setCharacteristic(import_dist.Characteristic.Name, name);
91395
- if (!service.optionalCharacteristics.some((characteristic) => characteristic.UUID === import_dist.Characteristic.ConfiguredName.UUID)) service.addOptionalCharacteristic(import_dist.Characteristic.ConfiguredName);
91396
- service.setCharacteristic(import_dist.Characteristic.ConfiguredName, name);
91397
- }
91398
- //#endregion
91399
93397
  //#region src/mappers/builders/privacy-switch.ts
91400
93398
  /**
91401
93399
  * Privacy-mask switch builder — turns the camstack `privacy-mask` cap's
@@ -91423,7 +93421,7 @@ async function buildPrivacySwitch(bctx) {
91423
93421
  const status = await proxy.privacyMask?.getStatus({});
91424
93422
  if (status && typeof status.enabled === "boolean") service.updateCharacteristic(import_dist.Characteristic.On, status.enabled);
91425
93423
  } catch (err) {
91426
- log.debug("export-hap: privacy-mask getStatus hydrate failed (non-fatal)", { meta: { error: errMsg$5(err) } });
93424
+ log.debug("export-hap: privacy-mask getStatus hydrate failed (non-fatal)", { meta: { error: errMsg$8(err) } });
91427
93425
  }
91428
93426
  service.getCharacteristic(import_dist.Characteristic.On).onSet(async (value) => {
91429
93427
  const enabled = value === true;
@@ -91432,7 +93430,7 @@ async function buildPrivacySwitch(bctx) {
91432
93430
  } catch (err) {
91433
93431
  log.warn("export-hap: privacy-mask setMask failed", { meta: {
91434
93432
  enabled,
91435
- error: errMsg$5(err)
93433
+ error: errMsg$8(err)
91436
93434
  } });
91437
93435
  }
91438
93436
  });
@@ -91450,7 +93448,7 @@ async function buildPrivacySwitch(bctx) {
91450
93448
  } catch {}
91451
93449
  } };
91452
93450
  }
91453
- function errMsg$5(err) {
93451
+ function errMsg$8(err) {
91454
93452
  return err instanceof Error ? err.message : String(err);
91455
93453
  }
91456
93454
  //#endregion
@@ -91541,7 +93539,7 @@ async function buildPtz(bctx) {
91541
93539
  } catch (err) {
91542
93540
  log.warn("export-hap: ptz.goToPreset failed", { meta: {
91543
93541
  presetId: preset.id,
91544
- error: errMsg$4(err)
93542
+ error: errMsg$7(err)
91545
93543
  } });
91546
93544
  }
91547
93545
  armReset(() => service.updateCharacteristic(import_dist.Characteristic.On, false), MOMENTARY_RESET_MS);
@@ -91559,14 +93557,14 @@ async function buildPtz(bctx) {
91559
93557
  try {
91560
93558
  await proxy.ptz?.stop({});
91561
93559
  } catch (err) {
91562
- log.debug("export-hap: ptz.stop failed (non-fatal)", { meta: { error: errMsg$4(err) } });
93560
+ log.debug("export-hap: ptz.stop failed (non-fatal)", { meta: { error: errMsg$7(err) } });
91563
93561
  }
91564
93562
  service.updateCharacteristic(import_dist.Characteristic.On, false);
91565
93563
  }, options.ptzPulseMs);
91566
93564
  } catch (err) {
91567
93565
  log.warn("export-hap: ptz.continuousMove failed", { meta: {
91568
93566
  dir: dir.label,
91569
- error: errMsg$4(err)
93567
+ error: errMsg$7(err)
91570
93568
  } });
91571
93569
  service.updateCharacteristic(import_dist.Characteristic.On, false);
91572
93570
  }
@@ -91589,7 +93587,7 @@ async function readPresets(bctx) {
91589
93587
  name: p.name
91590
93588
  }));
91591
93589
  } catch (err) {
91592
- ctx.logger.withTags({ deviceId: numericDeviceId }).debug("export-hap: ptz.getPresets failed (non-fatal)", { meta: { error: errMsg$4(err) } });
93590
+ ctx.logger.withTags({ deviceId: numericDeviceId }).debug("export-hap: ptz.getPresets failed (non-fatal)", { meta: { error: errMsg$7(err) } });
91593
93591
  return [];
91594
93592
  }
91595
93593
  }
@@ -91604,7 +93602,7 @@ async function tryBuildAutotrack(bctx) {
91604
93602
  const status = await proxy.ptzAutotrack.getStatus({});
91605
93603
  if (status && typeof status.enabled === "boolean") service.updateCharacteristic(import_dist.Characteristic.On, status.enabled);
91606
93604
  } catch (err) {
91607
- log.debug("export-hap: ptzAutotrack.getStatus failed (non-fatal)", { meta: { error: errMsg$4(err) } });
93605
+ log.debug("export-hap: ptzAutotrack.getStatus failed (non-fatal)", { meta: { error: errMsg$7(err) } });
91608
93606
  }
91609
93607
  service.getCharacteristic(import_dist.Characteristic.On).onSet(async (value) => {
91610
93608
  const enabled = value === true;
@@ -91613,13 +93611,13 @@ async function tryBuildAutotrack(bctx) {
91613
93611
  } catch (err) {
91614
93612
  log.warn("export-hap: ptzAutotrack.setEnabled failed", { meta: {
91615
93613
  enabled,
91616
- error: errMsg$4(err)
93614
+ error: errMsg$7(err)
91617
93615
  } });
91618
93616
  }
91619
93617
  });
91620
93618
  return { async dispose() {} };
91621
93619
  }
91622
- function errMsg$4(err) {
93620
+ function errMsg$7(err) {
91623
93621
  return err instanceof Error ? err.message : String(err);
91624
93622
  }
91625
93623
  //#endregion
@@ -92650,13 +94648,13 @@ async function buildChildSwitch(bctx, subtype, deviceType) {
92650
94648
  const switchStatus = await proxy.switch?.getStatus({});
92651
94649
  if (switchStatus && typeof switchStatus.on === "boolean") service.updateCharacteristic(import_dist.Characteristic.On, switchStatus.on);
92652
94650
  } catch (err) {
92653
- log.debug("export-hap: child switch.getStatus failed (non-fatal)", { meta: { error: errMsg$3(err) } });
94651
+ log.debug("export-hap: child switch.getStatus failed (non-fatal)", { meta: { error: errMsg$6(err) } });
92654
94652
  }
92655
94653
  if (useLightbulb) try {
92656
94654
  const status = await proxy.brightness?.getStatus({});
92657
94655
  if (status && typeof status.percentage === "number") service.updateCharacteristic(import_dist.Characteristic.Brightness, status.percentage);
92658
94656
  } catch (err) {
92659
- log.debug("export-hap: child brightness.getStatus failed (non-fatal)", { meta: { error: errMsg$3(err) } });
94657
+ log.debug("export-hap: child brightness.getStatus failed (non-fatal)", { meta: { error: errMsg$6(err) } });
92660
94658
  }
92661
94659
  if (hasSwitch) service.getCharacteristic(import_dist.Characteristic.On).onSet(async (value) => {
92662
94660
  const on = value === true;
@@ -92665,7 +94663,7 @@ async function buildChildSwitch(bctx, subtype, deviceType) {
92665
94663
  } catch (err) {
92666
94664
  log.warn("export-hap: child switch.setState failed", { meta: {
92667
94665
  on,
92668
- error: errMsg$3(err)
94666
+ error: errMsg$6(err)
92669
94667
  } });
92670
94668
  }
92671
94669
  });
@@ -92677,7 +94675,7 @@ async function buildChildSwitch(bctx, subtype, deviceType) {
92677
94675
  } catch (err) {
92678
94676
  log.warn("export-hap: child brightness.setBrightness failed", { meta: {
92679
94677
  percentage,
92680
- error: errMsg$3(err)
94678
+ error: errMsg$6(err)
92681
94679
  } });
92682
94680
  }
92683
94681
  });
@@ -92700,7 +94698,7 @@ async function buildChildSwitch(bctx, subtype, deviceType) {
92700
94698
  } catch {}
92701
94699
  } };
92702
94700
  }
92703
- function errMsg$3(err) {
94701
+ function errMsg$6(err) {
92704
94702
  return err instanceof Error ? err.message : String(err);
92705
94703
  }
92706
94704
  //#endregion
@@ -92727,7 +94725,7 @@ async function buildChildServicesFor(input) {
92727
94725
  for (const h of handles) try {
92728
94726
  await h.dispose();
92729
94727
  } catch (err) {
92730
- ctx.logger.withTags({ deviceId: parentNumericId }).debug("export-hap: child service dispose failed (continuing)", { meta: { error: errMsg$2(err) } });
94728
+ ctx.logger.withTags({ deviceId: parentNumericId }).debug("export-hap: child service dispose failed (continuing)", { meta: { error: errMsg$5(err) } });
92731
94729
  }
92732
94730
  } };
92733
94731
  }
@@ -92743,7 +94741,7 @@ async function listChildren(ctx, parentNumericId) {
92743
94741
  features: Array.isArray(c.features) ? c.features.filter((f) => typeof f === "string") : []
92744
94742
  }));
92745
94743
  } catch (err) {
92746
- ctx.logger.withTags({ deviceId: parentNumericId }).debug("export-hap: deviceManager.getChildren failed (non-fatal)", { meta: { error: errMsg$2(err) } });
94744
+ ctx.logger.withTags({ deviceId: parentNumericId }).debug("export-hap: deviceManager.getChildren failed (non-fatal)", { meta: { error: errMsg$5(err) } });
92747
94745
  return [];
92748
94746
  }
92749
94747
  }
@@ -92759,10 +94757,76 @@ function asDeviceType(raw) {
92759
94757
  for (const value of Object.values(DeviceType)) if (value === lower) return value;
92760
94758
  return DeviceType.Generic;
92761
94759
  }
92762
- function errMsg$2(err) {
94760
+ function errMsg$5(err) {
92763
94761
  return err instanceof Error ? err.message : String(err);
92764
94762
  }
92765
94763
  //#endregion
94764
+ //#region src/mappers/kind.ts
94765
+ /**
94766
+ * Which orchestrator a device gets, which device types the picker offers, and
94767
+ * what a device's HomeKit accessory is called on the wire.
94768
+ *
94769
+ * Separate from `index.ts` (the factory registry) only so the orchestrators can
94770
+ * import the uuid convention without importing the registry that imports them.
94771
+ */
94772
+ var SUPPORTED_MAPPER_KINDS = ["camera", "generic"];
94773
+ /**
94774
+ * The device types the Export picker offers, and the answer to the cap's
94775
+ * `listSupportedDeviceKinds`.
94776
+ *
94777
+ * A UI FILTER, nothing more. Whether a device actually exports anything is
94778
+ * decided by its capabilities (`rowsForCaps` in the capability table) — the
94779
+ * doctrine the HA exporter writes down, and the thing that confined the
94780
+ * previous exporter to cameras when it was ignored.
94781
+ *
94782
+ * Tier A: the types whose HomeKit services exist today and need no
94783
+ * feature-dependent shape. `cover`, `climate`, `fan`, `valve`, `humidifier`,
94784
+ * `water-heater`, `alarm-panel`, `button` and `media-player` are deliberately
94785
+ * absent — each needs a service whose semantics the table cannot yet honour,
94786
+ * and offering the tab before the mapping exists is how an operator gets an
94787
+ * accessory that pairs and does nothing.
94788
+ */
94789
+ var HAP_EXPORTABLE_DEVICE_TYPES = [
94790
+ DeviceType.Camera,
94791
+ DeviceType.Light,
94792
+ DeviceType.Switch,
94793
+ DeviceType.Siren,
94794
+ DeviceType.Sensor,
94795
+ DeviceType.Lock,
94796
+ DeviceType.Presence,
94797
+ DeviceType.Generic
94798
+ ];
94799
+ /**
94800
+ * Resolve the orchestrator for a device from its TYPE.
94801
+ *
94802
+ * `null` means "no Export tab": the picker must not offer a type no
94803
+ * orchestrator can build. An UNKNOWN type (the device-manager read failed) maps
94804
+ * to `camera` — that is what every entry persisted before this function existed
94805
+ * carries, and a transient API failure must never re-shape an exposed camera.
94806
+ */
94807
+ function pickMapperKind(deviceType) {
94808
+ if (deviceType === null || deviceType === void 0 || deviceType.length === 0) return "camera";
94809
+ if (deviceType === DeviceType.Camera) return "camera";
94810
+ return HAP_EXPORTABLE_DEVICE_TYPES.find((type) => type === deviceType) === void 0 ? null : "generic";
94811
+ }
94812
+ /**
94813
+ * The deterministic HAP accessory UUID for a device.
94814
+ *
94815
+ * One function, because three call sites depend on the answer agreeing: the
94816
+ * orchestrator that builds the accessory, `unexposeDevice` (which wipes
94817
+ * hap-nodejs' pairing blobs by uuid, on a device whose mapper is already gone)
94818
+ * and the export sync state that records it.
94819
+ *
94820
+ * The camera namespace is FROZEN — every camera paired to date derives its MAC
94821
+ * from `sha256("camstack:hap:" + uuid)` of this exact string, and changing it
94822
+ * would make every paired camera a stranger. Generic devices get their own
94823
+ * namespace so a switch and a camera that happen to share a device id are not
94824
+ * the same HomeKit accessory.
94825
+ */
94826
+ function accessoryUuidFor(kind, deviceId) {
94827
+ return import_dist.uuid.generate(kind === "camera" ? `camstack:camera:${deviceId}` : `camstack:device:${deviceId}`);
94828
+ }
94829
+ //#endregion
92766
94830
  //#region src/mappers/camera-accessory.ts
92767
94831
  /**
92768
94832
  * Camera accessory orchestrator — given a camstack deviceId, builds one
@@ -92795,9 +94859,9 @@ async function buildCameraAccessory(input) {
92795
94859
  const capNames = new Set(proxy.binding?.entries.map((e) => e.capName) ?? []);
92796
94860
  const isDoorbell = capNames.has("doorbell");
92797
94861
  const category = isDoorbell ? import_dist.Categories.VIDEO_DOORBELL : import_dist.Categories.IP_CAMERA;
92798
- const accessory = new import_dist.Accessory(displayName, import_dist.uuid.generate(`camstack:camera:${numericId}`));
94862
+ const accessory = new import_dist.Accessory(displayName, accessoryUuidFor("camera", numericId));
92799
94863
  accessory.category = category;
92800
- await populateAccessoryInfo(accessory, proxy, displayName);
94864
+ await populateAccessoryInfo(accessory, proxy, displayName, "Camera");
92801
94865
  const bctx = {
92802
94866
  ctx,
92803
94867
  accessory,
@@ -92855,55 +94919,462 @@ async function buildCameraAccessory(input) {
92855
94919
  for (const h of handles) try {
92856
94920
  await h.dispose();
92857
94921
  } catch (err) {
92858
- log.debug("export-hap: builder dispose failed (continuing)", { meta: { error: errMsg$1(err) } });
94922
+ log.debug("export-hap: builder dispose failed (continuing)", { meta: { error: errMsg$4(err) } });
92859
94923
  }
92860
94924
  try {
92861
94925
  await streams.dispose();
92862
94926
  } catch (err) {
92863
- log.debug("export-hap: streams dispose failed", { meta: { error: errMsg$1(err) } });
94927
+ log.debug("export-hap: streams dispose failed", { meta: { error: errMsg$4(err) } });
92864
94928
  }
92865
94929
  }
92866
94930
  };
92867
94931
  }
92868
- async function populateAccessoryInfo(accessory, proxy, displayName) {
92869
- const info = accessory.getService(import_dist.Service.AccessoryInformation);
92870
- if (!info) return;
94932
+ function errMsg$4(err) {
94933
+ return err instanceof Error ? err.message : String(err);
94934
+ }
94935
+ //#endregion
94936
+ //#region src/mappers/builders/generic/lock.ts
94937
+ /**
94938
+ * `lock-control` → `Service.LockMechanism`.
94939
+ *
94940
+ * Not a sensor row: HomeKit models a lock as two characteristics, a CURRENT
94941
+ * state the accessory owns and a TARGET state the controller writes, and the
94942
+ * cap's five states do not map onto either one alone.
94943
+ *
94944
+ * ```
94945
+ * cap state LockCurrentState LockTargetState
94946
+ * locked SECURED SECURED
94947
+ * unlocked UNSECURED UNSECURED
94948
+ * locking UNKNOWN SECURED (in flight — do not claim SECURED)
94949
+ * unlocking UNKNOWN UNSECURED
94950
+ * jammed JAMMED unchanged (the controller's intent stands)
94951
+ * ```
94952
+ *
94953
+ * Reporting SECURED while the bolt is still moving is the failure worth naming:
94954
+ * iOS renders the lock as closed, the operator walks away, and the motor stalls
94955
+ * behind them. `UNKNOWN` is the honest answer for an in-flight transition.
94956
+ *
94957
+ * `open` (the cap's third method, for locks with a latch/buzzer) has no HomeKit
94958
+ * counterpart on this service and is deliberately not wired — a Switch that
94959
+ * silently buzzes a door open would be a second knob nobody asked for.
94960
+ */
94961
+ var SUBTYPE = "lock-control";
94962
+ var readLock = reader(LockControlStatusSchema.pick({ state: true }), (status) => {
94963
+ const current = status.state === "locked" ? import_dist.Characteristic.LockCurrentState.SECURED : status.state === "unlocked" ? import_dist.Characteristic.LockCurrentState.UNSECURED : status.state === "jammed" ? import_dist.Characteristic.LockCurrentState.JAMMED : import_dist.Characteristic.LockCurrentState.UNKNOWN;
94964
+ const target = status.state === "locked" || status.state === "locking" ? import_dist.Characteristic.LockTargetState.SECURED : status.state === "unlocked" || status.state === "unlocking" ? import_dist.Characteristic.LockTargetState.UNSECURED : null;
94965
+ return [{
94966
+ characteristic: import_dist.Characteristic.LockCurrentState,
94967
+ value: current
94968
+ }, ...target === null ? [] : [{
94969
+ characteristic: import_dist.Characteristic.LockTargetState,
94970
+ value: target
94971
+ }]];
94972
+ });
94973
+ async function buildLockMechanism(bctx) {
94974
+ const { ctx, accessory, proxy, numericDeviceId, displayName } = bctx;
94975
+ const log = ctx.logger.withTags({ deviceId: numericDeviceId });
94976
+ const label = hapServiceName([displayName], `Lock ${numericDeviceId}`);
94977
+ const service = accessory.addService(import_dist.Service.LockMechanism, label, SUBTYPE);
94978
+ applyServiceLabel(service, label);
94979
+ const apply = (status, source) => {
94980
+ const updates = readLock(status);
94981
+ if (updates.length === 0) {
94982
+ log.debug("export-hap: lock status did not match the cap schema — no update", { meta: { source } });
94983
+ return;
94984
+ }
94985
+ applyUpdates(service, updates);
94986
+ };
92871
94987
  try {
92872
- const device = await proxy.deviceManager?.getDevice({});
92873
- const metadata = device?.metadata ?? null;
92874
- info.setCharacteristic(import_dist.Characteristic.Name, device?.name ?? displayName);
92875
- info.setCharacteristic(import_dist.Characteristic.Manufacturer, stringOr(metadata?.manufacturer, "CamStack"));
92876
- info.setCharacteristic(import_dist.Characteristic.Model, stringOr(metadata?.model, "Camera"));
92877
- info.setCharacteristic(import_dist.Characteristic.FirmwareRevision, stringOr(metadata?.firmware, "0.0.0"));
92878
- info.setCharacteristic(import_dist.Characteristic.SerialNumber, stringOr(metadata?.sn, `camstack-${proxy.deviceId}`));
92879
- } catch {}
94988
+ const status = await proxy.lockControl?.getStatus({});
94989
+ if (status !== void 0 && status !== null) apply(status, "getStatus");
94990
+ } catch (err) {
94991
+ log.debug("export-hap: lock getStatus hydrate failed (non-fatal)", { meta: { error: errMsg$3(err) } });
94992
+ }
94993
+ service.getCharacteristic(import_dist.Characteristic.LockTargetState).onSet(async (value) => {
94994
+ const secure = value === import_dist.Characteristic.LockTargetState.SECURED;
94995
+ try {
94996
+ if (secure) await proxy.lockControl?.lock({});
94997
+ else await proxy.lockControl?.unlock({});
94998
+ } catch (err) {
94999
+ log.warn("export-hap: lock command failed", { meta: {
95000
+ secure,
95001
+ error: errMsg$3(err)
95002
+ } });
95003
+ }
95004
+ });
95005
+ const unsubscribe = proxy.state.lockControl?.subscribe((value) => {
95006
+ if (value === void 0 || value === null) return;
95007
+ apply(value, "slice");
95008
+ }) ?? null;
95009
+ return { async dispose() {
95010
+ try {
95011
+ unsubscribe?.();
95012
+ } catch {}
95013
+ } };
92880
95014
  }
92881
- function stringOr(value, fallback) {
92882
- return typeof value === "string" && value.length > 0 ? value : fallback;
95015
+ function errMsg$3(err) {
95016
+ return err instanceof Error ? err.message : String(err);
95017
+ }
95018
+ //#endregion
95019
+ //#region src/mappers/builders/generic/sensor-service.ts
95020
+ async function buildSensorService(input) {
95021
+ const { bctx, spec, name, subtype } = input;
95022
+ const { ctx, accessory, proxy, numericDeviceId } = bctx;
95023
+ const log = ctx.logger.withTags({ deviceId: numericDeviceId });
95024
+ const label = hapServiceName([name], spec.label);
95025
+ const service = spec.addService(accessory, label, subtype);
95026
+ applyServiceLabel(service, label);
95027
+ const apply = (status, source) => {
95028
+ const updates = spec.read(status);
95029
+ if (updates.length === 0) {
95030
+ log.debug("export-hap: sensor status did not match the cap schema — no update", { meta: {
95031
+ subtype,
95032
+ source
95033
+ } });
95034
+ return;
95035
+ }
95036
+ applyUpdates(service, updates);
95037
+ };
95038
+ try {
95039
+ const status = await spec.getStatus(proxy);
95040
+ if (status !== void 0 && status !== null) apply(status, "getStatus");
95041
+ } catch (err) {
95042
+ log.debug("export-hap: sensor getStatus hydrate failed (non-fatal)", { meta: {
95043
+ subtype,
95044
+ error: errMsg$2(err)
95045
+ } });
95046
+ }
95047
+ const unsubscribe = spec.subscribe(proxy, (value) => {
95048
+ if (value === void 0 || value === null) return;
95049
+ apply(value, "slice");
95050
+ });
95051
+ return { async dispose() {
95052
+ try {
95053
+ unsubscribe?.();
95054
+ } catch {}
95055
+ } };
95056
+ }
95057
+ function errMsg$2(err) {
95058
+ return err instanceof Error ? err.message : String(err);
95059
+ }
95060
+ //#endregion
95061
+ //#region src/mappers/builders/generic/cap-service-table.ts
95062
+ /**
95063
+ * The capability→HomeKit-service TABLE for non-camera devices.
95064
+ *
95065
+ * This is the whole coverage decision for the generic export path, and it is
95066
+ * deliberately a table rather than a `switch` on `DeviceType`. The doctrine is
95067
+ * the one the Home Assistant exporter already writes down: *coverage is decided
95068
+ * by a device's capabilities, not its type — restricting the picker by type is
95069
+ * what confined the previous exporter to cameras.* A `DeviceType` here is only
95070
+ * ever a UI filter (`HAP_EXPORTABLE_DEVICE_TYPES`) or an icon
95071
+ * (`ACCESSORY_CATEGORY_BY_TYPE`); it never decides whether something exports.
95072
+ *
95073
+ * The shape mirrors `CAP_ENTITY_MAP` in
95074
+ * `packages/addon-provider-homeassistant/src/ha-export/entity-catalog.ts` so
95075
+ * the two can be unified later — one row per capability, keyed by the cap name
95076
+ * exactly as it appears in `proxy.binding.entries[].capName`.
95077
+ *
95078
+ * ## Reading a status
95079
+ *
95080
+ * Every row parses the cap's OWN Zod status schema rather than duck-typing the
95081
+ * payload, and it `.pick()`s only the fields it reads: a provider that omits a
95082
+ * timestamp must not silence a sensor, and a provider that sends the wrong
95083
+ * shape must not write a garbage characteristic. A parse that fails yields no
95084
+ * updates, and the builder that owns the service logs the drop — silence reads
95085
+ * as "the sensor never changed".
95086
+ *
95087
+ * ## Tier
95088
+ *
95089
+ * Tier A only: the caps whose HomeKit service exists today and needs no
95090
+ * feature-dependent shape (`cover` needs `cover-positionable`, `climate-control`
95091
+ * needs the dual-setpoint split, `alarm-panel` must survive a refused `arm`).
95092
+ * Those are a separate task; adding a row here must never mean adding a service
95093
+ * whose semantics this file cannot fully honour.
95094
+ */
95095
+ /** HomeKit's floor for `CurrentAmbientLightLevel`; 0 lux is out of range. */
95096
+ var MIN_LUX = 1e-4;
95097
+ var SENSOR_SPECS = {
95098
+ contact: {
95099
+ label: "Contact",
95100
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.ContactSensor, name, subtype),
95101
+ getStatus: (proxy) => proxy.contact?.getStatus({}),
95102
+ subscribe: (proxy, onValue) => proxy.state.contact?.subscribe(onValue) ?? null,
95103
+ read: reader(ContactStatusSchema.pick({ entryOpen: true }), (status) => [{
95104
+ characteristic: import_dist.Characteristic.ContactSensorState,
95105
+ value: status.entryOpen ? import_dist.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : import_dist.Characteristic.ContactSensorState.CONTACT_DETECTED
95106
+ }])
95107
+ },
95108
+ motion: {
95109
+ label: "Motion",
95110
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.MotionSensor, name, subtype),
95111
+ getStatus: (proxy) => proxy.motion?.getStatus({}),
95112
+ subscribe: (proxy, onValue) => proxy.state.motion?.subscribe(onValue) ?? null,
95113
+ read: reader(MotionStatusSchema.pick({ detected: true }), (status) => [{
95114
+ characteristic: import_dist.Characteristic.MotionDetected,
95115
+ value: status.detected
95116
+ }])
95117
+ },
95118
+ presence: {
95119
+ label: "Presence",
95120
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.OccupancySensor, name, subtype),
95121
+ getStatus: (proxy) => proxy.presence?.getStatus({}),
95122
+ subscribe: (proxy, onValue) => proxy.state.presence?.subscribe(onValue) ?? null,
95123
+ read: reader(PresenceStatusSchema.pick({ state: true }), (status) => [{
95124
+ characteristic: import_dist.Characteristic.OccupancyDetected,
95125
+ value: status.state === "home" ? import_dist.Characteristic.OccupancyDetected.OCCUPANCY_DETECTED : import_dist.Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED
95126
+ }])
95127
+ },
95128
+ smoke: {
95129
+ label: "Smoke",
95130
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.SmokeSensor, name, subtype),
95131
+ getStatus: (proxy) => proxy.smoke?.getStatus({}),
95132
+ subscribe: (proxy, onValue) => proxy.state.smoke?.subscribe(onValue) ?? null,
95133
+ read: reader(SmokeStatusSchema.pick({ detected: true }), (status) => [{
95134
+ characteristic: import_dist.Characteristic.SmokeDetected,
95135
+ value: status.detected ? import_dist.Characteristic.SmokeDetected.SMOKE_DETECTED : import_dist.Characteristic.SmokeDetected.SMOKE_NOT_DETECTED
95136
+ }])
95137
+ },
95138
+ "carbon-monoxide": {
95139
+ label: "Carbon monoxide",
95140
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.CarbonMonoxideSensor, name, subtype),
95141
+ getStatus: (proxy) => proxy.carbonMonoxide?.getStatus({}),
95142
+ subscribe: (proxy, onValue) => proxy.state.carbonMonoxide?.subscribe(onValue) ?? null,
95143
+ read: reader(CarbonMonoxideStatusSchema.pick({ detected: true }), (status) => [{
95144
+ characteristic: import_dist.Characteristic.CarbonMonoxideDetected,
95145
+ value: status.detected ? import_dist.Characteristic.CarbonMonoxideDetected.CO_LEVELS_ABNORMAL : import_dist.Characteristic.CarbonMonoxideDetected.CO_LEVELS_NORMAL
95146
+ }])
95147
+ },
95148
+ flood: {
95149
+ label: "Leak",
95150
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.LeakSensor, name, subtype),
95151
+ getStatus: (proxy) => proxy.flood?.getStatus({}),
95152
+ subscribe: (proxy, onValue) => proxy.state.flood?.subscribe(onValue) ?? null,
95153
+ read: reader(FloodStatusSchema.pick({ flooded: true }), (status) => [{
95154
+ characteristic: import_dist.Characteristic.LeakDetected,
95155
+ value: status.flooded ? import_dist.Characteristic.LeakDetected.LEAK_DETECTED : import_dist.Characteristic.LeakDetected.LEAK_NOT_DETECTED
95156
+ }])
95157
+ },
95158
+ "temperature-sensor": {
95159
+ label: "Temperature",
95160
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.TemperatureSensor, name, subtype),
95161
+ getStatus: (proxy) => proxy.temperatureSensor?.getStatus({}),
95162
+ subscribe: (proxy, onValue) => proxy.state.temperatureSensor?.subscribe(onValue) ?? null,
95163
+ read: reader(TemperatureSensorStatusSchema.pick({ celsius: true }), (status) => [{
95164
+ characteristic: import_dist.Characteristic.CurrentTemperature,
95165
+ value: status.celsius
95166
+ }])
95167
+ },
95168
+ "humidity-sensor": {
95169
+ label: "Humidity",
95170
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.HumiditySensor, name, subtype),
95171
+ getStatus: (proxy) => proxy.humiditySensor?.getStatus({}),
95172
+ subscribe: (proxy, onValue) => proxy.state.humiditySensor?.subscribe(onValue) ?? null,
95173
+ read: reader(HumiditySensorStatusSchema.pick({ percent: true }), (status) => [{
95174
+ characteristic: import_dist.Characteristic.CurrentRelativeHumidity,
95175
+ value: status.percent
95176
+ }])
95177
+ },
95178
+ "ambient-light-sensor": {
95179
+ label: "Light level",
95180
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.LightSensor, name, subtype),
95181
+ getStatus: (proxy) => proxy.ambientLightSensor?.getStatus({}),
95182
+ subscribe: (proxy, onValue) => proxy.state.ambientLightSensor?.subscribe(onValue) ?? null,
95183
+ read: reader(AmbientLightSensorStatusSchema.pick({ lux: true }), (status) => [{
95184
+ characteristic: import_dist.Characteristic.CurrentAmbientLightLevel,
95185
+ value: Math.max(MIN_LUX, status.lux)
95186
+ }])
95187
+ }
95188
+ };
95189
+ /**
95190
+ * The `battery` row reads like a sensor but writes three characteristics from
95191
+ * one status, and the camera path publishes the SAME service from the same cap.
95192
+ * The derivation therefore lives once, in `battery.ts`, and both callers read
95193
+ * it from there.
95194
+ */
95195
+ var BATTERY_SPEC = {
95196
+ label: "Battery",
95197
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.Battery, name, subtype),
95198
+ getStatus: (proxy) => proxy.battery?.getStatus({}),
95199
+ subscribe: (proxy, onValue) => proxy.state.battery?.subscribe(onValue) ?? null,
95200
+ read: batteryCharacteristicUpdates
95201
+ };
95202
+ function sensorRow(capName, spec) {
95203
+ return {
95204
+ caps: [capName],
95205
+ label: spec.label,
95206
+ build: ({ bctx, name }) => buildSensorService({
95207
+ bctx,
95208
+ spec,
95209
+ name,
95210
+ subtype: capName
95211
+ })
95212
+ };
95213
+ }
95214
+ /**
95215
+ * THE table. Order matters only for one thing: the first row that matches a
95216
+ * device decides the accessory's category when the device's own type does not.
95217
+ */
95218
+ var HAP_CAP_SERVICES = [
95219
+ {
95220
+ caps: ["switch", "brightness"],
95221
+ label: "Power",
95222
+ build: ({ bctx, name, deviceType }) => buildChildSwitch({
95223
+ ...bctx,
95224
+ displayName: name
95225
+ }, "switch", deviceType)
95226
+ },
95227
+ {
95228
+ caps: ["lock-control"],
95229
+ label: "Lock",
95230
+ build: ({ bctx, name }) => buildLockMechanism({
95231
+ ...bctx,
95232
+ displayName: name
95233
+ })
95234
+ },
95235
+ {
95236
+ caps: ["battery"],
95237
+ label: BATTERY_SPEC.label,
95238
+ build: ({ bctx, name }) => buildSensorService({
95239
+ bctx,
95240
+ spec: BATTERY_SPEC,
95241
+ name,
95242
+ subtype: "battery"
95243
+ })
95244
+ },
95245
+ ...Object.entries(SENSOR_SPECS).map(([capName, spec]) => sensorRow(capName, spec))
95246
+ ];
95247
+ /**
95248
+ * The rows a device's bound capabilities select, in table order.
95249
+ *
95250
+ * A row matches when ANY of its caps is bound: a lamp with `switch` but no
95251
+ * `brightness` is still a Lightbulb-shaped row, and the builder degrades on its
95252
+ * own.
95253
+ */
95254
+ function rowsForCaps(capNames) {
95255
+ return HAP_CAP_SERVICES.filter((row) => row.caps.some((cap) => capNames.has(cap)));
95256
+ }
95257
+ /**
95258
+ * The HomeKit accessory category per camstack device type — the icon iOS Home
95259
+ * draws and nothing else. Absent = `OTHER`, which is a valid accessory that
95260
+ * simply gets the generic tile.
95261
+ *
95262
+ * This is NOT a coverage decision. `HAP_EXPORTABLE_DEVICE_TYPES` (in
95263
+ * `mappers/index.ts`) filters the picker; whether a device exports anything is
95264
+ * decided by `rowsForCaps`.
95265
+ */
95266
+ var ACCESSORY_CATEGORY_BY_TYPE = {
95267
+ [DeviceType.Switch]: import_dist.Categories.SWITCH,
95268
+ [DeviceType.Light]: import_dist.Categories.LIGHTBULB,
95269
+ [DeviceType.Siren]: import_dist.Categories.SWITCH,
95270
+ [DeviceType.Lock]: import_dist.Categories.DOOR_LOCK,
95271
+ [DeviceType.Sensor]: import_dist.Categories.SENSOR,
95272
+ [DeviceType.Presence]: import_dist.Categories.SENSOR
95273
+ };
95274
+ function accessoryCategoryFor(deviceType) {
95275
+ return ACCESSORY_CATEGORY_BY_TYPE[deviceType] ?? import_dist.Categories.OTHER;
95276
+ }
95277
+ //#endregion
95278
+ //#region src/mappers/generic-accessory.ts
95279
+ /**
95280
+ * Generic accessory orchestrator — every camstack device that is NOT a camera.
95281
+ *
95282
+ * Same three steps as the camera orchestrator, and deliberately nothing more:
95283
+ * 1. resolve the typed `DeviceProxy`,
95284
+ * 2. select rows from the capability→service table with the device's BOUND
95285
+ * capabilities (never its type — see `builders/generic/cap-service-table.ts`),
95286
+ * 3. let each row add its service to one `Accessory`.
95287
+ *
95288
+ * It publishes standalone, like the camera path: one accessory, one mDNS
95289
+ * advertisement, one pairing, the shared setup code. This addon is not a
95290
+ * bridge — that decision predates this file and is not re-litigated here.
95291
+ *
95292
+ * ## It REFUSES rather than publishing an empty tile
95293
+ *
95294
+ * A device whose capabilities select no row throws. The alternative is an
95295
+ * accessory carrying nothing but `AccessoryInformation`: it pairs, it appears
95296
+ * in the Home app, it does nothing, and the operator has no way to tell that
95297
+ * from a broken integration. The Export tab is gated on the same question
95298
+ * (`hap-export.addon.ts`), so reaching this throw means the device changed
95299
+ * shape between the tab rendering and the toggle landing.
95300
+ */
95301
+ async function buildGenericAccessory(input) {
95302
+ const { ctx, deviceId, displayName, options } = input;
95303
+ const numericId = Number.parseInt(deviceId, 10);
95304
+ if (!Number.isFinite(numericId)) throw new Error(`export-hap: cannot map device '${deviceId}' — id is not numeric`);
95305
+ const log = ctx.logger.withTags({ deviceId: numericId });
95306
+ const proxy = await ctx.fetchDevice(numericId);
95307
+ const capNames = new Set(proxy.binding?.entries.map((e) => e.capName) ?? []);
95308
+ const rows = rowsForCaps(capNames);
95309
+ if (rows.length === 0) throw new Error(`export-hap: device ${numericId} carries no capability HomeKit can export (bound: ${[...capNames].toSorted().join(", ") || "none"})`);
95310
+ const deviceType = await resolveDeviceType(proxy);
95311
+ const accessory = new import_dist.Accessory(displayName, accessoryUuidFor("generic", numericId));
95312
+ accessory.category = accessoryCategoryFor(deviceType);
95313
+ await populateAccessoryInfo(accessory, proxy, displayName, "Accessory");
95314
+ const bctx = {
95315
+ ctx,
95316
+ accessory,
95317
+ proxy,
95318
+ numericDeviceId: numericId,
95319
+ displayName,
95320
+ options
95321
+ };
95322
+ const handles = [];
95323
+ for (const row of rows) {
95324
+ const name = rows.length === 1 ? displayName : row.label;
95325
+ handles.push(await row.build({
95326
+ bctx,
95327
+ name,
95328
+ deviceType
95329
+ }));
95330
+ }
95331
+ log.info("export-hap: built generic accessory", { meta: {
95332
+ deviceType,
95333
+ services: rows.map((row) => row.caps[0]).join(",")
95334
+ } });
95335
+ return {
95336
+ accessory,
95337
+ accessories: [accessory],
95338
+ async dispose() {
95339
+ for (const handle of handles) try {
95340
+ await handle.dispose();
95341
+ } catch (err) {
95342
+ log.debug("export-hap: generic builder dispose failed (continuing)", { meta: { error: errMsg$1(err) } });
95343
+ }
95344
+ }
95345
+ };
95346
+ }
95347
+ /**
95348
+ * The device's own type, or `Generic`.
95349
+ *
95350
+ * Used ONLY where one capability has two HomeKit shapes — a siren's
95351
+ * `brightness` is alarm volume, not luminosity — and for the accessory's icon.
95352
+ * `Generic` is the safe reading: it is the shape `child-switch.ts` already
95353
+ * treats as "a lamp if it dims, a switch otherwise".
95354
+ */
95355
+ async function resolveDeviceType(proxy) {
95356
+ try {
95357
+ const device = await proxy.deviceManager?.getDevice({});
95358
+ const raw = typeof device?.type === "string" ? device.type.toLowerCase() : "";
95359
+ return Object.values(DeviceType).find((value) => value === raw) ?? DeviceType.Generic;
95360
+ } catch {
95361
+ return DeviceType.Generic;
95362
+ }
92883
95363
  }
92884
95364
  function errMsg$1(err) {
92885
95365
  return err instanceof Error ? err.message : String(err);
92886
95366
  }
92887
95367
  //#endregion
92888
95368
  //#region src/mappers/index.ts
92889
- var SUPPORTED_MAPPER_KINDS = ["camera"];
92890
- var REGISTRY = { camera: buildCameraAccessory };
95369
+ var REGISTRY = {
95370
+ camera: buildCameraAccessory,
95371
+ generic: buildGenericAccessory
95372
+ };
92891
95373
  function getMapperFactory(kind) {
92892
95374
  const factory = REGISTRY[kind];
92893
95375
  if (!factory) throw new Error(`export-hap: no mapper registered for kind '${kind}'`);
92894
95376
  return factory;
92895
95377
  }
92896
- /**
92897
- * Resolve the best-fit mapper kind. With Round 2, the operator just
92898
- * picks a camera and the orchestrator does the rest — the single
92899
- * `camera` kind is returned unconditionally. We keep the function
92900
- * signature for backwards-compat with the addon's existing
92901
- * `exposeDevice` flow and to leave room for future device types (NVR,
92902
- * climate sensor, ...).
92903
- */
92904
- function pickMapperKind(_capabilities) {
92905
- return "camera";
92906
- }
92907
95378
  //#endregion
92908
95379
  //#region src/mappers/builders/stream-hwaccel-memo.ts
92909
95380
  /**
@@ -93041,8 +95512,8 @@ function syncStateToJson(map) {
93041
95512
  //#endregion
93042
95513
  //#region src/hap-export.addon.ts
93043
95514
  /**
93044
- * HomeKit (HAP) export addon — publishes a single HAP bridge process
93045
- * that exposes selected camstack devices as HomeKit accessories.
95515
+ * HomeKit (HAP) export addon — publishes selected camstack devices as
95516
+ * standalone HomeKit accessories.
93046
95517
  *
93047
95518
  * Operator flow:
93048
95519
  * 1. Install + enable the addon (hub-only, group `export-hap`).
@@ -93053,10 +95524,10 @@ function syncStateToJson(map) {
93053
95524
  * 3. The setup URI (`X-HM://…`) is logged AND surfaced via the
93054
95525
  * `getStatus` cap method so the wizard UI can render the QR.
93055
95526
  * 4. Operator opens iOS Home → + → scan QR → enter pincode.
93056
- * 5. Operator hits "Expose to HomeKit" on individual camstack
93057
- * devices; the addon attaches a MotionSensor accessory (MVP)
93058
- * to the bridge and persists the choice via the same
93059
- * `updateGlobalSettings` path.
95527
+ * 5. Operator hits "Expose to HomeKit" on individual camstack devices.
95528
+ * Each exposed device is published as its OWN accessory with its own
95529
+ * mDNS advertisement, sharing one setup code cameras cannot be
95530
+ * bridged, so nothing is.
93060
95531
  *
93061
95532
  * Exception — the hap-nodejs library's own `HAPStorage` lives under
93062
95533
  * `ctx.dataDir/hap-store/`. That directory is library-internal: the
@@ -93065,23 +95536,20 @@ function syncStateToJson(map) {
93065
95536
  * I/O for addon-owned data") explicitly scopes itself to OUR own data;
93066
95537
  * library-managed blobs are out of scope.
93067
95538
  *
93068
- * Round 2 scope: full camera bridge. `exposeDevice({deviceId})` builds
93069
- * one HomeKit Accessory per camstack camera with auto-detected feature
93070
- * services driven by the device's capability binding:
93071
- * - `camera-streams` cap CameraRTPStreamManagement via ffmpeg
93072
- * - `intercom` cap → Microphone + Speaker (audio bridge wiring is a
93073
- * Round 3 follow-up services are declared so iOS Home shows the
93074
- * talk-back button, but PCM upload is not yet routed)
93075
- * - `doorbell` cap DoorbellController + Doorbell service
93076
- * - `motion-detection` cap MotionSensor service
93077
- * - `ptz` cap preset switches + 4 directional momentary switches
93078
- * - `ptz-autotrack` cap → stateful "Autotrack" switch
93079
- * Children (siren, floodlight, spotlight, …) become independent
93080
- * accessories under the same Bridge see `mappers/child-accessory.ts`.
93081
- *
93082
- * What's deferred to Round 3+: cam→browser audio Opus transcode,
93083
- * intercom upload bridge, HomeKit Secure Video, recording, native
93084
- * H.264 stream tap (currently uses RTSP + ffmpeg copy).
95539
+ * ## Two accessory shapes, one rule about coverage
95540
+ *
95541
+ * `exposeDevice({deviceId})` resolves the device's TYPE to a mapper kind
95542
+ * (`mappers/kind.ts`) and hands off:
95543
+ *
95544
+ * - a camera `mappers/camera-accessory.ts`: streams, HKSV recording,
95545
+ * doorbell, motion, intercom, PTZ, battery, privacy and every accessory
95546
+ * child, all as services on one accessory;
95547
+ * - anything else → `mappers/generic-accessory.ts`, driven by the
95548
+ * capabilityservice table in `mappers/builders/generic/`.
95549
+ *
95550
+ * The type only picks the SHAPE. What a device publishes is decided by the
95551
+ * capabilities bound to itrestricting coverage by type is precisely what
95552
+ * confined this exporter to cameras for its first three rounds.
93085
95553
  */
93086
95554
  var DEFAULT_DEVICE_SETTINGS = {
93087
95555
  streamPreference: "auto",
@@ -93124,7 +95592,6 @@ var DEFAULT_CONFIG = {
93124
95592
  fixedPin: "",
93125
95593
  interfaceName: "",
93126
95594
  ptzPulseMs: 400,
93127
- hksvPreview: false,
93128
95595
  identity: {
93129
95596
  username: "",
93130
95597
  pincode: "",
@@ -93240,7 +95707,7 @@ var ExportHapAddon = class extends BaseAddon {
93240
95707
  ...setup ? { setup } : {}
93241
95708
  };
93242
95709
  },
93243
- listSupportedDeviceKinds: async () => [...SUPPORTED_MAPPER_KINDS],
95710
+ listSupportedDeviceKinds: async () => [...HAP_EXPORTABLE_DEVICE_TYPES],
93244
95711
  listExposedDevices: async () => Array.from(this.exposed.entries()).map(([deviceId, m]) => {
93245
95712
  const entry = this.config.exposed.find((e) => e.deviceId === deviceId);
93246
95713
  return {
@@ -93325,9 +95792,10 @@ var ExportHapAddon = class extends BaseAddon {
93325
95792
  log.debug("export-hap: device already exposed — refreshing capabilities");
93326
95793
  await this.detachMapper(deviceId);
93327
95794
  }
93328
- const mapperKind = pickMapperKind(capabilities);
93329
- if (!mapperKind) throw new Error(`export-hap: no mapper for capabilities ${JSON.stringify(capabilities ?? [])}`);
93330
- const displayName = await this.resolveDisplayName(deviceId);
95795
+ const summary = await this.fetchDeviceSummary(numericId);
95796
+ const mapperKind = pickMapperKind(summary?.type);
95797
+ if (!mapperKind) throw new Error(`export-hap: device ${numericId} has type '${summary?.type ?? "unknown"}', which HomeKit export does not support`);
95798
+ const displayName = summary?.name ?? `Device ${deviceId}`;
93331
95799
  const previous = this.config.exposed.find((e) => e.deviceId === deviceId);
93332
95800
  const baseEntry = carryForward({
93333
95801
  deviceId,
@@ -93357,10 +95825,11 @@ var ExportHapAddon = class extends BaseAddon {
93357
95825
  async unexposeDevice(deviceId, options = {}) {
93358
95826
  const numericId = Number.parseInt(deviceId, 10);
93359
95827
  const log = this.ctx.logger.withTags({ deviceId: numericId });
95828
+ const mapperKind = this.findEntry(numericId)?.mapperKind ?? "camera";
93360
95829
  await this.detachMapper(deviceId);
93361
95830
  const next = this.config.exposed.filter((e) => e.deviceId !== deviceId);
93362
95831
  if (next.length !== this.config.exposed.length) await this.updateGlobalSettings({ exposed: next });
93363
- if (options.clearPairing !== false) clearPairingFiles(import_dist.uuid.generate(`camstack:camera:${numericId}`), this.ctx.logger);
95832
+ if (options.clearPairing !== false) clearPairingFiles(accessoryUuidFor(mapperKind, numericId), this.ctx.logger);
93364
95833
  await this.forgetFingerprint(numericId);
93365
95834
  log.info("export-hap: unexposed device");
93366
95835
  }
@@ -93410,10 +95879,10 @@ var ExportHapAddon = class extends BaseAddon {
93410
95879
  /** Pending rebuild timers per deviceId — used to debounce. Cleared
93411
95880
  * on detach so stale timers can't republish a removed accessory. */
93412
95881
  pendingRebuildTimers = /* @__PURE__ */ new Map();
93413
- /** Deterministic HAP accessory UUID for a device (matches `buildCameraAccessory`
93414
- * and `unexposeDevice`). */
95882
+ /** Deterministic HAP accessory UUID for a device the same one the
95883
+ * orchestrator built it with (`mappers/kind.ts`). */
93415
95884
  hapAccessoryUuid(deviceId) {
93416
- return import_dist.uuid.generate(`camstack:camera:${deviceId}`);
95885
+ return accessoryUuidFor(this.findEntry(deviceId)?.mapperKind ?? "camera", deviceId);
93417
95886
  }
93418
95887
  /**
93419
95888
  * Export fingerprint of a device from its PERSISTED features + type
@@ -93572,31 +96041,79 @@ var ExportHapAddon = class extends BaseAddon {
93572
96041
  log.warn("export-hap: reconcile rebuild failed", { meta: { error: errMsg(err) } });
93573
96042
  }
93574
96043
  }
93575
- async resolveDisplayName(deviceId) {
93576
- const numeric = Number.parseInt(deviceId, 10);
93577
- if (!Number.isFinite(numeric)) return `Device ${deviceId}`;
96044
+ /**
96045
+ * The device's name and type, or `null` when the registry cannot answer.
96046
+ *
96047
+ * `null` is NOT "no such device" — it also covers a transient API failure,
96048
+ * and every caller treats it as "keep doing what was already being done"
96049
+ * rather than re-shaping an exposed accessory on incomplete information.
96050
+ */
96051
+ async fetchDeviceSummary(deviceId) {
96052
+ if (!Number.isFinite(deviceId)) return null;
93578
96053
  try {
93579
- const device = await this.ctx.api.deviceManager?.getDevice.query({ deviceId: numeric });
93580
- if (device?.name) return device.name;
96054
+ const device = await this.ctx.api.deviceManager?.getDevice.query({ deviceId });
96055
+ if (!device) return null;
96056
+ return {
96057
+ name: device.name,
96058
+ type: device.type
96059
+ };
93581
96060
  } catch (err) {
93582
- this.ctx.logger.withTags({ deviceId: numeric }).debug("export-hap: deviceManager.getDevice failed", { meta: { error: errMsg(err) } });
96061
+ this.ctx.logger.withTags({ deviceId }).debug("export-hap: deviceManager.getDevice failed", { meta: { error: errMsg(err) } });
96062
+ return null;
96063
+ }
96064
+ }
96065
+ /**
96066
+ * Which accessory shape this device would get, or `null` for "no Export tab".
96067
+ *
96068
+ * Two questions, in this order, because they fail differently:
96069
+ * 1. the TYPE — a type with no orchestrator is refused outright;
96070
+ * 2. for a non-camera, the CAPABILITIES — a device carrying nothing the
96071
+ * table maps would publish an accessory that pairs and does nothing, and
96072
+ * the operator cannot tell that from a broken integration.
96073
+ *
96074
+ * A camera skips (2): the camera orchestrator has always published on type
96075
+ * alone, and adding a cap gate here would be a new way for an existing camera
96076
+ * to lose its Export tab. A registry that cannot answer at all also resolves
96077
+ * to `camera`, which is what every persisted entry predating this method
96078
+ * carries.
96079
+ */
96080
+ async resolveExportKind(deviceId) {
96081
+ const summary = await this.fetchDeviceSummary(deviceId);
96082
+ const kind = pickMapperKind(summary?.type);
96083
+ if (kind === null) return null;
96084
+ if (kind === "camera") return "camera";
96085
+ try {
96086
+ const proxy = await this.ctx.fetchDevice(deviceId);
96087
+ const capNames = new Set(proxy.binding?.entries.map((entry) => entry.capName) ?? []);
96088
+ if (rowsForCaps(capNames).length > 0) return "generic";
96089
+ this.ctx.logger.withTags({ deviceId }).debug("export-hap: no Export tab — no mapped caps", { meta: {
96090
+ type: summary?.type,
96091
+ caps: [...capNames].toSorted().join(",")
96092
+ } });
96093
+ return null;
96094
+ } catch (err) {
96095
+ this.ctx.logger.withTags({ deviceId }).debug("export-hap: no Export tab — capability read failed", { meta: {
96096
+ type: summary?.type,
96097
+ error: errMsg(err)
96098
+ } });
96099
+ return null;
93583
96100
  }
93584
- return `Device ${deviceId}`;
93585
96101
  }
93586
96102
  globalSettingsSchema() {
93587
96103
  return this.schema({ sections: [{
93588
96104
  id: "export-hap",
93589
96105
  title: "HomeKit Export",
93590
- description: "Publishes a HomeKit bridge. After the addon boots, scan the pairing QR (or enter the setup code) from the Exported devices panel in the iOS Home app. After pairing, mark individual devices as \"Expose to HomeKit\" on each device's settings page.",
96106
+ description: "Publishes each exposed device as its own HomeKit accessory, sharing one setup code. Mark a device \"Expose to HomeKit\" on its settings page, then add it in the iOS Home app (+ Add Accessory) and enter the setup code shown there.",
93591
96107
  columns: 1,
93592
96108
  fields: [
93593
96109
  this.field({
93594
96110
  type: "text",
93595
96111
  key: "bridgeName",
93596
- label: "Bridge name",
93597
- description: "Name shown in iOS Home and in the Bonjour advertisement.",
96112
+ label: "Installation name",
96113
+ description: "Legacy. Every exposed device is published STANDALONE, under its own camstack device name — HomeKit cameras cannot be bridged, so nothing is bridged. This string only seeds the identity generated on first boot and is not shown in iOS Home.",
93598
96114
  default: DEFAULT_CONFIG.bridgeName,
93599
- requiresRestart: true
96115
+ requiresRestart: true,
96116
+ placement: { tab: "advanced" }
93600
96117
  }),
93601
96118
  this.field({
93602
96119
  type: "number",
@@ -93632,21 +96149,13 @@ var ExportHapAddon = class extends BaseAddon {
93632
96149
  description: "Duration of a single pan/tilt momentary command issued by the PTZ direction switches in Apple Home. Lower = finer steps; higher = bigger sweeps per tap. Defaults to 400ms.",
93633
96150
  default: DEFAULT_CONFIG.ptzPulseMs,
93634
96151
  placement: { tab: "advanced" }
93635
- }),
93636
- this.field({
93637
- type: "boolean",
93638
- key: "hksvPreview",
93639
- label: "HKSV Developer Preview (experimental)",
93640
- description: "Advertise Apple's HomeKit Secure Video Developer Preview services — HEVC streaming and the WebRTC stream-management surface — alongside the classic camera profile. Apple published this specification on 2026-06-03 and no shipping controller is known to negotiate it yet. Leave off unless you are testing against a preview build: advertising unknown services to a paired controller can disturb the classic path that works today.",
93641
- default: DEFAULT_CONFIG.hksvPreview,
93642
- requiresRestart: true,
93643
- placement: { tab: "advanced" }
93644
96152
  })
93645
96153
  ]
93646
96154
  }] });
93647
96155
  }
93648
96156
  async buildDeviceSettingsContribution(deviceId) {
93649
- if (!await this.isCameraDevice(deviceId)) return null;
96157
+ const kind = await this.resolveExportKind(deviceId);
96158
+ if (kind === null) return null;
93650
96159
  const entry = this.findEntry(deviceId);
93651
96160
  const settings = entry?.settings ?? DEFAULT_DEVICE_SETTINGS;
93652
96161
  const enabled = entry !== null;
@@ -93663,6 +96172,32 @@ var ExportHapAddon = class extends BaseAddon {
93663
96172
  } catch (err) {
93664
96173
  this.ctx.logger.withTags({ deviceId }).debug("export-hap: setupURI failed for per-device contribution", { meta: { error: errMsg(err) } });
93665
96174
  }
96175
+ const cameraFields = kind !== "camera" ? [] : [{
96176
+ type: "select",
96177
+ key: streamPreferenceKey,
96178
+ label: "Source stream (HomeKit)",
96179
+ description: "Which camstack profile slot HomeKit pulls. Auto = the broker picks the slot closest to 1080p at session start.",
96180
+ options: HAP_STREAM_PREFERENCE_OPTIONS,
96181
+ required: true,
96182
+ value: settings.streamPreference,
96183
+ showWhen: {
96184
+ field: enabledKey,
96185
+ equals: true
96186
+ },
96187
+ immediate: true
96188
+ }, {
96189
+ type: "boolean",
96190
+ key: hksvKey,
96191
+ label: "HomeKit recording (Secure Video)",
96192
+ description: "Offer “Stream and Allow Recording” in iOS Home. Requires iCloud+ and a home hub. Keeps a continuous 8s prebuffer for this camera (~0.7% of one CPU core, H.264 sources only).",
96193
+ style: "switch",
96194
+ value: resolveHksvRecording(settings),
96195
+ showWhen: {
96196
+ field: enabledKey,
96197
+ equals: true
96198
+ },
96199
+ immediate: true
96200
+ }];
93666
96201
  return {
93667
96202
  tabs: [{
93668
96203
  id: "export",
@@ -93673,7 +96208,7 @@ var ExportHapAddon = class extends BaseAddon {
93673
96208
  sections: [{
93674
96209
  id: "export-hap",
93675
96210
  title: "HomeKit Export",
93676
- description: "Mirror this camera into the HomeKit bridge so iOS Home can pair with it.",
96211
+ description: kind === "camera" ? "Publish this camera as its own HomeKit accessory so iOS Home can pair with it." : "Publish this device as its own HomeKit accessory. Its capabilities decide which controls iOS Home shows.",
93677
96212
  tab: "export",
93678
96213
  columns: 1,
93679
96214
  order: 10,
@@ -93689,7 +96224,7 @@ var ExportHapAddon = class extends BaseAddon {
93689
96224
  type: "qr-code",
93690
96225
  key: "__hap-pair-qr",
93691
96226
  label: paired ? "Pairing QR (already paired)" : "Pairing QR",
93692
- caption: paired ? `Already paired with at least one device. Scan from another iPhone / iPad to add it there too. Setup code: ${this.config.identity.pincode}.` : `Scan with the iOS Camera app to pair this camera in HomeKit. Setup code: ${this.config.identity.pincode}.`,
96227
+ caption: paired ? `Already paired with at least one device. Scan from another iPhone / iPad to add it there too. Setup code: ${this.config.identity.pincode}.` : `Scan with the iOS Camera app to pair this accessory in HomeKit. Setup code: ${this.config.identity.pincode}.`,
93693
96228
  value: qrValue,
93694
96229
  size: 192,
93695
96230
  alt: `HomeKit pairing QR for ${name}`,
@@ -93702,38 +96237,12 @@ var ExportHapAddon = class extends BaseAddon {
93702
96237
  type: "boolean",
93703
96238
  key: enabledKey,
93704
96239
  label: "Expose to HomeKit",
93705
- description: "Toggle to publish this camera onto the HAP bridge.",
96240
+ description: "Toggle to publish this device as a HomeKit accessory.",
93706
96241
  style: "switch",
93707
96242
  value: enabled,
93708
96243
  immediate: true
93709
96244
  },
93710
- {
93711
- type: "select",
93712
- key: streamPreferenceKey,
93713
- label: "Source stream (HomeKit)",
93714
- description: "Which camstack profile slot HomeKit pulls. Auto = the broker picks the slot closest to 1080p at session start.",
93715
- options: HAP_STREAM_PREFERENCE_OPTIONS,
93716
- required: true,
93717
- value: settings.streamPreference,
93718
- showWhen: {
93719
- field: enabledKey,
93720
- equals: true
93721
- },
93722
- immediate: true
93723
- },
93724
- {
93725
- type: "boolean",
93726
- key: hksvKey,
93727
- label: "HomeKit recording (Secure Video)",
93728
- description: "Offer “Stream and Allow Recording” in iOS Home. Requires iCloud+ and a home hub. Keeps a continuous 8s prebuffer for this camera (~0.7% of one CPU core, H.264 sources only).",
93729
- style: "switch",
93730
- value: resolveHksvRecording(settings),
93731
- showWhen: {
93732
- field: enabledKey,
93733
- equals: true
93734
- },
93735
- immediate: true
93736
- }
96245
+ ...cameraFields
93737
96246
  ]
93738
96247
  }]
93739
96248
  };
@@ -93799,23 +96308,6 @@ var ExportHapAddon = class extends BaseAddon {
93799
96308
  }
93800
96309
  return { success: true };
93801
96310
  }
93802
- /**
93803
- * Camera-only gate — HomeKit export only knows how to mirror cameras
93804
- * today. Mirrors the snapshot addon's source-side filter so the
93805
- * device-details page doesn't render an "Export" tab on lights /
93806
- * switches / sensors.
93807
- */
93808
- async isCameraDevice(deviceId) {
93809
- const api = this.ctx.api;
93810
- if (!api.deviceManager) return true;
93811
- try {
93812
- const dev = await api.deviceManager.getDevice.query({ deviceId });
93813
- if (!dev) return true;
93814
- return dev.type === DeviceType.Camera;
93815
- } catch {
93816
- return true;
93817
- }
93818
- }
93819
96311
  findEntry(deviceId) {
93820
96312
  const id = String(deviceId);
93821
96313
  return this.config.exposed.find((e) => e.deviceId === id) ?? null;