@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.
@@ -3070,6 +3070,9 @@ function handlePipeResult(left, next, ctx) {
3070
3070
  fallback: left.fallback
3071
3071
  }, ctx);
3072
3072
  }
3073
+ var $ZodPreprocess = /*@__PURE__*/ $constructor("$ZodPreprocess", (inst, def) => {
3074
+ $ZodPipe.init(inst, def);
3075
+ });
3073
3076
  var $ZodReadonly = /*@__PURE__*/ $constructor("$ZodReadonly", (inst, def) => {
3074
3077
  $ZodType.init(inst, def);
3075
3078
  defineLazy(inst._zod, "propValues", () => def.innerType._zod.propValues);
@@ -5255,6 +5258,10 @@ function pipe(in_, out) {
5255
5258
  out
5256
5259
  });
5257
5260
  }
5261
+ var ZodPreprocess = /*@__PURE__*/ $constructor("ZodPreprocess", (inst, def) => {
5262
+ ZodPipe.init(inst, def);
5263
+ $ZodPreprocess.init(inst, def);
5264
+ });
5258
5265
  var ZodReadonly = /*@__PURE__*/ $constructor("ZodReadonly", (inst, def) => {
5259
5266
  $ZodReadonly.init(inst, def);
5260
5267
  ZodType.init(inst, def);
@@ -5313,6 +5320,13 @@ function _instanceof(cls, params = {}) {
5313
5320
  };
5314
5321
  return inst;
5315
5322
  }
5323
+ function preprocess(fn, schema) {
5324
+ return new ZodPreprocess({
5325
+ type: "pipe",
5326
+ in: transform(fn),
5327
+ out: schema
5328
+ });
5329
+ }
5316
5330
  //#endregion
5317
5331
  //#region ../../node_modules/zod/v4/classic/compat.js
5318
5332
  /** @deprecated Use the raw string literal codes instead, e.g. "invalid_type". */
@@ -7589,7 +7603,7 @@ import { errMsg } from '@camstack/types'
7589
7603
  * Extract a human-readable message from an unknown error value.
7590
7604
  * Replaces the ubiquitous `errMsg(err)` pattern.
7591
7605
  */
7592
- function errMsg$12(err) {
7606
+ function errMsg$15(err) {
7593
7607
  if (err instanceof Error) return err.message;
7594
7608
  if (typeof err === "string") return err;
7595
7609
  return String(err);
@@ -13726,11 +13740,33 @@ var NotificationFormatSchema = _enum([
13726
13740
  * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
13727
13741
  * renderer's icon set; "acknowledge" survives an adapter that draws it
13728
13742
  * differently.
13743
+ *
13744
+ * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
13745
+ *
13746
+ * These names are for US. **No adapter may forward one verbatim.** Each maps
13747
+ * the whole set onto its own renderer's vocabulary through a
13748
+ * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
13749
+ * fallback, so adding a member here fails every adapter's build until someone
13750
+ * decides its glyph, which is the only place that decision can be made
13751
+ * honestly.
13752
+ *
13753
+ * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
13754
+ * `disarm` straight through; iOS feeds that string to
13755
+ * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
13756
+ * and every snooze and alarm button arrived BLANK. A pass-through is not a
13757
+ * mapping, and "the field is documented" is not "the value renders".
13758
+ *
13759
+ * Adding a member is TRAIN-BOUND. The enum lives in the published
13760
+ * `@camstack/server` closure and the cap seam validates against the HUB's copy,
13761
+ * so an addon that emits a token the running hub does not know does not lose an
13762
+ * icon — its whole `send` fails Zod validation and the notification never
13763
+ * arrives. Never emit a new token from an addon before the train carrying it.
13729
13764
  */
13730
13765
  var NotificationActionIconSchema = _enum([
13731
13766
  "acknowledge",
13732
13767
  "dismiss",
13733
13768
  "silence",
13769
+ "snooze",
13734
13770
  "view",
13735
13771
  "play",
13736
13772
  "open",
@@ -13738,9 +13774,13 @@ var NotificationActionIconSchema = _enum([
13738
13774
  "lock",
13739
13775
  "unlock",
13740
13776
  "arm",
13777
+ "arm-home",
13778
+ "arm-away",
13779
+ "arm-night",
13741
13780
  "disarm",
13742
13781
  "light",
13743
- "alert"
13782
+ "alert",
13783
+ "camera"
13744
13784
  ]);
13745
13785
  /** A single tap-through action button. */
13746
13786
  var NotificationActionSchema = object({
@@ -13940,6 +13980,24 @@ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSche
13940
13980
  targetId: string(),
13941
13981
  enabled: boolean()
13942
13982
  }), _void(), { kind: "mutation" });
13983
+ new Set([
13984
+ {
13985
+ id: "person",
13986
+ name: "Person"
13987
+ },
13988
+ {
13989
+ id: "vehicle",
13990
+ name: "Vehicle"
13991
+ },
13992
+ {
13993
+ id: "animal",
13994
+ name: "Animal"
13995
+ },
13996
+ {
13997
+ id: "package",
13998
+ name: "Package"
13999
+ }
14000
+ ].map((l) => l.id));
13943
14001
  var COCO_TO_MACRO = {
13944
14002
  mapping: {
13945
14003
  person: "person",
@@ -14737,6 +14795,8 @@ var NcSystemEventKindSchema = _enum([
14737
14795
  "alarm-triggered",
14738
14796
  "alarm-armed",
14739
14797
  "alarm-disarmed",
14798
+ "alarm-arming",
14799
+ "alarm-arm-refused",
14740
14800
  "camera-online",
14741
14801
  "camera-offline",
14742
14802
  "camera-disabled",
@@ -15945,7 +16005,87 @@ var MethodAccessSchema = _enum([
15945
16005
  var AllowedProviderSchema = union([literal("*"), array(string())]);
15946
16006
  var AllowedDevicesSchema = record(string(), union([literal("*"), array(string())]));
15947
16007
  var CapScopeSchema = _enum(["device", "system"]);
15948
- var TokenScopeSchema = discriminatedUnion("type", [
16008
+ /**
16009
+ * DeviceSelector (scope model v3 — 2026-08-12).
16010
+ *
16011
+ * A `device` grant no longer carries a frozen list of deviceIds. It carries
16012
+ * a SELECTOR the matcher resolves against the live fleet, so the grant can be
16013
+ * DYNAMIC: a `types:['camera']` selector automatically covers a camera added
16014
+ * AFTER the grant was minted — no re-grant, no re-login.
16015
+ *
16016
+ * - `all` — every device in the deployment. The broad viewer/operator
16017
+ * lever without a `category` grant (a `category` grant also covers device
16018
+ * caps that carry no deviceId; `all` is specifically the device set).
16019
+ * - `ids` — an explicit deviceId list. This is what a v2 `device:[…]`
16020
+ * grant migrates to (see {@link TokenScopeSchema}); STATIC — a new camera
16021
+ * is NOT covered until the grant is edited.
16022
+ * - `types` — every device of a `DeviceType` (e.g. every `camera`).
16023
+ * DYNAMIC. A device that changes type, or a new device of the type,
16024
+ * re-resolves on the next request.
16025
+ * - `locations` — every device whose operator-assigned `location` label is
16026
+ * in the set (e.g. "Garden", "Front door"). DYNAMIC. A device with a
16027
+ * null/unset location matches NO `locations` selector.
16028
+ */
16029
+ var DeviceSelectorSchema = discriminatedUnion("kind", [
16030
+ object({ kind: literal("all") }),
16031
+ object({
16032
+ kind: literal("ids"),
16033
+ ids: array(number().int()).min(1)
16034
+ }),
16035
+ object({
16036
+ kind: literal("types"),
16037
+ types: array(_enum(DeviceType)).min(1)
16038
+ }),
16039
+ object({
16040
+ kind: literal("locations"),
16041
+ locations: array(string().min(1)).min(1)
16042
+ })
16043
+ ]);
16044
+ var DeviceTokenScopeSchema = object({
16045
+ type: literal("device"),
16046
+ /** The device SET this grant covers — resolved against the live fleet. */
16047
+ selector: DeviceSelectorSchema,
16048
+ access: array(MethodAccessSchema).min(1),
16049
+ /**
16050
+ * Whether a grant on a PARENT device transparently covers its accessory
16051
+ * CHILDREN (siren / floodlight / PIR) via the persisted-parentage walk.
16052
+ * Direction is parent → children ONLY.
16053
+ *
16054
+ * Absent → the matcher DERIVES it from the access flavour: `view`
16055
+ * inherits (a camera viewer sees the camera's accessories), `create` /
16056
+ * `delete` do NOT (actuating/removing a child is an explicit act the
16057
+ * operator must grant on the child, not inherit from the parent). Set it
16058
+ * explicitly to override that default per grant.
16059
+ */
16060
+ includeLinked: boolean().optional()
16061
+ });
16062
+ /**
16063
+ * v2 → v3 lazy migration. A pre-v3 `device` grant carried
16064
+ * `targets: string[]` (stringified deviceIds); it rewrites to the equivalent
16065
+ * `selector: {kind:'ids', ids}`. Applied as a `preprocess` so it runs on
16066
+ * EVERY parse path — stored records AND the JWT-carried scope arrays
16067
+ * normalised at the request boundary ({@link normalizeTokenScopes} in
16068
+ * `device-selector.ts`). Chosen over a one-time DB migration because a
16069
+ * migration cannot reach a JWT already in a client's hands; parse-time
16070
+ * migration covers both without a flag day. No cast — the raw object is read
16071
+ * through `Reflect.get` (its static type is `unknown`).
16072
+ */
16073
+ function migrateLegacyTokenScope(raw) {
16074
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return raw;
16075
+ if (Reflect.get(raw, "type") !== "device") return raw;
16076
+ if (Reflect.get(raw, "selector") !== void 0) return raw;
16077
+ const targets = Reflect.get(raw, "targets");
16078
+ if (!Array.isArray(targets)) return raw;
16079
+ return {
16080
+ type: "device",
16081
+ selector: {
16082
+ kind: "ids",
16083
+ ids: targets.map((t) => typeof t === "string" ? Number(t) : t).filter((n) => typeof n === "number" && Number.isInteger(n))
16084
+ },
16085
+ access: Reflect.get(raw, "access")
16086
+ };
16087
+ }
16088
+ var TokenScopeSchema = preprocess(migrateLegacyTokenScope, discriminatedUnion("type", [
15949
16089
  object({
15950
16090
  type: literal("category"),
15951
16091
  target: CapScopeSchema,
@@ -15961,18 +16101,8 @@ var TokenScopeSchema = discriminatedUnion("type", [
15961
16101
  target: string(),
15962
16102
  access: array(MethodAccessSchema).min(1)
15963
16103
  }),
15964
- object({
15965
- type: literal("device"),
15966
- /**
15967
- * One or more deviceIds (serialised as strings for wire-format
15968
- * consistency with the rest of the union). Matcher accepts if
15969
- * `input.deviceId` ∈ `targets`. Array shape avoids the row-explosion
15970
- * of one scope-per-device when granting access to a set of cameras.
15971
- */
15972
- targets: array(string()).min(1),
15973
- access: array(MethodAccessSchema).min(1)
15974
- })
15975
- ]);
16104
+ DeviceTokenScopeSchema
16105
+ ]));
15976
16106
  object({
15977
16107
  id: string(),
15978
16108
  username: string(),
@@ -17969,10 +18099,26 @@ var detectionFpsField = {
17969
18099
  default: 10,
17970
18100
  step: 1
17971
18101
  };
18102
+ /**
18103
+ * The occupancy re-check interval. DEFAULT 300 s (2026-08-13 — was 30 s).
18104
+ *
18105
+ * The recheck is now on by default (a parked car is invisible to occupancy
18106
+ * rules until the stationary registry has been rebuilt by motion, which after a
18107
+ * restart may be never on a quiet camera). Each cycle re-subscribes a detection
18108
+ * session — an RTSP re-dial — so the switch is only affordable at a WIDE
18109
+ * interval: 300 s is ~12 re-dials an hour per camera, against 120 at the old
18110
+ * 30 s. A parked car is therefore counted within 5 minutes of a restart.
18111
+ *
18112
+ * Why not wider: `max` is 300 and raising it is TRAIN-BOUND, not addon-bound —
18113
+ * the host validates `attachCamera` against ITS copy of this schema, so a
18114
+ * runner asked for 600 would be rejected by the hub until a `@camstack/server`
18115
+ * carrying the wider bound is installed everywhere. 300 is the widest value
18116
+ * that ships with an addon deploy.
18117
+ */
17972
18118
  var occupancyRecheckSecField = {
17973
18119
  min: 0,
17974
18120
  max: 300,
17975
- default: 30,
18121
+ default: 300,
17976
18122
  step: 5
17977
18123
  };
17978
18124
  var occupancyRecheckFramesField = {
@@ -18170,15 +18316,21 @@ var RunnerCameraConfigSchema = object({
18170
18316
  */
18171
18317
  onboardMotionDrivesAnalyzer: boolean().default(true),
18172
18318
  /**
18173
- * Master toggle for the occupancy re-check. When `false` (DEFAULT) the runner
18174
- * never arms the periodic recheck timer, regardless of `occupancyRecheckSec`
18175
- * this is off by default because the recheck re-subscribes a detection session
18176
- * every N seconds while `watching`, a major source of pull-decoder re-dial
18177
- * churn (each cycle creates+tears a session → RTSP re-dial → latency). The
18319
+ * Master toggle for the occupancy re-check. When `false` the runner never arms
18320
+ * the periodic recheck timer, regardless of `occupancyRecheckSec`; the
18178
18321
  * `occupancyRecheckSec` / `occupancyRecheckFrames` sliders only take effect
18179
18322
  * (and only render) when this is enabled.
18180
- */
18181
- occupancyRecheckEnabled: boolean().default(false),
18323
+ *
18324
+ * DEFAULT `true` since 2026-08-13 (was `false`). It was off because the
18325
+ * recheck re-subscribes a detection session every N seconds while `watching`
18326
+ * — each cycle creates+tears a session ⇒ an RTSP re-dial ⇒ latency, a major
18327
+ * pull-decoder churn source. What that bought was a blind spot: a STATIONARY
18328
+ * object is counted only while the stationary registry holds it, and the
18329
+ * registry rebuilds from motion, so after a restart a parked car was invisible
18330
+ * to every occupancy rule until something moved in front of it. The churn is
18331
+ * now paid on the interval instead — see `occupancyRecheckSecField`.
18332
+ */
18333
+ occupancyRecheckEnabled: boolean().default(true),
18182
18334
  occupancyRecheckSec: number().min(occupancyRecheckSecField.min).max(occupancyRecheckSecField.max).default(occupancyRecheckSecField.default),
18183
18335
  occupancyRecheckFrames: number().min(occupancyRecheckFramesField.min).max(occupancyRecheckFramesField.max).default(occupancyRecheckFramesField.default),
18184
18336
  /**
@@ -20579,7 +20731,11 @@ object({
20579
20731
  precision: number().int().min(0).max(10).optional()
20580
20732
  });
20581
20733
  DeviceType.Sensor;
20582
- object({
20734
+ /**
20735
+ * Ambient illuminance reading in lux. Drives Home Assistant `sensor`
20736
+ * entries with `device_class: illuminance`.
20737
+ */
20738
+ var AmbientLightSensorStatusSchema = object({
20583
20739
  /** Current illuminance in lux (lx). */
20584
20740
  lux: number().min(0),
20585
20741
  /** Ms epoch when the slice was last updated. */
@@ -20852,7 +21008,11 @@ DeviceType.Camera, method(object({ deviceId: number() }), CameraCredentialsSchem
20852
21008
  kind: "query",
20853
21009
  auth: "admin"
20854
21010
  });
20855
- object({
21011
+ /**
21012
+ * Carbon-monoxide alarm sensor. Drives Home Assistant `binary_sensor`
21013
+ * entries with `device_class: carbon_monoxide`. Push-driven.
21014
+ */
21015
+ var CarbonMonoxideStatusSchema = object({
20856
21016
  detected: boolean(),
20857
21017
  /** Ms epoch of the last transition. 0 if never observed. */
20858
21018
  lastChangedAt: number()
@@ -21140,7 +21300,19 @@ Object.values(DeviceType), method(object({
21140
21300
  kind: "mutation",
21141
21301
  auth: "admin"
21142
21302
  }), ConsumablesStatusSchema.extend({ lastFetchedAt: number() });
21143
- object({
21303
+ /**
21304
+ * Door / window / opening / garage / valve contact sensor. Boolean
21305
+ * "is the entry currently open" with the timestamp of the last
21306
+ * transition. Drives Home Assistant `binary_sensor` entries whose
21307
+ * `device_class` is `door`, `window`, `opening`, `garage`, or
21308
+ * `garage_door` — and any future native integration that needs
21309
+ * the same semantics.
21310
+ *
21311
+ * Push-driven: providers update the slice on transition events from
21312
+ * the upstream source (HA WebSocket `state_changed`, ZWave
21313
+ * `notification` …). Consumers read the slice; no polling.
21314
+ */
21315
+ var ContactStatusSchema = object({
21144
21316
  /** True when the entry is open; false when closed. */
21145
21317
  entryOpen: boolean(),
21146
21318
  /** Ms epoch of the last open↔closed transition. 0 if never observed. */
@@ -21739,7 +21911,15 @@ object({
21739
21911
  deviceId: number(),
21740
21912
  status: FeatureProbeStatusSchema
21741
21913
  });
21742
- object({
21914
+ /**
21915
+ * Water leak / moisture sensor. Boolean "is liquid currently
21916
+ * detected" with the timestamp of the last transition. Drives Home
21917
+ * Assistant `binary_sensor` entries with `device_class: moisture`,
21918
+ * and any future native flood sensor.
21919
+ *
21920
+ * Push-driven from the upstream source.
21921
+ */
21922
+ var FloodStatusSchema = object({
21743
21923
  /** True when leak is currently detected. */
21744
21924
  flooded: boolean(),
21745
21925
  /** Ms epoch of the last flooded↔dry transition. 0 if never observed. */
@@ -21793,7 +21973,15 @@ DeviceType.Humidifier, method(object({
21793
21973
  kind: "mutation",
21794
21974
  auth: "admin"
21795
21975
  });
21796
- object({
21976
+ /**
21977
+ * Single-metric humidity reading. Drives Home Assistant `sensor`
21978
+ * entries with `device_class: humidity`.
21979
+ *
21980
+ * Unit normalisation: percent. The canonical display unit (`%`) is a
21981
+ * descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
21982
+ * `sourceInfo`.
21983
+ */
21984
+ var HumiditySensorStatusSchema = object({
21797
21985
  /** Current relative humidity, 0..100. */
21798
21986
  percent: number().min(0).max(100),
21799
21987
  /** Ms epoch when the slice was last updated. */
@@ -22415,7 +22603,7 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
22415
22603
  * tunnel always emits `https://` regardless. */
22416
22604
  scheme: _enum(["http", "https"]).optional()
22417
22605
  }), 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" });
22418
- object({
22606
+ var LockControlStatusSchema = object({
22419
22607
  /** Lifecycle state of the lock. `jammed` means the motor reported
22420
22608
  * failure to reach the target — operator intervention required. */
22421
22609
  state: _enum([
@@ -22753,7 +22941,19 @@ authKey: string().optional() }), object({
22753
22941
  /** Human-readable error when `ok: false`. */
22754
22942
  error: string().optional()
22755
22943
  }), { kind: "mutation" });
22756
- object({
22944
+ /**
22945
+ * Hardware / firmware motion sensor cap — binary detected state plus
22946
+ * a timestamp of the last observation. Distinct from
22947
+ * `motion-detection.cap.ts` which owns the LOCAL ML motion pipeline;
22948
+ * `motion` is the lightweight readout from on-camera motion (Reolink
22949
+ * `GetMdState`, Baichuan push `type: motion`, ONVIF analytics).
22950
+ *
22951
+ * Native-motion providers also fan out to `detection.camera-native`
22952
+ * with `source: 'onboard'` so cross-cutting system services
22953
+ * (alert-center, advanced-notifier) can subscribe once and receive
22954
+ * motion from every camera.
22955
+ */
22956
+ var MotionStatusSchema = object({
22757
22957
  detected: boolean(),
22758
22958
  /** Ms epoch of the last detected-true observation. Null if never detected. */
22759
22959
  lastDetectedAt: number().nullable(),
@@ -23865,7 +24065,7 @@ var GpsLocationSchema = object({
23865
24065
  /** Reported accuracy in meters (lower = better). */
23866
24066
  accuracyMeters: number().nonnegative()
23867
24067
  });
23868
- object({
24068
+ var PresenceStatusSchema = object({
23869
24069
  /** `home` / `not_home` / any user-defined zone name. */
23870
24070
  state: string(),
23871
24071
  /** Optional textual location label (zone name, city, address). Null
@@ -24281,7 +24481,7 @@ method(object({
24281
24481
  toMs: number()
24282
24482
  }), RecordingAvailabilitySchema, {
24283
24483
  kind: "query",
24284
- auth: "admin"
24484
+ auth: "protected"
24285
24485
  }), method(object({
24286
24486
  deviceId: number(),
24287
24487
  fromMs: number(),
@@ -24289,14 +24489,14 @@ method(object({
24289
24489
  tzOffsetMinutes: number()
24290
24490
  }), RecordingDaysSchema, {
24291
24491
  kind: "query",
24292
- auth: "admin"
24492
+ auth: "protected"
24293
24493
  }), method(object({
24294
24494
  deviceId: number(),
24295
24495
  fromMs: number(),
24296
24496
  toMs: number()
24297
24497
  }), RecordingManifestSchema, {
24298
24498
  kind: "query",
24299
- auth: "admin"
24499
+ auth: "protected"
24300
24500
  }), method(object({}), RecordingStorageUsageSchema, {
24301
24501
  kind: "query",
24302
24502
  auth: "admin"
@@ -24825,7 +25025,16 @@ DeviceType.Script, method(object({
24825
25025
  kind: "mutation",
24826
25026
  auth: "admin"
24827
25027
  });
24828
- object({
25028
+ /**
25029
+ * Smoke alarm sensor — boolean "is smoke currently detected" with
25030
+ * timestamp of the last transition. Drives Home Assistant
25031
+ * `binary_sensor` entries with `device_class: smoke`.
25032
+ *
25033
+ * Push-driven: a smoke event is critical, so the slice updates
25034
+ * immediately on the upstream signal. Auto-clearing back to false is
25035
+ * provider-controlled (some alarms latch until manually reset).
25036
+ */
25037
+ var SmokeStatusSchema = object({
24829
25038
  detected: boolean(),
24830
25039
  /** Ms epoch of the last transition. 0 if never observed. */
24831
25040
  lastChangedAt: number()
@@ -25034,7 +25243,23 @@ object({
25034
25243
  lastChangedAt: number()
25035
25244
  });
25036
25245
  DeviceType.Sensor;
25037
- object({
25246
+ /**
25247
+ * Single-metric temperature reading. Drives Home Assistant `sensor`
25248
+ * entries with `device_class: temperature` and any future native
25249
+ * thermometer.
25250
+ *
25251
+ * Unit normalisation: providers convert to Celsius before storing.
25252
+ * The slice value is always Celsius so cross-cap aggregators
25253
+ * (climate-control's `currentTemp`, energy analytics) can compose
25254
+ * without per-source unit fixups. The canonical display unit (`°C`) is
25255
+ * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
25256
+ * `sourceInfo`.
25257
+ *
25258
+ * Status `lastFetchedAt` lets staleness-aware consumers detect a
25259
+ * frozen feed (provider hung) distinct from a "temperature hasn't
25260
+ * changed" steady state.
25261
+ */
25262
+ var TemperatureSensorStatusSchema = object({
25038
25263
  /** Current temperature in Celsius. */
25039
25264
  celsius: number(),
25040
25265
  /** Ms epoch when the slice was last updated (push or poll). */
@@ -31295,6 +31520,1678 @@ Object.freeze({
31295
31520
  access: "create"
31296
31521
  }
31297
31522
  });
31523
+ Object.freeze({
31524
+ "accessories.setChildHidden": [{
31525
+ name: "childDeviceId",
31526
+ form: "single",
31527
+ optional: false
31528
+ }, {
31529
+ name: "deviceId",
31530
+ form: "single",
31531
+ optional: false
31532
+ }],
31533
+ "addonSettings.getDeviceSettings": [{
31534
+ name: "deviceId",
31535
+ form: "single",
31536
+ optional: false
31537
+ }],
31538
+ "addonSettings.updateDeviceSettings": [{
31539
+ name: "deviceId",
31540
+ form: "single",
31541
+ optional: false
31542
+ }],
31543
+ "alarmPanel.arm": [{
31544
+ name: "deviceId",
31545
+ form: "single",
31546
+ optional: false
31547
+ }],
31548
+ "alarmPanel.disarm": [{
31549
+ name: "deviceId",
31550
+ form: "single",
31551
+ optional: false
31552
+ }],
31553
+ "alarmPanel.trigger": [{
31554
+ name: "deviceId",
31555
+ form: "single",
31556
+ optional: false
31557
+ }],
31558
+ "audioAnalysis.resolveDeviceSettings": [{
31559
+ name: "deviceId",
31560
+ form: "single",
31561
+ optional: false
31562
+ }],
31563
+ "audioAnalyzer.classify": [{
31564
+ name: "deviceId",
31565
+ form: "single",
31566
+ optional: true
31567
+ }],
31568
+ "audioMetrics.getCurrentSnapshot": [{
31569
+ name: "deviceId",
31570
+ form: "single",
31571
+ optional: false
31572
+ }],
31573
+ "audioMetrics.getHistory": [{
31574
+ name: "deviceId",
31575
+ form: "single",
31576
+ optional: false
31577
+ }],
31578
+ "automationControl.disable": [{
31579
+ name: "deviceId",
31580
+ form: "single",
31581
+ optional: false
31582
+ }],
31583
+ "automationControl.enable": [{
31584
+ name: "deviceId",
31585
+ form: "single",
31586
+ optional: false
31587
+ }],
31588
+ "automationControl.trigger": [{
31589
+ name: "deviceId",
31590
+ form: "single",
31591
+ optional: false
31592
+ }],
31593
+ "battery.wakeForStream": [{
31594
+ name: "deviceId",
31595
+ form: "single",
31596
+ optional: false
31597
+ }],
31598
+ "brightness.setBrightness": [{
31599
+ name: "deviceId",
31600
+ form: "single",
31601
+ optional: false
31602
+ }],
31603
+ "button.press": [{
31604
+ name: "deviceId",
31605
+ form: "single",
31606
+ optional: false
31607
+ }],
31608
+ "cameraCredentials.getCredentials": [{
31609
+ name: "deviceId",
31610
+ form: "single",
31611
+ optional: false
31612
+ }],
31613
+ "cameraStreams.getBrokerStreams": [{
31614
+ name: "deviceId",
31615
+ form: "single",
31616
+ optional: false
31617
+ }],
31618
+ "cameraStreams.getCameraStreams": [{
31619
+ name: "deviceId",
31620
+ form: "single",
31621
+ optional: false
31622
+ }],
31623
+ "cameraStreams.getProfileRtspEntries": [{
31624
+ name: "deviceId",
31625
+ form: "single",
31626
+ optional: false
31627
+ }],
31628
+ "cameraStreams.getRtspEntries": [{
31629
+ name: "deviceId",
31630
+ form: "single",
31631
+ optional: false
31632
+ }],
31633
+ "cameraStreams.pickStream": [{
31634
+ name: "deviceId",
31635
+ form: "single",
31636
+ optional: false
31637
+ }],
31638
+ "climateControl.setFanMode": [{
31639
+ name: "deviceId",
31640
+ form: "single",
31641
+ optional: false
31642
+ }],
31643
+ "climateControl.setMode": [{
31644
+ name: "deviceId",
31645
+ form: "single",
31646
+ optional: false
31647
+ }],
31648
+ "climateControl.setPreset": [{
31649
+ name: "deviceId",
31650
+ form: "single",
31651
+ optional: false
31652
+ }],
31653
+ "climateControl.setSwingHorizontal": [{
31654
+ name: "deviceId",
31655
+ form: "single",
31656
+ optional: false
31657
+ }],
31658
+ "climateControl.setSwingVertical": [{
31659
+ name: "deviceId",
31660
+ form: "single",
31661
+ optional: false
31662
+ }],
31663
+ "climateControl.setTarget": [{
31664
+ name: "deviceId",
31665
+ form: "single",
31666
+ optional: false
31667
+ }],
31668
+ "climateControl.setTargetHumidity": [{
31669
+ name: "deviceId",
31670
+ form: "single",
31671
+ optional: false
31672
+ }],
31673
+ "climateControl.setTargetRange": [{
31674
+ name: "deviceId",
31675
+ form: "single",
31676
+ optional: false
31677
+ }],
31678
+ "color.setColor": [{
31679
+ name: "deviceId",
31680
+ form: "single",
31681
+ optional: false
31682
+ }],
31683
+ "consumables.reset": [{
31684
+ name: "deviceId",
31685
+ form: "single",
31686
+ optional: false
31687
+ }],
31688
+ "control.setValue": [{
31689
+ name: "deviceId",
31690
+ form: "single",
31691
+ optional: false
31692
+ }],
31693
+ "cover.close": [{
31694
+ name: "deviceId",
31695
+ form: "single",
31696
+ optional: false
31697
+ }],
31698
+ "cover.open": [{
31699
+ name: "deviceId",
31700
+ form: "single",
31701
+ optional: false
31702
+ }],
31703
+ "cover.setPosition": [{
31704
+ name: "deviceId",
31705
+ form: "single",
31706
+ optional: false
31707
+ }],
31708
+ "cover.setTiltPosition": [{
31709
+ name: "deviceId",
31710
+ form: "single",
31711
+ optional: false
31712
+ }],
31713
+ "cover.stop": [{
31714
+ name: "deviceId",
31715
+ form: "single",
31716
+ optional: false
31717
+ }],
31718
+ "dayNight.getOptions": [{
31719
+ name: "deviceId",
31720
+ form: "single",
31721
+ optional: false
31722
+ }],
31723
+ "dayNight.setSettings": [{
31724
+ name: "deviceId",
31725
+ form: "single",
31726
+ optional: false
31727
+ }],
31728
+ "decoder.createSession": [{
31729
+ name: "deviceId",
31730
+ form: "single",
31731
+ optional: true
31732
+ }],
31733
+ "deviceAdoption.release": [{
31734
+ name: "camDeviceId",
31735
+ form: "single",
31736
+ optional: false
31737
+ }],
31738
+ "deviceAdoption.resync": [{
31739
+ name: "camDeviceId",
31740
+ form: "single",
31741
+ optional: false
31742
+ }],
31743
+ "deviceDiscovery.adoptDevice": [{
31744
+ name: "deviceId",
31745
+ form: "single",
31746
+ optional: false
31747
+ }],
31748
+ "deviceDiscovery.listDiscovered": [{
31749
+ name: "deviceId",
31750
+ form: "single",
31751
+ optional: false
31752
+ }],
31753
+ "deviceDiscovery.refreshDiscovery": [{
31754
+ name: "deviceId",
31755
+ form: "single",
31756
+ optional: false
31757
+ }],
31758
+ "deviceDiscovery.releaseDevice": [{
31759
+ name: "childDeviceId",
31760
+ form: "single",
31761
+ optional: false
31762
+ }, {
31763
+ name: "deviceId",
31764
+ form: "single",
31765
+ optional: false
31766
+ }],
31767
+ "deviceManager.adoptionRelease": [{
31768
+ name: "camDeviceId",
31769
+ form: "single",
31770
+ optional: false
31771
+ }],
31772
+ "deviceManager.adoptionResync": [{
31773
+ name: "camDeviceId",
31774
+ form: "single",
31775
+ optional: false
31776
+ }],
31777
+ "deviceManager.applyInitialMeta": [{
31778
+ name: "deviceId",
31779
+ form: "single",
31780
+ optional: false
31781
+ }, {
31782
+ name: "linkDeviceId",
31783
+ form: "single",
31784
+ optional: true
31785
+ }],
31786
+ "deviceManager.disable": [{
31787
+ name: "deviceId",
31788
+ form: "single",
31789
+ optional: false
31790
+ }],
31791
+ "deviceManager.enable": [{
31792
+ name: "deviceId",
31793
+ form: "single",
31794
+ optional: false
31795
+ }],
31796
+ "deviceManager.getBindings": [{
31797
+ name: "deviceId",
31798
+ form: "single",
31799
+ optional: false
31800
+ }],
31801
+ "deviceManager.getChildren": [{
31802
+ name: "parentDeviceId",
31803
+ form: "single",
31804
+ optional: false
31805
+ }],
31806
+ "deviceManager.getConfigSchema": [{
31807
+ name: "deviceId",
31808
+ form: "single",
31809
+ optional: false
31810
+ }],
31811
+ "deviceManager.getDevice": [{
31812
+ name: "deviceId",
31813
+ form: "single",
31814
+ optional: false
31815
+ }],
31816
+ "deviceManager.getDeviceAggregate": [{
31817
+ name: "deviceId",
31818
+ form: "single",
31819
+ optional: false
31820
+ }],
31821
+ "deviceManager.getDeviceLiveInfoAggregate": [{
31822
+ name: "deviceId",
31823
+ form: "single",
31824
+ optional: false
31825
+ }],
31826
+ "deviceManager.getDeviceSettingsAggregate": [{
31827
+ name: "deviceId",
31828
+ form: "single",
31829
+ optional: false
31830
+ }],
31831
+ "deviceManager.getDeviceStatusAggregate": [{
31832
+ name: "deviceId",
31833
+ form: "single",
31834
+ optional: false
31835
+ }],
31836
+ "deviceManager.getDeviceStatusAggregateBatch": [{
31837
+ name: "deviceIds",
31838
+ form: "array",
31839
+ optional: false
31840
+ }],
31841
+ "deviceManager.getLinkedDevices": [{
31842
+ name: "deviceId",
31843
+ form: "single",
31844
+ optional: false
31845
+ }],
31846
+ "deviceManager.getSettingsSchema": [{
31847
+ name: "deviceId",
31848
+ form: "single",
31849
+ optional: false
31850
+ }],
31851
+ "deviceManager.getStreamProfileMap": [{
31852
+ name: "deviceId",
31853
+ form: "single",
31854
+ optional: false
31855
+ }],
31856
+ "deviceManager.getStreamSources": [{
31857
+ name: "deviceId",
31858
+ form: "single",
31859
+ optional: false
31860
+ }],
31861
+ "deviceManager.getWireableFields": [{
31862
+ name: "deviceId",
31863
+ form: "single",
31864
+ optional: false
31865
+ }],
31866
+ "deviceManager.loadConfig": [{
31867
+ name: "deviceId",
31868
+ form: "single",
31869
+ optional: false
31870
+ }],
31871
+ "deviceManager.loadMeta": [{
31872
+ name: "deviceId",
31873
+ form: "single",
31874
+ optional: false
31875
+ }],
31876
+ "deviceManager.loadRuntimeState": [{
31877
+ name: "deviceId",
31878
+ form: "single",
31879
+ optional: false
31880
+ }],
31881
+ "deviceManager.persistConfig": [{
31882
+ name: "deviceId",
31883
+ form: "single",
31884
+ optional: false
31885
+ }],
31886
+ "deviceManager.probeStreams": [{
31887
+ name: "deviceId",
31888
+ form: "single",
31889
+ optional: false
31890
+ }],
31891
+ "deviceManager.registerDevice": [{
31892
+ name: "parentDeviceId",
31893
+ form: "single",
31894
+ optional: true
31895
+ }],
31896
+ "deviceManager.remove": [{
31897
+ name: "deviceId",
31898
+ form: "single",
31899
+ optional: false
31900
+ }],
31901
+ "deviceManager.removeDevice": [{
31902
+ name: "deviceId",
31903
+ form: "single",
31904
+ optional: false
31905
+ }],
31906
+ "deviceManager.runDeviceAction": [{
31907
+ name: "deviceId",
31908
+ form: "single",
31909
+ optional: false
31910
+ }],
31911
+ "deviceManager.setChildLayout": [{
31912
+ name: "deviceId",
31913
+ form: "single",
31914
+ optional: false
31915
+ }],
31916
+ "deviceManager.setDisabled": [{
31917
+ name: "deviceId",
31918
+ form: "single",
31919
+ optional: false
31920
+ }],
31921
+ "deviceManager.setDisplay": [{
31922
+ name: "deviceId",
31923
+ form: "single",
31924
+ optional: false
31925
+ }],
31926
+ "deviceManager.setIntegrationId": [{
31927
+ name: "deviceId",
31928
+ form: "single",
31929
+ optional: false
31930
+ }],
31931
+ "deviceManager.setLinkDeviceId": [{
31932
+ name: "deviceId",
31933
+ form: "single",
31934
+ optional: false
31935
+ }, {
31936
+ name: "linkDeviceId",
31937
+ form: "single",
31938
+ optional: true
31939
+ }],
31940
+ "deviceManager.setLocation": [{
31941
+ name: "deviceId",
31942
+ form: "single",
31943
+ optional: false
31944
+ }],
31945
+ "deviceManager.setMetadata": [{
31946
+ name: "deviceId",
31947
+ form: "single",
31948
+ optional: false
31949
+ }],
31950
+ "deviceManager.setName": [{
31951
+ name: "deviceId",
31952
+ form: "single",
31953
+ optional: false
31954
+ }],
31955
+ "deviceManager.setPrimaryChildEntityId": [{
31956
+ name: "deviceId",
31957
+ form: "single",
31958
+ optional: false
31959
+ }],
31960
+ "deviceManager.setRole": [{
31961
+ name: "deviceId",
31962
+ form: "single",
31963
+ optional: false
31964
+ }],
31965
+ "deviceManager.setStreamProfileMap": [{
31966
+ name: "deviceId",
31967
+ form: "single",
31968
+ optional: false
31969
+ }],
31970
+ "deviceManager.setType": [{
31971
+ name: "deviceId",
31972
+ form: "single",
31973
+ optional: false
31974
+ }],
31975
+ "deviceManager.setWrapperActive": [{
31976
+ name: "deviceId",
31977
+ form: "single",
31978
+ optional: false
31979
+ }],
31980
+ "deviceManager.testField": [{
31981
+ name: "deviceId",
31982
+ form: "single",
31983
+ optional: false
31984
+ }],
31985
+ "deviceManager.updateConfig": [{
31986
+ name: "deviceId",
31987
+ form: "single",
31988
+ optional: false
31989
+ }],
31990
+ "deviceManager.updateDeviceField": [{
31991
+ name: "deviceId",
31992
+ form: "single",
31993
+ optional: false
31994
+ }],
31995
+ "deviceManager.updateDeviceFieldsBatch": [{
31996
+ name: "deviceId",
31997
+ form: "single",
31998
+ optional: false
31999
+ }],
32000
+ "deviceOps.getConfigEntries": [{
32001
+ name: "deviceId",
32002
+ form: "single",
32003
+ optional: false
32004
+ }],
32005
+ "deviceOps.getRawState": [{
32006
+ name: "deviceId",
32007
+ form: "single",
32008
+ optional: false
32009
+ }],
32010
+ "deviceOps.getSettingsSchema": [{
32011
+ name: "deviceId",
32012
+ form: "single",
32013
+ optional: false
32014
+ }],
32015
+ "deviceOps.getStreamSources": [{
32016
+ name: "deviceId",
32017
+ form: "single",
32018
+ optional: false
32019
+ }],
32020
+ "deviceOps.removeDevice": [{
32021
+ name: "deviceId",
32022
+ form: "single",
32023
+ optional: false
32024
+ }],
32025
+ "deviceOps.runAction": [{
32026
+ name: "deviceId",
32027
+ form: "single",
32028
+ optional: false
32029
+ }],
32030
+ "deviceOps.setConfig": [{
32031
+ name: "deviceId",
32032
+ form: "single",
32033
+ optional: false
32034
+ }],
32035
+ "deviceState.getCapSlice": [{
32036
+ name: "deviceId",
32037
+ form: "single",
32038
+ optional: false
32039
+ }],
32040
+ "deviceState.getSnapshot": [{
32041
+ name: "deviceId",
32042
+ form: "single",
32043
+ optional: false
32044
+ }],
32045
+ "deviceState.setCapSlice": [{
32046
+ name: "deviceId",
32047
+ form: "single",
32048
+ optional: false
32049
+ }],
32050
+ "events.getEventClipUrl": [{
32051
+ name: "deviceId",
32052
+ form: "single",
32053
+ optional: false
32054
+ }],
32055
+ "events.getEvents": [{
32056
+ name: "deviceId",
32057
+ form: "single",
32058
+ optional: false
32059
+ }],
32060
+ "events.getEventThumbnail": [{
32061
+ name: "deviceId",
32062
+ form: "single",
32063
+ optional: false
32064
+ }],
32065
+ "faceGallery.getFaceByTrack": [{
32066
+ name: "deviceId",
32067
+ form: "single",
32068
+ optional: false
32069
+ }],
32070
+ "faceGallery.listRecentFaces": [{
32071
+ name: "deviceId",
32072
+ form: "single",
32073
+ optional: true
32074
+ }],
32075
+ "fanControl.setDirection": [{
32076
+ name: "deviceId",
32077
+ form: "single",
32078
+ optional: false
32079
+ }],
32080
+ "fanControl.setOscillating": [{
32081
+ name: "deviceId",
32082
+ form: "single",
32083
+ optional: false
32084
+ }],
32085
+ "fanControl.setPercentage": [{
32086
+ name: "deviceId",
32087
+ form: "single",
32088
+ optional: false
32089
+ }],
32090
+ "fanControl.setPreset": [{
32091
+ name: "deviceId",
32092
+ form: "single",
32093
+ optional: false
32094
+ }],
32095
+ "humidifier.setMode": [{
32096
+ name: "deviceId",
32097
+ form: "single",
32098
+ optional: false
32099
+ }],
32100
+ "humidifier.setOn": [{
32101
+ name: "deviceId",
32102
+ form: "single",
32103
+ optional: false
32104
+ }],
32105
+ "humidifier.setTargetHumidity": [{
32106
+ name: "deviceId",
32107
+ form: "single",
32108
+ optional: false
32109
+ }],
32110
+ "imageSettings.getOptions": [{
32111
+ name: "deviceId",
32112
+ form: "single",
32113
+ optional: false
32114
+ }],
32115
+ "imageSettings.setSettings": [{
32116
+ name: "deviceId",
32117
+ form: "single",
32118
+ optional: false
32119
+ }],
32120
+ "intercom.endTalkSession": [{
32121
+ name: "deviceId",
32122
+ form: "single",
32123
+ optional: false
32124
+ }],
32125
+ "intercom.handleAnswer": [{
32126
+ name: "deviceId",
32127
+ form: "single",
32128
+ optional: false
32129
+ }],
32130
+ "intercom.pushTalkAudio": [{
32131
+ name: "deviceId",
32132
+ form: "single",
32133
+ optional: false
32134
+ }],
32135
+ "intercom.startSession": [{
32136
+ name: "deviceId",
32137
+ form: "single",
32138
+ optional: false
32139
+ }],
32140
+ "intercom.startTalkSession": [{
32141
+ name: "deviceId",
32142
+ form: "single",
32143
+ optional: false
32144
+ }],
32145
+ "intercom.stopSession": [{
32146
+ name: "deviceId",
32147
+ form: "single",
32148
+ optional: false
32149
+ }],
32150
+ "lawnMowerControl.dock": [{
32151
+ name: "deviceId",
32152
+ form: "single",
32153
+ optional: false
32154
+ }],
32155
+ "lawnMowerControl.pause": [{
32156
+ name: "deviceId",
32157
+ form: "single",
32158
+ optional: false
32159
+ }],
32160
+ "lawnMowerControl.startMowing": [{
32161
+ name: "deviceId",
32162
+ form: "single",
32163
+ optional: false
32164
+ }],
32165
+ "lockControl.lock": [{
32166
+ name: "deviceId",
32167
+ form: "single",
32168
+ optional: false
32169
+ }],
32170
+ "lockControl.open": [{
32171
+ name: "deviceId",
32172
+ form: "single",
32173
+ optional: false
32174
+ }],
32175
+ "lockControl.unlock": [{
32176
+ name: "deviceId",
32177
+ form: "single",
32178
+ optional: false
32179
+ }],
32180
+ "mediaPlayer.next": [{
32181
+ name: "deviceId",
32182
+ form: "single",
32183
+ optional: false
32184
+ }],
32185
+ "mediaPlayer.pause": [{
32186
+ name: "deviceId",
32187
+ form: "single",
32188
+ optional: false
32189
+ }],
32190
+ "mediaPlayer.play": [{
32191
+ name: "deviceId",
32192
+ form: "single",
32193
+ optional: false
32194
+ }],
32195
+ "mediaPlayer.playMedia": [{
32196
+ name: "deviceId",
32197
+ form: "single",
32198
+ optional: false
32199
+ }],
32200
+ "mediaPlayer.previous": [{
32201
+ name: "deviceId",
32202
+ form: "single",
32203
+ optional: false
32204
+ }],
32205
+ "mediaPlayer.seek": [{
32206
+ name: "deviceId",
32207
+ form: "single",
32208
+ optional: false
32209
+ }],
32210
+ "mediaPlayer.selectSource": [{
32211
+ name: "deviceId",
32212
+ form: "single",
32213
+ optional: false
32214
+ }],
32215
+ "mediaPlayer.setMute": [{
32216
+ name: "deviceId",
32217
+ form: "single",
32218
+ optional: false
32219
+ }],
32220
+ "mediaPlayer.setRepeat": [{
32221
+ name: "deviceId",
32222
+ form: "single",
32223
+ optional: false
32224
+ }],
32225
+ "mediaPlayer.setShuffle": [{
32226
+ name: "deviceId",
32227
+ form: "single",
32228
+ optional: false
32229
+ }],
32230
+ "mediaPlayer.setVolume": [{
32231
+ name: "deviceId",
32232
+ form: "single",
32233
+ optional: false
32234
+ }],
32235
+ "mediaPlayer.stop": [{
32236
+ name: "deviceId",
32237
+ form: "single",
32238
+ optional: false
32239
+ }],
32240
+ "motion.isDetected": [{
32241
+ name: "deviceId",
32242
+ form: "single",
32243
+ optional: false
32244
+ }],
32245
+ "motionDetection.analyze": [{
32246
+ name: "deviceId",
32247
+ form: "single",
32248
+ optional: false
32249
+ }],
32250
+ "motionDetection.removeCamera": [{
32251
+ name: "deviceId",
32252
+ form: "single",
32253
+ optional: false
32254
+ }],
32255
+ "motionTrigger.setMotionTrigger": [{
32256
+ name: "deviceId",
32257
+ form: "single",
32258
+ optional: false
32259
+ }],
32260
+ "motionZones.getOptions": [{
32261
+ name: "deviceId",
32262
+ form: "single",
32263
+ optional: false
32264
+ }],
32265
+ "motionZones.setZone": [{
32266
+ name: "deviceId",
32267
+ form: "single",
32268
+ optional: false
32269
+ }],
32270
+ "nativeObjectDetection.setEnabled": [{
32271
+ name: "deviceId",
32272
+ form: "single",
32273
+ optional: false
32274
+ }],
32275
+ "networkQuality.getDeviceStats": [{
32276
+ name: "deviceId",
32277
+ form: "single",
32278
+ optional: false
32279
+ }],
32280
+ "networkQuality.reportClientStats": [{
32281
+ name: "deviceId",
32282
+ form: "single",
32283
+ optional: false
32284
+ }],
32285
+ "notificationRules.setDeviceMuted": [{
32286
+ name: "deviceId",
32287
+ form: "single",
32288
+ optional: false
32289
+ }],
32290
+ "notifier.cancel": [{
32291
+ name: "deviceId",
32292
+ form: "single",
32293
+ optional: false
32294
+ }],
32295
+ "notifier.send": [{
32296
+ name: "deviceId",
32297
+ form: "single",
32298
+ optional: false
32299
+ }],
32300
+ "osd.setOverlay": [{
32301
+ name: "deviceId",
32302
+ form: "single",
32303
+ optional: false
32304
+ }],
32305
+ "osdManager.clearSlotBinding": [{
32306
+ name: "deviceId",
32307
+ form: "single",
32308
+ optional: false
32309
+ }],
32310
+ "osdManager.copyDeviceConfiguration": [{
32311
+ name: "sourceDeviceId",
32312
+ form: "single",
32313
+ optional: false
32314
+ }, {
32315
+ name: "targetDeviceId",
32316
+ form: "single",
32317
+ optional: false
32318
+ }],
32319
+ "osdManager.getDeviceOsd": [{
32320
+ name: "deviceId",
32321
+ form: "single",
32322
+ optional: false
32323
+ }],
32324
+ "osdManager.getSourceCatalog": [{
32325
+ name: "deviceId",
32326
+ form: "single",
32327
+ optional: false
32328
+ }],
32329
+ "osdManager.previewSlot": [{
32330
+ name: "deviceId",
32331
+ form: "single",
32332
+ optional: false
32333
+ }],
32334
+ "osdManager.renderDevice": [{
32335
+ name: "deviceId",
32336
+ form: "single",
32337
+ optional: false
32338
+ }],
32339
+ "osdManager.setSlotBinding": [{
32340
+ name: "deviceId",
32341
+ form: "single",
32342
+ optional: false
32343
+ }],
32344
+ "petFeeder.callPet": [{
32345
+ name: "deviceId",
32346
+ form: "single",
32347
+ optional: false
32348
+ }],
32349
+ "petFeeder.cancelFeed": [{
32350
+ name: "deviceId",
32351
+ form: "single",
32352
+ optional: false
32353
+ }],
32354
+ "petFeeder.feed": [{
32355
+ name: "deviceId",
32356
+ form: "single",
32357
+ optional: false
32358
+ }],
32359
+ "petFeeder.markFoodReplenished": [{
32360
+ name: "deviceId",
32361
+ form: "single",
32362
+ optional: false
32363
+ }],
32364
+ "petFeeder.playSound": [{
32365
+ name: "deviceId",
32366
+ form: "single",
32367
+ optional: false
32368
+ }],
32369
+ "petFeeder.resetDesiccant": [{
32370
+ name: "deviceId",
32371
+ form: "single",
32372
+ optional: false
32373
+ }],
32374
+ "petFeeder.setChildLock": [{
32375
+ name: "deviceId",
32376
+ form: "single",
32377
+ optional: false
32378
+ }],
32379
+ "petFeeder.setFeedSound": [{
32380
+ name: "deviceId",
32381
+ form: "single",
32382
+ optional: false
32383
+ }],
32384
+ "petFeeder.setIndicatorLight": [{
32385
+ name: "deviceId",
32386
+ form: "single",
32387
+ optional: false
32388
+ }],
32389
+ "petFeeder.setVolume": [{
32390
+ name: "deviceId",
32391
+ form: "single",
32392
+ optional: false
32393
+ }],
32394
+ "pipelineAnalytics.clearTracks": [{
32395
+ name: "deviceId",
32396
+ form: "single",
32397
+ optional: false
32398
+ }],
32399
+ "pipelineAnalytics.completeRetrainTrack": [{
32400
+ name: "deviceId",
32401
+ form: "single",
32402
+ optional: false
32403
+ }],
32404
+ "pipelineAnalytics.deleteDeviceEvents": [{
32405
+ name: "deviceId",
32406
+ form: "single",
32407
+ optional: false
32408
+ }],
32409
+ "pipelineAnalytics.deleteTracks": [{
32410
+ name: "deviceId",
32411
+ form: "single",
32412
+ optional: false
32413
+ }],
32414
+ "pipelineAnalytics.deselectRetrainFrame": [{
32415
+ name: "deviceId",
32416
+ form: "single",
32417
+ optional: false
32418
+ }],
32419
+ "pipelineAnalytics.getActiveTracks": [{
32420
+ name: "deviceId",
32421
+ form: "single",
32422
+ optional: false
32423
+ }],
32424
+ "pipelineAnalytics.getAudioEvents": [{
32425
+ name: "deviceId",
32426
+ form: "single",
32427
+ optional: false
32428
+ }],
32429
+ "pipelineAnalytics.getEventDensity": [{
32430
+ name: "deviceId",
32431
+ form: "single",
32432
+ optional: false
32433
+ }],
32434
+ "pipelineAnalytics.getKeyEvents": [{
32435
+ name: "deviceId",
32436
+ form: "single",
32437
+ optional: false
32438
+ }],
32439
+ "pipelineAnalytics.getMotionEvents": [{
32440
+ name: "deviceId",
32441
+ form: "single",
32442
+ optional: false
32443
+ }],
32444
+ "pipelineAnalytics.getObjectEvents": [{
32445
+ name: "deviceId",
32446
+ form: "single",
32447
+ optional: false
32448
+ }],
32449
+ "pipelineAnalytics.getRetrainExportUrl": [{
32450
+ name: "deviceIds",
32451
+ form: "array",
32452
+ optional: true
32453
+ }],
32454
+ "pipelineAnalytics.getSensorEvents": [{
32455
+ name: "deviceId",
32456
+ form: "single",
32457
+ optional: false
32458
+ }],
32459
+ "pipelineAnalytics.getTrack": [{
32460
+ name: "deviceId",
32461
+ form: "single",
32462
+ optional: false
32463
+ }],
32464
+ "pipelineAnalytics.getTrainingExportSummary": [{
32465
+ name: "deviceIds",
32466
+ form: "array",
32467
+ optional: true
32468
+ }],
32469
+ "pipelineAnalytics.getTrainingExportUrl": [{
32470
+ name: "deviceIds",
32471
+ form: "array",
32472
+ optional: true
32473
+ }],
32474
+ "pipelineAnalytics.listEventKinds": [{
32475
+ name: "deviceId",
32476
+ form: "single",
32477
+ optional: false
32478
+ }],
32479
+ "pipelineAnalytics.listEventKindsBatch": [{
32480
+ name: "deviceIds",
32481
+ form: "array",
32482
+ optional: false
32483
+ }],
32484
+ "pipelineAnalytics.listOpsLog": [{
32485
+ name: "deviceId",
32486
+ form: "single",
32487
+ optional: true
32488
+ }],
32489
+ "pipelineAnalytics.listRecentTracks": [{
32490
+ name: "deviceIds",
32491
+ form: "array",
32492
+ optional: false
32493
+ }],
32494
+ "pipelineAnalytics.listRetrainStaging": [{
32495
+ name: "deviceIds",
32496
+ form: "array",
32497
+ optional: true
32498
+ }],
32499
+ "pipelineAnalytics.listTracks": [{
32500
+ name: "deviceId",
32501
+ form: "single",
32502
+ optional: false
32503
+ }],
32504
+ "pipelineAnalytics.proposeRetrainAnnotations": [{
32505
+ name: "deviceId",
32506
+ form: "single",
32507
+ optional: false
32508
+ }],
32509
+ "pipelineAnalytics.pruneEventsBefore": [{
32510
+ name: "deviceId",
32511
+ form: "single",
32512
+ optional: false
32513
+ }],
32514
+ "pipelineAnalytics.pruneTracksBefore": [{
32515
+ name: "deviceId",
32516
+ form: "single",
32517
+ optional: false
32518
+ }],
32519
+ "pipelineAnalytics.rebuildObjectEmbeddings": [{
32520
+ name: "deviceId",
32521
+ form: "single",
32522
+ optional: true
32523
+ }],
32524
+ "pipelineAnalytics.restageRetrainTrack": [{
32525
+ name: "deviceId",
32526
+ form: "single",
32527
+ optional: false
32528
+ }],
32529
+ "pipelineAnalytics.saveRetrainAnnotations": [{
32530
+ name: "deviceId",
32531
+ form: "single",
32532
+ optional: false
32533
+ }],
32534
+ "pipelineAnalytics.searchObjectEvents": [{
32535
+ name: "deviceId",
32536
+ form: "single",
32537
+ optional: true
32538
+ }],
32539
+ "pipelineAnalytics.selectRetrainFrames": [{
32540
+ name: "deviceId",
32541
+ form: "single",
32542
+ optional: false
32543
+ }],
32544
+ "pipelineAnalytics.setTrackFlags": [{
32545
+ name: "deviceId",
32546
+ form: "single",
32547
+ optional: false
32548
+ }],
32549
+ "pipelineAnalytics.wipeAllAnalytics": [{
32550
+ name: "deviceId",
32551
+ form: "single",
32552
+ optional: false
32553
+ }],
32554
+ "pipelineExecutor.runPipeline": [{
32555
+ name: "deviceId",
32556
+ form: "single",
32557
+ optional: true
32558
+ }],
32559
+ "pipelineExecutor.runPipelineBatch": [{
32560
+ name: "deviceId",
32561
+ form: "single",
32562
+ optional: true
32563
+ }],
32564
+ "pipelineOrchestrator.assignAudio": [{
32565
+ name: "deviceId",
32566
+ form: "single",
32567
+ optional: false
32568
+ }],
32569
+ "pipelineOrchestrator.assignPipeline": [{
32570
+ name: "deviceId",
32571
+ form: "single",
32572
+ optional: false
32573
+ }],
32574
+ "pipelineOrchestrator.getAudioAssignment": [{
32575
+ name: "deviceId",
32576
+ form: "single",
32577
+ optional: false
32578
+ }],
32579
+ "pipelineOrchestrator.getCameraMetrics": [{
32580
+ name: "deviceId",
32581
+ form: "single",
32582
+ optional: false
32583
+ }],
32584
+ "pipelineOrchestrator.getCameraSettings": [{
32585
+ name: "deviceId",
32586
+ form: "single",
32587
+ optional: false
32588
+ }],
32589
+ "pipelineOrchestrator.getCameraStatus": [{
32590
+ name: "deviceId",
32591
+ form: "single",
32592
+ optional: false
32593
+ }],
32594
+ "pipelineOrchestrator.getCameraStatuses": [{
32595
+ name: "deviceIds",
32596
+ form: "array",
32597
+ optional: true
32598
+ }],
32599
+ "pipelineOrchestrator.getCameraStepOverrides": [{
32600
+ name: "deviceId",
32601
+ form: "single",
32602
+ optional: false
32603
+ }],
32604
+ "pipelineOrchestrator.getCameraSwitches": [{
32605
+ name: "deviceId",
32606
+ form: "single",
32607
+ optional: false
32608
+ }],
32609
+ "pipelineOrchestrator.getPipelineAssignment": [{
32610
+ name: "deviceId",
32611
+ form: "single",
32612
+ optional: false
32613
+ }],
32614
+ "pipelineOrchestrator.getPipelineDevicePin": [{
32615
+ name: "deviceId",
32616
+ form: "single",
32617
+ optional: false
32618
+ }],
32619
+ "pipelineOrchestrator.resolvePipeline": [{
32620
+ name: "deviceId",
32621
+ form: "single",
32622
+ optional: false
32623
+ }],
32624
+ "pipelineOrchestrator.setCameraPipelineForAgent": [{
32625
+ name: "deviceId",
32626
+ form: "single",
32627
+ optional: false
32628
+ }],
32629
+ "pipelineOrchestrator.setCameraStepOverride": [{
32630
+ name: "deviceId",
32631
+ form: "single",
32632
+ optional: false
32633
+ }],
32634
+ "pipelineOrchestrator.setCameraStepToggle": [{
32635
+ name: "deviceId",
32636
+ form: "single",
32637
+ optional: false
32638
+ }],
32639
+ "pipelineOrchestrator.setCameraSwitch": [{
32640
+ name: "deviceId",
32641
+ form: "single",
32642
+ optional: false
32643
+ }],
32644
+ "pipelineOrchestrator.setPipelineDevicePin": [{
32645
+ name: "deviceId",
32646
+ form: "single",
32647
+ optional: false
32648
+ }],
32649
+ "pipelineOrchestrator.unassignAudio": [{
32650
+ name: "deviceId",
32651
+ form: "single",
32652
+ optional: false
32653
+ }],
32654
+ "pipelineOrchestrator.unassignPipeline": [{
32655
+ name: "deviceId",
32656
+ form: "single",
32657
+ optional: false
32658
+ }],
32659
+ "pipelineRunner.attachCamera": [{
32660
+ name: "deviceId",
32661
+ form: "single",
32662
+ optional: false
32663
+ }],
32664
+ "pipelineRunner.detachCamera": [{
32665
+ name: "deviceId",
32666
+ form: "single",
32667
+ optional: false
32668
+ }],
32669
+ "pipelineRunner.getCameraMetrics": [{
32670
+ name: "deviceId",
32671
+ form: "single",
32672
+ optional: false
32673
+ }],
32674
+ "pipelineRunner.reportMotion": [{
32675
+ name: "deviceId",
32676
+ form: "single",
32677
+ optional: false
32678
+ }],
32679
+ "pipelineRunner.runDetailSubtree": [{
32680
+ name: "deviceId",
32681
+ form: "single",
32682
+ optional: false
32683
+ }],
32684
+ "pipelineRunner.runStatelessStep": [{
32685
+ name: "sourceDeviceId",
32686
+ form: "single",
32687
+ optional: false
32688
+ }],
32689
+ "plateGallery.getPlateByTrack": [{
32690
+ name: "deviceId",
32691
+ form: "single",
32692
+ optional: false
32693
+ }],
32694
+ "plateGallery.listPlates": [{
32695
+ name: "deviceId",
32696
+ form: "single",
32697
+ optional: true
32698
+ }],
32699
+ "privacyMask.getOptions": [{
32700
+ name: "deviceId",
32701
+ form: "single",
32702
+ optional: false
32703
+ }],
32704
+ "privacyMask.setAudioEnabled": [{
32705
+ name: "deviceId",
32706
+ form: "single",
32707
+ optional: false
32708
+ }],
32709
+ "privacyMask.setMask": [{
32710
+ name: "deviceId",
32711
+ form: "single",
32712
+ optional: false
32713
+ }],
32714
+ "ptz.continuousMove": [{
32715
+ name: "deviceId",
32716
+ form: "single",
32717
+ optional: false
32718
+ }],
32719
+ "ptz.deletePreset": [{
32720
+ name: "deviceId",
32721
+ form: "single",
32722
+ optional: false
32723
+ }],
32724
+ "ptz.getOptions": [{
32725
+ name: "deviceId",
32726
+ form: "single",
32727
+ optional: false
32728
+ }],
32729
+ "ptz.getPosition": [{
32730
+ name: "deviceId",
32731
+ form: "single",
32732
+ optional: false
32733
+ }],
32734
+ "ptz.getPresets": [{
32735
+ name: "deviceId",
32736
+ form: "single",
32737
+ optional: false
32738
+ }],
32739
+ "ptz.goHome": [{
32740
+ name: "deviceId",
32741
+ form: "single",
32742
+ optional: false
32743
+ }],
32744
+ "ptz.goToPreset": [{
32745
+ name: "deviceId",
32746
+ form: "single",
32747
+ optional: false
32748
+ }],
32749
+ "ptz.move": [{
32750
+ name: "deviceId",
32751
+ form: "single",
32752
+ optional: false
32753
+ }],
32754
+ "ptz.savePreset": [{
32755
+ name: "deviceId",
32756
+ form: "single",
32757
+ optional: false
32758
+ }],
32759
+ "ptz.setAutofocus": [{
32760
+ name: "deviceId",
32761
+ form: "single",
32762
+ optional: false
32763
+ }],
32764
+ "ptz.stop": [{
32765
+ name: "deviceId",
32766
+ form: "single",
32767
+ optional: false
32768
+ }],
32769
+ "ptzAutotrack.getSettings": [{
32770
+ name: "deviceId",
32771
+ form: "single",
32772
+ optional: false
32773
+ }],
32774
+ "ptzAutotrack.getStatus": [{
32775
+ name: "deviceId",
32776
+ form: "single",
32777
+ optional: false
32778
+ }],
32779
+ "ptzAutotrack.setEnabled": [{
32780
+ name: "deviceId",
32781
+ form: "single",
32782
+ optional: false
32783
+ }],
32784
+ "ptzAutotrack.setSettings": [{
32785
+ name: "deviceId",
32786
+ form: "single",
32787
+ optional: false
32788
+ }],
32789
+ "reboot.reboot": [{
32790
+ name: "deviceId",
32791
+ form: "single",
32792
+ optional: false
32793
+ }],
32794
+ "recording.deleteFootprint": [{
32795
+ name: "deviceId",
32796
+ form: "single",
32797
+ optional: false
32798
+ }],
32799
+ "recording.getAvailability": [{
32800
+ name: "deviceId",
32801
+ form: "single",
32802
+ optional: false
32803
+ }],
32804
+ "recording.getDaysWithRecordings": [{
32805
+ name: "deviceId",
32806
+ form: "single",
32807
+ optional: false
32808
+ }],
32809
+ "recording.getDeviceConfig": [{
32810
+ name: "deviceId",
32811
+ form: "single",
32812
+ optional: false
32813
+ }],
32814
+ "recording.getPlaybackManifest": [{
32815
+ name: "deviceId",
32816
+ form: "single",
32817
+ optional: false
32818
+ }],
32819
+ "recording.listOpsLog": [{
32820
+ name: "deviceId",
32821
+ form: "single",
32822
+ optional: true
32823
+ }],
32824
+ "recording.locateSegment": [{
32825
+ name: "deviceId",
32826
+ form: "single",
32827
+ optional: false
32828
+ }],
32829
+ "recording.pruneFootage": [{
32830
+ name: "deviceId",
32831
+ form: "single",
32832
+ optional: false
32833
+ }],
32834
+ "recording.readGopBytes": [{
32835
+ name: "deviceId",
32836
+ form: "single",
32837
+ optional: false
32838
+ }],
32839
+ "recording.readSegmentBytes": [{
32840
+ name: "deviceId",
32841
+ form: "single",
32842
+ optional: false
32843
+ }],
32844
+ "recording.relocateFootage": [{
32845
+ name: "deviceId",
32846
+ form: "single",
32847
+ optional: true
32848
+ }],
32849
+ "recording.renderClip": [{
32850
+ name: "deviceId",
32851
+ form: "single",
32852
+ optional: false
32853
+ }],
32854
+ "recording.renderGif": [{
32855
+ name: "deviceId",
32856
+ form: "single",
32857
+ optional: false
32858
+ }],
32859
+ "recording.rescanStorage": [{
32860
+ name: "deviceId",
32861
+ form: "single",
32862
+ optional: false
32863
+ }],
32864
+ "recording.setDeviceConfig": [{
32865
+ name: "deviceId",
32866
+ form: "single",
32867
+ optional: false
32868
+ }],
32869
+ "recording.startStorageMigrationMove": [{
32870
+ name: "deviceId",
32871
+ form: "single",
32872
+ optional: true
32873
+ }],
32874
+ "recordingExport.createExport": [{
32875
+ name: "deviceId",
32876
+ form: "single",
32877
+ optional: false
32878
+ }],
32879
+ "recordingExport.listExports": [{
32880
+ name: "deviceId",
32881
+ form: "single",
32882
+ optional: true
32883
+ }],
32884
+ "sceneMonitor.captureReference": [{
32885
+ name: "deviceId",
32886
+ form: "single",
32887
+ optional: false
32888
+ }],
32889
+ "sceneMonitor.createScene": [{
32890
+ name: "deviceId",
32891
+ form: "single",
32892
+ optional: false
32893
+ }],
32894
+ "sceneMonitor.deleteReference": [{
32895
+ name: "deviceId",
32896
+ form: "single",
32897
+ optional: false
32898
+ }],
32899
+ "sceneMonitor.deleteScene": [{
32900
+ name: "deviceId",
32901
+ form: "single",
32902
+ optional: false
32903
+ }],
32904
+ "sceneMonitor.listScenes": [{
32905
+ name: "deviceId",
32906
+ form: "single",
32907
+ optional: false
32908
+ }],
32909
+ "sceneMonitor.recheckNow": [{
32910
+ name: "deviceId",
32911
+ form: "single",
32912
+ optional: false
32913
+ }],
32914
+ "sceneMonitor.updateScene": [{
32915
+ name: "deviceId",
32916
+ form: "single",
32917
+ optional: false
32918
+ }],
32919
+ "scriptRunner.run": [{
32920
+ name: "deviceId",
32921
+ form: "single",
32922
+ optional: false
32923
+ }],
32924
+ "scriptRunner.stop": [{
32925
+ name: "deviceId",
32926
+ form: "single",
32927
+ optional: false
32928
+ }],
32929
+ "snapshot.getSnapshot": [{
32930
+ name: "deviceId",
32931
+ form: "single",
32932
+ optional: false
32933
+ }],
32934
+ "snapshot.getSnapshotOverview": [{
32935
+ name: "deviceIds",
32936
+ form: "array",
32937
+ optional: false
32938
+ }],
32939
+ "snapshot.invalidateCache": [{
32940
+ name: "deviceId",
32941
+ form: "single",
32942
+ optional: false
32943
+ }],
32944
+ "streamBroker.acquireEgressTranscode": [{
32945
+ name: "deviceId",
32946
+ form: "single",
32947
+ optional: false
32948
+ }],
32949
+ "streamBroker.assignProfile": [{
32950
+ name: "deviceId",
32951
+ form: "single",
32952
+ optional: false
32953
+ }],
32954
+ "streamBroker.getDeviceAudioMute": [{
32955
+ name: "deviceId",
32956
+ form: "single",
32957
+ optional: false
32958
+ }],
32959
+ "streamBroker.getStreamWithCodec": [{
32960
+ name: "deviceId",
32961
+ form: "single",
32962
+ optional: false
32963
+ }],
32964
+ "streamBroker.produceEventMedia": [{
32965
+ name: "deviceId",
32966
+ form: "single",
32967
+ optional: false
32968
+ }],
32969
+ "streamBroker.publishCameraStream": [{
32970
+ name: "deviceId",
32971
+ form: "single",
32972
+ optional: false
32973
+ }],
32974
+ "streamBroker.renderPreBufferClip": [{
32975
+ name: "deviceId",
32976
+ form: "single",
32977
+ optional: false
32978
+ }],
32979
+ "streamBroker.restartProfile": [{
32980
+ name: "deviceId",
32981
+ form: "single",
32982
+ optional: false
32983
+ }],
32984
+ "streamBroker.retractCameraStream": [{
32985
+ name: "deviceId",
32986
+ form: "single",
32987
+ optional: false
32988
+ }],
32989
+ "streamBroker.setDeviceAudioMute": [{
32990
+ name: "deviceId",
32991
+ form: "single",
32992
+ optional: false
32993
+ }],
32994
+ "streamBroker.unassignProfile": [{
32995
+ name: "deviceId",
32996
+ form: "single",
32997
+ optional: false
32998
+ }],
32999
+ "streamCatalog.getCatalog": [{
33000
+ name: "deviceId",
33001
+ form: "single",
33002
+ optional: false
33003
+ }],
33004
+ "streamParams.getConfigSchema": [{
33005
+ name: "deviceId",
33006
+ form: "single",
33007
+ optional: false
33008
+ }],
33009
+ "streamParams.getOptions": [{
33010
+ name: "deviceId",
33011
+ form: "single",
33012
+ optional: false
33013
+ }],
33014
+ "streamParams.setProfile": [{
33015
+ name: "deviceId",
33016
+ form: "single",
33017
+ optional: false
33018
+ }],
33019
+ "switch.setState": [{
33020
+ name: "deviceId",
33021
+ form: "single",
33022
+ optional: false
33023
+ }],
33024
+ "vacuumControl.locate": [{
33025
+ name: "deviceId",
33026
+ form: "single",
33027
+ optional: false
33028
+ }],
33029
+ "vacuumControl.pause": [{
33030
+ name: "deviceId",
33031
+ form: "single",
33032
+ optional: false
33033
+ }],
33034
+ "vacuumControl.returnToBase": [{
33035
+ name: "deviceId",
33036
+ form: "single",
33037
+ optional: false
33038
+ }],
33039
+ "vacuumControl.setFanSpeed": [{
33040
+ name: "deviceId",
33041
+ form: "single",
33042
+ optional: false
33043
+ }],
33044
+ "vacuumControl.start": [{
33045
+ name: "deviceId",
33046
+ form: "single",
33047
+ optional: false
33048
+ }],
33049
+ "vacuumControl.stop": [{
33050
+ name: "deviceId",
33051
+ form: "single",
33052
+ optional: false
33053
+ }],
33054
+ "valve.close": [{
33055
+ name: "deviceId",
33056
+ form: "single",
33057
+ optional: false
33058
+ }],
33059
+ "valve.open": [{
33060
+ name: "deviceId",
33061
+ form: "single",
33062
+ optional: false
33063
+ }],
33064
+ "valve.setPosition": [{
33065
+ name: "deviceId",
33066
+ form: "single",
33067
+ optional: false
33068
+ }],
33069
+ "valve.stop": [{
33070
+ name: "deviceId",
33071
+ form: "single",
33072
+ optional: false
33073
+ }],
33074
+ "videoclips.getClipPlayback": [{
33075
+ name: "deviceId",
33076
+ form: "single",
33077
+ optional: false
33078
+ }],
33079
+ "videoclips.listClips": [{
33080
+ name: "deviceId",
33081
+ form: "single",
33082
+ optional: false
33083
+ }],
33084
+ "waterHeater.setAway": [{
33085
+ name: "deviceId",
33086
+ form: "single",
33087
+ optional: false
33088
+ }],
33089
+ "waterHeater.setOperationMode": [{
33090
+ name: "deviceId",
33091
+ form: "single",
33092
+ optional: false
33093
+ }],
33094
+ "waterHeater.setTargetTemp": [{
33095
+ name: "deviceId",
33096
+ form: "single",
33097
+ optional: false
33098
+ }],
33099
+ "webrtcSession.addIceCandidate": [{
33100
+ name: "deviceId",
33101
+ form: "single",
33102
+ optional: false
33103
+ }],
33104
+ "webrtcSession.closeSession": [{
33105
+ name: "deviceId",
33106
+ form: "single",
33107
+ optional: false
33108
+ }],
33109
+ "webrtcSession.createSession": [{
33110
+ name: "deviceId",
33111
+ form: "single",
33112
+ optional: false
33113
+ }],
33114
+ "webrtcSession.getIceCandidates": [{
33115
+ name: "deviceId",
33116
+ form: "single",
33117
+ optional: false
33118
+ }],
33119
+ "webrtcSession.getSessionState": [{
33120
+ name: "deviceId",
33121
+ form: "single",
33122
+ optional: false
33123
+ }],
33124
+ "webrtcSession.handleAnswer": [{
33125
+ name: "deviceId",
33126
+ form: "single",
33127
+ optional: false
33128
+ }],
33129
+ "webrtcSession.handleOffer": [{
33130
+ name: "deviceId",
33131
+ form: "single",
33132
+ optional: false
33133
+ }],
33134
+ "webrtcSession.hasAdaptiveBitrate": [{
33135
+ name: "deviceId",
33136
+ form: "single",
33137
+ optional: false
33138
+ }],
33139
+ "webrtcSession.listStreams": [{
33140
+ name: "deviceId",
33141
+ form: "single",
33142
+ optional: false
33143
+ }],
33144
+ "zoneAnalytics.getCameraHistory": [{
33145
+ name: "deviceId",
33146
+ form: "single",
33147
+ optional: false
33148
+ }],
33149
+ "zoneAnalytics.getCurrentSnapshot": [{
33150
+ name: "deviceId",
33151
+ form: "single",
33152
+ optional: false
33153
+ }],
33154
+ "zoneAnalytics.getUnzonedHistory": [{
33155
+ name: "deviceId",
33156
+ form: "single",
33157
+ optional: false
33158
+ }],
33159
+ "zoneAnalytics.getZoneHistory": [{
33160
+ name: "deviceId",
33161
+ form: "single",
33162
+ optional: false
33163
+ }],
33164
+ "zoneRules.listRules": [{
33165
+ name: "deviceId",
33166
+ form: "single",
33167
+ optional: false
33168
+ }],
33169
+ "zoneRules.setRules": [{
33170
+ name: "deviceId",
33171
+ form: "single",
33172
+ optional: false
33173
+ }],
33174
+ "zones.addZone": [{
33175
+ name: "deviceId",
33176
+ form: "single",
33177
+ optional: false
33178
+ }],
33179
+ "zones.listZones": [{
33180
+ name: "deviceId",
33181
+ form: "single",
33182
+ optional: false
33183
+ }],
33184
+ "zones.removeZone": [{
33185
+ name: "deviceId",
33186
+ form: "single",
33187
+ optional: false
33188
+ }],
33189
+ "zones.updateZone": [{
33190
+ name: "deviceId",
33191
+ form: "single",
33192
+ optional: false
33193
+ }]
33194
+ });
31298
33195
  Object.freeze({
31299
33196
  "broker": "broker",
31300
33197
  "device-export": "device-export",
@@ -32014,7 +33911,7 @@ var Fmp4FragmentChild = class {
32014
33911
  meta: {
32015
33912
  sourceId: this.args.sourceId,
32016
33913
  decodeHwAccel: requested,
32017
- error: errMsg$12(err)
33914
+ error: errMsg$15(err)
32018
33915
  }
32019
33916
  });
32020
33917
  this.killChild();
@@ -32193,7 +34090,7 @@ var Fmp4FragmentChild = class {
32193
34090
  tags: { deviceId: this.args.deviceId },
32194
34091
  meta: {
32195
34092
  sourceId: this.args.sourceId,
32196
- error: errMsg$12(err)
34093
+ error: errMsg$15(err)
32197
34094
  }
32198
34095
  });
32199
34096
  }
@@ -78538,7 +80435,7 @@ function clearPairingFiles(accessoryUuid, logger) {
78538
80435
  }
78539
80436
  //#endregion
78540
80437
  //#region src/hap-setup-uri.ts
78541
- function errMsg$11(e) {
80438
+ function errMsg$14(e) {
78542
80439
  return e instanceof Error ? e.message : String(e);
78543
80440
  }
78544
80441
  /**
@@ -78565,11 +80462,79 @@ function firstExposedAccessorySetupUri(exposed, logger) {
78565
80462
  try {
78566
80463
  return first.setupURI();
78567
80464
  } catch (err) {
78568
- logger.debug("export-hap: setupURI failed on first exposed accessory", { meta: { error: errMsg$11(err) } });
80465
+ logger.debug("export-hap: setupURI failed on first exposed accessory", { meta: { error: errMsg$14(err) } });
78569
80466
  return;
78570
80467
  }
78571
80468
  }
78572
80469
  }
80470
+ //#endregion
80471
+ //#region src/mappers/builders/accessory-info.ts
80472
+ /**
80473
+ * `Service.AccessoryInformation` — the manufacturer / model / firmware /
80474
+ * serial block every HomeKit accessory carries.
80475
+ *
80476
+ * One implementation for both accessory shapes (camera and generic): the
80477
+ * fields come from the device's own metadata either way, and a second copy
80478
+ * would be a second answer to "what serial does this device publish".
80479
+ * Metadata is best-effort — a device that cannot answer still publishes.
80480
+ */
80481
+ async function populateAccessoryInfo(accessory, proxy, displayName, modelFallback) {
80482
+ const info = accessory.getService(import_dist.Service.AccessoryInformation);
80483
+ if (!info) return;
80484
+ try {
80485
+ const device = await proxy.deviceManager?.getDevice({});
80486
+ const metadata = readMetadata(device);
80487
+ info.setCharacteristic(import_dist.Characteristic.Name, device?.name ?? displayName);
80488
+ info.setCharacteristic(import_dist.Characteristic.Manufacturer, stringOr(metadata?.manufacturer, "CamStack"));
80489
+ info.setCharacteristic(import_dist.Characteristic.Model, stringOr(metadata?.model, modelFallback));
80490
+ info.setCharacteristic(import_dist.Characteristic.FirmwareRevision, stringOr(metadata?.firmware, "0.0.0"));
80491
+ info.setCharacteristic(import_dist.Characteristic.SerialNumber, stringOr(metadata?.sn, `camstack-${proxy.deviceId}`));
80492
+ } catch {}
80493
+ }
80494
+ /** The four fields this module reads, or `null` — the device record's
80495
+ * `metadata` is an open bag and nothing else here depends on its shape. */
80496
+ function readMetadata(device) {
80497
+ const metadata = device?.metadata;
80498
+ if (metadata === null || typeof metadata !== "object") return {};
80499
+ const entries = new Map(Object.entries(metadata));
80500
+ return {
80501
+ model: stringOrNull(entries.get("model")),
80502
+ manufacturer: stringOrNull(entries.get("manufacturer")),
80503
+ firmware: stringOrNull(entries.get("firmware")),
80504
+ sn: stringOrNull(entries.get("sn"))
80505
+ };
80506
+ }
80507
+ function stringOrNull(value) {
80508
+ return typeof value === "string" ? value : null;
80509
+ }
80510
+ function stringOr(value, fallback) {
80511
+ return typeof value === "string" && value.length > 0 ? value : fallback;
80512
+ }
80513
+ //#endregion
80514
+ //#region src/mappers/builders/generic/characteristic-update.ts
80515
+ /**
80516
+ * Parse `status` with the capability's OWN Zod schema and turn it into
80517
+ * characteristic writes.
80518
+ *
80519
+ * Duck-typing the payload here would be the second source of truth about what a
80520
+ * cap reports; the schema is the first and only one. A payload that does not
80521
+ * match yields NO updates — never a partial or invented value — and the caller
80522
+ * logs the drop, because a sensor that silently stops moving is
80523
+ * indistinguishable from a sensor that never changed.
80524
+ *
80525
+ * Rows `.pick()` only the fields they read, so a provider omitting a timestamp
80526
+ * cannot silence a sensor.
80527
+ */
80528
+ function reader(schema, toUpdates) {
80529
+ return (status) => {
80530
+ const parsed = schema.safeParse(status);
80531
+ return parsed.success ? toUpdates(parsed.data) : [];
80532
+ };
80533
+ }
80534
+ /** Push every update onto `service`. */
80535
+ function applyUpdates(service, updates) {
80536
+ for (const update of updates) service.updateCharacteristic(update.characteristic, update.value);
80537
+ }
78573
80538
  /**
78574
80539
  * hap-nodejs' `checkName` regex, verbatim.
78575
80540
  *
@@ -78706,6 +80671,92 @@ function titleCase(raw) {
78706
80671
  return raw.split(/[-_\s]+/u).filter((part) => part.length > 0).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
78707
80672
  }
78708
80673
  //#endregion
80674
+ //#region src/mappers/builders/service-label.ts
80675
+ /**
80676
+ * The ONE place a secondary service on the camera accessory gets its label.
80677
+ *
80678
+ * A "secondary service" here is a Switch or Lightbulb published alongside the
80679
+ * camera on the same accessory — the privacy switch, each accessory child
80680
+ * (siren, floodlight), each PTZ action. iOS Home renders these as their own
80681
+ * controls, and the operator has seen them as "Interruttore 1", "Interruttore
80682
+ * 2" through three separate rounds of fixes.
80683
+ *
80684
+ * ## Why `Name` alone cannot rename anything
80685
+ *
80686
+ * Two facts about hap-nodejs 2.1.7, both measured against the installed copy
80687
+ * rather than reasoned about:
80688
+ *
80689
+ * 1. `accessory.addService(Type, displayName, subtype)` ALREADY writes
80690
+ * `displayName` to `Characteristic.Name` (`Service` constructor). So every
80691
+ * round of this bug — including the one that moved the label onto
80692
+ * `ConfiguredName` — shipped with `Name` correctly set. "iOS had no name
80693
+ * to render" was never true.
80694
+ * 2. The mDNS configuration number (`c#`) is a sha1 over
80695
+ * `internalHAPRepresentation(false)`, which OMITS characteristic VALUES.
80696
+ * Changing the string in `Name` therefore does not bump `c#`, a paired
80697
+ * controller gets no signal to re-read `/accessories`, and the name it
80698
+ * cached at first enumeration stands forever.
80699
+ *
80700
+ * `Name` is also declared `pr` only — paired read, no write, no notify. It is
80701
+ * the seed a controller seeds its database from once; it is not a channel.
80702
+ *
80703
+ * ## Why `ConfiguredName`
80704
+ *
80705
+ * `ConfiguredName` (`000000E3`) is declared `pr | pw | ev` — the only name
80706
+ * characteristic a controller may write and may subscribe to. It is what iOS
80707
+ * 16+ reads for a service the user can rename, and adding it CHANGES the
80708
+ * accessory structure, so `c#` does bump and the controller re-reads.
80709
+ *
80710
+ * It was removed once because hap-nodejs logged
80711
+ *
80712
+ * ```
80713
+ * Characteristic not in required or optional characteristic section for
80714
+ * service Switch. Adding anyway.
80715
+ * ```
80716
+ *
80717
+ * That line is a WARNING, not a rejection: `Service.getCharacteristic` calls
80718
+ * `addCharacteristic` unconditionally and only then emits the warning. The
80719
+ * characteristic was always present and always published. hap-nodejs'
80720
+ * per-service optional lists simply predate `ConfiguredName` being valid on
80721
+ * any service.
80722
+ *
80723
+ * Registering it with {@link Service.addOptionalCharacteristic} first takes
80724
+ * the branch above the warning, so the accessory still builds with ZERO
80725
+ * characteristic warnings — which is what `service-naming.spec.ts` asserts.
80726
+ *
80727
+ * ## Scope: EVERY service, including the sensors
80728
+ *
80729
+ * An earlier round applied this to Switch- and Lightbulb-shaped services only,
80730
+ * on the theory that `Service.MotionSensor` and `Service.Battery` are not
80731
+ * separately named tiles in iOS Home and that naming them would be a guess.
80732
+ *
80733
+ * That theory was never measured, and it had a cost: it left services on the
80734
+ * accessory whose name a paired controller could never be told about, and it
80735
+ * made "did `ConfiguredName` fix the operator's 'Interruttore N'?" unanswerable
80736
+ * — a negative result on a partial application proves nothing about the
80737
+ * mechanism. Every service this addon publishes now carries both
80738
+ * characteristics, on the camera accessory and on the generic one.
80739
+ *
80740
+ * The reasoning above is mechanism, not measurement: it says why `Name` alone
80741
+ * CANNOT work and why `ConfiguredName` is the only characteristic that can. It
80742
+ * does not prove iOS renders it on every service shape. That is an observation
80743
+ * only a re-paired controller can make — and after a change like this one, the
80744
+ * controller must be re-paired, because a cached accessory database is exactly
80745
+ * what the whole mechanism is about.
80746
+ */
80747
+ /**
80748
+ * Publish `name` as both the immutable `Name` and the controller-visible
80749
+ * `ConfiguredName` of `service`.
80750
+ *
80751
+ * `name` must already be HAP-valid — build it with `service-names.ts`, which
80752
+ * cannot return a string hap-nodejs' `checkName` would warn about.
80753
+ */
80754
+ function applyServiceLabel(service, name) {
80755
+ service.setCharacteristic(import_dist.Characteristic.Name, name);
80756
+ if (!service.optionalCharacteristics.some((characteristic) => characteristic.UUID === import_dist.Characteristic.ConfiguredName.UUID)) service.addOptionalCharacteristic(import_dist.Characteristic.ConfiguredName);
80757
+ service.setCharacteristic(import_dist.Characteristic.ConfiguredName, name);
80758
+ }
80759
+ //#endregion
78709
80760
  //#region src/mappers/builders/battery.ts
78710
80761
  /**
78711
80762
  * Battery builder — surfaces a battery-operated camera's power state
@@ -78715,34 +80766,56 @@ function titleCase(raw) {
78715
80766
  * subscribes to the runtime-state slice so iOS Home reflects level /
78716
80767
  * charging changes pushed by the firmware without a poll loop.
78717
80768
  *
80769
+ * The status→characteristic mapping is {@link batteryCharacteristicUpdates},
80770
+ * exported because the generic (non-camera) export path publishes the same
80771
+ * `Service.Battery` from the same cap — one derivation, two accessory shapes.
80772
+ *
80773
+ * Skipped silently when the `battery` cap is not bound — caller checks
80774
+ * cap presence before invoking this builder (see `camera-accessory.ts`).
80775
+ */
80776
+ var LOW_BATTERY_THRESHOLD_PCT = 20;
80777
+ /**
78718
80778
  * Mapping:
78719
80779
  * - `BatteryStatus.percentage` (0..100) → `Characteristic.BatteryLevel`
78720
80780
  * - `BatteryStatus.charging`:
78721
80781
  * `'none'` → `ChargingState.NOT_CHARGING`
78722
80782
  * `'dc' | 'solar'` → `ChargingState.CHARGING`
78723
80783
  * - `percentage <= LOW_BATTERY_THRESHOLD_PCT` → `StatusLowBattery.LOW`
78724
- *
78725
- * Skipped silently when the `battery` cap is not bound — caller checks
78726
- * cap presence before invoking this builder (see `camera-accessory.ts`).
78727
80784
  */
78728
- var LOW_BATTERY_THRESHOLD_PCT = 20;
80785
+ var batteryCharacteristicUpdates = reader(BatteryStatusSchema.pick({ percentage: true }).extend({ charging: BatteryStatusSchema.shape.charging.optional() }), (status) => {
80786
+ const pct = Math.max(0, Math.min(100, Math.round(status.percentage)));
80787
+ return [
80788
+ {
80789
+ characteristic: import_dist.Characteristic.BatteryLevel,
80790
+ value: pct
80791
+ },
80792
+ ...status.charging === void 0 ? [] : [{
80793
+ characteristic: import_dist.Characteristic.ChargingState,
80794
+ value: status.charging === "none" ? import_dist.Characteristic.ChargingState.NOT_CHARGING : import_dist.Characteristic.ChargingState.CHARGING
80795
+ }],
80796
+ {
80797
+ characteristic: import_dist.Characteristic.StatusLowBattery,
80798
+ value: pct <= LOW_BATTERY_THRESHOLD_PCT ? import_dist.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : import_dist.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL
80799
+ }
80800
+ ];
80801
+ });
78729
80802
  async function buildBattery(bctx) {
78730
80803
  const { ctx, accessory, proxy, numericDeviceId, displayName } = bctx;
78731
80804
  const log = ctx.logger.withTags({ deviceId: numericDeviceId });
78732
- const service = accessory.addService(import_dist.Service.Battery, hapServiceName([displayName], `Camera ${numericDeviceId}`));
80805
+ const label = hapServiceName([displayName], `Camera ${numericDeviceId}`);
80806
+ const service = accessory.addService(import_dist.Service.Battery, label);
80807
+ applyServiceLabel(service, label);
78733
80808
  try {
78734
80809
  const status = await proxy.battery?.getStatus({});
78735
- if (status) applyToService(service, status);
80810
+ if (status !== void 0 && status !== null) applyToService(service, status);
78736
80811
  } catch (err) {
78737
- log.debug("export-hap: battery getStatus hydrate failed (non-fatal)", { meta: { error: errMsg$10(err) } });
80812
+ log.debug("export-hap: battery getStatus hydrate failed (non-fatal)", { meta: { error: errMsg$13(err) } });
78738
80813
  }
78739
80814
  const unsubscribes = [];
78740
80815
  if (proxy.state.battery) {
78741
80816
  const unsub = proxy.state.battery.subscribe((value) => {
78742
80817
  if (!value) return;
78743
- const status = value;
78744
- if (typeof status.percentage !== "number") return;
78745
- applyToService(service, status);
80818
+ applyToService(service, value);
78746
80819
  });
78747
80820
  unsubscribes.push(unsub);
78748
80821
  }
@@ -78753,14 +80826,9 @@ async function buildBattery(bctx) {
78753
80826
  } };
78754
80827
  }
78755
80828
  function applyToService(service, status) {
78756
- const pct = Math.max(0, Math.min(100, Math.round(status.percentage)));
78757
- service.updateCharacteristic(import_dist.Characteristic.BatteryLevel, pct);
78758
- const chargingState = status.charging === "none" ? import_dist.Characteristic.ChargingState.NOT_CHARGING : import_dist.Characteristic.ChargingState.CHARGING;
78759
- service.updateCharacteristic(import_dist.Characteristic.ChargingState, chargingState);
78760
- const lowBattery = pct <= LOW_BATTERY_THRESHOLD_PCT ? import_dist.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : import_dist.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL;
78761
- service.updateCharacteristic(import_dist.Characteristic.StatusLowBattery, lowBattery);
80829
+ applyUpdates(service, batteryCharacteristicUpdates(status));
78762
80830
  }
78763
- function errMsg$10(err) {
80831
+ function errMsg$13(err) {
78764
80832
  return err instanceof Error ? err.message : String(err);
78765
80833
  }
78766
80834
  //#endregion
@@ -89512,7 +91580,7 @@ var VIDEO_LOOPBACK_RCVBUF_BYTES = 8 * 1024 * 1024;
89512
91580
  * not burst size.
89513
91581
  */
89514
91582
  var AUDIO_LOOPBACK_RCVBUF_BYTES = 1024 * 1024;
89515
- function errMsg$9(err) {
91583
+ function errMsg$12(err) {
89516
91584
  return err instanceof Error ? err.message : String(err);
89517
91585
  }
89518
91586
  /**
@@ -89527,13 +91595,13 @@ function applyReceiveBuffer(socket, requestedBytes) {
89527
91595
  try {
89528
91596
  socket.setRecvBufferSize(requestedBytes);
89529
91597
  } catch (err) {
89530
- error = errMsg$9(err);
91598
+ error = errMsg$12(err);
89531
91599
  }
89532
91600
  let effectiveBytes = null;
89533
91601
  try {
89534
91602
  effectiveBytes = socket.getRecvBufferSize();
89535
91603
  } catch (err) {
89536
- if (error === null) error = errMsg$9(err);
91604
+ if (error === null) error = errMsg$12(err);
89537
91605
  }
89538
91606
  return {
89539
91607
  requestedBytes,
@@ -89793,20 +91861,20 @@ function buildCameraStreamingDelegate(bctx, advertised) {
89793
91861
  delegate: {
89794
91862
  handleSnapshotRequest(request, callback) {
89795
91863
  handleSnapshot(bctx, request).then((buf) => callback(void 0, buf)).catch((err) => {
89796
- log.warn("export-hap: snapshot failed", { meta: { error: errMsg$8(err) } });
89797
- callback(err instanceof Error ? err : new Error(errMsg$8(err)));
91864
+ log.warn("export-hap: snapshot failed", { meta: { error: errMsg$11(err) } });
91865
+ callback(err instanceof Error ? err : new Error(errMsg$11(err)));
89798
91866
  });
89799
91867
  },
89800
91868
  prepareStream(request, callback) {
89801
91869
  prepareStream(request, sessions, bctx).then((resp) => callback(void 0, resp)).catch((err) => {
89802
- log.warn("export-hap: prepareStream failed", { meta: { error: errMsg$8(err) } });
89803
- callback(err instanceof Error ? err : new Error(errMsg$8(err)));
91870
+ log.warn("export-hap: prepareStream failed", { meta: { error: errMsg$11(err) } });
91871
+ callback(err instanceof Error ? err : new Error(errMsg$11(err)));
89804
91872
  });
89805
91873
  },
89806
91874
  handleStreamRequest(request, callback) {
89807
91875
  handleStreamRequest(request, sessions, bctx, advertised).then(() => callback()).catch((err) => {
89808
- log.warn("export-hap: handleStreamRequest failed", { meta: { error: errMsg$8(err) } });
89809
- callback(err instanceof Error ? err : new Error(errMsg$8(err)));
91876
+ log.warn("export-hap: handleStreamRequest failed", { meta: { error: errMsg$11(err) } });
91877
+ callback(err instanceof Error ? err : new Error(errMsg$11(err)));
89810
91878
  });
89811
91879
  }
89812
91880
  },
@@ -89922,7 +91990,7 @@ async function prepareStream(request, sessions, bctx) {
89922
91990
  closeSocket(audioUdp);
89923
91991
  closeSocket(videoLoopUdp);
89924
91992
  closeSocket(audioLoopUdp);
89925
- throw new Error(`export-hap: outbound SrtpSession init failed: ${errMsg$8(err)}`, { cause: err });
91993
+ throw new Error(`export-hap: outbound SrtpSession init failed: ${errMsg$11(err)}`, { cause: err });
89926
91994
  }
89927
91995
  let upstreamAudioSrtp = null;
89928
91996
  try {
@@ -89936,7 +92004,7 @@ async function prepareStream(request, sessions, bctx) {
89936
92004
  profile: import_src.ProtectionProfileAes128CmHmacSha1_80
89937
92005
  });
89938
92006
  } catch (err) {
89939
- bctx.ctx.logger.withTags({ deviceId: bctx.numericDeviceId }).warn("export-hap: SrtpSession init failed (upstream audio decrypt disabled)", { meta: { error: errMsg$8(err) } });
92007
+ bctx.ctx.logger.withTags({ deviceId: bctx.numericDeviceId }).warn("export-hap: SrtpSession init failed (upstream audio decrypt disabled)", { meta: { error: errMsg$11(err) } });
89940
92008
  }
89941
92009
  const videoSsrc = randomSsrc();
89942
92010
  const audioSsrc = randomSsrc();
@@ -90053,7 +92121,7 @@ async function prepareStream(request, sessions, bctx) {
90053
92121
  });
90054
92122
  audioUdp.on("message", (packet, rinfo) => {
90055
92123
  handleIncomingAudioRtp(session, packet, rinfo.address, bctx).catch((err) => {
90056
- bctx.ctx.logger.withTags({ deviceId: bctx.numericDeviceId }).debug("export-hap: incoming-audio handler error (dropped)", { meta: { error: errMsg$8(err) } });
92124
+ bctx.ctx.logger.withTags({ deviceId: bctx.numericDeviceId }).debug("export-hap: incoming-audio handler error (dropped)", { meta: { error: errMsg$11(err) } });
90057
92125
  });
90058
92126
  });
90059
92127
  logLoopbackBuffer(tagLog, request.sessionID, "video", videoLoop.buffer);
@@ -90249,7 +92317,7 @@ function readControllerRtcp(session, leg, packet, log) {
90249
92317
  } catch (err) {
90250
92318
  drop(session, "inbound-rtcp-decrypt-failed");
90251
92319
  storeReceiverReports(session, leg, recordUnreadableRtcp(tally));
90252
- logUnreadableRtcp(session, leg, `decrypt: ${errMsg$8(err)}`, log);
92320
+ logUnreadableRtcp(session, leg, `decrypt: ${errMsg$11(err)}`, log);
90253
92321
  return;
90254
92322
  }
90255
92323
  const outcome = ingestDecryptedRtcp(plaintext, tally);
@@ -91015,7 +93083,7 @@ async function handleIncomingAudioRtp(session, packet, sourceAddress, bctx) {
91015
93083
  session.upstreamRtpDecryptFailures += 1;
91016
93084
  if (session.upstreamRtpDecryptFailures % 100 === 1) log.debug("export-hap: SRTP decrypt failed (rate-limited)", { meta: {
91017
93085
  failures: session.upstreamRtpDecryptFailures,
91018
- error: errMsg$8(err)
93086
+ error: errMsg$11(err)
91019
93087
  } });
91020
93088
  return;
91021
93089
  }
@@ -91029,7 +93097,7 @@ async function handleIncomingAudioRtp(session, packet, sourceAddress, bctx) {
91029
93097
  rtpPayloadType = parsedRtp.header.payloadType;
91030
93098
  } catch (err) {
91031
93099
  drop(session, "upstream-parse-failed");
91032
- log.debug("export-hap: RTP parse failed after decrypt", { meta: { error: errMsg$8(err) } });
93100
+ log.debug("export-hap: RTP parse failed after decrypt", { meta: { error: errMsg$11(err) } });
91033
93101
  return;
91034
93102
  }
91035
93103
  const negotiatedAudioPt = session.lastStartParams?.audioPt;
@@ -91053,7 +93121,7 @@ async function handleIncomingAudioRtp(session, packet, sourceAddress, bctx) {
91053
93121
  if (session.intercomTalkSessionId === null) {
91054
93122
  session.intercomTalkSessionId = "";
91055
93123
  const opened = await openIntercomTalkSession(bctx).catch((err) => {
91056
- log.warn("export-hap: intercom.startTalkSession failed", { meta: { error: errMsg$8(err) } });
93124
+ log.warn("export-hap: intercom.startTalkSession failed", { meta: { error: errMsg$11(err) } });
91057
93125
  return null;
91058
93126
  });
91059
93127
  if (opened) {
@@ -91080,7 +93148,7 @@ async function handleIncomingAudioRtp(session, packet, sourceAddress, bctx) {
91080
93148
  drop(session, "upstream-push-failed");
91081
93149
  log.debug("export-hap: intercom.pushTalkAudio failed (will re-open on next frame)", { meta: {
91082
93150
  sequenceNumber: session.intercomPcmSequence,
91083
- error: errMsg$8(err)
93151
+ error: errMsg$11(err)
91084
93152
  } });
91085
93153
  session.intercomTalkSessionId = null;
91086
93154
  }
@@ -91112,7 +93180,7 @@ async function closeIntercomTalkSession(session, bctx) {
91112
93180
  } catch (err) {
91113
93181
  log.debug("export-hap: intercom.endTalkSession failed (continuing)", { meta: {
91114
93182
  sessionId,
91115
- error: errMsg$8(err)
93183
+ error: errMsg$11(err)
91116
93184
  } });
91117
93185
  }
91118
93186
  }
@@ -91137,7 +93205,7 @@ function closeSocket(udp) {
91137
93205
  function randomSsrc() {
91138
93206
  return Math.floor(Math.random() * 2147483646) + 1 | 0;
91139
93207
  }
91140
- function errMsg$8(err) {
93208
+ function errMsg$11(err) {
91141
93209
  return err instanceof Error ? err.message : String(err);
91142
93210
  }
91143
93211
  //#endregion
@@ -91221,14 +93289,14 @@ async function buildDoorbell(input) {
91221
93289
  }
91222
93290
  log.info("export-hap: doorbell SINGLE_PRESS pushed to HomeKit", { meta: { ...delivery } });
91223
93291
  } catch (err) {
91224
- log.warn("export-hap: ringDoorbell() failed", { meta: { error: errMsg$7(err) } });
93292
+ log.warn("export-hap: ringDoorbell() failed", { meta: { error: errMsg$10(err) } });
91225
93293
  }
91226
93294
  });
91227
93295
  return { async dispose() {
91228
93296
  unsubscribe();
91229
93297
  } };
91230
93298
  }
91231
- function errMsg$7(err) {
93299
+ function errMsg$10(err) {
91232
93300
  return err instanceof Error ? err.message : String(err);
91233
93301
  }
91234
93302
  //#endregion
@@ -91268,13 +93336,15 @@ var RESET_DEBOUNCE_MS = 5e3;
91268
93336
  */
91269
93337
  async function buildMotionSensor(bctx, existing = null) {
91270
93338
  const { ctx, accessory, proxy, numericDeviceId, displayName } = bctx;
91271
- const motionService = existing ?? accessory.addService(import_dist.Service.MotionSensor, hapServiceName([displayName], `Camera ${numericDeviceId}`));
93339
+ const label = hapServiceName([displayName], `Camera ${numericDeviceId}`);
93340
+ const motionService = existing ?? accessory.addService(import_dist.Service.MotionSensor, label);
93341
+ applyServiceLabel(motionService, label);
91272
93342
  motionService.setCharacteristic(import_dist.Characteristic.MotionDetected, false);
91273
93343
  try {
91274
93344
  const detected = await proxy.motion?.isDetected({});
91275
93345
  if (typeof detected === "boolean") motionService.updateCharacteristic(import_dist.Characteristic.MotionDetected, detected);
91276
93346
  } catch (err) {
91277
- ctx.logger.withTags({ deviceId: numericDeviceId }).debug("export-hap: initial motion hydrate failed (non-fatal)", { meta: { error: errMsg$6(err) } });
93347
+ ctx.logger.withTags({ deviceId: numericDeviceId }).debug("export-hap: initial motion hydrate failed (non-fatal)", { meta: { error: errMsg$9(err) } });
91278
93348
  }
91279
93349
  let resetTimer = null;
91280
93350
  const armReset = () => {
@@ -91308,82 +93378,10 @@ async function buildMotionSensor(bctx, existing = null) {
91308
93378
  }
91309
93379
  } };
91310
93380
  }
91311
- function errMsg$6(err) {
93381
+ function errMsg$9(err) {
91312
93382
  return err instanceof Error ? err.message : String(err);
91313
93383
  }
91314
93384
  //#endregion
91315
- //#region src/mappers/builders/service-label.ts
91316
- /**
91317
- * The ONE place a secondary service on the camera accessory gets its label.
91318
- *
91319
- * A "secondary service" here is a Switch or Lightbulb published alongside the
91320
- * camera on the same accessory — the privacy switch, each accessory child
91321
- * (siren, floodlight), each PTZ action. iOS Home renders these as their own
91322
- * controls, and the operator has seen them as "Interruttore 1", "Interruttore
91323
- * 2" through three separate rounds of fixes.
91324
- *
91325
- * ## Why `Name` alone cannot rename anything
91326
- *
91327
- * Two facts about hap-nodejs 2.1.7, both measured against the installed copy
91328
- * rather than reasoned about:
91329
- *
91330
- * 1. `accessory.addService(Type, displayName, subtype)` ALREADY writes
91331
- * `displayName` to `Characteristic.Name` (`Service` constructor). So every
91332
- * round of this bug — including the one that moved the label onto
91333
- * `ConfiguredName` — shipped with `Name` correctly set. "iOS had no name
91334
- * to render" was never true.
91335
- * 2. The mDNS configuration number (`c#`) is a sha1 over
91336
- * `internalHAPRepresentation(false)`, which OMITS characteristic VALUES.
91337
- * Changing the string in `Name` therefore does not bump `c#`, a paired
91338
- * controller gets no signal to re-read `/accessories`, and the name it
91339
- * cached at first enumeration stands forever.
91340
- *
91341
- * `Name` is also declared `pr` only — paired read, no write, no notify. It is
91342
- * the seed a controller seeds its database from once; it is not a channel.
91343
- *
91344
- * ## Why `ConfiguredName`
91345
- *
91346
- * `ConfiguredName` (`000000E3`) is declared `pr | pw | ev` — the only name
91347
- * characteristic a controller may write and may subscribe to. It is what iOS
91348
- * 16+ reads for a service the user can rename, and adding it CHANGES the
91349
- * accessory structure, so `c#` does bump and the controller re-reads.
91350
- *
91351
- * It was removed once because hap-nodejs logged
91352
- *
91353
- * ```
91354
- * Characteristic not in required or optional characteristic section for
91355
- * service Switch. Adding anyway.
91356
- * ```
91357
- *
91358
- * That line is a WARNING, not a rejection: `Service.getCharacteristic` calls
91359
- * `addCharacteristic` unconditionally and only then emits the warning. The
91360
- * characteristic was always present and always published. hap-nodejs'
91361
- * per-service optional lists simply predate `ConfiguredName` being valid on
91362
- * any service.
91363
- *
91364
- * Registering it with {@link Service.addOptionalCharacteristic} first takes
91365
- * the branch above the warning, so the accessory still builds with ZERO
91366
- * characteristic warnings — which is what `service-naming.spec.ts` asserts.
91367
- *
91368
- * ## Scope
91369
- *
91370
- * Switch- and Lightbulb-shaped services only. `Service.MotionSensor` on a
91371
- * camera accessory is not a separately named tile in iOS Home, so giving it a
91372
- * writable name would be a guess, and this module does not guess.
91373
- */
91374
- /**
91375
- * Publish `name` as both the immutable `Name` and the controller-visible
91376
- * `ConfiguredName` of `service`.
91377
- *
91378
- * `name` must already be HAP-valid — build it with `service-names.ts`, which
91379
- * cannot return a string hap-nodejs' `checkName` would warn about.
91380
- */
91381
- function applyServiceLabel(service, name) {
91382
- service.setCharacteristic(import_dist.Characteristic.Name, name);
91383
- if (!service.optionalCharacteristics.some((characteristic) => characteristic.UUID === import_dist.Characteristic.ConfiguredName.UUID)) service.addOptionalCharacteristic(import_dist.Characteristic.ConfiguredName);
91384
- service.setCharacteristic(import_dist.Characteristic.ConfiguredName, name);
91385
- }
91386
- //#endregion
91387
93385
  //#region src/mappers/builders/privacy-switch.ts
91388
93386
  /**
91389
93387
  * Privacy-mask switch builder — turns the camstack `privacy-mask` cap's
@@ -91411,7 +93409,7 @@ async function buildPrivacySwitch(bctx) {
91411
93409
  const status = await proxy.privacyMask?.getStatus({});
91412
93410
  if (status && typeof status.enabled === "boolean") service.updateCharacteristic(import_dist.Characteristic.On, status.enabled);
91413
93411
  } catch (err) {
91414
- log.debug("export-hap: privacy-mask getStatus hydrate failed (non-fatal)", { meta: { error: errMsg$5(err) } });
93412
+ log.debug("export-hap: privacy-mask getStatus hydrate failed (non-fatal)", { meta: { error: errMsg$8(err) } });
91415
93413
  }
91416
93414
  service.getCharacteristic(import_dist.Characteristic.On).onSet(async (value) => {
91417
93415
  const enabled = value === true;
@@ -91420,7 +93418,7 @@ async function buildPrivacySwitch(bctx) {
91420
93418
  } catch (err) {
91421
93419
  log.warn("export-hap: privacy-mask setMask failed", { meta: {
91422
93420
  enabled,
91423
- error: errMsg$5(err)
93421
+ error: errMsg$8(err)
91424
93422
  } });
91425
93423
  }
91426
93424
  });
@@ -91438,7 +93436,7 @@ async function buildPrivacySwitch(bctx) {
91438
93436
  } catch {}
91439
93437
  } };
91440
93438
  }
91441
- function errMsg$5(err) {
93439
+ function errMsg$8(err) {
91442
93440
  return err instanceof Error ? err.message : String(err);
91443
93441
  }
91444
93442
  //#endregion
@@ -91529,7 +93527,7 @@ async function buildPtz(bctx) {
91529
93527
  } catch (err) {
91530
93528
  log.warn("export-hap: ptz.goToPreset failed", { meta: {
91531
93529
  presetId: preset.id,
91532
- error: errMsg$4(err)
93530
+ error: errMsg$7(err)
91533
93531
  } });
91534
93532
  }
91535
93533
  armReset(() => service.updateCharacteristic(import_dist.Characteristic.On, false), MOMENTARY_RESET_MS);
@@ -91547,14 +93545,14 @@ async function buildPtz(bctx) {
91547
93545
  try {
91548
93546
  await proxy.ptz?.stop({});
91549
93547
  } catch (err) {
91550
- log.debug("export-hap: ptz.stop failed (non-fatal)", { meta: { error: errMsg$4(err) } });
93548
+ log.debug("export-hap: ptz.stop failed (non-fatal)", { meta: { error: errMsg$7(err) } });
91551
93549
  }
91552
93550
  service.updateCharacteristic(import_dist.Characteristic.On, false);
91553
93551
  }, options.ptzPulseMs);
91554
93552
  } catch (err) {
91555
93553
  log.warn("export-hap: ptz.continuousMove failed", { meta: {
91556
93554
  dir: dir.label,
91557
- error: errMsg$4(err)
93555
+ error: errMsg$7(err)
91558
93556
  } });
91559
93557
  service.updateCharacteristic(import_dist.Characteristic.On, false);
91560
93558
  }
@@ -91577,7 +93575,7 @@ async function readPresets(bctx) {
91577
93575
  name: p.name
91578
93576
  }));
91579
93577
  } catch (err) {
91580
- ctx.logger.withTags({ deviceId: numericDeviceId }).debug("export-hap: ptz.getPresets failed (non-fatal)", { meta: { error: errMsg$4(err) } });
93578
+ ctx.logger.withTags({ deviceId: numericDeviceId }).debug("export-hap: ptz.getPresets failed (non-fatal)", { meta: { error: errMsg$7(err) } });
91581
93579
  return [];
91582
93580
  }
91583
93581
  }
@@ -91592,7 +93590,7 @@ async function tryBuildAutotrack(bctx) {
91592
93590
  const status = await proxy.ptzAutotrack.getStatus({});
91593
93591
  if (status && typeof status.enabled === "boolean") service.updateCharacteristic(import_dist.Characteristic.On, status.enabled);
91594
93592
  } catch (err) {
91595
- log.debug("export-hap: ptzAutotrack.getStatus failed (non-fatal)", { meta: { error: errMsg$4(err) } });
93593
+ log.debug("export-hap: ptzAutotrack.getStatus failed (non-fatal)", { meta: { error: errMsg$7(err) } });
91596
93594
  }
91597
93595
  service.getCharacteristic(import_dist.Characteristic.On).onSet(async (value) => {
91598
93596
  const enabled = value === true;
@@ -91601,13 +93599,13 @@ async function tryBuildAutotrack(bctx) {
91601
93599
  } catch (err) {
91602
93600
  log.warn("export-hap: ptzAutotrack.setEnabled failed", { meta: {
91603
93601
  enabled,
91604
- error: errMsg$4(err)
93602
+ error: errMsg$7(err)
91605
93603
  } });
91606
93604
  }
91607
93605
  });
91608
93606
  return { async dispose() {} };
91609
93607
  }
91610
- function errMsg$4(err) {
93608
+ function errMsg$7(err) {
91611
93609
  return err instanceof Error ? err.message : String(err);
91612
93610
  }
91613
93611
  //#endregion
@@ -92638,13 +94636,13 @@ async function buildChildSwitch(bctx, subtype, deviceType) {
92638
94636
  const switchStatus = await proxy.switch?.getStatus({});
92639
94637
  if (switchStatus && typeof switchStatus.on === "boolean") service.updateCharacteristic(import_dist.Characteristic.On, switchStatus.on);
92640
94638
  } catch (err) {
92641
- log.debug("export-hap: child switch.getStatus failed (non-fatal)", { meta: { error: errMsg$3(err) } });
94639
+ log.debug("export-hap: child switch.getStatus failed (non-fatal)", { meta: { error: errMsg$6(err) } });
92642
94640
  }
92643
94641
  if (useLightbulb) try {
92644
94642
  const status = await proxy.brightness?.getStatus({});
92645
94643
  if (status && typeof status.percentage === "number") service.updateCharacteristic(import_dist.Characteristic.Brightness, status.percentage);
92646
94644
  } catch (err) {
92647
- log.debug("export-hap: child brightness.getStatus failed (non-fatal)", { meta: { error: errMsg$3(err) } });
94645
+ log.debug("export-hap: child brightness.getStatus failed (non-fatal)", { meta: { error: errMsg$6(err) } });
92648
94646
  }
92649
94647
  if (hasSwitch) service.getCharacteristic(import_dist.Characteristic.On).onSet(async (value) => {
92650
94648
  const on = value === true;
@@ -92653,7 +94651,7 @@ async function buildChildSwitch(bctx, subtype, deviceType) {
92653
94651
  } catch (err) {
92654
94652
  log.warn("export-hap: child switch.setState failed", { meta: {
92655
94653
  on,
92656
- error: errMsg$3(err)
94654
+ error: errMsg$6(err)
92657
94655
  } });
92658
94656
  }
92659
94657
  });
@@ -92665,7 +94663,7 @@ async function buildChildSwitch(bctx, subtype, deviceType) {
92665
94663
  } catch (err) {
92666
94664
  log.warn("export-hap: child brightness.setBrightness failed", { meta: {
92667
94665
  percentage,
92668
- error: errMsg$3(err)
94666
+ error: errMsg$6(err)
92669
94667
  } });
92670
94668
  }
92671
94669
  });
@@ -92688,7 +94686,7 @@ async function buildChildSwitch(bctx, subtype, deviceType) {
92688
94686
  } catch {}
92689
94687
  } };
92690
94688
  }
92691
- function errMsg$3(err) {
94689
+ function errMsg$6(err) {
92692
94690
  return err instanceof Error ? err.message : String(err);
92693
94691
  }
92694
94692
  //#endregion
@@ -92715,7 +94713,7 @@ async function buildChildServicesFor(input) {
92715
94713
  for (const h of handles) try {
92716
94714
  await h.dispose();
92717
94715
  } catch (err) {
92718
- ctx.logger.withTags({ deviceId: parentNumericId }).debug("export-hap: child service dispose failed (continuing)", { meta: { error: errMsg$2(err) } });
94716
+ ctx.logger.withTags({ deviceId: parentNumericId }).debug("export-hap: child service dispose failed (continuing)", { meta: { error: errMsg$5(err) } });
92719
94717
  }
92720
94718
  } };
92721
94719
  }
@@ -92731,7 +94729,7 @@ async function listChildren(ctx, parentNumericId) {
92731
94729
  features: Array.isArray(c.features) ? c.features.filter((f) => typeof f === "string") : []
92732
94730
  }));
92733
94731
  } catch (err) {
92734
- ctx.logger.withTags({ deviceId: parentNumericId }).debug("export-hap: deviceManager.getChildren failed (non-fatal)", { meta: { error: errMsg$2(err) } });
94732
+ ctx.logger.withTags({ deviceId: parentNumericId }).debug("export-hap: deviceManager.getChildren failed (non-fatal)", { meta: { error: errMsg$5(err) } });
92735
94733
  return [];
92736
94734
  }
92737
94735
  }
@@ -92747,10 +94745,76 @@ function asDeviceType(raw) {
92747
94745
  for (const value of Object.values(DeviceType)) if (value === lower) return value;
92748
94746
  return DeviceType.Generic;
92749
94747
  }
92750
- function errMsg$2(err) {
94748
+ function errMsg$5(err) {
92751
94749
  return err instanceof Error ? err.message : String(err);
92752
94750
  }
92753
94751
  //#endregion
94752
+ //#region src/mappers/kind.ts
94753
+ /**
94754
+ * Which orchestrator a device gets, which device types the picker offers, and
94755
+ * what a device's HomeKit accessory is called on the wire.
94756
+ *
94757
+ * Separate from `index.ts` (the factory registry) only so the orchestrators can
94758
+ * import the uuid convention without importing the registry that imports them.
94759
+ */
94760
+ var SUPPORTED_MAPPER_KINDS = ["camera", "generic"];
94761
+ /**
94762
+ * The device types the Export picker offers, and the answer to the cap's
94763
+ * `listSupportedDeviceKinds`.
94764
+ *
94765
+ * A UI FILTER, nothing more. Whether a device actually exports anything is
94766
+ * decided by its capabilities (`rowsForCaps` in the capability table) — the
94767
+ * doctrine the HA exporter writes down, and the thing that confined the
94768
+ * previous exporter to cameras when it was ignored.
94769
+ *
94770
+ * Tier A: the types whose HomeKit services exist today and need no
94771
+ * feature-dependent shape. `cover`, `climate`, `fan`, `valve`, `humidifier`,
94772
+ * `water-heater`, `alarm-panel`, `button` and `media-player` are deliberately
94773
+ * absent — each needs a service whose semantics the table cannot yet honour,
94774
+ * and offering the tab before the mapping exists is how an operator gets an
94775
+ * accessory that pairs and does nothing.
94776
+ */
94777
+ var HAP_EXPORTABLE_DEVICE_TYPES = [
94778
+ DeviceType.Camera,
94779
+ DeviceType.Light,
94780
+ DeviceType.Switch,
94781
+ DeviceType.Siren,
94782
+ DeviceType.Sensor,
94783
+ DeviceType.Lock,
94784
+ DeviceType.Presence,
94785
+ DeviceType.Generic
94786
+ ];
94787
+ /**
94788
+ * Resolve the orchestrator for a device from its TYPE.
94789
+ *
94790
+ * `null` means "no Export tab": the picker must not offer a type no
94791
+ * orchestrator can build. An UNKNOWN type (the device-manager read failed) maps
94792
+ * to `camera` — that is what every entry persisted before this function existed
94793
+ * carries, and a transient API failure must never re-shape an exposed camera.
94794
+ */
94795
+ function pickMapperKind(deviceType) {
94796
+ if (deviceType === null || deviceType === void 0 || deviceType.length === 0) return "camera";
94797
+ if (deviceType === DeviceType.Camera) return "camera";
94798
+ return HAP_EXPORTABLE_DEVICE_TYPES.find((type) => type === deviceType) === void 0 ? null : "generic";
94799
+ }
94800
+ /**
94801
+ * The deterministic HAP accessory UUID for a device.
94802
+ *
94803
+ * One function, because three call sites depend on the answer agreeing: the
94804
+ * orchestrator that builds the accessory, `unexposeDevice` (which wipes
94805
+ * hap-nodejs' pairing blobs by uuid, on a device whose mapper is already gone)
94806
+ * and the export sync state that records it.
94807
+ *
94808
+ * The camera namespace is FROZEN — every camera paired to date derives its MAC
94809
+ * from `sha256("camstack:hap:" + uuid)` of this exact string, and changing it
94810
+ * would make every paired camera a stranger. Generic devices get their own
94811
+ * namespace so a switch and a camera that happen to share a device id are not
94812
+ * the same HomeKit accessory.
94813
+ */
94814
+ function accessoryUuidFor(kind, deviceId) {
94815
+ return import_dist.uuid.generate(kind === "camera" ? `camstack:camera:${deviceId}` : `camstack:device:${deviceId}`);
94816
+ }
94817
+ //#endregion
92754
94818
  //#region src/mappers/camera-accessory.ts
92755
94819
  /**
92756
94820
  * Camera accessory orchestrator — given a camstack deviceId, builds one
@@ -92783,9 +94847,9 @@ async function buildCameraAccessory(input) {
92783
94847
  const capNames = new Set(proxy.binding?.entries.map((e) => e.capName) ?? []);
92784
94848
  const isDoorbell = capNames.has("doorbell");
92785
94849
  const category = isDoorbell ? import_dist.Categories.VIDEO_DOORBELL : import_dist.Categories.IP_CAMERA;
92786
- const accessory = new import_dist.Accessory(displayName, import_dist.uuid.generate(`camstack:camera:${numericId}`));
94850
+ const accessory = new import_dist.Accessory(displayName, accessoryUuidFor("camera", numericId));
92787
94851
  accessory.category = category;
92788
- await populateAccessoryInfo(accessory, proxy, displayName);
94852
+ await populateAccessoryInfo(accessory, proxy, displayName, "Camera");
92789
94853
  const bctx = {
92790
94854
  ctx,
92791
94855
  accessory,
@@ -92843,55 +94907,462 @@ async function buildCameraAccessory(input) {
92843
94907
  for (const h of handles) try {
92844
94908
  await h.dispose();
92845
94909
  } catch (err) {
92846
- log.debug("export-hap: builder dispose failed (continuing)", { meta: { error: errMsg$1(err) } });
94910
+ log.debug("export-hap: builder dispose failed (continuing)", { meta: { error: errMsg$4(err) } });
92847
94911
  }
92848
94912
  try {
92849
94913
  await streams.dispose();
92850
94914
  } catch (err) {
92851
- log.debug("export-hap: streams dispose failed", { meta: { error: errMsg$1(err) } });
94915
+ log.debug("export-hap: streams dispose failed", { meta: { error: errMsg$4(err) } });
92852
94916
  }
92853
94917
  }
92854
94918
  };
92855
94919
  }
92856
- async function populateAccessoryInfo(accessory, proxy, displayName) {
92857
- const info = accessory.getService(import_dist.Service.AccessoryInformation);
92858
- if (!info) return;
94920
+ function errMsg$4(err) {
94921
+ return err instanceof Error ? err.message : String(err);
94922
+ }
94923
+ //#endregion
94924
+ //#region src/mappers/builders/generic/lock.ts
94925
+ /**
94926
+ * `lock-control` → `Service.LockMechanism`.
94927
+ *
94928
+ * Not a sensor row: HomeKit models a lock as two characteristics, a CURRENT
94929
+ * state the accessory owns and a TARGET state the controller writes, and the
94930
+ * cap's five states do not map onto either one alone.
94931
+ *
94932
+ * ```
94933
+ * cap state LockCurrentState LockTargetState
94934
+ * locked SECURED SECURED
94935
+ * unlocked UNSECURED UNSECURED
94936
+ * locking UNKNOWN SECURED (in flight — do not claim SECURED)
94937
+ * unlocking UNKNOWN UNSECURED
94938
+ * jammed JAMMED unchanged (the controller's intent stands)
94939
+ * ```
94940
+ *
94941
+ * Reporting SECURED while the bolt is still moving is the failure worth naming:
94942
+ * iOS renders the lock as closed, the operator walks away, and the motor stalls
94943
+ * behind them. `UNKNOWN` is the honest answer for an in-flight transition.
94944
+ *
94945
+ * `open` (the cap's third method, for locks with a latch/buzzer) has no HomeKit
94946
+ * counterpart on this service and is deliberately not wired — a Switch that
94947
+ * silently buzzes a door open would be a second knob nobody asked for.
94948
+ */
94949
+ var SUBTYPE = "lock-control";
94950
+ var readLock = reader(LockControlStatusSchema.pick({ state: true }), (status) => {
94951
+ 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;
94952
+ 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;
94953
+ return [{
94954
+ characteristic: import_dist.Characteristic.LockCurrentState,
94955
+ value: current
94956
+ }, ...target === null ? [] : [{
94957
+ characteristic: import_dist.Characteristic.LockTargetState,
94958
+ value: target
94959
+ }]];
94960
+ });
94961
+ async function buildLockMechanism(bctx) {
94962
+ const { ctx, accessory, proxy, numericDeviceId, displayName } = bctx;
94963
+ const log = ctx.logger.withTags({ deviceId: numericDeviceId });
94964
+ const label = hapServiceName([displayName], `Lock ${numericDeviceId}`);
94965
+ const service = accessory.addService(import_dist.Service.LockMechanism, label, SUBTYPE);
94966
+ applyServiceLabel(service, label);
94967
+ const apply = (status, source) => {
94968
+ const updates = readLock(status);
94969
+ if (updates.length === 0) {
94970
+ log.debug("export-hap: lock status did not match the cap schema — no update", { meta: { source } });
94971
+ return;
94972
+ }
94973
+ applyUpdates(service, updates);
94974
+ };
92859
94975
  try {
92860
- const device = await proxy.deviceManager?.getDevice({});
92861
- const metadata = device?.metadata ?? null;
92862
- info.setCharacteristic(import_dist.Characteristic.Name, device?.name ?? displayName);
92863
- info.setCharacteristic(import_dist.Characteristic.Manufacturer, stringOr(metadata?.manufacturer, "CamStack"));
92864
- info.setCharacteristic(import_dist.Characteristic.Model, stringOr(metadata?.model, "Camera"));
92865
- info.setCharacteristic(import_dist.Characteristic.FirmwareRevision, stringOr(metadata?.firmware, "0.0.0"));
92866
- info.setCharacteristic(import_dist.Characteristic.SerialNumber, stringOr(metadata?.sn, `camstack-${proxy.deviceId}`));
92867
- } catch {}
94976
+ const status = await proxy.lockControl?.getStatus({});
94977
+ if (status !== void 0 && status !== null) apply(status, "getStatus");
94978
+ } catch (err) {
94979
+ log.debug("export-hap: lock getStatus hydrate failed (non-fatal)", { meta: { error: errMsg$3(err) } });
94980
+ }
94981
+ service.getCharacteristic(import_dist.Characteristic.LockTargetState).onSet(async (value) => {
94982
+ const secure = value === import_dist.Characteristic.LockTargetState.SECURED;
94983
+ try {
94984
+ if (secure) await proxy.lockControl?.lock({});
94985
+ else await proxy.lockControl?.unlock({});
94986
+ } catch (err) {
94987
+ log.warn("export-hap: lock command failed", { meta: {
94988
+ secure,
94989
+ error: errMsg$3(err)
94990
+ } });
94991
+ }
94992
+ });
94993
+ const unsubscribe = proxy.state.lockControl?.subscribe((value) => {
94994
+ if (value === void 0 || value === null) return;
94995
+ apply(value, "slice");
94996
+ }) ?? null;
94997
+ return { async dispose() {
94998
+ try {
94999
+ unsubscribe?.();
95000
+ } catch {}
95001
+ } };
92868
95002
  }
92869
- function stringOr(value, fallback) {
92870
- return typeof value === "string" && value.length > 0 ? value : fallback;
95003
+ function errMsg$3(err) {
95004
+ return err instanceof Error ? err.message : String(err);
95005
+ }
95006
+ //#endregion
95007
+ //#region src/mappers/builders/generic/sensor-service.ts
95008
+ async function buildSensorService(input) {
95009
+ const { bctx, spec, name, subtype } = input;
95010
+ const { ctx, accessory, proxy, numericDeviceId } = bctx;
95011
+ const log = ctx.logger.withTags({ deviceId: numericDeviceId });
95012
+ const label = hapServiceName([name], spec.label);
95013
+ const service = spec.addService(accessory, label, subtype);
95014
+ applyServiceLabel(service, label);
95015
+ const apply = (status, source) => {
95016
+ const updates = spec.read(status);
95017
+ if (updates.length === 0) {
95018
+ log.debug("export-hap: sensor status did not match the cap schema — no update", { meta: {
95019
+ subtype,
95020
+ source
95021
+ } });
95022
+ return;
95023
+ }
95024
+ applyUpdates(service, updates);
95025
+ };
95026
+ try {
95027
+ const status = await spec.getStatus(proxy);
95028
+ if (status !== void 0 && status !== null) apply(status, "getStatus");
95029
+ } catch (err) {
95030
+ log.debug("export-hap: sensor getStatus hydrate failed (non-fatal)", { meta: {
95031
+ subtype,
95032
+ error: errMsg$2(err)
95033
+ } });
95034
+ }
95035
+ const unsubscribe = spec.subscribe(proxy, (value) => {
95036
+ if (value === void 0 || value === null) return;
95037
+ apply(value, "slice");
95038
+ });
95039
+ return { async dispose() {
95040
+ try {
95041
+ unsubscribe?.();
95042
+ } catch {}
95043
+ } };
95044
+ }
95045
+ function errMsg$2(err) {
95046
+ return err instanceof Error ? err.message : String(err);
95047
+ }
95048
+ //#endregion
95049
+ //#region src/mappers/builders/generic/cap-service-table.ts
95050
+ /**
95051
+ * The capability→HomeKit-service TABLE for non-camera devices.
95052
+ *
95053
+ * This is the whole coverage decision for the generic export path, and it is
95054
+ * deliberately a table rather than a `switch` on `DeviceType`. The doctrine is
95055
+ * the one the Home Assistant exporter already writes down: *coverage is decided
95056
+ * by a device's capabilities, not its type — restricting the picker by type is
95057
+ * what confined the previous exporter to cameras.* A `DeviceType` here is only
95058
+ * ever a UI filter (`HAP_EXPORTABLE_DEVICE_TYPES`) or an icon
95059
+ * (`ACCESSORY_CATEGORY_BY_TYPE`); it never decides whether something exports.
95060
+ *
95061
+ * The shape mirrors `CAP_ENTITY_MAP` in
95062
+ * `packages/addon-provider-homeassistant/src/ha-export/entity-catalog.ts` so
95063
+ * the two can be unified later — one row per capability, keyed by the cap name
95064
+ * exactly as it appears in `proxy.binding.entries[].capName`.
95065
+ *
95066
+ * ## Reading a status
95067
+ *
95068
+ * Every row parses the cap's OWN Zod status schema rather than duck-typing the
95069
+ * payload, and it `.pick()`s only the fields it reads: a provider that omits a
95070
+ * timestamp must not silence a sensor, and a provider that sends the wrong
95071
+ * shape must not write a garbage characteristic. A parse that fails yields no
95072
+ * updates, and the builder that owns the service logs the drop — silence reads
95073
+ * as "the sensor never changed".
95074
+ *
95075
+ * ## Tier
95076
+ *
95077
+ * Tier A only: the caps whose HomeKit service exists today and needs no
95078
+ * feature-dependent shape (`cover` needs `cover-positionable`, `climate-control`
95079
+ * needs the dual-setpoint split, `alarm-panel` must survive a refused `arm`).
95080
+ * Those are a separate task; adding a row here must never mean adding a service
95081
+ * whose semantics this file cannot fully honour.
95082
+ */
95083
+ /** HomeKit's floor for `CurrentAmbientLightLevel`; 0 lux is out of range. */
95084
+ var MIN_LUX = 1e-4;
95085
+ var SENSOR_SPECS = {
95086
+ contact: {
95087
+ label: "Contact",
95088
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.ContactSensor, name, subtype),
95089
+ getStatus: (proxy) => proxy.contact?.getStatus({}),
95090
+ subscribe: (proxy, onValue) => proxy.state.contact?.subscribe(onValue) ?? null,
95091
+ read: reader(ContactStatusSchema.pick({ entryOpen: true }), (status) => [{
95092
+ characteristic: import_dist.Characteristic.ContactSensorState,
95093
+ value: status.entryOpen ? import_dist.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : import_dist.Characteristic.ContactSensorState.CONTACT_DETECTED
95094
+ }])
95095
+ },
95096
+ motion: {
95097
+ label: "Motion",
95098
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.MotionSensor, name, subtype),
95099
+ getStatus: (proxy) => proxy.motion?.getStatus({}),
95100
+ subscribe: (proxy, onValue) => proxy.state.motion?.subscribe(onValue) ?? null,
95101
+ read: reader(MotionStatusSchema.pick({ detected: true }), (status) => [{
95102
+ characteristic: import_dist.Characteristic.MotionDetected,
95103
+ value: status.detected
95104
+ }])
95105
+ },
95106
+ presence: {
95107
+ label: "Presence",
95108
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.OccupancySensor, name, subtype),
95109
+ getStatus: (proxy) => proxy.presence?.getStatus({}),
95110
+ subscribe: (proxy, onValue) => proxy.state.presence?.subscribe(onValue) ?? null,
95111
+ read: reader(PresenceStatusSchema.pick({ state: true }), (status) => [{
95112
+ characteristic: import_dist.Characteristic.OccupancyDetected,
95113
+ value: status.state === "home" ? import_dist.Characteristic.OccupancyDetected.OCCUPANCY_DETECTED : import_dist.Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED
95114
+ }])
95115
+ },
95116
+ smoke: {
95117
+ label: "Smoke",
95118
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.SmokeSensor, name, subtype),
95119
+ getStatus: (proxy) => proxy.smoke?.getStatus({}),
95120
+ subscribe: (proxy, onValue) => proxy.state.smoke?.subscribe(onValue) ?? null,
95121
+ read: reader(SmokeStatusSchema.pick({ detected: true }), (status) => [{
95122
+ characteristic: import_dist.Characteristic.SmokeDetected,
95123
+ value: status.detected ? import_dist.Characteristic.SmokeDetected.SMOKE_DETECTED : import_dist.Characteristic.SmokeDetected.SMOKE_NOT_DETECTED
95124
+ }])
95125
+ },
95126
+ "carbon-monoxide": {
95127
+ label: "Carbon monoxide",
95128
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.CarbonMonoxideSensor, name, subtype),
95129
+ getStatus: (proxy) => proxy.carbonMonoxide?.getStatus({}),
95130
+ subscribe: (proxy, onValue) => proxy.state.carbonMonoxide?.subscribe(onValue) ?? null,
95131
+ read: reader(CarbonMonoxideStatusSchema.pick({ detected: true }), (status) => [{
95132
+ characteristic: import_dist.Characteristic.CarbonMonoxideDetected,
95133
+ value: status.detected ? import_dist.Characteristic.CarbonMonoxideDetected.CO_LEVELS_ABNORMAL : import_dist.Characteristic.CarbonMonoxideDetected.CO_LEVELS_NORMAL
95134
+ }])
95135
+ },
95136
+ flood: {
95137
+ label: "Leak",
95138
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.LeakSensor, name, subtype),
95139
+ getStatus: (proxy) => proxy.flood?.getStatus({}),
95140
+ subscribe: (proxy, onValue) => proxy.state.flood?.subscribe(onValue) ?? null,
95141
+ read: reader(FloodStatusSchema.pick({ flooded: true }), (status) => [{
95142
+ characteristic: import_dist.Characteristic.LeakDetected,
95143
+ value: status.flooded ? import_dist.Characteristic.LeakDetected.LEAK_DETECTED : import_dist.Characteristic.LeakDetected.LEAK_NOT_DETECTED
95144
+ }])
95145
+ },
95146
+ "temperature-sensor": {
95147
+ label: "Temperature",
95148
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.TemperatureSensor, name, subtype),
95149
+ getStatus: (proxy) => proxy.temperatureSensor?.getStatus({}),
95150
+ subscribe: (proxy, onValue) => proxy.state.temperatureSensor?.subscribe(onValue) ?? null,
95151
+ read: reader(TemperatureSensorStatusSchema.pick({ celsius: true }), (status) => [{
95152
+ characteristic: import_dist.Characteristic.CurrentTemperature,
95153
+ value: status.celsius
95154
+ }])
95155
+ },
95156
+ "humidity-sensor": {
95157
+ label: "Humidity",
95158
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.HumiditySensor, name, subtype),
95159
+ getStatus: (proxy) => proxy.humiditySensor?.getStatus({}),
95160
+ subscribe: (proxy, onValue) => proxy.state.humiditySensor?.subscribe(onValue) ?? null,
95161
+ read: reader(HumiditySensorStatusSchema.pick({ percent: true }), (status) => [{
95162
+ characteristic: import_dist.Characteristic.CurrentRelativeHumidity,
95163
+ value: status.percent
95164
+ }])
95165
+ },
95166
+ "ambient-light-sensor": {
95167
+ label: "Light level",
95168
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.LightSensor, name, subtype),
95169
+ getStatus: (proxy) => proxy.ambientLightSensor?.getStatus({}),
95170
+ subscribe: (proxy, onValue) => proxy.state.ambientLightSensor?.subscribe(onValue) ?? null,
95171
+ read: reader(AmbientLightSensorStatusSchema.pick({ lux: true }), (status) => [{
95172
+ characteristic: import_dist.Characteristic.CurrentAmbientLightLevel,
95173
+ value: Math.max(MIN_LUX, status.lux)
95174
+ }])
95175
+ }
95176
+ };
95177
+ /**
95178
+ * The `battery` row reads like a sensor but writes three characteristics from
95179
+ * one status, and the camera path publishes the SAME service from the same cap.
95180
+ * The derivation therefore lives once, in `battery.ts`, and both callers read
95181
+ * it from there.
95182
+ */
95183
+ var BATTERY_SPEC = {
95184
+ label: "Battery",
95185
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.Battery, name, subtype),
95186
+ getStatus: (proxy) => proxy.battery?.getStatus({}),
95187
+ subscribe: (proxy, onValue) => proxy.state.battery?.subscribe(onValue) ?? null,
95188
+ read: batteryCharacteristicUpdates
95189
+ };
95190
+ function sensorRow(capName, spec) {
95191
+ return {
95192
+ caps: [capName],
95193
+ label: spec.label,
95194
+ build: ({ bctx, name }) => buildSensorService({
95195
+ bctx,
95196
+ spec,
95197
+ name,
95198
+ subtype: capName
95199
+ })
95200
+ };
95201
+ }
95202
+ /**
95203
+ * THE table. Order matters only for one thing: the first row that matches a
95204
+ * device decides the accessory's category when the device's own type does not.
95205
+ */
95206
+ var HAP_CAP_SERVICES = [
95207
+ {
95208
+ caps: ["switch", "brightness"],
95209
+ label: "Power",
95210
+ build: ({ bctx, name, deviceType }) => buildChildSwitch({
95211
+ ...bctx,
95212
+ displayName: name
95213
+ }, "switch", deviceType)
95214
+ },
95215
+ {
95216
+ caps: ["lock-control"],
95217
+ label: "Lock",
95218
+ build: ({ bctx, name }) => buildLockMechanism({
95219
+ ...bctx,
95220
+ displayName: name
95221
+ })
95222
+ },
95223
+ {
95224
+ caps: ["battery"],
95225
+ label: BATTERY_SPEC.label,
95226
+ build: ({ bctx, name }) => buildSensorService({
95227
+ bctx,
95228
+ spec: BATTERY_SPEC,
95229
+ name,
95230
+ subtype: "battery"
95231
+ })
95232
+ },
95233
+ ...Object.entries(SENSOR_SPECS).map(([capName, spec]) => sensorRow(capName, spec))
95234
+ ];
95235
+ /**
95236
+ * The rows a device's bound capabilities select, in table order.
95237
+ *
95238
+ * A row matches when ANY of its caps is bound: a lamp with `switch` but no
95239
+ * `brightness` is still a Lightbulb-shaped row, and the builder degrades on its
95240
+ * own.
95241
+ */
95242
+ function rowsForCaps(capNames) {
95243
+ return HAP_CAP_SERVICES.filter((row) => row.caps.some((cap) => capNames.has(cap)));
95244
+ }
95245
+ /**
95246
+ * The HomeKit accessory category per camstack device type — the icon iOS Home
95247
+ * draws and nothing else. Absent = `OTHER`, which is a valid accessory that
95248
+ * simply gets the generic tile.
95249
+ *
95250
+ * This is NOT a coverage decision. `HAP_EXPORTABLE_DEVICE_TYPES` (in
95251
+ * `mappers/index.ts`) filters the picker; whether a device exports anything is
95252
+ * decided by `rowsForCaps`.
95253
+ */
95254
+ var ACCESSORY_CATEGORY_BY_TYPE = {
95255
+ [DeviceType.Switch]: import_dist.Categories.SWITCH,
95256
+ [DeviceType.Light]: import_dist.Categories.LIGHTBULB,
95257
+ [DeviceType.Siren]: import_dist.Categories.SWITCH,
95258
+ [DeviceType.Lock]: import_dist.Categories.DOOR_LOCK,
95259
+ [DeviceType.Sensor]: import_dist.Categories.SENSOR,
95260
+ [DeviceType.Presence]: import_dist.Categories.SENSOR
95261
+ };
95262
+ function accessoryCategoryFor(deviceType) {
95263
+ return ACCESSORY_CATEGORY_BY_TYPE[deviceType] ?? import_dist.Categories.OTHER;
95264
+ }
95265
+ //#endregion
95266
+ //#region src/mappers/generic-accessory.ts
95267
+ /**
95268
+ * Generic accessory orchestrator — every camstack device that is NOT a camera.
95269
+ *
95270
+ * Same three steps as the camera orchestrator, and deliberately nothing more:
95271
+ * 1. resolve the typed `DeviceProxy`,
95272
+ * 2. select rows from the capability→service table with the device's BOUND
95273
+ * capabilities (never its type — see `builders/generic/cap-service-table.ts`),
95274
+ * 3. let each row add its service to one `Accessory`.
95275
+ *
95276
+ * It publishes standalone, like the camera path: one accessory, one mDNS
95277
+ * advertisement, one pairing, the shared setup code. This addon is not a
95278
+ * bridge — that decision predates this file and is not re-litigated here.
95279
+ *
95280
+ * ## It REFUSES rather than publishing an empty tile
95281
+ *
95282
+ * A device whose capabilities select no row throws. The alternative is an
95283
+ * accessory carrying nothing but `AccessoryInformation`: it pairs, it appears
95284
+ * in the Home app, it does nothing, and the operator has no way to tell that
95285
+ * from a broken integration. The Export tab is gated on the same question
95286
+ * (`hap-export.addon.ts`), so reaching this throw means the device changed
95287
+ * shape between the tab rendering and the toggle landing.
95288
+ */
95289
+ async function buildGenericAccessory(input) {
95290
+ const { ctx, deviceId, displayName, options } = input;
95291
+ const numericId = Number.parseInt(deviceId, 10);
95292
+ if (!Number.isFinite(numericId)) throw new Error(`export-hap: cannot map device '${deviceId}' — id is not numeric`);
95293
+ const log = ctx.logger.withTags({ deviceId: numericId });
95294
+ const proxy = await ctx.fetchDevice(numericId);
95295
+ const capNames = new Set(proxy.binding?.entries.map((e) => e.capName) ?? []);
95296
+ const rows = rowsForCaps(capNames);
95297
+ if (rows.length === 0) throw new Error(`export-hap: device ${numericId} carries no capability HomeKit can export (bound: ${[...capNames].toSorted().join(", ") || "none"})`);
95298
+ const deviceType = await resolveDeviceType(proxy);
95299
+ const accessory = new import_dist.Accessory(displayName, accessoryUuidFor("generic", numericId));
95300
+ accessory.category = accessoryCategoryFor(deviceType);
95301
+ await populateAccessoryInfo(accessory, proxy, displayName, "Accessory");
95302
+ const bctx = {
95303
+ ctx,
95304
+ accessory,
95305
+ proxy,
95306
+ numericDeviceId: numericId,
95307
+ displayName,
95308
+ options
95309
+ };
95310
+ const handles = [];
95311
+ for (const row of rows) {
95312
+ const name = rows.length === 1 ? displayName : row.label;
95313
+ handles.push(await row.build({
95314
+ bctx,
95315
+ name,
95316
+ deviceType
95317
+ }));
95318
+ }
95319
+ log.info("export-hap: built generic accessory", { meta: {
95320
+ deviceType,
95321
+ services: rows.map((row) => row.caps[0]).join(",")
95322
+ } });
95323
+ return {
95324
+ accessory,
95325
+ accessories: [accessory],
95326
+ async dispose() {
95327
+ for (const handle of handles) try {
95328
+ await handle.dispose();
95329
+ } catch (err) {
95330
+ log.debug("export-hap: generic builder dispose failed (continuing)", { meta: { error: errMsg$1(err) } });
95331
+ }
95332
+ }
95333
+ };
95334
+ }
95335
+ /**
95336
+ * The device's own type, or `Generic`.
95337
+ *
95338
+ * Used ONLY where one capability has two HomeKit shapes — a siren's
95339
+ * `brightness` is alarm volume, not luminosity — and for the accessory's icon.
95340
+ * `Generic` is the safe reading: it is the shape `child-switch.ts` already
95341
+ * treats as "a lamp if it dims, a switch otherwise".
95342
+ */
95343
+ async function resolveDeviceType(proxy) {
95344
+ try {
95345
+ const device = await proxy.deviceManager?.getDevice({});
95346
+ const raw = typeof device?.type === "string" ? device.type.toLowerCase() : "";
95347
+ return Object.values(DeviceType).find((value) => value === raw) ?? DeviceType.Generic;
95348
+ } catch {
95349
+ return DeviceType.Generic;
95350
+ }
92871
95351
  }
92872
95352
  function errMsg$1(err) {
92873
95353
  return err instanceof Error ? err.message : String(err);
92874
95354
  }
92875
95355
  //#endregion
92876
95356
  //#region src/mappers/index.ts
92877
- var SUPPORTED_MAPPER_KINDS = ["camera"];
92878
- var REGISTRY = { camera: buildCameraAccessory };
95357
+ var REGISTRY = {
95358
+ camera: buildCameraAccessory,
95359
+ generic: buildGenericAccessory
95360
+ };
92879
95361
  function getMapperFactory(kind) {
92880
95362
  const factory = REGISTRY[kind];
92881
95363
  if (!factory) throw new Error(`export-hap: no mapper registered for kind '${kind}'`);
92882
95364
  return factory;
92883
95365
  }
92884
- /**
92885
- * Resolve the best-fit mapper kind. With Round 2, the operator just
92886
- * picks a camera and the orchestrator does the rest — the single
92887
- * `camera` kind is returned unconditionally. We keep the function
92888
- * signature for backwards-compat with the addon's existing
92889
- * `exposeDevice` flow and to leave room for future device types (NVR,
92890
- * climate sensor, ...).
92891
- */
92892
- function pickMapperKind(_capabilities) {
92893
- return "camera";
92894
- }
92895
95366
  //#endregion
92896
95367
  //#region src/mappers/builders/stream-hwaccel-memo.ts
92897
95368
  /**
@@ -93029,8 +95500,8 @@ function syncStateToJson(map) {
93029
95500
  //#endregion
93030
95501
  //#region src/hap-export.addon.ts
93031
95502
  /**
93032
- * HomeKit (HAP) export addon — publishes a single HAP bridge process
93033
- * that exposes selected camstack devices as HomeKit accessories.
95503
+ * HomeKit (HAP) export addon — publishes selected camstack devices as
95504
+ * standalone HomeKit accessories.
93034
95505
  *
93035
95506
  * Operator flow:
93036
95507
  * 1. Install + enable the addon (hub-only, group `export-hap`).
@@ -93041,10 +95512,10 @@ function syncStateToJson(map) {
93041
95512
  * 3. The setup URI (`X-HM://…`) is logged AND surfaced via the
93042
95513
  * `getStatus` cap method so the wizard UI can render the QR.
93043
95514
  * 4. Operator opens iOS Home → + → scan QR → enter pincode.
93044
- * 5. Operator hits "Expose to HomeKit" on individual camstack
93045
- * devices; the addon attaches a MotionSensor accessory (MVP)
93046
- * to the bridge and persists the choice via the same
93047
- * `updateGlobalSettings` path.
95515
+ * 5. Operator hits "Expose to HomeKit" on individual camstack devices.
95516
+ * Each exposed device is published as its OWN accessory with its own
95517
+ * mDNS advertisement, sharing one setup code cameras cannot be
95518
+ * bridged, so nothing is.
93048
95519
  *
93049
95520
  * Exception — the hap-nodejs library's own `HAPStorage` lives under
93050
95521
  * `ctx.dataDir/hap-store/`. That directory is library-internal: the
@@ -93053,23 +95524,20 @@ function syncStateToJson(map) {
93053
95524
  * I/O for addon-owned data") explicitly scopes itself to OUR own data;
93054
95525
  * library-managed blobs are out of scope.
93055
95526
  *
93056
- * Round 2 scope: full camera bridge. `exposeDevice({deviceId})` builds
93057
- * one HomeKit Accessory per camstack camera with auto-detected feature
93058
- * services driven by the device's capability binding:
93059
- * - `camera-streams` cap CameraRTPStreamManagement via ffmpeg
93060
- * - `intercom` cap → Microphone + Speaker (audio bridge wiring is a
93061
- * Round 3 follow-up services are declared so iOS Home shows the
93062
- * talk-back button, but PCM upload is not yet routed)
93063
- * - `doorbell` cap DoorbellController + Doorbell service
93064
- * - `motion-detection` cap MotionSensor service
93065
- * - `ptz` cap preset switches + 4 directional momentary switches
93066
- * - `ptz-autotrack` cap → stateful "Autotrack" switch
93067
- * Children (siren, floodlight, spotlight, …) become independent
93068
- * accessories under the same Bridge see `mappers/child-accessory.ts`.
93069
- *
93070
- * What's deferred to Round 3+: cam→browser audio Opus transcode,
93071
- * intercom upload bridge, HomeKit Secure Video, recording, native
93072
- * H.264 stream tap (currently uses RTSP + ffmpeg copy).
95527
+ * ## Two accessory shapes, one rule about coverage
95528
+ *
95529
+ * `exposeDevice({deviceId})` resolves the device's TYPE to a mapper kind
95530
+ * (`mappers/kind.ts`) and hands off:
95531
+ *
95532
+ * - a camera `mappers/camera-accessory.ts`: streams, HKSV recording,
95533
+ * doorbell, motion, intercom, PTZ, battery, privacy and every accessory
95534
+ * child, all as services on one accessory;
95535
+ * - anything else → `mappers/generic-accessory.ts`, driven by the
95536
+ * capabilityservice table in `mappers/builders/generic/`.
95537
+ *
95538
+ * The type only picks the SHAPE. What a device publishes is decided by the
95539
+ * capabilities bound to itrestricting coverage by type is precisely what
95540
+ * confined this exporter to cameras for its first three rounds.
93073
95541
  */
93074
95542
  var DEFAULT_DEVICE_SETTINGS = {
93075
95543
  streamPreference: "auto",
@@ -93112,7 +95580,6 @@ var DEFAULT_CONFIG = {
93112
95580
  fixedPin: "",
93113
95581
  interfaceName: "",
93114
95582
  ptzPulseMs: 400,
93115
- hksvPreview: false,
93116
95583
  identity: {
93117
95584
  username: "",
93118
95585
  pincode: "",
@@ -93228,7 +95695,7 @@ var ExportHapAddon = class extends BaseAddon {
93228
95695
  ...setup ? { setup } : {}
93229
95696
  };
93230
95697
  },
93231
- listSupportedDeviceKinds: async () => [...SUPPORTED_MAPPER_KINDS],
95698
+ listSupportedDeviceKinds: async () => [...HAP_EXPORTABLE_DEVICE_TYPES],
93232
95699
  listExposedDevices: async () => Array.from(this.exposed.entries()).map(([deviceId, m]) => {
93233
95700
  const entry = this.config.exposed.find((e) => e.deviceId === deviceId);
93234
95701
  return {
@@ -93313,9 +95780,10 @@ var ExportHapAddon = class extends BaseAddon {
93313
95780
  log.debug("export-hap: device already exposed — refreshing capabilities");
93314
95781
  await this.detachMapper(deviceId);
93315
95782
  }
93316
- const mapperKind = pickMapperKind(capabilities);
93317
- if (!mapperKind) throw new Error(`export-hap: no mapper for capabilities ${JSON.stringify(capabilities ?? [])}`);
93318
- const displayName = await this.resolveDisplayName(deviceId);
95783
+ const summary = await this.fetchDeviceSummary(numericId);
95784
+ const mapperKind = pickMapperKind(summary?.type);
95785
+ if (!mapperKind) throw new Error(`export-hap: device ${numericId} has type '${summary?.type ?? "unknown"}', which HomeKit export does not support`);
95786
+ const displayName = summary?.name ?? `Device ${deviceId}`;
93319
95787
  const previous = this.config.exposed.find((e) => e.deviceId === deviceId);
93320
95788
  const baseEntry = carryForward({
93321
95789
  deviceId,
@@ -93345,10 +95813,11 @@ var ExportHapAddon = class extends BaseAddon {
93345
95813
  async unexposeDevice(deviceId, options = {}) {
93346
95814
  const numericId = Number.parseInt(deviceId, 10);
93347
95815
  const log = this.ctx.logger.withTags({ deviceId: numericId });
95816
+ const mapperKind = this.findEntry(numericId)?.mapperKind ?? "camera";
93348
95817
  await this.detachMapper(deviceId);
93349
95818
  const next = this.config.exposed.filter((e) => e.deviceId !== deviceId);
93350
95819
  if (next.length !== this.config.exposed.length) await this.updateGlobalSettings({ exposed: next });
93351
- if (options.clearPairing !== false) clearPairingFiles(import_dist.uuid.generate(`camstack:camera:${numericId}`), this.ctx.logger);
95820
+ if (options.clearPairing !== false) clearPairingFiles(accessoryUuidFor(mapperKind, numericId), this.ctx.logger);
93352
95821
  await this.forgetFingerprint(numericId);
93353
95822
  log.info("export-hap: unexposed device");
93354
95823
  }
@@ -93398,10 +95867,10 @@ var ExportHapAddon = class extends BaseAddon {
93398
95867
  /** Pending rebuild timers per deviceId — used to debounce. Cleared
93399
95868
  * on detach so stale timers can't republish a removed accessory. */
93400
95869
  pendingRebuildTimers = /* @__PURE__ */ new Map();
93401
- /** Deterministic HAP accessory UUID for a device (matches `buildCameraAccessory`
93402
- * and `unexposeDevice`). */
95870
+ /** Deterministic HAP accessory UUID for a device the same one the
95871
+ * orchestrator built it with (`mappers/kind.ts`). */
93403
95872
  hapAccessoryUuid(deviceId) {
93404
- return import_dist.uuid.generate(`camstack:camera:${deviceId}`);
95873
+ return accessoryUuidFor(this.findEntry(deviceId)?.mapperKind ?? "camera", deviceId);
93405
95874
  }
93406
95875
  /**
93407
95876
  * Export fingerprint of a device from its PERSISTED features + type
@@ -93560,31 +96029,79 @@ var ExportHapAddon = class extends BaseAddon {
93560
96029
  log.warn("export-hap: reconcile rebuild failed", { meta: { error: errMsg(err) } });
93561
96030
  }
93562
96031
  }
93563
- async resolveDisplayName(deviceId) {
93564
- const numeric = Number.parseInt(deviceId, 10);
93565
- if (!Number.isFinite(numeric)) return `Device ${deviceId}`;
96032
+ /**
96033
+ * The device's name and type, or `null` when the registry cannot answer.
96034
+ *
96035
+ * `null` is NOT "no such device" — it also covers a transient API failure,
96036
+ * and every caller treats it as "keep doing what was already being done"
96037
+ * rather than re-shaping an exposed accessory on incomplete information.
96038
+ */
96039
+ async fetchDeviceSummary(deviceId) {
96040
+ if (!Number.isFinite(deviceId)) return null;
93566
96041
  try {
93567
- const device = await this.ctx.api.deviceManager?.getDevice.query({ deviceId: numeric });
93568
- if (device?.name) return device.name;
96042
+ const device = await this.ctx.api.deviceManager?.getDevice.query({ deviceId });
96043
+ if (!device) return null;
96044
+ return {
96045
+ name: device.name,
96046
+ type: device.type
96047
+ };
93569
96048
  } catch (err) {
93570
- this.ctx.logger.withTags({ deviceId: numeric }).debug("export-hap: deviceManager.getDevice failed", { meta: { error: errMsg(err) } });
96049
+ this.ctx.logger.withTags({ deviceId }).debug("export-hap: deviceManager.getDevice failed", { meta: { error: errMsg(err) } });
96050
+ return null;
96051
+ }
96052
+ }
96053
+ /**
96054
+ * Which accessory shape this device would get, or `null` for "no Export tab".
96055
+ *
96056
+ * Two questions, in this order, because they fail differently:
96057
+ * 1. the TYPE — a type with no orchestrator is refused outright;
96058
+ * 2. for a non-camera, the CAPABILITIES — a device carrying nothing the
96059
+ * table maps would publish an accessory that pairs and does nothing, and
96060
+ * the operator cannot tell that from a broken integration.
96061
+ *
96062
+ * A camera skips (2): the camera orchestrator has always published on type
96063
+ * alone, and adding a cap gate here would be a new way for an existing camera
96064
+ * to lose its Export tab. A registry that cannot answer at all also resolves
96065
+ * to `camera`, which is what every persisted entry predating this method
96066
+ * carries.
96067
+ */
96068
+ async resolveExportKind(deviceId) {
96069
+ const summary = await this.fetchDeviceSummary(deviceId);
96070
+ const kind = pickMapperKind(summary?.type);
96071
+ if (kind === null) return null;
96072
+ if (kind === "camera") return "camera";
96073
+ try {
96074
+ const proxy = await this.ctx.fetchDevice(deviceId);
96075
+ const capNames = new Set(proxy.binding?.entries.map((entry) => entry.capName) ?? []);
96076
+ if (rowsForCaps(capNames).length > 0) return "generic";
96077
+ this.ctx.logger.withTags({ deviceId }).debug("export-hap: no Export tab — no mapped caps", { meta: {
96078
+ type: summary?.type,
96079
+ caps: [...capNames].toSorted().join(",")
96080
+ } });
96081
+ return null;
96082
+ } catch (err) {
96083
+ this.ctx.logger.withTags({ deviceId }).debug("export-hap: no Export tab — capability read failed", { meta: {
96084
+ type: summary?.type,
96085
+ error: errMsg(err)
96086
+ } });
96087
+ return null;
93571
96088
  }
93572
- return `Device ${deviceId}`;
93573
96089
  }
93574
96090
  globalSettingsSchema() {
93575
96091
  return this.schema({ sections: [{
93576
96092
  id: "export-hap",
93577
96093
  title: "HomeKit Export",
93578
- 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.",
96094
+ 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.",
93579
96095
  columns: 1,
93580
96096
  fields: [
93581
96097
  this.field({
93582
96098
  type: "text",
93583
96099
  key: "bridgeName",
93584
- label: "Bridge name",
93585
- description: "Name shown in iOS Home and in the Bonjour advertisement.",
96100
+ label: "Installation name",
96101
+ 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.",
93586
96102
  default: DEFAULT_CONFIG.bridgeName,
93587
- requiresRestart: true
96103
+ requiresRestart: true,
96104
+ placement: { tab: "advanced" }
93588
96105
  }),
93589
96106
  this.field({
93590
96107
  type: "number",
@@ -93620,21 +96137,13 @@ var ExportHapAddon = class extends BaseAddon {
93620
96137
  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.",
93621
96138
  default: DEFAULT_CONFIG.ptzPulseMs,
93622
96139
  placement: { tab: "advanced" }
93623
- }),
93624
- this.field({
93625
- type: "boolean",
93626
- key: "hksvPreview",
93627
- label: "HKSV Developer Preview (experimental)",
93628
- 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.",
93629
- default: DEFAULT_CONFIG.hksvPreview,
93630
- requiresRestart: true,
93631
- placement: { tab: "advanced" }
93632
96140
  })
93633
96141
  ]
93634
96142
  }] });
93635
96143
  }
93636
96144
  async buildDeviceSettingsContribution(deviceId) {
93637
- if (!await this.isCameraDevice(deviceId)) return null;
96145
+ const kind = await this.resolveExportKind(deviceId);
96146
+ if (kind === null) return null;
93638
96147
  const entry = this.findEntry(deviceId);
93639
96148
  const settings = entry?.settings ?? DEFAULT_DEVICE_SETTINGS;
93640
96149
  const enabled = entry !== null;
@@ -93651,6 +96160,32 @@ var ExportHapAddon = class extends BaseAddon {
93651
96160
  } catch (err) {
93652
96161
  this.ctx.logger.withTags({ deviceId }).debug("export-hap: setupURI failed for per-device contribution", { meta: { error: errMsg(err) } });
93653
96162
  }
96163
+ const cameraFields = kind !== "camera" ? [] : [{
96164
+ type: "select",
96165
+ key: streamPreferenceKey,
96166
+ label: "Source stream (HomeKit)",
96167
+ description: "Which camstack profile slot HomeKit pulls. Auto = the broker picks the slot closest to 1080p at session start.",
96168
+ options: HAP_STREAM_PREFERENCE_OPTIONS,
96169
+ required: true,
96170
+ value: settings.streamPreference,
96171
+ showWhen: {
96172
+ field: enabledKey,
96173
+ equals: true
96174
+ },
96175
+ immediate: true
96176
+ }, {
96177
+ type: "boolean",
96178
+ key: hksvKey,
96179
+ label: "HomeKit recording (Secure Video)",
96180
+ 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).",
96181
+ style: "switch",
96182
+ value: resolveHksvRecording(settings),
96183
+ showWhen: {
96184
+ field: enabledKey,
96185
+ equals: true
96186
+ },
96187
+ immediate: true
96188
+ }];
93654
96189
  return {
93655
96190
  tabs: [{
93656
96191
  id: "export",
@@ -93661,7 +96196,7 @@ var ExportHapAddon = class extends BaseAddon {
93661
96196
  sections: [{
93662
96197
  id: "export-hap",
93663
96198
  title: "HomeKit Export",
93664
- description: "Mirror this camera into the HomeKit bridge so iOS Home can pair with it.",
96199
+ 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.",
93665
96200
  tab: "export",
93666
96201
  columns: 1,
93667
96202
  order: 10,
@@ -93677,7 +96212,7 @@ var ExportHapAddon = class extends BaseAddon {
93677
96212
  type: "qr-code",
93678
96213
  key: "__hap-pair-qr",
93679
96214
  label: paired ? "Pairing QR (already paired)" : "Pairing QR",
93680
- 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}.`,
96215
+ 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}.`,
93681
96216
  value: qrValue,
93682
96217
  size: 192,
93683
96218
  alt: `HomeKit pairing QR for ${name}`,
@@ -93690,38 +96225,12 @@ var ExportHapAddon = class extends BaseAddon {
93690
96225
  type: "boolean",
93691
96226
  key: enabledKey,
93692
96227
  label: "Expose to HomeKit",
93693
- description: "Toggle to publish this camera onto the HAP bridge.",
96228
+ description: "Toggle to publish this device as a HomeKit accessory.",
93694
96229
  style: "switch",
93695
96230
  value: enabled,
93696
96231
  immediate: true
93697
96232
  },
93698
- {
93699
- type: "select",
93700
- key: streamPreferenceKey,
93701
- label: "Source stream (HomeKit)",
93702
- description: "Which camstack profile slot HomeKit pulls. Auto = the broker picks the slot closest to 1080p at session start.",
93703
- options: HAP_STREAM_PREFERENCE_OPTIONS,
93704
- required: true,
93705
- value: settings.streamPreference,
93706
- showWhen: {
93707
- field: enabledKey,
93708
- equals: true
93709
- },
93710
- immediate: true
93711
- },
93712
- {
93713
- type: "boolean",
93714
- key: hksvKey,
93715
- label: "HomeKit recording (Secure Video)",
93716
- 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).",
93717
- style: "switch",
93718
- value: resolveHksvRecording(settings),
93719
- showWhen: {
93720
- field: enabledKey,
93721
- equals: true
93722
- },
93723
- immediate: true
93724
- }
96233
+ ...cameraFields
93725
96234
  ]
93726
96235
  }]
93727
96236
  };
@@ -93787,23 +96296,6 @@ var ExportHapAddon = class extends BaseAddon {
93787
96296
  }
93788
96297
  return { success: true };
93789
96298
  }
93790
- /**
93791
- * Camera-only gate — HomeKit export only knows how to mirror cameras
93792
- * today. Mirrors the snapshot addon's source-side filter so the
93793
- * device-details page doesn't render an "Export" tab on lights /
93794
- * switches / sensors.
93795
- */
93796
- async isCameraDevice(deviceId) {
93797
- const api = this.ctx.api;
93798
- if (!api.deviceManager) return true;
93799
- try {
93800
- const dev = await api.deviceManager.getDevice.query({ deviceId });
93801
- if (!dev) return true;
93802
- return dev.type === DeviceType.Camera;
93803
- } catch {
93804
- return true;
93805
- }
93806
- }
93807
96299
  findEntry(deviceId) {
93808
96300
  const id = String(deviceId);
93809
96301
  return this.config.exposed.find((e) => e.deviceId === id) ?? null;