@camstack/addon-export-hap 1.2.27 → 1.2.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3082,6 +3082,9 @@ function handlePipeResult(left, next, ctx) {
3082
3082
  fallback: left.fallback
3083
3083
  }, ctx);
3084
3084
  }
3085
+ var $ZodPreprocess = /*@__PURE__*/ $constructor("$ZodPreprocess", (inst, def) => {
3086
+ $ZodPipe.init(inst, def);
3087
+ });
3085
3088
  var $ZodReadonly = /*@__PURE__*/ $constructor("$ZodReadonly", (inst, def) => {
3086
3089
  $ZodType.init(inst, def);
3087
3090
  defineLazy(inst._zod, "propValues", () => def.innerType._zod.propValues);
@@ -5267,6 +5270,10 @@ function pipe(in_, out) {
5267
5270
  out
5268
5271
  });
5269
5272
  }
5273
+ var ZodPreprocess = /*@__PURE__*/ $constructor("ZodPreprocess", (inst, def) => {
5274
+ ZodPipe.init(inst, def);
5275
+ $ZodPreprocess.init(inst, def);
5276
+ });
5270
5277
  var ZodReadonly = /*@__PURE__*/ $constructor("ZodReadonly", (inst, def) => {
5271
5278
  $ZodReadonly.init(inst, def);
5272
5279
  ZodType.init(inst, def);
@@ -5325,6 +5332,13 @@ function _instanceof(cls, params = {}) {
5325
5332
  };
5326
5333
  return inst;
5327
5334
  }
5335
+ function preprocess(fn, schema) {
5336
+ return new ZodPreprocess({
5337
+ type: "pipe",
5338
+ in: transform(fn),
5339
+ out: schema
5340
+ });
5341
+ }
5328
5342
  //#endregion
5329
5343
  //#region ../../node_modules/zod/v4/classic/compat.js
5330
5344
  /** @deprecated Use the raw string literal codes instead, e.g. "invalid_type". */
@@ -7601,7 +7615,7 @@ import { errMsg } from '@camstack/types'
7601
7615
  * Extract a human-readable message from an unknown error value.
7602
7616
  * Replaces the ubiquitous `errMsg(err)` pattern.
7603
7617
  */
7604
- function errMsg$12(err) {
7618
+ function errMsg$15(err) {
7605
7619
  if (err instanceof Error) return err.message;
7606
7620
  if (typeof err === "string") return err;
7607
7621
  return String(err);
@@ -16003,7 +16017,87 @@ var MethodAccessSchema = _enum([
16003
16017
  var AllowedProviderSchema = union([literal("*"), array(string())]);
16004
16018
  var AllowedDevicesSchema = record(string(), union([literal("*"), array(string())]));
16005
16019
  var CapScopeSchema = _enum(["device", "system"]);
16006
- var TokenScopeSchema = discriminatedUnion("type", [
16020
+ /**
16021
+ * DeviceSelector (scope model v3 — 2026-08-12).
16022
+ *
16023
+ * A `device` grant no longer carries a frozen list of deviceIds. It carries
16024
+ * a SELECTOR the matcher resolves against the live fleet, so the grant can be
16025
+ * DYNAMIC: a `types:['camera']` selector automatically covers a camera added
16026
+ * AFTER the grant was minted — no re-grant, no re-login.
16027
+ *
16028
+ * - `all` — every device in the deployment. The broad viewer/operator
16029
+ * lever without a `category` grant (a `category` grant also covers device
16030
+ * caps that carry no deviceId; `all` is specifically the device set).
16031
+ * - `ids` — an explicit deviceId list. This is what a v2 `device:[…]`
16032
+ * grant migrates to (see {@link TokenScopeSchema}); STATIC — a new camera
16033
+ * is NOT covered until the grant is edited.
16034
+ * - `types` — every device of a `DeviceType` (e.g. every `camera`).
16035
+ * DYNAMIC. A device that changes type, or a new device of the type,
16036
+ * re-resolves on the next request.
16037
+ * - `locations` — every device whose operator-assigned `location` label is
16038
+ * in the set (e.g. "Garden", "Front door"). DYNAMIC. A device with a
16039
+ * null/unset location matches NO `locations` selector.
16040
+ */
16041
+ var DeviceSelectorSchema = discriminatedUnion("kind", [
16042
+ object({ kind: literal("all") }),
16043
+ object({
16044
+ kind: literal("ids"),
16045
+ ids: array(number().int()).min(1)
16046
+ }),
16047
+ object({
16048
+ kind: literal("types"),
16049
+ types: array(_enum(DeviceType)).min(1)
16050
+ }),
16051
+ object({
16052
+ kind: literal("locations"),
16053
+ locations: array(string().min(1)).min(1)
16054
+ })
16055
+ ]);
16056
+ var DeviceTokenScopeSchema = object({
16057
+ type: literal("device"),
16058
+ /** The device SET this grant covers — resolved against the live fleet. */
16059
+ selector: DeviceSelectorSchema,
16060
+ access: array(MethodAccessSchema).min(1),
16061
+ /**
16062
+ * Whether a grant on a PARENT device transparently covers its accessory
16063
+ * CHILDREN (siren / floodlight / PIR) via the persisted-parentage walk.
16064
+ * Direction is parent → children ONLY.
16065
+ *
16066
+ * Absent → the matcher DERIVES it from the access flavour: `view`
16067
+ * inherits (a camera viewer sees the camera's accessories), `create` /
16068
+ * `delete` do NOT (actuating/removing a child is an explicit act the
16069
+ * operator must grant on the child, not inherit from the parent). Set it
16070
+ * explicitly to override that default per grant.
16071
+ */
16072
+ includeLinked: boolean().optional()
16073
+ });
16074
+ /**
16075
+ * v2 → v3 lazy migration. A pre-v3 `device` grant carried
16076
+ * `targets: string[]` (stringified deviceIds); it rewrites to the equivalent
16077
+ * `selector: {kind:'ids', ids}`. Applied as a `preprocess` so it runs on
16078
+ * EVERY parse path — stored records AND the JWT-carried scope arrays
16079
+ * normalised at the request boundary ({@link normalizeTokenScopes} in
16080
+ * `device-selector.ts`). Chosen over a one-time DB migration because a
16081
+ * migration cannot reach a JWT already in a client's hands; parse-time
16082
+ * migration covers both without a flag day. No cast — the raw object is read
16083
+ * through `Reflect.get` (its static type is `unknown`).
16084
+ */
16085
+ function migrateLegacyTokenScope(raw) {
16086
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return raw;
16087
+ if (Reflect.get(raw, "type") !== "device") return raw;
16088
+ if (Reflect.get(raw, "selector") !== void 0) return raw;
16089
+ const targets = Reflect.get(raw, "targets");
16090
+ if (!Array.isArray(targets)) return raw;
16091
+ return {
16092
+ type: "device",
16093
+ selector: {
16094
+ kind: "ids",
16095
+ ids: targets.map((t) => typeof t === "string" ? Number(t) : t).filter((n) => typeof n === "number" && Number.isInteger(n))
16096
+ },
16097
+ access: Reflect.get(raw, "access")
16098
+ };
16099
+ }
16100
+ var TokenScopeSchema = preprocess(migrateLegacyTokenScope, discriminatedUnion("type", [
16007
16101
  object({
16008
16102
  type: literal("category"),
16009
16103
  target: CapScopeSchema,
@@ -16019,18 +16113,8 @@ var TokenScopeSchema = discriminatedUnion("type", [
16019
16113
  target: string(),
16020
16114
  access: array(MethodAccessSchema).min(1)
16021
16115
  }),
16022
- object({
16023
- type: literal("device"),
16024
- /**
16025
- * One or more deviceIds (serialised as strings for wire-format
16026
- * consistency with the rest of the union). Matcher accepts if
16027
- * `input.deviceId` ∈ `targets`. Array shape avoids the row-explosion
16028
- * of one scope-per-device when granting access to a set of cameras.
16029
- */
16030
- targets: array(string()).min(1),
16031
- access: array(MethodAccessSchema).min(1)
16032
- })
16033
- ]);
16116
+ DeviceTokenScopeSchema
16117
+ ]));
16034
16118
  object({
16035
16119
  id: string(),
16036
16120
  username: string(),
@@ -18046,7 +18130,7 @@ var detectionFpsField = {
18046
18130
  var occupancyRecheckSecField = {
18047
18131
  min: 0,
18048
18132
  max: 300,
18049
- default: 30,
18133
+ default: 300,
18050
18134
  step: 5
18051
18135
  };
18052
18136
  var occupancyRecheckFramesField = {
@@ -20659,7 +20743,11 @@ object({
20659
20743
  precision: number().int().min(0).max(10).optional()
20660
20744
  });
20661
20745
  DeviceType.Sensor;
20662
- object({
20746
+ /**
20747
+ * Ambient illuminance reading in lux. Drives Home Assistant `sensor`
20748
+ * entries with `device_class: illuminance`.
20749
+ */
20750
+ var AmbientLightSensorStatusSchema = object({
20663
20751
  /** Current illuminance in lux (lx). */
20664
20752
  lux: number().min(0),
20665
20753
  /** Ms epoch when the slice was last updated. */
@@ -20932,7 +21020,11 @@ DeviceType.Camera, method(object({ deviceId: number() }), CameraCredentialsSchem
20932
21020
  kind: "query",
20933
21021
  auth: "admin"
20934
21022
  });
20935
- object({
21023
+ /**
21024
+ * Carbon-monoxide alarm sensor. Drives Home Assistant `binary_sensor`
21025
+ * entries with `device_class: carbon_monoxide`. Push-driven.
21026
+ */
21027
+ var CarbonMonoxideStatusSchema = object({
20936
21028
  detected: boolean(),
20937
21029
  /** Ms epoch of the last transition. 0 if never observed. */
20938
21030
  lastChangedAt: number()
@@ -21220,7 +21312,19 @@ Object.values(DeviceType), method(object({
21220
21312
  kind: "mutation",
21221
21313
  auth: "admin"
21222
21314
  }), ConsumablesStatusSchema.extend({ lastFetchedAt: number() });
21223
- object({
21315
+ /**
21316
+ * Door / window / opening / garage / valve contact sensor. Boolean
21317
+ * "is the entry currently open" with the timestamp of the last
21318
+ * transition. Drives Home Assistant `binary_sensor` entries whose
21319
+ * `device_class` is `door`, `window`, `opening`, `garage`, or
21320
+ * `garage_door` — and any future native integration that needs
21321
+ * the same semantics.
21322
+ *
21323
+ * Push-driven: providers update the slice on transition events from
21324
+ * the upstream source (HA WebSocket `state_changed`, ZWave
21325
+ * `notification` …). Consumers read the slice; no polling.
21326
+ */
21327
+ var ContactStatusSchema = object({
21224
21328
  /** True when the entry is open; false when closed. */
21225
21329
  entryOpen: boolean(),
21226
21330
  /** Ms epoch of the last open↔closed transition. 0 if never observed. */
@@ -21819,7 +21923,15 @@ object({
21819
21923
  deviceId: number(),
21820
21924
  status: FeatureProbeStatusSchema
21821
21925
  });
21822
- object({
21926
+ /**
21927
+ * Water leak / moisture sensor. Boolean "is liquid currently
21928
+ * detected" with the timestamp of the last transition. Drives Home
21929
+ * Assistant `binary_sensor` entries with `device_class: moisture`,
21930
+ * and any future native flood sensor.
21931
+ *
21932
+ * Push-driven from the upstream source.
21933
+ */
21934
+ var FloodStatusSchema = object({
21823
21935
  /** True when leak is currently detected. */
21824
21936
  flooded: boolean(),
21825
21937
  /** Ms epoch of the last flooded↔dry transition. 0 if never observed. */
@@ -21873,7 +21985,15 @@ DeviceType.Humidifier, method(object({
21873
21985
  kind: "mutation",
21874
21986
  auth: "admin"
21875
21987
  });
21876
- object({
21988
+ /**
21989
+ * Single-metric humidity reading. Drives Home Assistant `sensor`
21990
+ * entries with `device_class: humidity`.
21991
+ *
21992
+ * Unit normalisation: percent. The canonical display unit (`%`) is a
21993
+ * descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
21994
+ * `sourceInfo`.
21995
+ */
21996
+ var HumiditySensorStatusSchema = object({
21877
21997
  /** Current relative humidity, 0..100. */
21878
21998
  percent: number().min(0).max(100),
21879
21999
  /** Ms epoch when the slice was last updated. */
@@ -22495,7 +22615,7 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
22495
22615
  * tunnel always emits `https://` regardless. */
22496
22616
  scheme: _enum(["http", "https"]).optional()
22497
22617
  }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
22498
- object({
22618
+ var LockControlStatusSchema = object({
22499
22619
  /** Lifecycle state of the lock. `jammed` means the motor reported
22500
22620
  * failure to reach the target — operator intervention required. */
22501
22621
  state: _enum([
@@ -22833,7 +22953,19 @@ authKey: string().optional() }), object({
22833
22953
  /** Human-readable error when `ok: false`. */
22834
22954
  error: string().optional()
22835
22955
  }), { kind: "mutation" });
22836
- object({
22956
+ /**
22957
+ * Hardware / firmware motion sensor cap — binary detected state plus
22958
+ * a timestamp of the last observation. Distinct from
22959
+ * `motion-detection.cap.ts` which owns the LOCAL ML motion pipeline;
22960
+ * `motion` is the lightweight readout from on-camera motion (Reolink
22961
+ * `GetMdState`, Baichuan push `type: motion`, ONVIF analytics).
22962
+ *
22963
+ * Native-motion providers also fan out to `detection.camera-native`
22964
+ * with `source: 'onboard'` so cross-cutting system services
22965
+ * (alert-center, advanced-notifier) can subscribe once and receive
22966
+ * motion from every camera.
22967
+ */
22968
+ var MotionStatusSchema = object({
22837
22969
  detected: boolean(),
22838
22970
  /** Ms epoch of the last detected-true observation. Null if never detected. */
22839
22971
  lastDetectedAt: number().nullable(),
@@ -23945,7 +24077,7 @@ var GpsLocationSchema = object({
23945
24077
  /** Reported accuracy in meters (lower = better). */
23946
24078
  accuracyMeters: number().nonnegative()
23947
24079
  });
23948
- object({
24080
+ var PresenceStatusSchema = object({
23949
24081
  /** `home` / `not_home` / any user-defined zone name. */
23950
24082
  state: string(),
23951
24083
  /** Optional textual location label (zone name, city, address). Null
@@ -24361,7 +24493,7 @@ method(object({
24361
24493
  toMs: number()
24362
24494
  }), RecordingAvailabilitySchema, {
24363
24495
  kind: "query",
24364
- auth: "admin"
24496
+ auth: "protected"
24365
24497
  }), method(object({
24366
24498
  deviceId: number(),
24367
24499
  fromMs: number(),
@@ -24369,14 +24501,14 @@ method(object({
24369
24501
  tzOffsetMinutes: number()
24370
24502
  }), RecordingDaysSchema, {
24371
24503
  kind: "query",
24372
- auth: "admin"
24504
+ auth: "protected"
24373
24505
  }), method(object({
24374
24506
  deviceId: number(),
24375
24507
  fromMs: number(),
24376
24508
  toMs: number()
24377
24509
  }), RecordingManifestSchema, {
24378
24510
  kind: "query",
24379
- auth: "admin"
24511
+ auth: "protected"
24380
24512
  }), method(object({}), RecordingStorageUsageSchema, {
24381
24513
  kind: "query",
24382
24514
  auth: "admin"
@@ -24905,7 +25037,16 @@ DeviceType.Script, method(object({
24905
25037
  kind: "mutation",
24906
25038
  auth: "admin"
24907
25039
  });
24908
- object({
25040
+ /**
25041
+ * Smoke alarm sensor — boolean "is smoke currently detected" with
25042
+ * timestamp of the last transition. Drives Home Assistant
25043
+ * `binary_sensor` entries with `device_class: smoke`.
25044
+ *
25045
+ * Push-driven: a smoke event is critical, so the slice updates
25046
+ * immediately on the upstream signal. Auto-clearing back to false is
25047
+ * provider-controlled (some alarms latch until manually reset).
25048
+ */
25049
+ var SmokeStatusSchema = object({
24909
25050
  detected: boolean(),
24910
25051
  /** Ms epoch of the last transition. 0 if never observed. */
24911
25052
  lastChangedAt: number()
@@ -25114,7 +25255,23 @@ object({
25114
25255
  lastChangedAt: number()
25115
25256
  });
25116
25257
  DeviceType.Sensor;
25117
- object({
25258
+ /**
25259
+ * Single-metric temperature reading. Drives Home Assistant `sensor`
25260
+ * entries with `device_class: temperature` and any future native
25261
+ * thermometer.
25262
+ *
25263
+ * Unit normalisation: providers convert to Celsius before storing.
25264
+ * The slice value is always Celsius so cross-cap aggregators
25265
+ * (climate-control's `currentTemp`, energy analytics) can compose
25266
+ * without per-source unit fixups. The canonical display unit (`°C`) is
25267
+ * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
25268
+ * `sourceInfo`.
25269
+ *
25270
+ * Status `lastFetchedAt` lets staleness-aware consumers detect a
25271
+ * frozen feed (provider hung) distinct from a "temperature hasn't
25272
+ * changed" steady state.
25273
+ */
25274
+ var TemperatureSensorStatusSchema = object({
25118
25275
  /** Current temperature in Celsius. */
25119
25276
  celsius: number(),
25120
25277
  /** Ms epoch when the slice was last updated (push or poll). */
@@ -31375,6 +31532,1678 @@ Object.freeze({
31375
31532
  access: "create"
31376
31533
  }
31377
31534
  });
31535
+ Object.freeze({
31536
+ "accessories.setChildHidden": [{
31537
+ name: "childDeviceId",
31538
+ form: "single",
31539
+ optional: false
31540
+ }, {
31541
+ name: "deviceId",
31542
+ form: "single",
31543
+ optional: false
31544
+ }],
31545
+ "addonSettings.getDeviceSettings": [{
31546
+ name: "deviceId",
31547
+ form: "single",
31548
+ optional: false
31549
+ }],
31550
+ "addonSettings.updateDeviceSettings": [{
31551
+ name: "deviceId",
31552
+ form: "single",
31553
+ optional: false
31554
+ }],
31555
+ "alarmPanel.arm": [{
31556
+ name: "deviceId",
31557
+ form: "single",
31558
+ optional: false
31559
+ }],
31560
+ "alarmPanel.disarm": [{
31561
+ name: "deviceId",
31562
+ form: "single",
31563
+ optional: false
31564
+ }],
31565
+ "alarmPanel.trigger": [{
31566
+ name: "deviceId",
31567
+ form: "single",
31568
+ optional: false
31569
+ }],
31570
+ "audioAnalysis.resolveDeviceSettings": [{
31571
+ name: "deviceId",
31572
+ form: "single",
31573
+ optional: false
31574
+ }],
31575
+ "audioAnalyzer.classify": [{
31576
+ name: "deviceId",
31577
+ form: "single",
31578
+ optional: true
31579
+ }],
31580
+ "audioMetrics.getCurrentSnapshot": [{
31581
+ name: "deviceId",
31582
+ form: "single",
31583
+ optional: false
31584
+ }],
31585
+ "audioMetrics.getHistory": [{
31586
+ name: "deviceId",
31587
+ form: "single",
31588
+ optional: false
31589
+ }],
31590
+ "automationControl.disable": [{
31591
+ name: "deviceId",
31592
+ form: "single",
31593
+ optional: false
31594
+ }],
31595
+ "automationControl.enable": [{
31596
+ name: "deviceId",
31597
+ form: "single",
31598
+ optional: false
31599
+ }],
31600
+ "automationControl.trigger": [{
31601
+ name: "deviceId",
31602
+ form: "single",
31603
+ optional: false
31604
+ }],
31605
+ "battery.wakeForStream": [{
31606
+ name: "deviceId",
31607
+ form: "single",
31608
+ optional: false
31609
+ }],
31610
+ "brightness.setBrightness": [{
31611
+ name: "deviceId",
31612
+ form: "single",
31613
+ optional: false
31614
+ }],
31615
+ "button.press": [{
31616
+ name: "deviceId",
31617
+ form: "single",
31618
+ optional: false
31619
+ }],
31620
+ "cameraCredentials.getCredentials": [{
31621
+ name: "deviceId",
31622
+ form: "single",
31623
+ optional: false
31624
+ }],
31625
+ "cameraStreams.getBrokerStreams": [{
31626
+ name: "deviceId",
31627
+ form: "single",
31628
+ optional: false
31629
+ }],
31630
+ "cameraStreams.getCameraStreams": [{
31631
+ name: "deviceId",
31632
+ form: "single",
31633
+ optional: false
31634
+ }],
31635
+ "cameraStreams.getProfileRtspEntries": [{
31636
+ name: "deviceId",
31637
+ form: "single",
31638
+ optional: false
31639
+ }],
31640
+ "cameraStreams.getRtspEntries": [{
31641
+ name: "deviceId",
31642
+ form: "single",
31643
+ optional: false
31644
+ }],
31645
+ "cameraStreams.pickStream": [{
31646
+ name: "deviceId",
31647
+ form: "single",
31648
+ optional: false
31649
+ }],
31650
+ "climateControl.setFanMode": [{
31651
+ name: "deviceId",
31652
+ form: "single",
31653
+ optional: false
31654
+ }],
31655
+ "climateControl.setMode": [{
31656
+ name: "deviceId",
31657
+ form: "single",
31658
+ optional: false
31659
+ }],
31660
+ "climateControl.setPreset": [{
31661
+ name: "deviceId",
31662
+ form: "single",
31663
+ optional: false
31664
+ }],
31665
+ "climateControl.setSwingHorizontal": [{
31666
+ name: "deviceId",
31667
+ form: "single",
31668
+ optional: false
31669
+ }],
31670
+ "climateControl.setSwingVertical": [{
31671
+ name: "deviceId",
31672
+ form: "single",
31673
+ optional: false
31674
+ }],
31675
+ "climateControl.setTarget": [{
31676
+ name: "deviceId",
31677
+ form: "single",
31678
+ optional: false
31679
+ }],
31680
+ "climateControl.setTargetHumidity": [{
31681
+ name: "deviceId",
31682
+ form: "single",
31683
+ optional: false
31684
+ }],
31685
+ "climateControl.setTargetRange": [{
31686
+ name: "deviceId",
31687
+ form: "single",
31688
+ optional: false
31689
+ }],
31690
+ "color.setColor": [{
31691
+ name: "deviceId",
31692
+ form: "single",
31693
+ optional: false
31694
+ }],
31695
+ "consumables.reset": [{
31696
+ name: "deviceId",
31697
+ form: "single",
31698
+ optional: false
31699
+ }],
31700
+ "control.setValue": [{
31701
+ name: "deviceId",
31702
+ form: "single",
31703
+ optional: false
31704
+ }],
31705
+ "cover.close": [{
31706
+ name: "deviceId",
31707
+ form: "single",
31708
+ optional: false
31709
+ }],
31710
+ "cover.open": [{
31711
+ name: "deviceId",
31712
+ form: "single",
31713
+ optional: false
31714
+ }],
31715
+ "cover.setPosition": [{
31716
+ name: "deviceId",
31717
+ form: "single",
31718
+ optional: false
31719
+ }],
31720
+ "cover.setTiltPosition": [{
31721
+ name: "deviceId",
31722
+ form: "single",
31723
+ optional: false
31724
+ }],
31725
+ "cover.stop": [{
31726
+ name: "deviceId",
31727
+ form: "single",
31728
+ optional: false
31729
+ }],
31730
+ "dayNight.getOptions": [{
31731
+ name: "deviceId",
31732
+ form: "single",
31733
+ optional: false
31734
+ }],
31735
+ "dayNight.setSettings": [{
31736
+ name: "deviceId",
31737
+ form: "single",
31738
+ optional: false
31739
+ }],
31740
+ "decoder.createSession": [{
31741
+ name: "deviceId",
31742
+ form: "single",
31743
+ optional: true
31744
+ }],
31745
+ "deviceAdoption.release": [{
31746
+ name: "camDeviceId",
31747
+ form: "single",
31748
+ optional: false
31749
+ }],
31750
+ "deviceAdoption.resync": [{
31751
+ name: "camDeviceId",
31752
+ form: "single",
31753
+ optional: false
31754
+ }],
31755
+ "deviceDiscovery.adoptDevice": [{
31756
+ name: "deviceId",
31757
+ form: "single",
31758
+ optional: false
31759
+ }],
31760
+ "deviceDiscovery.listDiscovered": [{
31761
+ name: "deviceId",
31762
+ form: "single",
31763
+ optional: false
31764
+ }],
31765
+ "deviceDiscovery.refreshDiscovery": [{
31766
+ name: "deviceId",
31767
+ form: "single",
31768
+ optional: false
31769
+ }],
31770
+ "deviceDiscovery.releaseDevice": [{
31771
+ name: "childDeviceId",
31772
+ form: "single",
31773
+ optional: false
31774
+ }, {
31775
+ name: "deviceId",
31776
+ form: "single",
31777
+ optional: false
31778
+ }],
31779
+ "deviceManager.adoptionRelease": [{
31780
+ name: "camDeviceId",
31781
+ form: "single",
31782
+ optional: false
31783
+ }],
31784
+ "deviceManager.adoptionResync": [{
31785
+ name: "camDeviceId",
31786
+ form: "single",
31787
+ optional: false
31788
+ }],
31789
+ "deviceManager.applyInitialMeta": [{
31790
+ name: "deviceId",
31791
+ form: "single",
31792
+ optional: false
31793
+ }, {
31794
+ name: "linkDeviceId",
31795
+ form: "single",
31796
+ optional: true
31797
+ }],
31798
+ "deviceManager.disable": [{
31799
+ name: "deviceId",
31800
+ form: "single",
31801
+ optional: false
31802
+ }],
31803
+ "deviceManager.enable": [{
31804
+ name: "deviceId",
31805
+ form: "single",
31806
+ optional: false
31807
+ }],
31808
+ "deviceManager.getBindings": [{
31809
+ name: "deviceId",
31810
+ form: "single",
31811
+ optional: false
31812
+ }],
31813
+ "deviceManager.getChildren": [{
31814
+ name: "parentDeviceId",
31815
+ form: "single",
31816
+ optional: false
31817
+ }],
31818
+ "deviceManager.getConfigSchema": [{
31819
+ name: "deviceId",
31820
+ form: "single",
31821
+ optional: false
31822
+ }],
31823
+ "deviceManager.getDevice": [{
31824
+ name: "deviceId",
31825
+ form: "single",
31826
+ optional: false
31827
+ }],
31828
+ "deviceManager.getDeviceAggregate": [{
31829
+ name: "deviceId",
31830
+ form: "single",
31831
+ optional: false
31832
+ }],
31833
+ "deviceManager.getDeviceLiveInfoAggregate": [{
31834
+ name: "deviceId",
31835
+ form: "single",
31836
+ optional: false
31837
+ }],
31838
+ "deviceManager.getDeviceSettingsAggregate": [{
31839
+ name: "deviceId",
31840
+ form: "single",
31841
+ optional: false
31842
+ }],
31843
+ "deviceManager.getDeviceStatusAggregate": [{
31844
+ name: "deviceId",
31845
+ form: "single",
31846
+ optional: false
31847
+ }],
31848
+ "deviceManager.getDeviceStatusAggregateBatch": [{
31849
+ name: "deviceIds",
31850
+ form: "array",
31851
+ optional: false
31852
+ }],
31853
+ "deviceManager.getLinkedDevices": [{
31854
+ name: "deviceId",
31855
+ form: "single",
31856
+ optional: false
31857
+ }],
31858
+ "deviceManager.getSettingsSchema": [{
31859
+ name: "deviceId",
31860
+ form: "single",
31861
+ optional: false
31862
+ }],
31863
+ "deviceManager.getStreamProfileMap": [{
31864
+ name: "deviceId",
31865
+ form: "single",
31866
+ optional: false
31867
+ }],
31868
+ "deviceManager.getStreamSources": [{
31869
+ name: "deviceId",
31870
+ form: "single",
31871
+ optional: false
31872
+ }],
31873
+ "deviceManager.getWireableFields": [{
31874
+ name: "deviceId",
31875
+ form: "single",
31876
+ optional: false
31877
+ }],
31878
+ "deviceManager.loadConfig": [{
31879
+ name: "deviceId",
31880
+ form: "single",
31881
+ optional: false
31882
+ }],
31883
+ "deviceManager.loadMeta": [{
31884
+ name: "deviceId",
31885
+ form: "single",
31886
+ optional: false
31887
+ }],
31888
+ "deviceManager.loadRuntimeState": [{
31889
+ name: "deviceId",
31890
+ form: "single",
31891
+ optional: false
31892
+ }],
31893
+ "deviceManager.persistConfig": [{
31894
+ name: "deviceId",
31895
+ form: "single",
31896
+ optional: false
31897
+ }],
31898
+ "deviceManager.probeStreams": [{
31899
+ name: "deviceId",
31900
+ form: "single",
31901
+ optional: false
31902
+ }],
31903
+ "deviceManager.registerDevice": [{
31904
+ name: "parentDeviceId",
31905
+ form: "single",
31906
+ optional: true
31907
+ }],
31908
+ "deviceManager.remove": [{
31909
+ name: "deviceId",
31910
+ form: "single",
31911
+ optional: false
31912
+ }],
31913
+ "deviceManager.removeDevice": [{
31914
+ name: "deviceId",
31915
+ form: "single",
31916
+ optional: false
31917
+ }],
31918
+ "deviceManager.runDeviceAction": [{
31919
+ name: "deviceId",
31920
+ form: "single",
31921
+ optional: false
31922
+ }],
31923
+ "deviceManager.setChildLayout": [{
31924
+ name: "deviceId",
31925
+ form: "single",
31926
+ optional: false
31927
+ }],
31928
+ "deviceManager.setDisabled": [{
31929
+ name: "deviceId",
31930
+ form: "single",
31931
+ optional: false
31932
+ }],
31933
+ "deviceManager.setDisplay": [{
31934
+ name: "deviceId",
31935
+ form: "single",
31936
+ optional: false
31937
+ }],
31938
+ "deviceManager.setIntegrationId": [{
31939
+ name: "deviceId",
31940
+ form: "single",
31941
+ optional: false
31942
+ }],
31943
+ "deviceManager.setLinkDeviceId": [{
31944
+ name: "deviceId",
31945
+ form: "single",
31946
+ optional: false
31947
+ }, {
31948
+ name: "linkDeviceId",
31949
+ form: "single",
31950
+ optional: true
31951
+ }],
31952
+ "deviceManager.setLocation": [{
31953
+ name: "deviceId",
31954
+ form: "single",
31955
+ optional: false
31956
+ }],
31957
+ "deviceManager.setMetadata": [{
31958
+ name: "deviceId",
31959
+ form: "single",
31960
+ optional: false
31961
+ }],
31962
+ "deviceManager.setName": [{
31963
+ name: "deviceId",
31964
+ form: "single",
31965
+ optional: false
31966
+ }],
31967
+ "deviceManager.setPrimaryChildEntityId": [{
31968
+ name: "deviceId",
31969
+ form: "single",
31970
+ optional: false
31971
+ }],
31972
+ "deviceManager.setRole": [{
31973
+ name: "deviceId",
31974
+ form: "single",
31975
+ optional: false
31976
+ }],
31977
+ "deviceManager.setStreamProfileMap": [{
31978
+ name: "deviceId",
31979
+ form: "single",
31980
+ optional: false
31981
+ }],
31982
+ "deviceManager.setType": [{
31983
+ name: "deviceId",
31984
+ form: "single",
31985
+ optional: false
31986
+ }],
31987
+ "deviceManager.setWrapperActive": [{
31988
+ name: "deviceId",
31989
+ form: "single",
31990
+ optional: false
31991
+ }],
31992
+ "deviceManager.testField": [{
31993
+ name: "deviceId",
31994
+ form: "single",
31995
+ optional: false
31996
+ }],
31997
+ "deviceManager.updateConfig": [{
31998
+ name: "deviceId",
31999
+ form: "single",
32000
+ optional: false
32001
+ }],
32002
+ "deviceManager.updateDeviceField": [{
32003
+ name: "deviceId",
32004
+ form: "single",
32005
+ optional: false
32006
+ }],
32007
+ "deviceManager.updateDeviceFieldsBatch": [{
32008
+ name: "deviceId",
32009
+ form: "single",
32010
+ optional: false
32011
+ }],
32012
+ "deviceOps.getConfigEntries": [{
32013
+ name: "deviceId",
32014
+ form: "single",
32015
+ optional: false
32016
+ }],
32017
+ "deviceOps.getRawState": [{
32018
+ name: "deviceId",
32019
+ form: "single",
32020
+ optional: false
32021
+ }],
32022
+ "deviceOps.getSettingsSchema": [{
32023
+ name: "deviceId",
32024
+ form: "single",
32025
+ optional: false
32026
+ }],
32027
+ "deviceOps.getStreamSources": [{
32028
+ name: "deviceId",
32029
+ form: "single",
32030
+ optional: false
32031
+ }],
32032
+ "deviceOps.removeDevice": [{
32033
+ name: "deviceId",
32034
+ form: "single",
32035
+ optional: false
32036
+ }],
32037
+ "deviceOps.runAction": [{
32038
+ name: "deviceId",
32039
+ form: "single",
32040
+ optional: false
32041
+ }],
32042
+ "deviceOps.setConfig": [{
32043
+ name: "deviceId",
32044
+ form: "single",
32045
+ optional: false
32046
+ }],
32047
+ "deviceState.getCapSlice": [{
32048
+ name: "deviceId",
32049
+ form: "single",
32050
+ optional: false
32051
+ }],
32052
+ "deviceState.getSnapshot": [{
32053
+ name: "deviceId",
32054
+ form: "single",
32055
+ optional: false
32056
+ }],
32057
+ "deviceState.setCapSlice": [{
32058
+ name: "deviceId",
32059
+ form: "single",
32060
+ optional: false
32061
+ }],
32062
+ "events.getEventClipUrl": [{
32063
+ name: "deviceId",
32064
+ form: "single",
32065
+ optional: false
32066
+ }],
32067
+ "events.getEvents": [{
32068
+ name: "deviceId",
32069
+ form: "single",
32070
+ optional: false
32071
+ }],
32072
+ "events.getEventThumbnail": [{
32073
+ name: "deviceId",
32074
+ form: "single",
32075
+ optional: false
32076
+ }],
32077
+ "faceGallery.getFaceByTrack": [{
32078
+ name: "deviceId",
32079
+ form: "single",
32080
+ optional: false
32081
+ }],
32082
+ "faceGallery.listRecentFaces": [{
32083
+ name: "deviceId",
32084
+ form: "single",
32085
+ optional: true
32086
+ }],
32087
+ "fanControl.setDirection": [{
32088
+ name: "deviceId",
32089
+ form: "single",
32090
+ optional: false
32091
+ }],
32092
+ "fanControl.setOscillating": [{
32093
+ name: "deviceId",
32094
+ form: "single",
32095
+ optional: false
32096
+ }],
32097
+ "fanControl.setPercentage": [{
32098
+ name: "deviceId",
32099
+ form: "single",
32100
+ optional: false
32101
+ }],
32102
+ "fanControl.setPreset": [{
32103
+ name: "deviceId",
32104
+ form: "single",
32105
+ optional: false
32106
+ }],
32107
+ "humidifier.setMode": [{
32108
+ name: "deviceId",
32109
+ form: "single",
32110
+ optional: false
32111
+ }],
32112
+ "humidifier.setOn": [{
32113
+ name: "deviceId",
32114
+ form: "single",
32115
+ optional: false
32116
+ }],
32117
+ "humidifier.setTargetHumidity": [{
32118
+ name: "deviceId",
32119
+ form: "single",
32120
+ optional: false
32121
+ }],
32122
+ "imageSettings.getOptions": [{
32123
+ name: "deviceId",
32124
+ form: "single",
32125
+ optional: false
32126
+ }],
32127
+ "imageSettings.setSettings": [{
32128
+ name: "deviceId",
32129
+ form: "single",
32130
+ optional: false
32131
+ }],
32132
+ "intercom.endTalkSession": [{
32133
+ name: "deviceId",
32134
+ form: "single",
32135
+ optional: false
32136
+ }],
32137
+ "intercom.handleAnswer": [{
32138
+ name: "deviceId",
32139
+ form: "single",
32140
+ optional: false
32141
+ }],
32142
+ "intercom.pushTalkAudio": [{
32143
+ name: "deviceId",
32144
+ form: "single",
32145
+ optional: false
32146
+ }],
32147
+ "intercom.startSession": [{
32148
+ name: "deviceId",
32149
+ form: "single",
32150
+ optional: false
32151
+ }],
32152
+ "intercom.startTalkSession": [{
32153
+ name: "deviceId",
32154
+ form: "single",
32155
+ optional: false
32156
+ }],
32157
+ "intercom.stopSession": [{
32158
+ name: "deviceId",
32159
+ form: "single",
32160
+ optional: false
32161
+ }],
32162
+ "lawnMowerControl.dock": [{
32163
+ name: "deviceId",
32164
+ form: "single",
32165
+ optional: false
32166
+ }],
32167
+ "lawnMowerControl.pause": [{
32168
+ name: "deviceId",
32169
+ form: "single",
32170
+ optional: false
32171
+ }],
32172
+ "lawnMowerControl.startMowing": [{
32173
+ name: "deviceId",
32174
+ form: "single",
32175
+ optional: false
32176
+ }],
32177
+ "lockControl.lock": [{
32178
+ name: "deviceId",
32179
+ form: "single",
32180
+ optional: false
32181
+ }],
32182
+ "lockControl.open": [{
32183
+ name: "deviceId",
32184
+ form: "single",
32185
+ optional: false
32186
+ }],
32187
+ "lockControl.unlock": [{
32188
+ name: "deviceId",
32189
+ form: "single",
32190
+ optional: false
32191
+ }],
32192
+ "mediaPlayer.next": [{
32193
+ name: "deviceId",
32194
+ form: "single",
32195
+ optional: false
32196
+ }],
32197
+ "mediaPlayer.pause": [{
32198
+ name: "deviceId",
32199
+ form: "single",
32200
+ optional: false
32201
+ }],
32202
+ "mediaPlayer.play": [{
32203
+ name: "deviceId",
32204
+ form: "single",
32205
+ optional: false
32206
+ }],
32207
+ "mediaPlayer.playMedia": [{
32208
+ name: "deviceId",
32209
+ form: "single",
32210
+ optional: false
32211
+ }],
32212
+ "mediaPlayer.previous": [{
32213
+ name: "deviceId",
32214
+ form: "single",
32215
+ optional: false
32216
+ }],
32217
+ "mediaPlayer.seek": [{
32218
+ name: "deviceId",
32219
+ form: "single",
32220
+ optional: false
32221
+ }],
32222
+ "mediaPlayer.selectSource": [{
32223
+ name: "deviceId",
32224
+ form: "single",
32225
+ optional: false
32226
+ }],
32227
+ "mediaPlayer.setMute": [{
32228
+ name: "deviceId",
32229
+ form: "single",
32230
+ optional: false
32231
+ }],
32232
+ "mediaPlayer.setRepeat": [{
32233
+ name: "deviceId",
32234
+ form: "single",
32235
+ optional: false
32236
+ }],
32237
+ "mediaPlayer.setShuffle": [{
32238
+ name: "deviceId",
32239
+ form: "single",
32240
+ optional: false
32241
+ }],
32242
+ "mediaPlayer.setVolume": [{
32243
+ name: "deviceId",
32244
+ form: "single",
32245
+ optional: false
32246
+ }],
32247
+ "mediaPlayer.stop": [{
32248
+ name: "deviceId",
32249
+ form: "single",
32250
+ optional: false
32251
+ }],
32252
+ "motion.isDetected": [{
32253
+ name: "deviceId",
32254
+ form: "single",
32255
+ optional: false
32256
+ }],
32257
+ "motionDetection.analyze": [{
32258
+ name: "deviceId",
32259
+ form: "single",
32260
+ optional: false
32261
+ }],
32262
+ "motionDetection.removeCamera": [{
32263
+ name: "deviceId",
32264
+ form: "single",
32265
+ optional: false
32266
+ }],
32267
+ "motionTrigger.setMotionTrigger": [{
32268
+ name: "deviceId",
32269
+ form: "single",
32270
+ optional: false
32271
+ }],
32272
+ "motionZones.getOptions": [{
32273
+ name: "deviceId",
32274
+ form: "single",
32275
+ optional: false
32276
+ }],
32277
+ "motionZones.setZone": [{
32278
+ name: "deviceId",
32279
+ form: "single",
32280
+ optional: false
32281
+ }],
32282
+ "nativeObjectDetection.setEnabled": [{
32283
+ name: "deviceId",
32284
+ form: "single",
32285
+ optional: false
32286
+ }],
32287
+ "networkQuality.getDeviceStats": [{
32288
+ name: "deviceId",
32289
+ form: "single",
32290
+ optional: false
32291
+ }],
32292
+ "networkQuality.reportClientStats": [{
32293
+ name: "deviceId",
32294
+ form: "single",
32295
+ optional: false
32296
+ }],
32297
+ "notificationRules.setDeviceMuted": [{
32298
+ name: "deviceId",
32299
+ form: "single",
32300
+ optional: false
32301
+ }],
32302
+ "notifier.cancel": [{
32303
+ name: "deviceId",
32304
+ form: "single",
32305
+ optional: false
32306
+ }],
32307
+ "notifier.send": [{
32308
+ name: "deviceId",
32309
+ form: "single",
32310
+ optional: false
32311
+ }],
32312
+ "osd.setOverlay": [{
32313
+ name: "deviceId",
32314
+ form: "single",
32315
+ optional: false
32316
+ }],
32317
+ "osdManager.clearSlotBinding": [{
32318
+ name: "deviceId",
32319
+ form: "single",
32320
+ optional: false
32321
+ }],
32322
+ "osdManager.copyDeviceConfiguration": [{
32323
+ name: "sourceDeviceId",
32324
+ form: "single",
32325
+ optional: false
32326
+ }, {
32327
+ name: "targetDeviceId",
32328
+ form: "single",
32329
+ optional: false
32330
+ }],
32331
+ "osdManager.getDeviceOsd": [{
32332
+ name: "deviceId",
32333
+ form: "single",
32334
+ optional: false
32335
+ }],
32336
+ "osdManager.getSourceCatalog": [{
32337
+ name: "deviceId",
32338
+ form: "single",
32339
+ optional: false
32340
+ }],
32341
+ "osdManager.previewSlot": [{
32342
+ name: "deviceId",
32343
+ form: "single",
32344
+ optional: false
32345
+ }],
32346
+ "osdManager.renderDevice": [{
32347
+ name: "deviceId",
32348
+ form: "single",
32349
+ optional: false
32350
+ }],
32351
+ "osdManager.setSlotBinding": [{
32352
+ name: "deviceId",
32353
+ form: "single",
32354
+ optional: false
32355
+ }],
32356
+ "petFeeder.callPet": [{
32357
+ name: "deviceId",
32358
+ form: "single",
32359
+ optional: false
32360
+ }],
32361
+ "petFeeder.cancelFeed": [{
32362
+ name: "deviceId",
32363
+ form: "single",
32364
+ optional: false
32365
+ }],
32366
+ "petFeeder.feed": [{
32367
+ name: "deviceId",
32368
+ form: "single",
32369
+ optional: false
32370
+ }],
32371
+ "petFeeder.markFoodReplenished": [{
32372
+ name: "deviceId",
32373
+ form: "single",
32374
+ optional: false
32375
+ }],
32376
+ "petFeeder.playSound": [{
32377
+ name: "deviceId",
32378
+ form: "single",
32379
+ optional: false
32380
+ }],
32381
+ "petFeeder.resetDesiccant": [{
32382
+ name: "deviceId",
32383
+ form: "single",
32384
+ optional: false
32385
+ }],
32386
+ "petFeeder.setChildLock": [{
32387
+ name: "deviceId",
32388
+ form: "single",
32389
+ optional: false
32390
+ }],
32391
+ "petFeeder.setFeedSound": [{
32392
+ name: "deviceId",
32393
+ form: "single",
32394
+ optional: false
32395
+ }],
32396
+ "petFeeder.setIndicatorLight": [{
32397
+ name: "deviceId",
32398
+ form: "single",
32399
+ optional: false
32400
+ }],
32401
+ "petFeeder.setVolume": [{
32402
+ name: "deviceId",
32403
+ form: "single",
32404
+ optional: false
32405
+ }],
32406
+ "pipelineAnalytics.clearTracks": [{
32407
+ name: "deviceId",
32408
+ form: "single",
32409
+ optional: false
32410
+ }],
32411
+ "pipelineAnalytics.completeRetrainTrack": [{
32412
+ name: "deviceId",
32413
+ form: "single",
32414
+ optional: false
32415
+ }],
32416
+ "pipelineAnalytics.deleteDeviceEvents": [{
32417
+ name: "deviceId",
32418
+ form: "single",
32419
+ optional: false
32420
+ }],
32421
+ "pipelineAnalytics.deleteTracks": [{
32422
+ name: "deviceId",
32423
+ form: "single",
32424
+ optional: false
32425
+ }],
32426
+ "pipelineAnalytics.deselectRetrainFrame": [{
32427
+ name: "deviceId",
32428
+ form: "single",
32429
+ optional: false
32430
+ }],
32431
+ "pipelineAnalytics.getActiveTracks": [{
32432
+ name: "deviceId",
32433
+ form: "single",
32434
+ optional: false
32435
+ }],
32436
+ "pipelineAnalytics.getAudioEvents": [{
32437
+ name: "deviceId",
32438
+ form: "single",
32439
+ optional: false
32440
+ }],
32441
+ "pipelineAnalytics.getEventDensity": [{
32442
+ name: "deviceId",
32443
+ form: "single",
32444
+ optional: false
32445
+ }],
32446
+ "pipelineAnalytics.getKeyEvents": [{
32447
+ name: "deviceId",
32448
+ form: "single",
32449
+ optional: false
32450
+ }],
32451
+ "pipelineAnalytics.getMotionEvents": [{
32452
+ name: "deviceId",
32453
+ form: "single",
32454
+ optional: false
32455
+ }],
32456
+ "pipelineAnalytics.getObjectEvents": [{
32457
+ name: "deviceId",
32458
+ form: "single",
32459
+ optional: false
32460
+ }],
32461
+ "pipelineAnalytics.getRetrainExportUrl": [{
32462
+ name: "deviceIds",
32463
+ form: "array",
32464
+ optional: true
32465
+ }],
32466
+ "pipelineAnalytics.getSensorEvents": [{
32467
+ name: "deviceId",
32468
+ form: "single",
32469
+ optional: false
32470
+ }],
32471
+ "pipelineAnalytics.getTrack": [{
32472
+ name: "deviceId",
32473
+ form: "single",
32474
+ optional: false
32475
+ }],
32476
+ "pipelineAnalytics.getTrainingExportSummary": [{
32477
+ name: "deviceIds",
32478
+ form: "array",
32479
+ optional: true
32480
+ }],
32481
+ "pipelineAnalytics.getTrainingExportUrl": [{
32482
+ name: "deviceIds",
32483
+ form: "array",
32484
+ optional: true
32485
+ }],
32486
+ "pipelineAnalytics.listEventKinds": [{
32487
+ name: "deviceId",
32488
+ form: "single",
32489
+ optional: false
32490
+ }],
32491
+ "pipelineAnalytics.listEventKindsBatch": [{
32492
+ name: "deviceIds",
32493
+ form: "array",
32494
+ optional: false
32495
+ }],
32496
+ "pipelineAnalytics.listOpsLog": [{
32497
+ name: "deviceId",
32498
+ form: "single",
32499
+ optional: true
32500
+ }],
32501
+ "pipelineAnalytics.listRecentTracks": [{
32502
+ name: "deviceIds",
32503
+ form: "array",
32504
+ optional: false
32505
+ }],
32506
+ "pipelineAnalytics.listRetrainStaging": [{
32507
+ name: "deviceIds",
32508
+ form: "array",
32509
+ optional: true
32510
+ }],
32511
+ "pipelineAnalytics.listTracks": [{
32512
+ name: "deviceId",
32513
+ form: "single",
32514
+ optional: false
32515
+ }],
32516
+ "pipelineAnalytics.proposeRetrainAnnotations": [{
32517
+ name: "deviceId",
32518
+ form: "single",
32519
+ optional: false
32520
+ }],
32521
+ "pipelineAnalytics.pruneEventsBefore": [{
32522
+ name: "deviceId",
32523
+ form: "single",
32524
+ optional: false
32525
+ }],
32526
+ "pipelineAnalytics.pruneTracksBefore": [{
32527
+ name: "deviceId",
32528
+ form: "single",
32529
+ optional: false
32530
+ }],
32531
+ "pipelineAnalytics.rebuildObjectEmbeddings": [{
32532
+ name: "deviceId",
32533
+ form: "single",
32534
+ optional: true
32535
+ }],
32536
+ "pipelineAnalytics.restageRetrainTrack": [{
32537
+ name: "deviceId",
32538
+ form: "single",
32539
+ optional: false
32540
+ }],
32541
+ "pipelineAnalytics.saveRetrainAnnotations": [{
32542
+ name: "deviceId",
32543
+ form: "single",
32544
+ optional: false
32545
+ }],
32546
+ "pipelineAnalytics.searchObjectEvents": [{
32547
+ name: "deviceId",
32548
+ form: "single",
32549
+ optional: true
32550
+ }],
32551
+ "pipelineAnalytics.selectRetrainFrames": [{
32552
+ name: "deviceId",
32553
+ form: "single",
32554
+ optional: false
32555
+ }],
32556
+ "pipelineAnalytics.setTrackFlags": [{
32557
+ name: "deviceId",
32558
+ form: "single",
32559
+ optional: false
32560
+ }],
32561
+ "pipelineAnalytics.wipeAllAnalytics": [{
32562
+ name: "deviceId",
32563
+ form: "single",
32564
+ optional: false
32565
+ }],
32566
+ "pipelineExecutor.runPipeline": [{
32567
+ name: "deviceId",
32568
+ form: "single",
32569
+ optional: true
32570
+ }],
32571
+ "pipelineExecutor.runPipelineBatch": [{
32572
+ name: "deviceId",
32573
+ form: "single",
32574
+ optional: true
32575
+ }],
32576
+ "pipelineOrchestrator.assignAudio": [{
32577
+ name: "deviceId",
32578
+ form: "single",
32579
+ optional: false
32580
+ }],
32581
+ "pipelineOrchestrator.assignPipeline": [{
32582
+ name: "deviceId",
32583
+ form: "single",
32584
+ optional: false
32585
+ }],
32586
+ "pipelineOrchestrator.getAudioAssignment": [{
32587
+ name: "deviceId",
32588
+ form: "single",
32589
+ optional: false
32590
+ }],
32591
+ "pipelineOrchestrator.getCameraMetrics": [{
32592
+ name: "deviceId",
32593
+ form: "single",
32594
+ optional: false
32595
+ }],
32596
+ "pipelineOrchestrator.getCameraSettings": [{
32597
+ name: "deviceId",
32598
+ form: "single",
32599
+ optional: false
32600
+ }],
32601
+ "pipelineOrchestrator.getCameraStatus": [{
32602
+ name: "deviceId",
32603
+ form: "single",
32604
+ optional: false
32605
+ }],
32606
+ "pipelineOrchestrator.getCameraStatuses": [{
32607
+ name: "deviceIds",
32608
+ form: "array",
32609
+ optional: true
32610
+ }],
32611
+ "pipelineOrchestrator.getCameraStepOverrides": [{
32612
+ name: "deviceId",
32613
+ form: "single",
32614
+ optional: false
32615
+ }],
32616
+ "pipelineOrchestrator.getCameraSwitches": [{
32617
+ name: "deviceId",
32618
+ form: "single",
32619
+ optional: false
32620
+ }],
32621
+ "pipelineOrchestrator.getPipelineAssignment": [{
32622
+ name: "deviceId",
32623
+ form: "single",
32624
+ optional: false
32625
+ }],
32626
+ "pipelineOrchestrator.getPipelineDevicePin": [{
32627
+ name: "deviceId",
32628
+ form: "single",
32629
+ optional: false
32630
+ }],
32631
+ "pipelineOrchestrator.resolvePipeline": [{
32632
+ name: "deviceId",
32633
+ form: "single",
32634
+ optional: false
32635
+ }],
32636
+ "pipelineOrchestrator.setCameraPipelineForAgent": [{
32637
+ name: "deviceId",
32638
+ form: "single",
32639
+ optional: false
32640
+ }],
32641
+ "pipelineOrchestrator.setCameraStepOverride": [{
32642
+ name: "deviceId",
32643
+ form: "single",
32644
+ optional: false
32645
+ }],
32646
+ "pipelineOrchestrator.setCameraStepToggle": [{
32647
+ name: "deviceId",
32648
+ form: "single",
32649
+ optional: false
32650
+ }],
32651
+ "pipelineOrchestrator.setCameraSwitch": [{
32652
+ name: "deviceId",
32653
+ form: "single",
32654
+ optional: false
32655
+ }],
32656
+ "pipelineOrchestrator.setPipelineDevicePin": [{
32657
+ name: "deviceId",
32658
+ form: "single",
32659
+ optional: false
32660
+ }],
32661
+ "pipelineOrchestrator.unassignAudio": [{
32662
+ name: "deviceId",
32663
+ form: "single",
32664
+ optional: false
32665
+ }],
32666
+ "pipelineOrchestrator.unassignPipeline": [{
32667
+ name: "deviceId",
32668
+ form: "single",
32669
+ optional: false
32670
+ }],
32671
+ "pipelineRunner.attachCamera": [{
32672
+ name: "deviceId",
32673
+ form: "single",
32674
+ optional: false
32675
+ }],
32676
+ "pipelineRunner.detachCamera": [{
32677
+ name: "deviceId",
32678
+ form: "single",
32679
+ optional: false
32680
+ }],
32681
+ "pipelineRunner.getCameraMetrics": [{
32682
+ name: "deviceId",
32683
+ form: "single",
32684
+ optional: false
32685
+ }],
32686
+ "pipelineRunner.reportMotion": [{
32687
+ name: "deviceId",
32688
+ form: "single",
32689
+ optional: false
32690
+ }],
32691
+ "pipelineRunner.runDetailSubtree": [{
32692
+ name: "deviceId",
32693
+ form: "single",
32694
+ optional: false
32695
+ }],
32696
+ "pipelineRunner.runStatelessStep": [{
32697
+ name: "sourceDeviceId",
32698
+ form: "single",
32699
+ optional: false
32700
+ }],
32701
+ "plateGallery.getPlateByTrack": [{
32702
+ name: "deviceId",
32703
+ form: "single",
32704
+ optional: false
32705
+ }],
32706
+ "plateGallery.listPlates": [{
32707
+ name: "deviceId",
32708
+ form: "single",
32709
+ optional: true
32710
+ }],
32711
+ "privacyMask.getOptions": [{
32712
+ name: "deviceId",
32713
+ form: "single",
32714
+ optional: false
32715
+ }],
32716
+ "privacyMask.setAudioEnabled": [{
32717
+ name: "deviceId",
32718
+ form: "single",
32719
+ optional: false
32720
+ }],
32721
+ "privacyMask.setMask": [{
32722
+ name: "deviceId",
32723
+ form: "single",
32724
+ optional: false
32725
+ }],
32726
+ "ptz.continuousMove": [{
32727
+ name: "deviceId",
32728
+ form: "single",
32729
+ optional: false
32730
+ }],
32731
+ "ptz.deletePreset": [{
32732
+ name: "deviceId",
32733
+ form: "single",
32734
+ optional: false
32735
+ }],
32736
+ "ptz.getOptions": [{
32737
+ name: "deviceId",
32738
+ form: "single",
32739
+ optional: false
32740
+ }],
32741
+ "ptz.getPosition": [{
32742
+ name: "deviceId",
32743
+ form: "single",
32744
+ optional: false
32745
+ }],
32746
+ "ptz.getPresets": [{
32747
+ name: "deviceId",
32748
+ form: "single",
32749
+ optional: false
32750
+ }],
32751
+ "ptz.goHome": [{
32752
+ name: "deviceId",
32753
+ form: "single",
32754
+ optional: false
32755
+ }],
32756
+ "ptz.goToPreset": [{
32757
+ name: "deviceId",
32758
+ form: "single",
32759
+ optional: false
32760
+ }],
32761
+ "ptz.move": [{
32762
+ name: "deviceId",
32763
+ form: "single",
32764
+ optional: false
32765
+ }],
32766
+ "ptz.savePreset": [{
32767
+ name: "deviceId",
32768
+ form: "single",
32769
+ optional: false
32770
+ }],
32771
+ "ptz.setAutofocus": [{
32772
+ name: "deviceId",
32773
+ form: "single",
32774
+ optional: false
32775
+ }],
32776
+ "ptz.stop": [{
32777
+ name: "deviceId",
32778
+ form: "single",
32779
+ optional: false
32780
+ }],
32781
+ "ptzAutotrack.getSettings": [{
32782
+ name: "deviceId",
32783
+ form: "single",
32784
+ optional: false
32785
+ }],
32786
+ "ptzAutotrack.getStatus": [{
32787
+ name: "deviceId",
32788
+ form: "single",
32789
+ optional: false
32790
+ }],
32791
+ "ptzAutotrack.setEnabled": [{
32792
+ name: "deviceId",
32793
+ form: "single",
32794
+ optional: false
32795
+ }],
32796
+ "ptzAutotrack.setSettings": [{
32797
+ name: "deviceId",
32798
+ form: "single",
32799
+ optional: false
32800
+ }],
32801
+ "reboot.reboot": [{
32802
+ name: "deviceId",
32803
+ form: "single",
32804
+ optional: false
32805
+ }],
32806
+ "recording.deleteFootprint": [{
32807
+ name: "deviceId",
32808
+ form: "single",
32809
+ optional: false
32810
+ }],
32811
+ "recording.getAvailability": [{
32812
+ name: "deviceId",
32813
+ form: "single",
32814
+ optional: false
32815
+ }],
32816
+ "recording.getDaysWithRecordings": [{
32817
+ name: "deviceId",
32818
+ form: "single",
32819
+ optional: false
32820
+ }],
32821
+ "recording.getDeviceConfig": [{
32822
+ name: "deviceId",
32823
+ form: "single",
32824
+ optional: false
32825
+ }],
32826
+ "recording.getPlaybackManifest": [{
32827
+ name: "deviceId",
32828
+ form: "single",
32829
+ optional: false
32830
+ }],
32831
+ "recording.listOpsLog": [{
32832
+ name: "deviceId",
32833
+ form: "single",
32834
+ optional: true
32835
+ }],
32836
+ "recording.locateSegment": [{
32837
+ name: "deviceId",
32838
+ form: "single",
32839
+ optional: false
32840
+ }],
32841
+ "recording.pruneFootage": [{
32842
+ name: "deviceId",
32843
+ form: "single",
32844
+ optional: false
32845
+ }],
32846
+ "recording.readGopBytes": [{
32847
+ name: "deviceId",
32848
+ form: "single",
32849
+ optional: false
32850
+ }],
32851
+ "recording.readSegmentBytes": [{
32852
+ name: "deviceId",
32853
+ form: "single",
32854
+ optional: false
32855
+ }],
32856
+ "recording.relocateFootage": [{
32857
+ name: "deviceId",
32858
+ form: "single",
32859
+ optional: true
32860
+ }],
32861
+ "recording.renderClip": [{
32862
+ name: "deviceId",
32863
+ form: "single",
32864
+ optional: false
32865
+ }],
32866
+ "recording.renderGif": [{
32867
+ name: "deviceId",
32868
+ form: "single",
32869
+ optional: false
32870
+ }],
32871
+ "recording.rescanStorage": [{
32872
+ name: "deviceId",
32873
+ form: "single",
32874
+ optional: false
32875
+ }],
32876
+ "recording.setDeviceConfig": [{
32877
+ name: "deviceId",
32878
+ form: "single",
32879
+ optional: false
32880
+ }],
32881
+ "recording.startStorageMigrationMove": [{
32882
+ name: "deviceId",
32883
+ form: "single",
32884
+ optional: true
32885
+ }],
32886
+ "recordingExport.createExport": [{
32887
+ name: "deviceId",
32888
+ form: "single",
32889
+ optional: false
32890
+ }],
32891
+ "recordingExport.listExports": [{
32892
+ name: "deviceId",
32893
+ form: "single",
32894
+ optional: true
32895
+ }],
32896
+ "sceneMonitor.captureReference": [{
32897
+ name: "deviceId",
32898
+ form: "single",
32899
+ optional: false
32900
+ }],
32901
+ "sceneMonitor.createScene": [{
32902
+ name: "deviceId",
32903
+ form: "single",
32904
+ optional: false
32905
+ }],
32906
+ "sceneMonitor.deleteReference": [{
32907
+ name: "deviceId",
32908
+ form: "single",
32909
+ optional: false
32910
+ }],
32911
+ "sceneMonitor.deleteScene": [{
32912
+ name: "deviceId",
32913
+ form: "single",
32914
+ optional: false
32915
+ }],
32916
+ "sceneMonitor.listScenes": [{
32917
+ name: "deviceId",
32918
+ form: "single",
32919
+ optional: false
32920
+ }],
32921
+ "sceneMonitor.recheckNow": [{
32922
+ name: "deviceId",
32923
+ form: "single",
32924
+ optional: false
32925
+ }],
32926
+ "sceneMonitor.updateScene": [{
32927
+ name: "deviceId",
32928
+ form: "single",
32929
+ optional: false
32930
+ }],
32931
+ "scriptRunner.run": [{
32932
+ name: "deviceId",
32933
+ form: "single",
32934
+ optional: false
32935
+ }],
32936
+ "scriptRunner.stop": [{
32937
+ name: "deviceId",
32938
+ form: "single",
32939
+ optional: false
32940
+ }],
32941
+ "snapshot.getSnapshot": [{
32942
+ name: "deviceId",
32943
+ form: "single",
32944
+ optional: false
32945
+ }],
32946
+ "snapshot.getSnapshotOverview": [{
32947
+ name: "deviceIds",
32948
+ form: "array",
32949
+ optional: false
32950
+ }],
32951
+ "snapshot.invalidateCache": [{
32952
+ name: "deviceId",
32953
+ form: "single",
32954
+ optional: false
32955
+ }],
32956
+ "streamBroker.acquireEgressTranscode": [{
32957
+ name: "deviceId",
32958
+ form: "single",
32959
+ optional: false
32960
+ }],
32961
+ "streamBroker.assignProfile": [{
32962
+ name: "deviceId",
32963
+ form: "single",
32964
+ optional: false
32965
+ }],
32966
+ "streamBroker.getDeviceAudioMute": [{
32967
+ name: "deviceId",
32968
+ form: "single",
32969
+ optional: false
32970
+ }],
32971
+ "streamBroker.getStreamWithCodec": [{
32972
+ name: "deviceId",
32973
+ form: "single",
32974
+ optional: false
32975
+ }],
32976
+ "streamBroker.produceEventMedia": [{
32977
+ name: "deviceId",
32978
+ form: "single",
32979
+ optional: false
32980
+ }],
32981
+ "streamBroker.publishCameraStream": [{
32982
+ name: "deviceId",
32983
+ form: "single",
32984
+ optional: false
32985
+ }],
32986
+ "streamBroker.renderPreBufferClip": [{
32987
+ name: "deviceId",
32988
+ form: "single",
32989
+ optional: false
32990
+ }],
32991
+ "streamBroker.restartProfile": [{
32992
+ name: "deviceId",
32993
+ form: "single",
32994
+ optional: false
32995
+ }],
32996
+ "streamBroker.retractCameraStream": [{
32997
+ name: "deviceId",
32998
+ form: "single",
32999
+ optional: false
33000
+ }],
33001
+ "streamBroker.setDeviceAudioMute": [{
33002
+ name: "deviceId",
33003
+ form: "single",
33004
+ optional: false
33005
+ }],
33006
+ "streamBroker.unassignProfile": [{
33007
+ name: "deviceId",
33008
+ form: "single",
33009
+ optional: false
33010
+ }],
33011
+ "streamCatalog.getCatalog": [{
33012
+ name: "deviceId",
33013
+ form: "single",
33014
+ optional: false
33015
+ }],
33016
+ "streamParams.getConfigSchema": [{
33017
+ name: "deviceId",
33018
+ form: "single",
33019
+ optional: false
33020
+ }],
33021
+ "streamParams.getOptions": [{
33022
+ name: "deviceId",
33023
+ form: "single",
33024
+ optional: false
33025
+ }],
33026
+ "streamParams.setProfile": [{
33027
+ name: "deviceId",
33028
+ form: "single",
33029
+ optional: false
33030
+ }],
33031
+ "switch.setState": [{
33032
+ name: "deviceId",
33033
+ form: "single",
33034
+ optional: false
33035
+ }],
33036
+ "vacuumControl.locate": [{
33037
+ name: "deviceId",
33038
+ form: "single",
33039
+ optional: false
33040
+ }],
33041
+ "vacuumControl.pause": [{
33042
+ name: "deviceId",
33043
+ form: "single",
33044
+ optional: false
33045
+ }],
33046
+ "vacuumControl.returnToBase": [{
33047
+ name: "deviceId",
33048
+ form: "single",
33049
+ optional: false
33050
+ }],
33051
+ "vacuumControl.setFanSpeed": [{
33052
+ name: "deviceId",
33053
+ form: "single",
33054
+ optional: false
33055
+ }],
33056
+ "vacuumControl.start": [{
33057
+ name: "deviceId",
33058
+ form: "single",
33059
+ optional: false
33060
+ }],
33061
+ "vacuumControl.stop": [{
33062
+ name: "deviceId",
33063
+ form: "single",
33064
+ optional: false
33065
+ }],
33066
+ "valve.close": [{
33067
+ name: "deviceId",
33068
+ form: "single",
33069
+ optional: false
33070
+ }],
33071
+ "valve.open": [{
33072
+ name: "deviceId",
33073
+ form: "single",
33074
+ optional: false
33075
+ }],
33076
+ "valve.setPosition": [{
33077
+ name: "deviceId",
33078
+ form: "single",
33079
+ optional: false
33080
+ }],
33081
+ "valve.stop": [{
33082
+ name: "deviceId",
33083
+ form: "single",
33084
+ optional: false
33085
+ }],
33086
+ "videoclips.getClipPlayback": [{
33087
+ name: "deviceId",
33088
+ form: "single",
33089
+ optional: false
33090
+ }],
33091
+ "videoclips.listClips": [{
33092
+ name: "deviceId",
33093
+ form: "single",
33094
+ optional: false
33095
+ }],
33096
+ "waterHeater.setAway": [{
33097
+ name: "deviceId",
33098
+ form: "single",
33099
+ optional: false
33100
+ }],
33101
+ "waterHeater.setOperationMode": [{
33102
+ name: "deviceId",
33103
+ form: "single",
33104
+ optional: false
33105
+ }],
33106
+ "waterHeater.setTargetTemp": [{
33107
+ name: "deviceId",
33108
+ form: "single",
33109
+ optional: false
33110
+ }],
33111
+ "webrtcSession.addIceCandidate": [{
33112
+ name: "deviceId",
33113
+ form: "single",
33114
+ optional: false
33115
+ }],
33116
+ "webrtcSession.closeSession": [{
33117
+ name: "deviceId",
33118
+ form: "single",
33119
+ optional: false
33120
+ }],
33121
+ "webrtcSession.createSession": [{
33122
+ name: "deviceId",
33123
+ form: "single",
33124
+ optional: false
33125
+ }],
33126
+ "webrtcSession.getIceCandidates": [{
33127
+ name: "deviceId",
33128
+ form: "single",
33129
+ optional: false
33130
+ }],
33131
+ "webrtcSession.getSessionState": [{
33132
+ name: "deviceId",
33133
+ form: "single",
33134
+ optional: false
33135
+ }],
33136
+ "webrtcSession.handleAnswer": [{
33137
+ name: "deviceId",
33138
+ form: "single",
33139
+ optional: false
33140
+ }],
33141
+ "webrtcSession.handleOffer": [{
33142
+ name: "deviceId",
33143
+ form: "single",
33144
+ optional: false
33145
+ }],
33146
+ "webrtcSession.hasAdaptiveBitrate": [{
33147
+ name: "deviceId",
33148
+ form: "single",
33149
+ optional: false
33150
+ }],
33151
+ "webrtcSession.listStreams": [{
33152
+ name: "deviceId",
33153
+ form: "single",
33154
+ optional: false
33155
+ }],
33156
+ "zoneAnalytics.getCameraHistory": [{
33157
+ name: "deviceId",
33158
+ form: "single",
33159
+ optional: false
33160
+ }],
33161
+ "zoneAnalytics.getCurrentSnapshot": [{
33162
+ name: "deviceId",
33163
+ form: "single",
33164
+ optional: false
33165
+ }],
33166
+ "zoneAnalytics.getUnzonedHistory": [{
33167
+ name: "deviceId",
33168
+ form: "single",
33169
+ optional: false
33170
+ }],
33171
+ "zoneAnalytics.getZoneHistory": [{
33172
+ name: "deviceId",
33173
+ form: "single",
33174
+ optional: false
33175
+ }],
33176
+ "zoneRules.listRules": [{
33177
+ name: "deviceId",
33178
+ form: "single",
33179
+ optional: false
33180
+ }],
33181
+ "zoneRules.setRules": [{
33182
+ name: "deviceId",
33183
+ form: "single",
33184
+ optional: false
33185
+ }],
33186
+ "zones.addZone": [{
33187
+ name: "deviceId",
33188
+ form: "single",
33189
+ optional: false
33190
+ }],
33191
+ "zones.listZones": [{
33192
+ name: "deviceId",
33193
+ form: "single",
33194
+ optional: false
33195
+ }],
33196
+ "zones.removeZone": [{
33197
+ name: "deviceId",
33198
+ form: "single",
33199
+ optional: false
33200
+ }],
33201
+ "zones.updateZone": [{
33202
+ name: "deviceId",
33203
+ form: "single",
33204
+ optional: false
33205
+ }]
33206
+ });
31378
33207
  Object.freeze({
31379
33208
  "broker": "broker",
31380
33209
  "device-export": "device-export",
@@ -32094,7 +33923,7 @@ var Fmp4FragmentChild = class {
32094
33923
  meta: {
32095
33924
  sourceId: this.args.sourceId,
32096
33925
  decodeHwAccel: requested,
32097
- error: errMsg$12(err)
33926
+ error: errMsg$15(err)
32098
33927
  }
32099
33928
  });
32100
33929
  this.killChild();
@@ -32273,7 +34102,7 @@ var Fmp4FragmentChild = class {
32273
34102
  tags: { deviceId: this.args.deviceId },
32274
34103
  meta: {
32275
34104
  sourceId: this.args.sourceId,
32276
- error: errMsg$12(err)
34105
+ error: errMsg$15(err)
32277
34106
  }
32278
34107
  });
32279
34108
  }
@@ -78618,7 +80447,7 @@ function clearPairingFiles(accessoryUuid, logger) {
78618
80447
  }
78619
80448
  //#endregion
78620
80449
  //#region src/hap-setup-uri.ts
78621
- function errMsg$11(e) {
80450
+ function errMsg$14(e) {
78622
80451
  return e instanceof Error ? e.message : String(e);
78623
80452
  }
78624
80453
  /**
@@ -78645,11 +80474,79 @@ function firstExposedAccessorySetupUri(exposed, logger) {
78645
80474
  try {
78646
80475
  return first.setupURI();
78647
80476
  } catch (err) {
78648
- logger.debug("export-hap: setupURI failed on first exposed accessory", { meta: { error: errMsg$11(err) } });
80477
+ logger.debug("export-hap: setupURI failed on first exposed accessory", { meta: { error: errMsg$14(err) } });
78649
80478
  return;
78650
80479
  }
78651
80480
  }
78652
80481
  }
80482
+ //#endregion
80483
+ //#region src/mappers/builders/accessory-info.ts
80484
+ /**
80485
+ * `Service.AccessoryInformation` — the manufacturer / model / firmware /
80486
+ * serial block every HomeKit accessory carries.
80487
+ *
80488
+ * One implementation for both accessory shapes (camera and generic): the
80489
+ * fields come from the device's own metadata either way, and a second copy
80490
+ * would be a second answer to "what serial does this device publish".
80491
+ * Metadata is best-effort — a device that cannot answer still publishes.
80492
+ */
80493
+ async function populateAccessoryInfo(accessory, proxy, displayName, modelFallback) {
80494
+ const info = accessory.getService(import_dist.Service.AccessoryInformation);
80495
+ if (!info) return;
80496
+ try {
80497
+ const device = await proxy.deviceManager?.getDevice({});
80498
+ const metadata = readMetadata(device);
80499
+ info.setCharacteristic(import_dist.Characteristic.Name, device?.name ?? displayName);
80500
+ info.setCharacteristic(import_dist.Characteristic.Manufacturer, stringOr(metadata?.manufacturer, "CamStack"));
80501
+ info.setCharacteristic(import_dist.Characteristic.Model, stringOr(metadata?.model, modelFallback));
80502
+ info.setCharacteristic(import_dist.Characteristic.FirmwareRevision, stringOr(metadata?.firmware, "0.0.0"));
80503
+ info.setCharacteristic(import_dist.Characteristic.SerialNumber, stringOr(metadata?.sn, `camstack-${proxy.deviceId}`));
80504
+ } catch {}
80505
+ }
80506
+ /** The four fields this module reads, or `null` — the device record's
80507
+ * `metadata` is an open bag and nothing else here depends on its shape. */
80508
+ function readMetadata(device) {
80509
+ const metadata = device?.metadata;
80510
+ if (metadata === null || typeof metadata !== "object") return {};
80511
+ const entries = new Map(Object.entries(metadata));
80512
+ return {
80513
+ model: stringOrNull(entries.get("model")),
80514
+ manufacturer: stringOrNull(entries.get("manufacturer")),
80515
+ firmware: stringOrNull(entries.get("firmware")),
80516
+ sn: stringOrNull(entries.get("sn"))
80517
+ };
80518
+ }
80519
+ function stringOrNull(value) {
80520
+ return typeof value === "string" ? value : null;
80521
+ }
80522
+ function stringOr(value, fallback) {
80523
+ return typeof value === "string" && value.length > 0 ? value : fallback;
80524
+ }
80525
+ //#endregion
80526
+ //#region src/mappers/builders/generic/characteristic-update.ts
80527
+ /**
80528
+ * Parse `status` with the capability's OWN Zod schema and turn it into
80529
+ * characteristic writes.
80530
+ *
80531
+ * Duck-typing the payload here would be the second source of truth about what a
80532
+ * cap reports; the schema is the first and only one. A payload that does not
80533
+ * match yields NO updates — never a partial or invented value — and the caller
80534
+ * logs the drop, because a sensor that silently stops moving is
80535
+ * indistinguishable from a sensor that never changed.
80536
+ *
80537
+ * Rows `.pick()` only the fields they read, so a provider omitting a timestamp
80538
+ * cannot silence a sensor.
80539
+ */
80540
+ function reader(schema, toUpdates) {
80541
+ return (status) => {
80542
+ const parsed = schema.safeParse(status);
80543
+ return parsed.success ? toUpdates(parsed.data) : [];
80544
+ };
80545
+ }
80546
+ /** Push every update onto `service`. */
80547
+ function applyUpdates(service, updates) {
80548
+ for (const update of updates) service.updateCharacteristic(update.characteristic, update.value);
80549
+ }
78653
80550
  /**
78654
80551
  * hap-nodejs' `checkName` regex, verbatim.
78655
80552
  *
@@ -78786,6 +80683,92 @@ function titleCase(raw) {
78786
80683
  return raw.split(/[-_\s]+/u).filter((part) => part.length > 0).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
78787
80684
  }
78788
80685
  //#endregion
80686
+ //#region src/mappers/builders/service-label.ts
80687
+ /**
80688
+ * The ONE place a secondary service on the camera accessory gets its label.
80689
+ *
80690
+ * A "secondary service" here is a Switch or Lightbulb published alongside the
80691
+ * camera on the same accessory — the privacy switch, each accessory child
80692
+ * (siren, floodlight), each PTZ action. iOS Home renders these as their own
80693
+ * controls, and the operator has seen them as "Interruttore 1", "Interruttore
80694
+ * 2" through three separate rounds of fixes.
80695
+ *
80696
+ * ## Why `Name` alone cannot rename anything
80697
+ *
80698
+ * Two facts about hap-nodejs 2.1.7, both measured against the installed copy
80699
+ * rather than reasoned about:
80700
+ *
80701
+ * 1. `accessory.addService(Type, displayName, subtype)` ALREADY writes
80702
+ * `displayName` to `Characteristic.Name` (`Service` constructor). So every
80703
+ * round of this bug — including the one that moved the label onto
80704
+ * `ConfiguredName` — shipped with `Name` correctly set. "iOS had no name
80705
+ * to render" was never true.
80706
+ * 2. The mDNS configuration number (`c#`) is a sha1 over
80707
+ * `internalHAPRepresentation(false)`, which OMITS characteristic VALUES.
80708
+ * Changing the string in `Name` therefore does not bump `c#`, a paired
80709
+ * controller gets no signal to re-read `/accessories`, and the name it
80710
+ * cached at first enumeration stands forever.
80711
+ *
80712
+ * `Name` is also declared `pr` only — paired read, no write, no notify. It is
80713
+ * the seed a controller seeds its database from once; it is not a channel.
80714
+ *
80715
+ * ## Why `ConfiguredName`
80716
+ *
80717
+ * `ConfiguredName` (`000000E3`) is declared `pr | pw | ev` — the only name
80718
+ * characteristic a controller may write and may subscribe to. It is what iOS
80719
+ * 16+ reads for a service the user can rename, and adding it CHANGES the
80720
+ * accessory structure, so `c#` does bump and the controller re-reads.
80721
+ *
80722
+ * It was removed once because hap-nodejs logged
80723
+ *
80724
+ * ```
80725
+ * Characteristic not in required or optional characteristic section for
80726
+ * service Switch. Adding anyway.
80727
+ * ```
80728
+ *
80729
+ * That line is a WARNING, not a rejection: `Service.getCharacteristic` calls
80730
+ * `addCharacteristic` unconditionally and only then emits the warning. The
80731
+ * characteristic was always present and always published. hap-nodejs'
80732
+ * per-service optional lists simply predate `ConfiguredName` being valid on
80733
+ * any service.
80734
+ *
80735
+ * Registering it with {@link Service.addOptionalCharacteristic} first takes
80736
+ * the branch above the warning, so the accessory still builds with ZERO
80737
+ * characteristic warnings — which is what `service-naming.spec.ts` asserts.
80738
+ *
80739
+ * ## Scope: EVERY service, including the sensors
80740
+ *
80741
+ * An earlier round applied this to Switch- and Lightbulb-shaped services only,
80742
+ * on the theory that `Service.MotionSensor` and `Service.Battery` are not
80743
+ * separately named tiles in iOS Home and that naming them would be a guess.
80744
+ *
80745
+ * That theory was never measured, and it had a cost: it left services on the
80746
+ * accessory whose name a paired controller could never be told about, and it
80747
+ * made "did `ConfiguredName` fix the operator's 'Interruttore N'?" unanswerable
80748
+ * — a negative result on a partial application proves nothing about the
80749
+ * mechanism. Every service this addon publishes now carries both
80750
+ * characteristics, on the camera accessory and on the generic one.
80751
+ *
80752
+ * The reasoning above is mechanism, not measurement: it says why `Name` alone
80753
+ * CANNOT work and why `ConfiguredName` is the only characteristic that can. It
80754
+ * does not prove iOS renders it on every service shape. That is an observation
80755
+ * only a re-paired controller can make — and after a change like this one, the
80756
+ * controller must be re-paired, because a cached accessory database is exactly
80757
+ * what the whole mechanism is about.
80758
+ */
80759
+ /**
80760
+ * Publish `name` as both the immutable `Name` and the controller-visible
80761
+ * `ConfiguredName` of `service`.
80762
+ *
80763
+ * `name` must already be HAP-valid — build it with `service-names.ts`, which
80764
+ * cannot return a string hap-nodejs' `checkName` would warn about.
80765
+ */
80766
+ function applyServiceLabel(service, name) {
80767
+ service.setCharacteristic(import_dist.Characteristic.Name, name);
80768
+ if (!service.optionalCharacteristics.some((characteristic) => characteristic.UUID === import_dist.Characteristic.ConfiguredName.UUID)) service.addOptionalCharacteristic(import_dist.Characteristic.ConfiguredName);
80769
+ service.setCharacteristic(import_dist.Characteristic.ConfiguredName, name);
80770
+ }
80771
+ //#endregion
78789
80772
  //#region src/mappers/builders/battery.ts
78790
80773
  /**
78791
80774
  * Battery builder — surfaces a battery-operated camera's power state
@@ -78795,34 +80778,56 @@ function titleCase(raw) {
78795
80778
  * subscribes to the runtime-state slice so iOS Home reflects level /
78796
80779
  * charging changes pushed by the firmware without a poll loop.
78797
80780
  *
80781
+ * The status→characteristic mapping is {@link batteryCharacteristicUpdates},
80782
+ * exported because the generic (non-camera) export path publishes the same
80783
+ * `Service.Battery` from the same cap — one derivation, two accessory shapes.
80784
+ *
80785
+ * Skipped silently when the `battery` cap is not bound — caller checks
80786
+ * cap presence before invoking this builder (see `camera-accessory.ts`).
80787
+ */
80788
+ var LOW_BATTERY_THRESHOLD_PCT = 20;
80789
+ /**
78798
80790
  * Mapping:
78799
80791
  * - `BatteryStatus.percentage` (0..100) → `Characteristic.BatteryLevel`
78800
80792
  * - `BatteryStatus.charging`:
78801
80793
  * `'none'` → `ChargingState.NOT_CHARGING`
78802
80794
  * `'dc' | 'solar'` → `ChargingState.CHARGING`
78803
80795
  * - `percentage <= LOW_BATTERY_THRESHOLD_PCT` → `StatusLowBattery.LOW`
78804
- *
78805
- * Skipped silently when the `battery` cap is not bound — caller checks
78806
- * cap presence before invoking this builder (see `camera-accessory.ts`).
78807
80796
  */
78808
- var LOW_BATTERY_THRESHOLD_PCT = 20;
80797
+ var batteryCharacteristicUpdates = reader(BatteryStatusSchema.pick({ percentage: true }).extend({ charging: BatteryStatusSchema.shape.charging.optional() }), (status) => {
80798
+ const pct = Math.max(0, Math.min(100, Math.round(status.percentage)));
80799
+ return [
80800
+ {
80801
+ characteristic: import_dist.Characteristic.BatteryLevel,
80802
+ value: pct
80803
+ },
80804
+ ...status.charging === void 0 ? [] : [{
80805
+ characteristic: import_dist.Characteristic.ChargingState,
80806
+ value: status.charging === "none" ? import_dist.Characteristic.ChargingState.NOT_CHARGING : import_dist.Characteristic.ChargingState.CHARGING
80807
+ }],
80808
+ {
80809
+ characteristic: import_dist.Characteristic.StatusLowBattery,
80810
+ value: pct <= LOW_BATTERY_THRESHOLD_PCT ? import_dist.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : import_dist.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL
80811
+ }
80812
+ ];
80813
+ });
78809
80814
  async function buildBattery(bctx) {
78810
80815
  const { ctx, accessory, proxy, numericDeviceId, displayName } = bctx;
78811
80816
  const log = ctx.logger.withTags({ deviceId: numericDeviceId });
78812
- const service = accessory.addService(import_dist.Service.Battery, hapServiceName([displayName], `Camera ${numericDeviceId}`));
80817
+ const label = hapServiceName([displayName], `Camera ${numericDeviceId}`);
80818
+ const service = accessory.addService(import_dist.Service.Battery, label);
80819
+ applyServiceLabel(service, label);
78813
80820
  try {
78814
80821
  const status = await proxy.battery?.getStatus({});
78815
- if (status) applyToService(service, status);
80822
+ if (status !== void 0 && status !== null) applyToService(service, status);
78816
80823
  } catch (err) {
78817
- log.debug("export-hap: battery getStatus hydrate failed (non-fatal)", { meta: { error: errMsg$10(err) } });
80824
+ log.debug("export-hap: battery getStatus hydrate failed (non-fatal)", { meta: { error: errMsg$13(err) } });
78818
80825
  }
78819
80826
  const unsubscribes = [];
78820
80827
  if (proxy.state.battery) {
78821
80828
  const unsub = proxy.state.battery.subscribe((value) => {
78822
80829
  if (!value) return;
78823
- const status = value;
78824
- if (typeof status.percentage !== "number") return;
78825
- applyToService(service, status);
80830
+ applyToService(service, value);
78826
80831
  });
78827
80832
  unsubscribes.push(unsub);
78828
80833
  }
@@ -78833,14 +80838,9 @@ async function buildBattery(bctx) {
78833
80838
  } };
78834
80839
  }
78835
80840
  function applyToService(service, status) {
78836
- const pct = Math.max(0, Math.min(100, Math.round(status.percentage)));
78837
- service.updateCharacteristic(import_dist.Characteristic.BatteryLevel, pct);
78838
- const chargingState = status.charging === "none" ? import_dist.Characteristic.ChargingState.NOT_CHARGING : import_dist.Characteristic.ChargingState.CHARGING;
78839
- service.updateCharacteristic(import_dist.Characteristic.ChargingState, chargingState);
78840
- const lowBattery = pct <= LOW_BATTERY_THRESHOLD_PCT ? import_dist.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : import_dist.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL;
78841
- service.updateCharacteristic(import_dist.Characteristic.StatusLowBattery, lowBattery);
80841
+ applyUpdates(service, batteryCharacteristicUpdates(status));
78842
80842
  }
78843
- function errMsg$10(err) {
80843
+ function errMsg$13(err) {
78844
80844
  return err instanceof Error ? err.message : String(err);
78845
80845
  }
78846
80846
  //#endregion
@@ -89592,7 +91592,7 @@ var VIDEO_LOOPBACK_RCVBUF_BYTES = 8 * 1024 * 1024;
89592
91592
  * not burst size.
89593
91593
  */
89594
91594
  var AUDIO_LOOPBACK_RCVBUF_BYTES = 1024 * 1024;
89595
- function errMsg$9(err) {
91595
+ function errMsg$12(err) {
89596
91596
  return err instanceof Error ? err.message : String(err);
89597
91597
  }
89598
91598
  /**
@@ -89607,13 +91607,13 @@ function applyReceiveBuffer(socket, requestedBytes) {
89607
91607
  try {
89608
91608
  socket.setRecvBufferSize(requestedBytes);
89609
91609
  } catch (err) {
89610
- error = errMsg$9(err);
91610
+ error = errMsg$12(err);
89611
91611
  }
89612
91612
  let effectiveBytes = null;
89613
91613
  try {
89614
91614
  effectiveBytes = socket.getRecvBufferSize();
89615
91615
  } catch (err) {
89616
- if (error === null) error = errMsg$9(err);
91616
+ if (error === null) error = errMsg$12(err);
89617
91617
  }
89618
91618
  return {
89619
91619
  requestedBytes,
@@ -89873,20 +91873,20 @@ function buildCameraStreamingDelegate(bctx, advertised) {
89873
91873
  delegate: {
89874
91874
  handleSnapshotRequest(request, callback) {
89875
91875
  handleSnapshot(bctx, request).then((buf) => callback(void 0, buf)).catch((err) => {
89876
- log.warn("export-hap: snapshot failed", { meta: { error: errMsg$8(err) } });
89877
- callback(err instanceof Error ? err : new Error(errMsg$8(err)));
91876
+ log.warn("export-hap: snapshot failed", { meta: { error: errMsg$11(err) } });
91877
+ callback(err instanceof Error ? err : new Error(errMsg$11(err)));
89878
91878
  });
89879
91879
  },
89880
91880
  prepareStream(request, callback) {
89881
91881
  prepareStream(request, sessions, bctx).then((resp) => callback(void 0, resp)).catch((err) => {
89882
- log.warn("export-hap: prepareStream failed", { meta: { error: errMsg$8(err) } });
89883
- callback(err instanceof Error ? err : new Error(errMsg$8(err)));
91882
+ log.warn("export-hap: prepareStream failed", { meta: { error: errMsg$11(err) } });
91883
+ callback(err instanceof Error ? err : new Error(errMsg$11(err)));
89884
91884
  });
89885
91885
  },
89886
91886
  handleStreamRequest(request, callback) {
89887
91887
  handleStreamRequest(request, sessions, bctx, advertised).then(() => callback()).catch((err) => {
89888
- log.warn("export-hap: handleStreamRequest failed", { meta: { error: errMsg$8(err) } });
89889
- callback(err instanceof Error ? err : new Error(errMsg$8(err)));
91888
+ log.warn("export-hap: handleStreamRequest failed", { meta: { error: errMsg$11(err) } });
91889
+ callback(err instanceof Error ? err : new Error(errMsg$11(err)));
89890
91890
  });
89891
91891
  }
89892
91892
  },
@@ -90002,7 +92002,7 @@ async function prepareStream(request, sessions, bctx) {
90002
92002
  closeSocket(audioUdp);
90003
92003
  closeSocket(videoLoopUdp);
90004
92004
  closeSocket(audioLoopUdp);
90005
- throw new Error(`export-hap: outbound SrtpSession init failed: ${errMsg$8(err)}`, { cause: err });
92005
+ throw new Error(`export-hap: outbound SrtpSession init failed: ${errMsg$11(err)}`, { cause: err });
90006
92006
  }
90007
92007
  let upstreamAudioSrtp = null;
90008
92008
  try {
@@ -90016,7 +92016,7 @@ async function prepareStream(request, sessions, bctx) {
90016
92016
  profile: import_src.ProtectionProfileAes128CmHmacSha1_80
90017
92017
  });
90018
92018
  } catch (err) {
90019
- bctx.ctx.logger.withTags({ deviceId: bctx.numericDeviceId }).warn("export-hap: SrtpSession init failed (upstream audio decrypt disabled)", { meta: { error: errMsg$8(err) } });
92019
+ bctx.ctx.logger.withTags({ deviceId: bctx.numericDeviceId }).warn("export-hap: SrtpSession init failed (upstream audio decrypt disabled)", { meta: { error: errMsg$11(err) } });
90020
92020
  }
90021
92021
  const videoSsrc = randomSsrc();
90022
92022
  const audioSsrc = randomSsrc();
@@ -90133,7 +92133,7 @@ async function prepareStream(request, sessions, bctx) {
90133
92133
  });
90134
92134
  audioUdp.on("message", (packet, rinfo) => {
90135
92135
  handleIncomingAudioRtp(session, packet, rinfo.address, bctx).catch((err) => {
90136
- bctx.ctx.logger.withTags({ deviceId: bctx.numericDeviceId }).debug("export-hap: incoming-audio handler error (dropped)", { meta: { error: errMsg$8(err) } });
92136
+ bctx.ctx.logger.withTags({ deviceId: bctx.numericDeviceId }).debug("export-hap: incoming-audio handler error (dropped)", { meta: { error: errMsg$11(err) } });
90137
92137
  });
90138
92138
  });
90139
92139
  logLoopbackBuffer(tagLog, request.sessionID, "video", videoLoop.buffer);
@@ -90329,7 +92329,7 @@ function readControllerRtcp(session, leg, packet, log) {
90329
92329
  } catch (err) {
90330
92330
  drop(session, "inbound-rtcp-decrypt-failed");
90331
92331
  storeReceiverReports(session, leg, recordUnreadableRtcp(tally));
90332
- logUnreadableRtcp(session, leg, `decrypt: ${errMsg$8(err)}`, log);
92332
+ logUnreadableRtcp(session, leg, `decrypt: ${errMsg$11(err)}`, log);
90333
92333
  return;
90334
92334
  }
90335
92335
  const outcome = ingestDecryptedRtcp(plaintext, tally);
@@ -91095,7 +93095,7 @@ async function handleIncomingAudioRtp(session, packet, sourceAddress, bctx) {
91095
93095
  session.upstreamRtpDecryptFailures += 1;
91096
93096
  if (session.upstreamRtpDecryptFailures % 100 === 1) log.debug("export-hap: SRTP decrypt failed (rate-limited)", { meta: {
91097
93097
  failures: session.upstreamRtpDecryptFailures,
91098
- error: errMsg$8(err)
93098
+ error: errMsg$11(err)
91099
93099
  } });
91100
93100
  return;
91101
93101
  }
@@ -91109,7 +93109,7 @@ async function handleIncomingAudioRtp(session, packet, sourceAddress, bctx) {
91109
93109
  rtpPayloadType = parsedRtp.header.payloadType;
91110
93110
  } catch (err) {
91111
93111
  drop(session, "upstream-parse-failed");
91112
- log.debug("export-hap: RTP parse failed after decrypt", { meta: { error: errMsg$8(err) } });
93112
+ log.debug("export-hap: RTP parse failed after decrypt", { meta: { error: errMsg$11(err) } });
91113
93113
  return;
91114
93114
  }
91115
93115
  const negotiatedAudioPt = session.lastStartParams?.audioPt;
@@ -91133,7 +93133,7 @@ async function handleIncomingAudioRtp(session, packet, sourceAddress, bctx) {
91133
93133
  if (session.intercomTalkSessionId === null) {
91134
93134
  session.intercomTalkSessionId = "";
91135
93135
  const opened = await openIntercomTalkSession(bctx).catch((err) => {
91136
- log.warn("export-hap: intercom.startTalkSession failed", { meta: { error: errMsg$8(err) } });
93136
+ log.warn("export-hap: intercom.startTalkSession failed", { meta: { error: errMsg$11(err) } });
91137
93137
  return null;
91138
93138
  });
91139
93139
  if (opened) {
@@ -91160,7 +93160,7 @@ async function handleIncomingAudioRtp(session, packet, sourceAddress, bctx) {
91160
93160
  drop(session, "upstream-push-failed");
91161
93161
  log.debug("export-hap: intercom.pushTalkAudio failed (will re-open on next frame)", { meta: {
91162
93162
  sequenceNumber: session.intercomPcmSequence,
91163
- error: errMsg$8(err)
93163
+ error: errMsg$11(err)
91164
93164
  } });
91165
93165
  session.intercomTalkSessionId = null;
91166
93166
  }
@@ -91192,7 +93192,7 @@ async function closeIntercomTalkSession(session, bctx) {
91192
93192
  } catch (err) {
91193
93193
  log.debug("export-hap: intercom.endTalkSession failed (continuing)", { meta: {
91194
93194
  sessionId,
91195
- error: errMsg$8(err)
93195
+ error: errMsg$11(err)
91196
93196
  } });
91197
93197
  }
91198
93198
  }
@@ -91217,7 +93217,7 @@ function closeSocket(udp) {
91217
93217
  function randomSsrc() {
91218
93218
  return Math.floor(Math.random() * 2147483646) + 1 | 0;
91219
93219
  }
91220
- function errMsg$8(err) {
93220
+ function errMsg$11(err) {
91221
93221
  return err instanceof Error ? err.message : String(err);
91222
93222
  }
91223
93223
  //#endregion
@@ -91301,14 +93301,14 @@ async function buildDoorbell(input) {
91301
93301
  }
91302
93302
  log.info("export-hap: doorbell SINGLE_PRESS pushed to HomeKit", { meta: { ...delivery } });
91303
93303
  } catch (err) {
91304
- log.warn("export-hap: ringDoorbell() failed", { meta: { error: errMsg$7(err) } });
93304
+ log.warn("export-hap: ringDoorbell() failed", { meta: { error: errMsg$10(err) } });
91305
93305
  }
91306
93306
  });
91307
93307
  return { async dispose() {
91308
93308
  unsubscribe();
91309
93309
  } };
91310
93310
  }
91311
- function errMsg$7(err) {
93311
+ function errMsg$10(err) {
91312
93312
  return err instanceof Error ? err.message : String(err);
91313
93313
  }
91314
93314
  //#endregion
@@ -91348,13 +93348,15 @@ var RESET_DEBOUNCE_MS = 5e3;
91348
93348
  */
91349
93349
  async function buildMotionSensor(bctx, existing = null) {
91350
93350
  const { ctx, accessory, proxy, numericDeviceId, displayName } = bctx;
91351
- const motionService = existing ?? accessory.addService(import_dist.Service.MotionSensor, hapServiceName([displayName], `Camera ${numericDeviceId}`));
93351
+ const label = hapServiceName([displayName], `Camera ${numericDeviceId}`);
93352
+ const motionService = existing ?? accessory.addService(import_dist.Service.MotionSensor, label);
93353
+ applyServiceLabel(motionService, label);
91352
93354
  motionService.setCharacteristic(import_dist.Characteristic.MotionDetected, false);
91353
93355
  try {
91354
93356
  const detected = await proxy.motion?.isDetected({});
91355
93357
  if (typeof detected === "boolean") motionService.updateCharacteristic(import_dist.Characteristic.MotionDetected, detected);
91356
93358
  } catch (err) {
91357
- ctx.logger.withTags({ deviceId: numericDeviceId }).debug("export-hap: initial motion hydrate failed (non-fatal)", { meta: { error: errMsg$6(err) } });
93359
+ ctx.logger.withTags({ deviceId: numericDeviceId }).debug("export-hap: initial motion hydrate failed (non-fatal)", { meta: { error: errMsg$9(err) } });
91358
93360
  }
91359
93361
  let resetTimer = null;
91360
93362
  const armReset = () => {
@@ -91388,82 +93390,10 @@ async function buildMotionSensor(bctx, existing = null) {
91388
93390
  }
91389
93391
  } };
91390
93392
  }
91391
- function errMsg$6(err) {
93393
+ function errMsg$9(err) {
91392
93394
  return err instanceof Error ? err.message : String(err);
91393
93395
  }
91394
93396
  //#endregion
91395
- //#region src/mappers/builders/service-label.ts
91396
- /**
91397
- * The ONE place a secondary service on the camera accessory gets its label.
91398
- *
91399
- * A "secondary service" here is a Switch or Lightbulb published alongside the
91400
- * camera on the same accessory — the privacy switch, each accessory child
91401
- * (siren, floodlight), each PTZ action. iOS Home renders these as their own
91402
- * controls, and the operator has seen them as "Interruttore 1", "Interruttore
91403
- * 2" through three separate rounds of fixes.
91404
- *
91405
- * ## Why `Name` alone cannot rename anything
91406
- *
91407
- * Two facts about hap-nodejs 2.1.7, both measured against the installed copy
91408
- * rather than reasoned about:
91409
- *
91410
- * 1. `accessory.addService(Type, displayName, subtype)` ALREADY writes
91411
- * `displayName` to `Characteristic.Name` (`Service` constructor). So every
91412
- * round of this bug — including the one that moved the label onto
91413
- * `ConfiguredName` — shipped with `Name` correctly set. "iOS had no name
91414
- * to render" was never true.
91415
- * 2. The mDNS configuration number (`c#`) is a sha1 over
91416
- * `internalHAPRepresentation(false)`, which OMITS characteristic VALUES.
91417
- * Changing the string in `Name` therefore does not bump `c#`, a paired
91418
- * controller gets no signal to re-read `/accessories`, and the name it
91419
- * cached at first enumeration stands forever.
91420
- *
91421
- * `Name` is also declared `pr` only — paired read, no write, no notify. It is
91422
- * the seed a controller seeds its database from once; it is not a channel.
91423
- *
91424
- * ## Why `ConfiguredName`
91425
- *
91426
- * `ConfiguredName` (`000000E3`) is declared `pr | pw | ev` — the only name
91427
- * characteristic a controller may write and may subscribe to. It is what iOS
91428
- * 16+ reads for a service the user can rename, and adding it CHANGES the
91429
- * accessory structure, so `c#` does bump and the controller re-reads.
91430
- *
91431
- * It was removed once because hap-nodejs logged
91432
- *
91433
- * ```
91434
- * Characteristic not in required or optional characteristic section for
91435
- * service Switch. Adding anyway.
91436
- * ```
91437
- *
91438
- * That line is a WARNING, not a rejection: `Service.getCharacteristic` calls
91439
- * `addCharacteristic` unconditionally and only then emits the warning. The
91440
- * characteristic was always present and always published. hap-nodejs'
91441
- * per-service optional lists simply predate `ConfiguredName` being valid on
91442
- * any service.
91443
- *
91444
- * Registering it with {@link Service.addOptionalCharacteristic} first takes
91445
- * the branch above the warning, so the accessory still builds with ZERO
91446
- * characteristic warnings — which is what `service-naming.spec.ts` asserts.
91447
- *
91448
- * ## Scope
91449
- *
91450
- * Switch- and Lightbulb-shaped services only. `Service.MotionSensor` on a
91451
- * camera accessory is not a separately named tile in iOS Home, so giving it a
91452
- * writable name would be a guess, and this module does not guess.
91453
- */
91454
- /**
91455
- * Publish `name` as both the immutable `Name` and the controller-visible
91456
- * `ConfiguredName` of `service`.
91457
- *
91458
- * `name` must already be HAP-valid — build it with `service-names.ts`, which
91459
- * cannot return a string hap-nodejs' `checkName` would warn about.
91460
- */
91461
- function applyServiceLabel(service, name) {
91462
- service.setCharacteristic(import_dist.Characteristic.Name, name);
91463
- if (!service.optionalCharacteristics.some((characteristic) => characteristic.UUID === import_dist.Characteristic.ConfiguredName.UUID)) service.addOptionalCharacteristic(import_dist.Characteristic.ConfiguredName);
91464
- service.setCharacteristic(import_dist.Characteristic.ConfiguredName, name);
91465
- }
91466
- //#endregion
91467
93397
  //#region src/mappers/builders/privacy-switch.ts
91468
93398
  /**
91469
93399
  * Privacy-mask switch builder — turns the camstack `privacy-mask` cap's
@@ -91491,7 +93421,7 @@ async function buildPrivacySwitch(bctx) {
91491
93421
  const status = await proxy.privacyMask?.getStatus({});
91492
93422
  if (status && typeof status.enabled === "boolean") service.updateCharacteristic(import_dist.Characteristic.On, status.enabled);
91493
93423
  } catch (err) {
91494
- log.debug("export-hap: privacy-mask getStatus hydrate failed (non-fatal)", { meta: { error: errMsg$5(err) } });
93424
+ log.debug("export-hap: privacy-mask getStatus hydrate failed (non-fatal)", { meta: { error: errMsg$8(err) } });
91495
93425
  }
91496
93426
  service.getCharacteristic(import_dist.Characteristic.On).onSet(async (value) => {
91497
93427
  const enabled = value === true;
@@ -91500,7 +93430,7 @@ async function buildPrivacySwitch(bctx) {
91500
93430
  } catch (err) {
91501
93431
  log.warn("export-hap: privacy-mask setMask failed", { meta: {
91502
93432
  enabled,
91503
- error: errMsg$5(err)
93433
+ error: errMsg$8(err)
91504
93434
  } });
91505
93435
  }
91506
93436
  });
@@ -91518,7 +93448,7 @@ async function buildPrivacySwitch(bctx) {
91518
93448
  } catch {}
91519
93449
  } };
91520
93450
  }
91521
- function errMsg$5(err) {
93451
+ function errMsg$8(err) {
91522
93452
  return err instanceof Error ? err.message : String(err);
91523
93453
  }
91524
93454
  //#endregion
@@ -91609,7 +93539,7 @@ async function buildPtz(bctx) {
91609
93539
  } catch (err) {
91610
93540
  log.warn("export-hap: ptz.goToPreset failed", { meta: {
91611
93541
  presetId: preset.id,
91612
- error: errMsg$4(err)
93542
+ error: errMsg$7(err)
91613
93543
  } });
91614
93544
  }
91615
93545
  armReset(() => service.updateCharacteristic(import_dist.Characteristic.On, false), MOMENTARY_RESET_MS);
@@ -91627,14 +93557,14 @@ async function buildPtz(bctx) {
91627
93557
  try {
91628
93558
  await proxy.ptz?.stop({});
91629
93559
  } catch (err) {
91630
- log.debug("export-hap: ptz.stop failed (non-fatal)", { meta: { error: errMsg$4(err) } });
93560
+ log.debug("export-hap: ptz.stop failed (non-fatal)", { meta: { error: errMsg$7(err) } });
91631
93561
  }
91632
93562
  service.updateCharacteristic(import_dist.Characteristic.On, false);
91633
93563
  }, options.ptzPulseMs);
91634
93564
  } catch (err) {
91635
93565
  log.warn("export-hap: ptz.continuousMove failed", { meta: {
91636
93566
  dir: dir.label,
91637
- error: errMsg$4(err)
93567
+ error: errMsg$7(err)
91638
93568
  } });
91639
93569
  service.updateCharacteristic(import_dist.Characteristic.On, false);
91640
93570
  }
@@ -91657,7 +93587,7 @@ async function readPresets(bctx) {
91657
93587
  name: p.name
91658
93588
  }));
91659
93589
  } catch (err) {
91660
- ctx.logger.withTags({ deviceId: numericDeviceId }).debug("export-hap: ptz.getPresets failed (non-fatal)", { meta: { error: errMsg$4(err) } });
93590
+ ctx.logger.withTags({ deviceId: numericDeviceId }).debug("export-hap: ptz.getPresets failed (non-fatal)", { meta: { error: errMsg$7(err) } });
91661
93591
  return [];
91662
93592
  }
91663
93593
  }
@@ -91672,7 +93602,7 @@ async function tryBuildAutotrack(bctx) {
91672
93602
  const status = await proxy.ptzAutotrack.getStatus({});
91673
93603
  if (status && typeof status.enabled === "boolean") service.updateCharacteristic(import_dist.Characteristic.On, status.enabled);
91674
93604
  } catch (err) {
91675
- log.debug("export-hap: ptzAutotrack.getStatus failed (non-fatal)", { meta: { error: errMsg$4(err) } });
93605
+ log.debug("export-hap: ptzAutotrack.getStatus failed (non-fatal)", { meta: { error: errMsg$7(err) } });
91676
93606
  }
91677
93607
  service.getCharacteristic(import_dist.Characteristic.On).onSet(async (value) => {
91678
93608
  const enabled = value === true;
@@ -91681,13 +93611,13 @@ async function tryBuildAutotrack(bctx) {
91681
93611
  } catch (err) {
91682
93612
  log.warn("export-hap: ptzAutotrack.setEnabled failed", { meta: {
91683
93613
  enabled,
91684
- error: errMsg$4(err)
93614
+ error: errMsg$7(err)
91685
93615
  } });
91686
93616
  }
91687
93617
  });
91688
93618
  return { async dispose() {} };
91689
93619
  }
91690
- function errMsg$4(err) {
93620
+ function errMsg$7(err) {
91691
93621
  return err instanceof Error ? err.message : String(err);
91692
93622
  }
91693
93623
  //#endregion
@@ -92718,13 +94648,13 @@ async function buildChildSwitch(bctx, subtype, deviceType) {
92718
94648
  const switchStatus = await proxy.switch?.getStatus({});
92719
94649
  if (switchStatus && typeof switchStatus.on === "boolean") service.updateCharacteristic(import_dist.Characteristic.On, switchStatus.on);
92720
94650
  } catch (err) {
92721
- log.debug("export-hap: child switch.getStatus failed (non-fatal)", { meta: { error: errMsg$3(err) } });
94651
+ log.debug("export-hap: child switch.getStatus failed (non-fatal)", { meta: { error: errMsg$6(err) } });
92722
94652
  }
92723
94653
  if (useLightbulb) try {
92724
94654
  const status = await proxy.brightness?.getStatus({});
92725
94655
  if (status && typeof status.percentage === "number") service.updateCharacteristic(import_dist.Characteristic.Brightness, status.percentage);
92726
94656
  } catch (err) {
92727
- log.debug("export-hap: child brightness.getStatus failed (non-fatal)", { meta: { error: errMsg$3(err) } });
94657
+ log.debug("export-hap: child brightness.getStatus failed (non-fatal)", { meta: { error: errMsg$6(err) } });
92728
94658
  }
92729
94659
  if (hasSwitch) service.getCharacteristic(import_dist.Characteristic.On).onSet(async (value) => {
92730
94660
  const on = value === true;
@@ -92733,7 +94663,7 @@ async function buildChildSwitch(bctx, subtype, deviceType) {
92733
94663
  } catch (err) {
92734
94664
  log.warn("export-hap: child switch.setState failed", { meta: {
92735
94665
  on,
92736
- error: errMsg$3(err)
94666
+ error: errMsg$6(err)
92737
94667
  } });
92738
94668
  }
92739
94669
  });
@@ -92745,7 +94675,7 @@ async function buildChildSwitch(bctx, subtype, deviceType) {
92745
94675
  } catch (err) {
92746
94676
  log.warn("export-hap: child brightness.setBrightness failed", { meta: {
92747
94677
  percentage,
92748
- error: errMsg$3(err)
94678
+ error: errMsg$6(err)
92749
94679
  } });
92750
94680
  }
92751
94681
  });
@@ -92768,7 +94698,7 @@ async function buildChildSwitch(bctx, subtype, deviceType) {
92768
94698
  } catch {}
92769
94699
  } };
92770
94700
  }
92771
- function errMsg$3(err) {
94701
+ function errMsg$6(err) {
92772
94702
  return err instanceof Error ? err.message : String(err);
92773
94703
  }
92774
94704
  //#endregion
@@ -92795,7 +94725,7 @@ async function buildChildServicesFor(input) {
92795
94725
  for (const h of handles) try {
92796
94726
  await h.dispose();
92797
94727
  } catch (err) {
92798
- ctx.logger.withTags({ deviceId: parentNumericId }).debug("export-hap: child service dispose failed (continuing)", { meta: { error: errMsg$2(err) } });
94728
+ ctx.logger.withTags({ deviceId: parentNumericId }).debug("export-hap: child service dispose failed (continuing)", { meta: { error: errMsg$5(err) } });
92799
94729
  }
92800
94730
  } };
92801
94731
  }
@@ -92811,7 +94741,7 @@ async function listChildren(ctx, parentNumericId) {
92811
94741
  features: Array.isArray(c.features) ? c.features.filter((f) => typeof f === "string") : []
92812
94742
  }));
92813
94743
  } catch (err) {
92814
- ctx.logger.withTags({ deviceId: parentNumericId }).debug("export-hap: deviceManager.getChildren failed (non-fatal)", { meta: { error: errMsg$2(err) } });
94744
+ ctx.logger.withTags({ deviceId: parentNumericId }).debug("export-hap: deviceManager.getChildren failed (non-fatal)", { meta: { error: errMsg$5(err) } });
92815
94745
  return [];
92816
94746
  }
92817
94747
  }
@@ -92827,10 +94757,76 @@ function asDeviceType(raw) {
92827
94757
  for (const value of Object.values(DeviceType)) if (value === lower) return value;
92828
94758
  return DeviceType.Generic;
92829
94759
  }
92830
- function errMsg$2(err) {
94760
+ function errMsg$5(err) {
92831
94761
  return err instanceof Error ? err.message : String(err);
92832
94762
  }
92833
94763
  //#endregion
94764
+ //#region src/mappers/kind.ts
94765
+ /**
94766
+ * Which orchestrator a device gets, which device types the picker offers, and
94767
+ * what a device's HomeKit accessory is called on the wire.
94768
+ *
94769
+ * Separate from `index.ts` (the factory registry) only so the orchestrators can
94770
+ * import the uuid convention without importing the registry that imports them.
94771
+ */
94772
+ var SUPPORTED_MAPPER_KINDS = ["camera", "generic"];
94773
+ /**
94774
+ * The device types the Export picker offers, and the answer to the cap's
94775
+ * `listSupportedDeviceKinds`.
94776
+ *
94777
+ * A UI FILTER, nothing more. Whether a device actually exports anything is
94778
+ * decided by its capabilities (`rowsForCaps` in the capability table) — the
94779
+ * doctrine the HA exporter writes down, and the thing that confined the
94780
+ * previous exporter to cameras when it was ignored.
94781
+ *
94782
+ * Tier A: the types whose HomeKit services exist today and need no
94783
+ * feature-dependent shape. `cover`, `climate`, `fan`, `valve`, `humidifier`,
94784
+ * `water-heater`, `alarm-panel`, `button` and `media-player` are deliberately
94785
+ * absent — each needs a service whose semantics the table cannot yet honour,
94786
+ * and offering the tab before the mapping exists is how an operator gets an
94787
+ * accessory that pairs and does nothing.
94788
+ */
94789
+ var HAP_EXPORTABLE_DEVICE_TYPES = [
94790
+ DeviceType.Camera,
94791
+ DeviceType.Light,
94792
+ DeviceType.Switch,
94793
+ DeviceType.Siren,
94794
+ DeviceType.Sensor,
94795
+ DeviceType.Lock,
94796
+ DeviceType.Presence,
94797
+ DeviceType.Generic
94798
+ ];
94799
+ /**
94800
+ * Resolve the orchestrator for a device from its TYPE.
94801
+ *
94802
+ * `null` means "no Export tab": the picker must not offer a type no
94803
+ * orchestrator can build. An UNKNOWN type (the device-manager read failed) maps
94804
+ * to `camera` — that is what every entry persisted before this function existed
94805
+ * carries, and a transient API failure must never re-shape an exposed camera.
94806
+ */
94807
+ function pickMapperKind(deviceType) {
94808
+ if (deviceType === null || deviceType === void 0 || deviceType.length === 0) return "camera";
94809
+ if (deviceType === DeviceType.Camera) return "camera";
94810
+ return HAP_EXPORTABLE_DEVICE_TYPES.find((type) => type === deviceType) === void 0 ? null : "generic";
94811
+ }
94812
+ /**
94813
+ * The deterministic HAP accessory UUID for a device.
94814
+ *
94815
+ * One function, because three call sites depend on the answer agreeing: the
94816
+ * orchestrator that builds the accessory, `unexposeDevice` (which wipes
94817
+ * hap-nodejs' pairing blobs by uuid, on a device whose mapper is already gone)
94818
+ * and the export sync state that records it.
94819
+ *
94820
+ * The camera namespace is FROZEN — every camera paired to date derives its MAC
94821
+ * from `sha256("camstack:hap:" + uuid)` of this exact string, and changing it
94822
+ * would make every paired camera a stranger. Generic devices get their own
94823
+ * namespace so a switch and a camera that happen to share a device id are not
94824
+ * the same HomeKit accessory.
94825
+ */
94826
+ function accessoryUuidFor(kind, deviceId) {
94827
+ return import_dist.uuid.generate(kind === "camera" ? `camstack:camera:${deviceId}` : `camstack:device:${deviceId}`);
94828
+ }
94829
+ //#endregion
92834
94830
  //#region src/mappers/camera-accessory.ts
92835
94831
  /**
92836
94832
  * Camera accessory orchestrator — given a camstack deviceId, builds one
@@ -92863,9 +94859,9 @@ async function buildCameraAccessory(input) {
92863
94859
  const capNames = new Set(proxy.binding?.entries.map((e) => e.capName) ?? []);
92864
94860
  const isDoorbell = capNames.has("doorbell");
92865
94861
  const category = isDoorbell ? import_dist.Categories.VIDEO_DOORBELL : import_dist.Categories.IP_CAMERA;
92866
- const accessory = new import_dist.Accessory(displayName, import_dist.uuid.generate(`camstack:camera:${numericId}`));
94862
+ const accessory = new import_dist.Accessory(displayName, accessoryUuidFor("camera", numericId));
92867
94863
  accessory.category = category;
92868
- await populateAccessoryInfo(accessory, proxy, displayName);
94864
+ await populateAccessoryInfo(accessory, proxy, displayName, "Camera");
92869
94865
  const bctx = {
92870
94866
  ctx,
92871
94867
  accessory,
@@ -92923,55 +94919,462 @@ async function buildCameraAccessory(input) {
92923
94919
  for (const h of handles) try {
92924
94920
  await h.dispose();
92925
94921
  } catch (err) {
92926
- log.debug("export-hap: builder dispose failed (continuing)", { meta: { error: errMsg$1(err) } });
94922
+ log.debug("export-hap: builder dispose failed (continuing)", { meta: { error: errMsg$4(err) } });
92927
94923
  }
92928
94924
  try {
92929
94925
  await streams.dispose();
92930
94926
  } catch (err) {
92931
- log.debug("export-hap: streams dispose failed", { meta: { error: errMsg$1(err) } });
94927
+ log.debug("export-hap: streams dispose failed", { meta: { error: errMsg$4(err) } });
92932
94928
  }
92933
94929
  }
92934
94930
  };
92935
94931
  }
92936
- async function populateAccessoryInfo(accessory, proxy, displayName) {
92937
- const info = accessory.getService(import_dist.Service.AccessoryInformation);
92938
- if (!info) return;
94932
+ function errMsg$4(err) {
94933
+ return err instanceof Error ? err.message : String(err);
94934
+ }
94935
+ //#endregion
94936
+ //#region src/mappers/builders/generic/lock.ts
94937
+ /**
94938
+ * `lock-control` → `Service.LockMechanism`.
94939
+ *
94940
+ * Not a sensor row: HomeKit models a lock as two characteristics, a CURRENT
94941
+ * state the accessory owns and a TARGET state the controller writes, and the
94942
+ * cap's five states do not map onto either one alone.
94943
+ *
94944
+ * ```
94945
+ * cap state LockCurrentState LockTargetState
94946
+ * locked SECURED SECURED
94947
+ * unlocked UNSECURED UNSECURED
94948
+ * locking UNKNOWN SECURED (in flight — do not claim SECURED)
94949
+ * unlocking UNKNOWN UNSECURED
94950
+ * jammed JAMMED unchanged (the controller's intent stands)
94951
+ * ```
94952
+ *
94953
+ * Reporting SECURED while the bolt is still moving is the failure worth naming:
94954
+ * iOS renders the lock as closed, the operator walks away, and the motor stalls
94955
+ * behind them. `UNKNOWN` is the honest answer for an in-flight transition.
94956
+ *
94957
+ * `open` (the cap's third method, for locks with a latch/buzzer) has no HomeKit
94958
+ * counterpart on this service and is deliberately not wired — a Switch that
94959
+ * silently buzzes a door open would be a second knob nobody asked for.
94960
+ */
94961
+ var SUBTYPE = "lock-control";
94962
+ var readLock = reader(LockControlStatusSchema.pick({ state: true }), (status) => {
94963
+ const current = status.state === "locked" ? import_dist.Characteristic.LockCurrentState.SECURED : status.state === "unlocked" ? import_dist.Characteristic.LockCurrentState.UNSECURED : status.state === "jammed" ? import_dist.Characteristic.LockCurrentState.JAMMED : import_dist.Characteristic.LockCurrentState.UNKNOWN;
94964
+ const target = status.state === "locked" || status.state === "locking" ? import_dist.Characteristic.LockTargetState.SECURED : status.state === "unlocked" || status.state === "unlocking" ? import_dist.Characteristic.LockTargetState.UNSECURED : null;
94965
+ return [{
94966
+ characteristic: import_dist.Characteristic.LockCurrentState,
94967
+ value: current
94968
+ }, ...target === null ? [] : [{
94969
+ characteristic: import_dist.Characteristic.LockTargetState,
94970
+ value: target
94971
+ }]];
94972
+ });
94973
+ async function buildLockMechanism(bctx) {
94974
+ const { ctx, accessory, proxy, numericDeviceId, displayName } = bctx;
94975
+ const log = ctx.logger.withTags({ deviceId: numericDeviceId });
94976
+ const label = hapServiceName([displayName], `Lock ${numericDeviceId}`);
94977
+ const service = accessory.addService(import_dist.Service.LockMechanism, label, SUBTYPE);
94978
+ applyServiceLabel(service, label);
94979
+ const apply = (status, source) => {
94980
+ const updates = readLock(status);
94981
+ if (updates.length === 0) {
94982
+ log.debug("export-hap: lock status did not match the cap schema — no update", { meta: { source } });
94983
+ return;
94984
+ }
94985
+ applyUpdates(service, updates);
94986
+ };
92939
94987
  try {
92940
- const device = await proxy.deviceManager?.getDevice({});
92941
- const metadata = device?.metadata ?? null;
92942
- info.setCharacteristic(import_dist.Characteristic.Name, device?.name ?? displayName);
92943
- info.setCharacteristic(import_dist.Characteristic.Manufacturer, stringOr(metadata?.manufacturer, "CamStack"));
92944
- info.setCharacteristic(import_dist.Characteristic.Model, stringOr(metadata?.model, "Camera"));
92945
- info.setCharacteristic(import_dist.Characteristic.FirmwareRevision, stringOr(metadata?.firmware, "0.0.0"));
92946
- info.setCharacteristic(import_dist.Characteristic.SerialNumber, stringOr(metadata?.sn, `camstack-${proxy.deviceId}`));
92947
- } catch {}
94988
+ const status = await proxy.lockControl?.getStatus({});
94989
+ if (status !== void 0 && status !== null) apply(status, "getStatus");
94990
+ } catch (err) {
94991
+ log.debug("export-hap: lock getStatus hydrate failed (non-fatal)", { meta: { error: errMsg$3(err) } });
94992
+ }
94993
+ service.getCharacteristic(import_dist.Characteristic.LockTargetState).onSet(async (value) => {
94994
+ const secure = value === import_dist.Characteristic.LockTargetState.SECURED;
94995
+ try {
94996
+ if (secure) await proxy.lockControl?.lock({});
94997
+ else await proxy.lockControl?.unlock({});
94998
+ } catch (err) {
94999
+ log.warn("export-hap: lock command failed", { meta: {
95000
+ secure,
95001
+ error: errMsg$3(err)
95002
+ } });
95003
+ }
95004
+ });
95005
+ const unsubscribe = proxy.state.lockControl?.subscribe((value) => {
95006
+ if (value === void 0 || value === null) return;
95007
+ apply(value, "slice");
95008
+ }) ?? null;
95009
+ return { async dispose() {
95010
+ try {
95011
+ unsubscribe?.();
95012
+ } catch {}
95013
+ } };
92948
95014
  }
92949
- function stringOr(value, fallback) {
92950
- return typeof value === "string" && value.length > 0 ? value : fallback;
95015
+ function errMsg$3(err) {
95016
+ return err instanceof Error ? err.message : String(err);
95017
+ }
95018
+ //#endregion
95019
+ //#region src/mappers/builders/generic/sensor-service.ts
95020
+ async function buildSensorService(input) {
95021
+ const { bctx, spec, name, subtype } = input;
95022
+ const { ctx, accessory, proxy, numericDeviceId } = bctx;
95023
+ const log = ctx.logger.withTags({ deviceId: numericDeviceId });
95024
+ const label = hapServiceName([name], spec.label);
95025
+ const service = spec.addService(accessory, label, subtype);
95026
+ applyServiceLabel(service, label);
95027
+ const apply = (status, source) => {
95028
+ const updates = spec.read(status);
95029
+ if (updates.length === 0) {
95030
+ log.debug("export-hap: sensor status did not match the cap schema — no update", { meta: {
95031
+ subtype,
95032
+ source
95033
+ } });
95034
+ return;
95035
+ }
95036
+ applyUpdates(service, updates);
95037
+ };
95038
+ try {
95039
+ const status = await spec.getStatus(proxy);
95040
+ if (status !== void 0 && status !== null) apply(status, "getStatus");
95041
+ } catch (err) {
95042
+ log.debug("export-hap: sensor getStatus hydrate failed (non-fatal)", { meta: {
95043
+ subtype,
95044
+ error: errMsg$2(err)
95045
+ } });
95046
+ }
95047
+ const unsubscribe = spec.subscribe(proxy, (value) => {
95048
+ if (value === void 0 || value === null) return;
95049
+ apply(value, "slice");
95050
+ });
95051
+ return { async dispose() {
95052
+ try {
95053
+ unsubscribe?.();
95054
+ } catch {}
95055
+ } };
95056
+ }
95057
+ function errMsg$2(err) {
95058
+ return err instanceof Error ? err.message : String(err);
95059
+ }
95060
+ //#endregion
95061
+ //#region src/mappers/builders/generic/cap-service-table.ts
95062
+ /**
95063
+ * The capability→HomeKit-service TABLE for non-camera devices.
95064
+ *
95065
+ * This is the whole coverage decision for the generic export path, and it is
95066
+ * deliberately a table rather than a `switch` on `DeviceType`. The doctrine is
95067
+ * the one the Home Assistant exporter already writes down: *coverage is decided
95068
+ * by a device's capabilities, not its type — restricting the picker by type is
95069
+ * what confined the previous exporter to cameras.* A `DeviceType` here is only
95070
+ * ever a UI filter (`HAP_EXPORTABLE_DEVICE_TYPES`) or an icon
95071
+ * (`ACCESSORY_CATEGORY_BY_TYPE`); it never decides whether something exports.
95072
+ *
95073
+ * The shape mirrors `CAP_ENTITY_MAP` in
95074
+ * `packages/addon-provider-homeassistant/src/ha-export/entity-catalog.ts` so
95075
+ * the two can be unified later — one row per capability, keyed by the cap name
95076
+ * exactly as it appears in `proxy.binding.entries[].capName`.
95077
+ *
95078
+ * ## Reading a status
95079
+ *
95080
+ * Every row parses the cap's OWN Zod status schema rather than duck-typing the
95081
+ * payload, and it `.pick()`s only the fields it reads: a provider that omits a
95082
+ * timestamp must not silence a sensor, and a provider that sends the wrong
95083
+ * shape must not write a garbage characteristic. A parse that fails yields no
95084
+ * updates, and the builder that owns the service logs the drop — silence reads
95085
+ * as "the sensor never changed".
95086
+ *
95087
+ * ## Tier
95088
+ *
95089
+ * Tier A only: the caps whose HomeKit service exists today and needs no
95090
+ * feature-dependent shape (`cover` needs `cover-positionable`, `climate-control`
95091
+ * needs the dual-setpoint split, `alarm-panel` must survive a refused `arm`).
95092
+ * Those are a separate task; adding a row here must never mean adding a service
95093
+ * whose semantics this file cannot fully honour.
95094
+ */
95095
+ /** HomeKit's floor for `CurrentAmbientLightLevel`; 0 lux is out of range. */
95096
+ var MIN_LUX = 1e-4;
95097
+ var SENSOR_SPECS = {
95098
+ contact: {
95099
+ label: "Contact",
95100
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.ContactSensor, name, subtype),
95101
+ getStatus: (proxy) => proxy.contact?.getStatus({}),
95102
+ subscribe: (proxy, onValue) => proxy.state.contact?.subscribe(onValue) ?? null,
95103
+ read: reader(ContactStatusSchema.pick({ entryOpen: true }), (status) => [{
95104
+ characteristic: import_dist.Characteristic.ContactSensorState,
95105
+ value: status.entryOpen ? import_dist.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : import_dist.Characteristic.ContactSensorState.CONTACT_DETECTED
95106
+ }])
95107
+ },
95108
+ motion: {
95109
+ label: "Motion",
95110
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.MotionSensor, name, subtype),
95111
+ getStatus: (proxy) => proxy.motion?.getStatus({}),
95112
+ subscribe: (proxy, onValue) => proxy.state.motion?.subscribe(onValue) ?? null,
95113
+ read: reader(MotionStatusSchema.pick({ detected: true }), (status) => [{
95114
+ characteristic: import_dist.Characteristic.MotionDetected,
95115
+ value: status.detected
95116
+ }])
95117
+ },
95118
+ presence: {
95119
+ label: "Presence",
95120
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.OccupancySensor, name, subtype),
95121
+ getStatus: (proxy) => proxy.presence?.getStatus({}),
95122
+ subscribe: (proxy, onValue) => proxy.state.presence?.subscribe(onValue) ?? null,
95123
+ read: reader(PresenceStatusSchema.pick({ state: true }), (status) => [{
95124
+ characteristic: import_dist.Characteristic.OccupancyDetected,
95125
+ value: status.state === "home" ? import_dist.Characteristic.OccupancyDetected.OCCUPANCY_DETECTED : import_dist.Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED
95126
+ }])
95127
+ },
95128
+ smoke: {
95129
+ label: "Smoke",
95130
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.SmokeSensor, name, subtype),
95131
+ getStatus: (proxy) => proxy.smoke?.getStatus({}),
95132
+ subscribe: (proxy, onValue) => proxy.state.smoke?.subscribe(onValue) ?? null,
95133
+ read: reader(SmokeStatusSchema.pick({ detected: true }), (status) => [{
95134
+ characteristic: import_dist.Characteristic.SmokeDetected,
95135
+ value: status.detected ? import_dist.Characteristic.SmokeDetected.SMOKE_DETECTED : import_dist.Characteristic.SmokeDetected.SMOKE_NOT_DETECTED
95136
+ }])
95137
+ },
95138
+ "carbon-monoxide": {
95139
+ label: "Carbon monoxide",
95140
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.CarbonMonoxideSensor, name, subtype),
95141
+ getStatus: (proxy) => proxy.carbonMonoxide?.getStatus({}),
95142
+ subscribe: (proxy, onValue) => proxy.state.carbonMonoxide?.subscribe(onValue) ?? null,
95143
+ read: reader(CarbonMonoxideStatusSchema.pick({ detected: true }), (status) => [{
95144
+ characteristic: import_dist.Characteristic.CarbonMonoxideDetected,
95145
+ value: status.detected ? import_dist.Characteristic.CarbonMonoxideDetected.CO_LEVELS_ABNORMAL : import_dist.Characteristic.CarbonMonoxideDetected.CO_LEVELS_NORMAL
95146
+ }])
95147
+ },
95148
+ flood: {
95149
+ label: "Leak",
95150
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.LeakSensor, name, subtype),
95151
+ getStatus: (proxy) => proxy.flood?.getStatus({}),
95152
+ subscribe: (proxy, onValue) => proxy.state.flood?.subscribe(onValue) ?? null,
95153
+ read: reader(FloodStatusSchema.pick({ flooded: true }), (status) => [{
95154
+ characteristic: import_dist.Characteristic.LeakDetected,
95155
+ value: status.flooded ? import_dist.Characteristic.LeakDetected.LEAK_DETECTED : import_dist.Characteristic.LeakDetected.LEAK_NOT_DETECTED
95156
+ }])
95157
+ },
95158
+ "temperature-sensor": {
95159
+ label: "Temperature",
95160
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.TemperatureSensor, name, subtype),
95161
+ getStatus: (proxy) => proxy.temperatureSensor?.getStatus({}),
95162
+ subscribe: (proxy, onValue) => proxy.state.temperatureSensor?.subscribe(onValue) ?? null,
95163
+ read: reader(TemperatureSensorStatusSchema.pick({ celsius: true }), (status) => [{
95164
+ characteristic: import_dist.Characteristic.CurrentTemperature,
95165
+ value: status.celsius
95166
+ }])
95167
+ },
95168
+ "humidity-sensor": {
95169
+ label: "Humidity",
95170
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.HumiditySensor, name, subtype),
95171
+ getStatus: (proxy) => proxy.humiditySensor?.getStatus({}),
95172
+ subscribe: (proxy, onValue) => proxy.state.humiditySensor?.subscribe(onValue) ?? null,
95173
+ read: reader(HumiditySensorStatusSchema.pick({ percent: true }), (status) => [{
95174
+ characteristic: import_dist.Characteristic.CurrentRelativeHumidity,
95175
+ value: status.percent
95176
+ }])
95177
+ },
95178
+ "ambient-light-sensor": {
95179
+ label: "Light level",
95180
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.LightSensor, name, subtype),
95181
+ getStatus: (proxy) => proxy.ambientLightSensor?.getStatus({}),
95182
+ subscribe: (proxy, onValue) => proxy.state.ambientLightSensor?.subscribe(onValue) ?? null,
95183
+ read: reader(AmbientLightSensorStatusSchema.pick({ lux: true }), (status) => [{
95184
+ characteristic: import_dist.Characteristic.CurrentAmbientLightLevel,
95185
+ value: Math.max(MIN_LUX, status.lux)
95186
+ }])
95187
+ }
95188
+ };
95189
+ /**
95190
+ * The `battery` row reads like a sensor but writes three characteristics from
95191
+ * one status, and the camera path publishes the SAME service from the same cap.
95192
+ * The derivation therefore lives once, in `battery.ts`, and both callers read
95193
+ * it from there.
95194
+ */
95195
+ var BATTERY_SPEC = {
95196
+ label: "Battery",
95197
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.Battery, name, subtype),
95198
+ getStatus: (proxy) => proxy.battery?.getStatus({}),
95199
+ subscribe: (proxy, onValue) => proxy.state.battery?.subscribe(onValue) ?? null,
95200
+ read: batteryCharacteristicUpdates
95201
+ };
95202
+ function sensorRow(capName, spec) {
95203
+ return {
95204
+ caps: [capName],
95205
+ label: spec.label,
95206
+ build: ({ bctx, name }) => buildSensorService({
95207
+ bctx,
95208
+ spec,
95209
+ name,
95210
+ subtype: capName
95211
+ })
95212
+ };
95213
+ }
95214
+ /**
95215
+ * THE table. Order matters only for one thing: the first row that matches a
95216
+ * device decides the accessory's category when the device's own type does not.
95217
+ */
95218
+ var HAP_CAP_SERVICES = [
95219
+ {
95220
+ caps: ["switch", "brightness"],
95221
+ label: "Power",
95222
+ build: ({ bctx, name, deviceType }) => buildChildSwitch({
95223
+ ...bctx,
95224
+ displayName: name
95225
+ }, "switch", deviceType)
95226
+ },
95227
+ {
95228
+ caps: ["lock-control"],
95229
+ label: "Lock",
95230
+ build: ({ bctx, name }) => buildLockMechanism({
95231
+ ...bctx,
95232
+ displayName: name
95233
+ })
95234
+ },
95235
+ {
95236
+ caps: ["battery"],
95237
+ label: BATTERY_SPEC.label,
95238
+ build: ({ bctx, name }) => buildSensorService({
95239
+ bctx,
95240
+ spec: BATTERY_SPEC,
95241
+ name,
95242
+ subtype: "battery"
95243
+ })
95244
+ },
95245
+ ...Object.entries(SENSOR_SPECS).map(([capName, spec]) => sensorRow(capName, spec))
95246
+ ];
95247
+ /**
95248
+ * The rows a device's bound capabilities select, in table order.
95249
+ *
95250
+ * A row matches when ANY of its caps is bound: a lamp with `switch` but no
95251
+ * `brightness` is still a Lightbulb-shaped row, and the builder degrades on its
95252
+ * own.
95253
+ */
95254
+ function rowsForCaps(capNames) {
95255
+ return HAP_CAP_SERVICES.filter((row) => row.caps.some((cap) => capNames.has(cap)));
95256
+ }
95257
+ /**
95258
+ * The HomeKit accessory category per camstack device type — the icon iOS Home
95259
+ * draws and nothing else. Absent = `OTHER`, which is a valid accessory that
95260
+ * simply gets the generic tile.
95261
+ *
95262
+ * This is NOT a coverage decision. `HAP_EXPORTABLE_DEVICE_TYPES` (in
95263
+ * `mappers/index.ts`) filters the picker; whether a device exports anything is
95264
+ * decided by `rowsForCaps`.
95265
+ */
95266
+ var ACCESSORY_CATEGORY_BY_TYPE = {
95267
+ [DeviceType.Switch]: import_dist.Categories.SWITCH,
95268
+ [DeviceType.Light]: import_dist.Categories.LIGHTBULB,
95269
+ [DeviceType.Siren]: import_dist.Categories.SWITCH,
95270
+ [DeviceType.Lock]: import_dist.Categories.DOOR_LOCK,
95271
+ [DeviceType.Sensor]: import_dist.Categories.SENSOR,
95272
+ [DeviceType.Presence]: import_dist.Categories.SENSOR
95273
+ };
95274
+ function accessoryCategoryFor(deviceType) {
95275
+ return ACCESSORY_CATEGORY_BY_TYPE[deviceType] ?? import_dist.Categories.OTHER;
95276
+ }
95277
+ //#endregion
95278
+ //#region src/mappers/generic-accessory.ts
95279
+ /**
95280
+ * Generic accessory orchestrator — every camstack device that is NOT a camera.
95281
+ *
95282
+ * Same three steps as the camera orchestrator, and deliberately nothing more:
95283
+ * 1. resolve the typed `DeviceProxy`,
95284
+ * 2. select rows from the capability→service table with the device's BOUND
95285
+ * capabilities (never its type — see `builders/generic/cap-service-table.ts`),
95286
+ * 3. let each row add its service to one `Accessory`.
95287
+ *
95288
+ * It publishes standalone, like the camera path: one accessory, one mDNS
95289
+ * advertisement, one pairing, the shared setup code. This addon is not a
95290
+ * bridge — that decision predates this file and is not re-litigated here.
95291
+ *
95292
+ * ## It REFUSES rather than publishing an empty tile
95293
+ *
95294
+ * A device whose capabilities select no row throws. The alternative is an
95295
+ * accessory carrying nothing but `AccessoryInformation`: it pairs, it appears
95296
+ * in the Home app, it does nothing, and the operator has no way to tell that
95297
+ * from a broken integration. The Export tab is gated on the same question
95298
+ * (`hap-export.addon.ts`), so reaching this throw means the device changed
95299
+ * shape between the tab rendering and the toggle landing.
95300
+ */
95301
+ async function buildGenericAccessory(input) {
95302
+ const { ctx, deviceId, displayName, options } = input;
95303
+ const numericId = Number.parseInt(deviceId, 10);
95304
+ if (!Number.isFinite(numericId)) throw new Error(`export-hap: cannot map device '${deviceId}' — id is not numeric`);
95305
+ const log = ctx.logger.withTags({ deviceId: numericId });
95306
+ const proxy = await ctx.fetchDevice(numericId);
95307
+ const capNames = new Set(proxy.binding?.entries.map((e) => e.capName) ?? []);
95308
+ const rows = rowsForCaps(capNames);
95309
+ if (rows.length === 0) throw new Error(`export-hap: device ${numericId} carries no capability HomeKit can export (bound: ${[...capNames].toSorted().join(", ") || "none"})`);
95310
+ const deviceType = await resolveDeviceType(proxy);
95311
+ const accessory = new import_dist.Accessory(displayName, accessoryUuidFor("generic", numericId));
95312
+ accessory.category = accessoryCategoryFor(deviceType);
95313
+ await populateAccessoryInfo(accessory, proxy, displayName, "Accessory");
95314
+ const bctx = {
95315
+ ctx,
95316
+ accessory,
95317
+ proxy,
95318
+ numericDeviceId: numericId,
95319
+ displayName,
95320
+ options
95321
+ };
95322
+ const handles = [];
95323
+ for (const row of rows) {
95324
+ const name = rows.length === 1 ? displayName : row.label;
95325
+ handles.push(await row.build({
95326
+ bctx,
95327
+ name,
95328
+ deviceType
95329
+ }));
95330
+ }
95331
+ log.info("export-hap: built generic accessory", { meta: {
95332
+ deviceType,
95333
+ services: rows.map((row) => row.caps[0]).join(",")
95334
+ } });
95335
+ return {
95336
+ accessory,
95337
+ accessories: [accessory],
95338
+ async dispose() {
95339
+ for (const handle of handles) try {
95340
+ await handle.dispose();
95341
+ } catch (err) {
95342
+ log.debug("export-hap: generic builder dispose failed (continuing)", { meta: { error: errMsg$1(err) } });
95343
+ }
95344
+ }
95345
+ };
95346
+ }
95347
+ /**
95348
+ * The device's own type, or `Generic`.
95349
+ *
95350
+ * Used ONLY where one capability has two HomeKit shapes — a siren's
95351
+ * `brightness` is alarm volume, not luminosity — and for the accessory's icon.
95352
+ * `Generic` is the safe reading: it is the shape `child-switch.ts` already
95353
+ * treats as "a lamp if it dims, a switch otherwise".
95354
+ */
95355
+ async function resolveDeviceType(proxy) {
95356
+ try {
95357
+ const device = await proxy.deviceManager?.getDevice({});
95358
+ const raw = typeof device?.type === "string" ? device.type.toLowerCase() : "";
95359
+ return Object.values(DeviceType).find((value) => value === raw) ?? DeviceType.Generic;
95360
+ } catch {
95361
+ return DeviceType.Generic;
95362
+ }
92951
95363
  }
92952
95364
  function errMsg$1(err) {
92953
95365
  return err instanceof Error ? err.message : String(err);
92954
95366
  }
92955
95367
  //#endregion
92956
95368
  //#region src/mappers/index.ts
92957
- var SUPPORTED_MAPPER_KINDS = ["camera"];
92958
- var REGISTRY = { camera: buildCameraAccessory };
95369
+ var REGISTRY = {
95370
+ camera: buildCameraAccessory,
95371
+ generic: buildGenericAccessory
95372
+ };
92959
95373
  function getMapperFactory(kind) {
92960
95374
  const factory = REGISTRY[kind];
92961
95375
  if (!factory) throw new Error(`export-hap: no mapper registered for kind '${kind}'`);
92962
95376
  return factory;
92963
95377
  }
92964
- /**
92965
- * Resolve the best-fit mapper kind. With Round 2, the operator just
92966
- * picks a camera and the orchestrator does the rest — the single
92967
- * `camera` kind is returned unconditionally. We keep the function
92968
- * signature for backwards-compat with the addon's existing
92969
- * `exposeDevice` flow and to leave room for future device types (NVR,
92970
- * climate sensor, ...).
92971
- */
92972
- function pickMapperKind(_capabilities) {
92973
- return "camera";
92974
- }
92975
95378
  //#endregion
92976
95379
  //#region src/mappers/builders/stream-hwaccel-memo.ts
92977
95380
  /**
@@ -93109,8 +95512,8 @@ function syncStateToJson(map) {
93109
95512
  //#endregion
93110
95513
  //#region src/hap-export.addon.ts
93111
95514
  /**
93112
- * HomeKit (HAP) export addon — publishes a single HAP bridge process
93113
- * that exposes selected camstack devices as HomeKit accessories.
95515
+ * HomeKit (HAP) export addon — publishes selected camstack devices as
95516
+ * standalone HomeKit accessories.
93114
95517
  *
93115
95518
  * Operator flow:
93116
95519
  * 1. Install + enable the addon (hub-only, group `export-hap`).
@@ -93121,10 +95524,10 @@ function syncStateToJson(map) {
93121
95524
  * 3. The setup URI (`X-HM://…`) is logged AND surfaced via the
93122
95525
  * `getStatus` cap method so the wizard UI can render the QR.
93123
95526
  * 4. Operator opens iOS Home → + → scan QR → enter pincode.
93124
- * 5. Operator hits "Expose to HomeKit" on individual camstack
93125
- * devices; the addon attaches a MotionSensor accessory (MVP)
93126
- * to the bridge and persists the choice via the same
93127
- * `updateGlobalSettings` path.
95527
+ * 5. Operator hits "Expose to HomeKit" on individual camstack devices.
95528
+ * Each exposed device is published as its OWN accessory with its own
95529
+ * mDNS advertisement, sharing one setup code cameras cannot be
95530
+ * bridged, so nothing is.
93128
95531
  *
93129
95532
  * Exception — the hap-nodejs library's own `HAPStorage` lives under
93130
95533
  * `ctx.dataDir/hap-store/`. That directory is library-internal: the
@@ -93133,23 +95536,20 @@ function syncStateToJson(map) {
93133
95536
  * I/O for addon-owned data") explicitly scopes itself to OUR own data;
93134
95537
  * library-managed blobs are out of scope.
93135
95538
  *
93136
- * Round 2 scope: full camera bridge. `exposeDevice({deviceId})` builds
93137
- * one HomeKit Accessory per camstack camera with auto-detected feature
93138
- * services driven by the device's capability binding:
93139
- * - `camera-streams` cap CameraRTPStreamManagement via ffmpeg
93140
- * - `intercom` cap → Microphone + Speaker (audio bridge wiring is a
93141
- * Round 3 follow-up services are declared so iOS Home shows the
93142
- * talk-back button, but PCM upload is not yet routed)
93143
- * - `doorbell` cap DoorbellController + Doorbell service
93144
- * - `motion-detection` cap MotionSensor service
93145
- * - `ptz` cap preset switches + 4 directional momentary switches
93146
- * - `ptz-autotrack` cap → stateful "Autotrack" switch
93147
- * Children (siren, floodlight, spotlight, …) become independent
93148
- * accessories under the same Bridge see `mappers/child-accessory.ts`.
93149
- *
93150
- * What's deferred to Round 3+: cam→browser audio Opus transcode,
93151
- * intercom upload bridge, HomeKit Secure Video, recording, native
93152
- * H.264 stream tap (currently uses RTSP + ffmpeg copy).
95539
+ * ## Two accessory shapes, one rule about coverage
95540
+ *
95541
+ * `exposeDevice({deviceId})` resolves the device's TYPE to a mapper kind
95542
+ * (`mappers/kind.ts`) and hands off:
95543
+ *
95544
+ * - a camera `mappers/camera-accessory.ts`: streams, HKSV recording,
95545
+ * doorbell, motion, intercom, PTZ, battery, privacy and every accessory
95546
+ * child, all as services on one accessory;
95547
+ * - anything else → `mappers/generic-accessory.ts`, driven by the
95548
+ * capabilityservice table in `mappers/builders/generic/`.
95549
+ *
95550
+ * The type only picks the SHAPE. What a device publishes is decided by the
95551
+ * capabilities bound to itrestricting coverage by type is precisely what
95552
+ * confined this exporter to cameras for its first three rounds.
93153
95553
  */
93154
95554
  var DEFAULT_DEVICE_SETTINGS = {
93155
95555
  streamPreference: "auto",
@@ -93192,7 +95592,6 @@ var DEFAULT_CONFIG = {
93192
95592
  fixedPin: "",
93193
95593
  interfaceName: "",
93194
95594
  ptzPulseMs: 400,
93195
- hksvPreview: false,
93196
95595
  identity: {
93197
95596
  username: "",
93198
95597
  pincode: "",
@@ -93308,7 +95707,7 @@ var ExportHapAddon = class extends BaseAddon {
93308
95707
  ...setup ? { setup } : {}
93309
95708
  };
93310
95709
  },
93311
- listSupportedDeviceKinds: async () => [...SUPPORTED_MAPPER_KINDS],
95710
+ listSupportedDeviceKinds: async () => [...HAP_EXPORTABLE_DEVICE_TYPES],
93312
95711
  listExposedDevices: async () => Array.from(this.exposed.entries()).map(([deviceId, m]) => {
93313
95712
  const entry = this.config.exposed.find((e) => e.deviceId === deviceId);
93314
95713
  return {
@@ -93393,9 +95792,10 @@ var ExportHapAddon = class extends BaseAddon {
93393
95792
  log.debug("export-hap: device already exposed — refreshing capabilities");
93394
95793
  await this.detachMapper(deviceId);
93395
95794
  }
93396
- const mapperKind = pickMapperKind(capabilities);
93397
- if (!mapperKind) throw new Error(`export-hap: no mapper for capabilities ${JSON.stringify(capabilities ?? [])}`);
93398
- const displayName = await this.resolveDisplayName(deviceId);
95795
+ const summary = await this.fetchDeviceSummary(numericId);
95796
+ const mapperKind = pickMapperKind(summary?.type);
95797
+ if (!mapperKind) throw new Error(`export-hap: device ${numericId} has type '${summary?.type ?? "unknown"}', which HomeKit export does not support`);
95798
+ const displayName = summary?.name ?? `Device ${deviceId}`;
93399
95799
  const previous = this.config.exposed.find((e) => e.deviceId === deviceId);
93400
95800
  const baseEntry = carryForward({
93401
95801
  deviceId,
@@ -93425,10 +95825,11 @@ var ExportHapAddon = class extends BaseAddon {
93425
95825
  async unexposeDevice(deviceId, options = {}) {
93426
95826
  const numericId = Number.parseInt(deviceId, 10);
93427
95827
  const log = this.ctx.logger.withTags({ deviceId: numericId });
95828
+ const mapperKind = this.findEntry(numericId)?.mapperKind ?? "camera";
93428
95829
  await this.detachMapper(deviceId);
93429
95830
  const next = this.config.exposed.filter((e) => e.deviceId !== deviceId);
93430
95831
  if (next.length !== this.config.exposed.length) await this.updateGlobalSettings({ exposed: next });
93431
- if (options.clearPairing !== false) clearPairingFiles(import_dist.uuid.generate(`camstack:camera:${numericId}`), this.ctx.logger);
95832
+ if (options.clearPairing !== false) clearPairingFiles(accessoryUuidFor(mapperKind, numericId), this.ctx.logger);
93432
95833
  await this.forgetFingerprint(numericId);
93433
95834
  log.info("export-hap: unexposed device");
93434
95835
  }
@@ -93478,10 +95879,10 @@ var ExportHapAddon = class extends BaseAddon {
93478
95879
  /** Pending rebuild timers per deviceId — used to debounce. Cleared
93479
95880
  * on detach so stale timers can't republish a removed accessory. */
93480
95881
  pendingRebuildTimers = /* @__PURE__ */ new Map();
93481
- /** Deterministic HAP accessory UUID for a device (matches `buildCameraAccessory`
93482
- * and `unexposeDevice`). */
95882
+ /** Deterministic HAP accessory UUID for a device the same one the
95883
+ * orchestrator built it with (`mappers/kind.ts`). */
93483
95884
  hapAccessoryUuid(deviceId) {
93484
- return import_dist.uuid.generate(`camstack:camera:${deviceId}`);
95885
+ return accessoryUuidFor(this.findEntry(deviceId)?.mapperKind ?? "camera", deviceId);
93485
95886
  }
93486
95887
  /**
93487
95888
  * Export fingerprint of a device from its PERSISTED features + type
@@ -93640,31 +96041,79 @@ var ExportHapAddon = class extends BaseAddon {
93640
96041
  log.warn("export-hap: reconcile rebuild failed", { meta: { error: errMsg(err) } });
93641
96042
  }
93642
96043
  }
93643
- async resolveDisplayName(deviceId) {
93644
- const numeric = Number.parseInt(deviceId, 10);
93645
- if (!Number.isFinite(numeric)) return `Device ${deviceId}`;
96044
+ /**
96045
+ * The device's name and type, or `null` when the registry cannot answer.
96046
+ *
96047
+ * `null` is NOT "no such device" — it also covers a transient API failure,
96048
+ * and every caller treats it as "keep doing what was already being done"
96049
+ * rather than re-shaping an exposed accessory on incomplete information.
96050
+ */
96051
+ async fetchDeviceSummary(deviceId) {
96052
+ if (!Number.isFinite(deviceId)) return null;
96053
+ try {
96054
+ const device = await this.ctx.api.deviceManager?.getDevice.query({ deviceId });
96055
+ if (!device) return null;
96056
+ return {
96057
+ name: device.name,
96058
+ type: device.type
96059
+ };
96060
+ } catch (err) {
96061
+ this.ctx.logger.withTags({ deviceId }).debug("export-hap: deviceManager.getDevice failed", { meta: { error: errMsg(err) } });
96062
+ return null;
96063
+ }
96064
+ }
96065
+ /**
96066
+ * Which accessory shape this device would get, or `null` for "no Export tab".
96067
+ *
96068
+ * Two questions, in this order, because they fail differently:
96069
+ * 1. the TYPE — a type with no orchestrator is refused outright;
96070
+ * 2. for a non-camera, the CAPABILITIES — a device carrying nothing the
96071
+ * table maps would publish an accessory that pairs and does nothing, and
96072
+ * the operator cannot tell that from a broken integration.
96073
+ *
96074
+ * A camera skips (2): the camera orchestrator has always published on type
96075
+ * alone, and adding a cap gate here would be a new way for an existing camera
96076
+ * to lose its Export tab. A registry that cannot answer at all also resolves
96077
+ * to `camera`, which is what every persisted entry predating this method
96078
+ * carries.
96079
+ */
96080
+ async resolveExportKind(deviceId) {
96081
+ const summary = await this.fetchDeviceSummary(deviceId);
96082
+ const kind = pickMapperKind(summary?.type);
96083
+ if (kind === null) return null;
96084
+ if (kind === "camera") return "camera";
93646
96085
  try {
93647
- const device = await this.ctx.api.deviceManager?.getDevice.query({ deviceId: numeric });
93648
- if (device?.name) return device.name;
96086
+ const proxy = await this.ctx.fetchDevice(deviceId);
96087
+ const capNames = new Set(proxy.binding?.entries.map((entry) => entry.capName) ?? []);
96088
+ if (rowsForCaps(capNames).length > 0) return "generic";
96089
+ this.ctx.logger.withTags({ deviceId }).debug("export-hap: no Export tab — no mapped caps", { meta: {
96090
+ type: summary?.type,
96091
+ caps: [...capNames].toSorted().join(",")
96092
+ } });
96093
+ return null;
93649
96094
  } catch (err) {
93650
- this.ctx.logger.withTags({ deviceId: numeric }).debug("export-hap: deviceManager.getDevice failed", { meta: { error: errMsg(err) } });
96095
+ this.ctx.logger.withTags({ deviceId }).debug("export-hap: no Export tab — capability read failed", { meta: {
96096
+ type: summary?.type,
96097
+ error: errMsg(err)
96098
+ } });
96099
+ return null;
93651
96100
  }
93652
- return `Device ${deviceId}`;
93653
96101
  }
93654
96102
  globalSettingsSchema() {
93655
96103
  return this.schema({ sections: [{
93656
96104
  id: "export-hap",
93657
96105
  title: "HomeKit Export",
93658
- description: "Publishes a HomeKit bridge. After the addon boots, scan the pairing QR (or enter the setup code) from the Exported devices panel in the iOS Home app. After pairing, mark individual devices as \"Expose to HomeKit\" on each device's settings page.",
96106
+ description: "Publishes each exposed device as its own HomeKit accessory, sharing one setup code. Mark a device \"Expose to HomeKit\" on its settings page, then add it in the iOS Home app (+ Add Accessory) and enter the setup code shown there.",
93659
96107
  columns: 1,
93660
96108
  fields: [
93661
96109
  this.field({
93662
96110
  type: "text",
93663
96111
  key: "bridgeName",
93664
- label: "Bridge name",
93665
- description: "Name shown in iOS Home and in the Bonjour advertisement.",
96112
+ label: "Installation name",
96113
+ description: "Legacy. Every exposed device is published STANDALONE, under its own camstack device name — HomeKit cameras cannot be bridged, so nothing is bridged. This string only seeds the identity generated on first boot and is not shown in iOS Home.",
93666
96114
  default: DEFAULT_CONFIG.bridgeName,
93667
- requiresRestart: true
96115
+ requiresRestart: true,
96116
+ placement: { tab: "advanced" }
93668
96117
  }),
93669
96118
  this.field({
93670
96119
  type: "number",
@@ -93700,21 +96149,13 @@ var ExportHapAddon = class extends BaseAddon {
93700
96149
  description: "Duration of a single pan/tilt momentary command issued by the PTZ direction switches in Apple Home. Lower = finer steps; higher = bigger sweeps per tap. Defaults to 400ms.",
93701
96150
  default: DEFAULT_CONFIG.ptzPulseMs,
93702
96151
  placement: { tab: "advanced" }
93703
- }),
93704
- this.field({
93705
- type: "boolean",
93706
- key: "hksvPreview",
93707
- label: "HKSV Developer Preview (experimental)",
93708
- 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.",
93709
- default: DEFAULT_CONFIG.hksvPreview,
93710
- requiresRestart: true,
93711
- placement: { tab: "advanced" }
93712
96152
  })
93713
96153
  ]
93714
96154
  }] });
93715
96155
  }
93716
96156
  async buildDeviceSettingsContribution(deviceId) {
93717
- if (!await this.isCameraDevice(deviceId)) return null;
96157
+ const kind = await this.resolveExportKind(deviceId);
96158
+ if (kind === null) return null;
93718
96159
  const entry = this.findEntry(deviceId);
93719
96160
  const settings = entry?.settings ?? DEFAULT_DEVICE_SETTINGS;
93720
96161
  const enabled = entry !== null;
@@ -93731,6 +96172,32 @@ var ExportHapAddon = class extends BaseAddon {
93731
96172
  } catch (err) {
93732
96173
  this.ctx.logger.withTags({ deviceId }).debug("export-hap: setupURI failed for per-device contribution", { meta: { error: errMsg(err) } });
93733
96174
  }
96175
+ const cameraFields = kind !== "camera" ? [] : [{
96176
+ type: "select",
96177
+ key: streamPreferenceKey,
96178
+ label: "Source stream (HomeKit)",
96179
+ description: "Which camstack profile slot HomeKit pulls. Auto = the broker picks the slot closest to 1080p at session start.",
96180
+ options: HAP_STREAM_PREFERENCE_OPTIONS,
96181
+ required: true,
96182
+ value: settings.streamPreference,
96183
+ showWhen: {
96184
+ field: enabledKey,
96185
+ equals: true
96186
+ },
96187
+ immediate: true
96188
+ }, {
96189
+ type: "boolean",
96190
+ key: hksvKey,
96191
+ label: "HomeKit recording (Secure Video)",
96192
+ description: "Offer “Stream and Allow Recording” in iOS Home. Requires iCloud+ and a home hub. Keeps a continuous 8s prebuffer for this camera (~0.7% of one CPU core, H.264 sources only).",
96193
+ style: "switch",
96194
+ value: resolveHksvRecording(settings),
96195
+ showWhen: {
96196
+ field: enabledKey,
96197
+ equals: true
96198
+ },
96199
+ immediate: true
96200
+ }];
93734
96201
  return {
93735
96202
  tabs: [{
93736
96203
  id: "export",
@@ -93741,7 +96208,7 @@ var ExportHapAddon = class extends BaseAddon {
93741
96208
  sections: [{
93742
96209
  id: "export-hap",
93743
96210
  title: "HomeKit Export",
93744
- description: "Mirror this camera into the HomeKit bridge so iOS Home can pair with it.",
96211
+ description: kind === "camera" ? "Publish this camera as its own HomeKit accessory so iOS Home can pair with it." : "Publish this device as its own HomeKit accessory. Its capabilities decide which controls iOS Home shows.",
93745
96212
  tab: "export",
93746
96213
  columns: 1,
93747
96214
  order: 10,
@@ -93757,7 +96224,7 @@ var ExportHapAddon = class extends BaseAddon {
93757
96224
  type: "qr-code",
93758
96225
  key: "__hap-pair-qr",
93759
96226
  label: paired ? "Pairing QR (already paired)" : "Pairing QR",
93760
- caption: paired ? `Already paired with at least one device. Scan from another iPhone / iPad to add it there too. Setup code: ${this.config.identity.pincode}.` : `Scan with the iOS Camera app to pair this camera in HomeKit. Setup code: ${this.config.identity.pincode}.`,
96227
+ caption: paired ? `Already paired with at least one device. Scan from another iPhone / iPad to add it there too. Setup code: ${this.config.identity.pincode}.` : `Scan with the iOS Camera app to pair this accessory in HomeKit. Setup code: ${this.config.identity.pincode}.`,
93761
96228
  value: qrValue,
93762
96229
  size: 192,
93763
96230
  alt: `HomeKit pairing QR for ${name}`,
@@ -93770,38 +96237,12 @@ var ExportHapAddon = class extends BaseAddon {
93770
96237
  type: "boolean",
93771
96238
  key: enabledKey,
93772
96239
  label: "Expose to HomeKit",
93773
- description: "Toggle to publish this camera onto the HAP bridge.",
96240
+ description: "Toggle to publish this device as a HomeKit accessory.",
93774
96241
  style: "switch",
93775
96242
  value: enabled,
93776
96243
  immediate: true
93777
96244
  },
93778
- {
93779
- type: "select",
93780
- key: streamPreferenceKey,
93781
- label: "Source stream (HomeKit)",
93782
- description: "Which camstack profile slot HomeKit pulls. Auto = the broker picks the slot closest to 1080p at session start.",
93783
- options: HAP_STREAM_PREFERENCE_OPTIONS,
93784
- required: true,
93785
- value: settings.streamPreference,
93786
- showWhen: {
93787
- field: enabledKey,
93788
- equals: true
93789
- },
93790
- immediate: true
93791
- },
93792
- {
93793
- type: "boolean",
93794
- key: hksvKey,
93795
- label: "HomeKit recording (Secure Video)",
93796
- 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).",
93797
- style: "switch",
93798
- value: resolveHksvRecording(settings),
93799
- showWhen: {
93800
- field: enabledKey,
93801
- equals: true
93802
- },
93803
- immediate: true
93804
- }
96245
+ ...cameraFields
93805
96246
  ]
93806
96247
  }]
93807
96248
  };
@@ -93867,23 +96308,6 @@ var ExportHapAddon = class extends BaseAddon {
93867
96308
  }
93868
96309
  return { success: true };
93869
96310
  }
93870
- /**
93871
- * Camera-only gate — HomeKit export only knows how to mirror cameras
93872
- * today. Mirrors the snapshot addon's source-side filter so the
93873
- * device-details page doesn't render an "Export" tab on lights /
93874
- * switches / sensors.
93875
- */
93876
- async isCameraDevice(deviceId) {
93877
- const api = this.ctx.api;
93878
- if (!api.deviceManager) return true;
93879
- try {
93880
- const dev = await api.deviceManager.getDevice.query({ deviceId });
93881
- if (!dev) return true;
93882
- return dev.type === DeviceType.Camera;
93883
- } catch {
93884
- return true;
93885
- }
93886
- }
93887
96311
  findEntry(deviceId) {
93888
96312
  const id = String(deviceId);
93889
96313
  return this.config.exposed.find((e) => e.deviceId === id) ?? null;