@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.
@@ -3070,6 +3070,9 @@ function handlePipeResult(left, next, ctx) {
3070
3070
  fallback: left.fallback
3071
3071
  }, ctx);
3072
3072
  }
3073
+ var $ZodPreprocess = /*@__PURE__*/ $constructor("$ZodPreprocess", (inst, def) => {
3074
+ $ZodPipe.init(inst, def);
3075
+ });
3073
3076
  var $ZodReadonly = /*@__PURE__*/ $constructor("$ZodReadonly", (inst, def) => {
3074
3077
  $ZodType.init(inst, def);
3075
3078
  defineLazy(inst._zod, "propValues", () => def.innerType._zod.propValues);
@@ -5255,6 +5258,10 @@ function pipe(in_, out) {
5255
5258
  out
5256
5259
  });
5257
5260
  }
5261
+ var ZodPreprocess = /*@__PURE__*/ $constructor("ZodPreprocess", (inst, def) => {
5262
+ ZodPipe.init(inst, def);
5263
+ $ZodPreprocess.init(inst, def);
5264
+ });
5258
5265
  var ZodReadonly = /*@__PURE__*/ $constructor("ZodReadonly", (inst, def) => {
5259
5266
  $ZodReadonly.init(inst, def);
5260
5267
  ZodType.init(inst, def);
@@ -5313,6 +5320,13 @@ function _instanceof(cls, params = {}) {
5313
5320
  };
5314
5321
  return inst;
5315
5322
  }
5323
+ function preprocess(fn, schema) {
5324
+ return new ZodPreprocess({
5325
+ type: "pipe",
5326
+ in: transform(fn),
5327
+ out: schema
5328
+ });
5329
+ }
5316
5330
  //#endregion
5317
5331
  //#region ../../node_modules/zod/v4/classic/compat.js
5318
5332
  /** @deprecated Use the raw string literal codes instead, e.g. "invalid_type". */
@@ -7589,7 +7603,7 @@ import { errMsg } from '@camstack/types'
7589
7603
  * Extract a human-readable message from an unknown error value.
7590
7604
  * Replaces the ubiquitous `errMsg(err)` pattern.
7591
7605
  */
7592
- function errMsg$12(err) {
7606
+ function errMsg$15(err) {
7593
7607
  if (err instanceof Error) return err.message;
7594
7608
  if (typeof err === "string") return err;
7595
7609
  return String(err);
@@ -15991,7 +16005,87 @@ var MethodAccessSchema = _enum([
15991
16005
  var AllowedProviderSchema = union([literal("*"), array(string())]);
15992
16006
  var AllowedDevicesSchema = record(string(), union([literal("*"), array(string())]));
15993
16007
  var CapScopeSchema = _enum(["device", "system"]);
15994
- var TokenScopeSchema = discriminatedUnion("type", [
16008
+ /**
16009
+ * DeviceSelector (scope model v3 — 2026-08-12).
16010
+ *
16011
+ * A `device` grant no longer carries a frozen list of deviceIds. It carries
16012
+ * a SELECTOR the matcher resolves against the live fleet, so the grant can be
16013
+ * DYNAMIC: a `types:['camera']` selector automatically covers a camera added
16014
+ * AFTER the grant was minted — no re-grant, no re-login.
16015
+ *
16016
+ * - `all` — every device in the deployment. The broad viewer/operator
16017
+ * lever without a `category` grant (a `category` grant also covers device
16018
+ * caps that carry no deviceId; `all` is specifically the device set).
16019
+ * - `ids` — an explicit deviceId list. This is what a v2 `device:[…]`
16020
+ * grant migrates to (see {@link TokenScopeSchema}); STATIC — a new camera
16021
+ * is NOT covered until the grant is edited.
16022
+ * - `types` — every device of a `DeviceType` (e.g. every `camera`).
16023
+ * DYNAMIC. A device that changes type, or a new device of the type,
16024
+ * re-resolves on the next request.
16025
+ * - `locations` — every device whose operator-assigned `location` label is
16026
+ * in the set (e.g. "Garden", "Front door"). DYNAMIC. A device with a
16027
+ * null/unset location matches NO `locations` selector.
16028
+ */
16029
+ var DeviceSelectorSchema = discriminatedUnion("kind", [
16030
+ object({ kind: literal("all") }),
16031
+ object({
16032
+ kind: literal("ids"),
16033
+ ids: array(number().int()).min(1)
16034
+ }),
16035
+ object({
16036
+ kind: literal("types"),
16037
+ types: array(_enum(DeviceType)).min(1)
16038
+ }),
16039
+ object({
16040
+ kind: literal("locations"),
16041
+ locations: array(string().min(1)).min(1)
16042
+ })
16043
+ ]);
16044
+ var DeviceTokenScopeSchema = object({
16045
+ type: literal("device"),
16046
+ /** The device SET this grant covers — resolved against the live fleet. */
16047
+ selector: DeviceSelectorSchema,
16048
+ access: array(MethodAccessSchema).min(1),
16049
+ /**
16050
+ * Whether a grant on a PARENT device transparently covers its accessory
16051
+ * CHILDREN (siren / floodlight / PIR) via the persisted-parentage walk.
16052
+ * Direction is parent → children ONLY.
16053
+ *
16054
+ * Absent → the matcher DERIVES it from the access flavour: `view`
16055
+ * inherits (a camera viewer sees the camera's accessories), `create` /
16056
+ * `delete` do NOT (actuating/removing a child is an explicit act the
16057
+ * operator must grant on the child, not inherit from the parent). Set it
16058
+ * explicitly to override that default per grant.
16059
+ */
16060
+ includeLinked: boolean().optional()
16061
+ });
16062
+ /**
16063
+ * v2 → v3 lazy migration. A pre-v3 `device` grant carried
16064
+ * `targets: string[]` (stringified deviceIds); it rewrites to the equivalent
16065
+ * `selector: {kind:'ids', ids}`. Applied as a `preprocess` so it runs on
16066
+ * EVERY parse path — stored records AND the JWT-carried scope arrays
16067
+ * normalised at the request boundary ({@link normalizeTokenScopes} in
16068
+ * `device-selector.ts`). Chosen over a one-time DB migration because a
16069
+ * migration cannot reach a JWT already in a client's hands; parse-time
16070
+ * migration covers both without a flag day. No cast — the raw object is read
16071
+ * through `Reflect.get` (its static type is `unknown`).
16072
+ */
16073
+ function migrateLegacyTokenScope(raw) {
16074
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return raw;
16075
+ if (Reflect.get(raw, "type") !== "device") return raw;
16076
+ if (Reflect.get(raw, "selector") !== void 0) return raw;
16077
+ const targets = Reflect.get(raw, "targets");
16078
+ if (!Array.isArray(targets)) return raw;
16079
+ return {
16080
+ type: "device",
16081
+ selector: {
16082
+ kind: "ids",
16083
+ ids: targets.map((t) => typeof t === "string" ? Number(t) : t).filter((n) => typeof n === "number" && Number.isInteger(n))
16084
+ },
16085
+ access: Reflect.get(raw, "access")
16086
+ };
16087
+ }
16088
+ var TokenScopeSchema = preprocess(migrateLegacyTokenScope, discriminatedUnion("type", [
15995
16089
  object({
15996
16090
  type: literal("category"),
15997
16091
  target: CapScopeSchema,
@@ -16007,18 +16101,8 @@ var TokenScopeSchema = discriminatedUnion("type", [
16007
16101
  target: string(),
16008
16102
  access: array(MethodAccessSchema).min(1)
16009
16103
  }),
16010
- object({
16011
- type: literal("device"),
16012
- /**
16013
- * One or more deviceIds (serialised as strings for wire-format
16014
- * consistency with the rest of the union). Matcher accepts if
16015
- * `input.deviceId` ∈ `targets`. Array shape avoids the row-explosion
16016
- * of one scope-per-device when granting access to a set of cameras.
16017
- */
16018
- targets: array(string()).min(1),
16019
- access: array(MethodAccessSchema).min(1)
16020
- })
16021
- ]);
16104
+ DeviceTokenScopeSchema
16105
+ ]));
16022
16106
  object({
16023
16107
  id: string(),
16024
16108
  username: string(),
@@ -18034,7 +18118,7 @@ var detectionFpsField = {
18034
18118
  var occupancyRecheckSecField = {
18035
18119
  min: 0,
18036
18120
  max: 300,
18037
- default: 30,
18121
+ default: 300,
18038
18122
  step: 5
18039
18123
  };
18040
18124
  var occupancyRecheckFramesField = {
@@ -20647,7 +20731,11 @@ object({
20647
20731
  precision: number().int().min(0).max(10).optional()
20648
20732
  });
20649
20733
  DeviceType.Sensor;
20650
- object({
20734
+ /**
20735
+ * Ambient illuminance reading in lux. Drives Home Assistant `sensor`
20736
+ * entries with `device_class: illuminance`.
20737
+ */
20738
+ var AmbientLightSensorStatusSchema = object({
20651
20739
  /** Current illuminance in lux (lx). */
20652
20740
  lux: number().min(0),
20653
20741
  /** Ms epoch when the slice was last updated. */
@@ -20920,7 +21008,11 @@ DeviceType.Camera, method(object({ deviceId: number() }), CameraCredentialsSchem
20920
21008
  kind: "query",
20921
21009
  auth: "admin"
20922
21010
  });
20923
- object({
21011
+ /**
21012
+ * Carbon-monoxide alarm sensor. Drives Home Assistant `binary_sensor`
21013
+ * entries with `device_class: carbon_monoxide`. Push-driven.
21014
+ */
21015
+ var CarbonMonoxideStatusSchema = object({
20924
21016
  detected: boolean(),
20925
21017
  /** Ms epoch of the last transition. 0 if never observed. */
20926
21018
  lastChangedAt: number()
@@ -21208,7 +21300,19 @@ Object.values(DeviceType), method(object({
21208
21300
  kind: "mutation",
21209
21301
  auth: "admin"
21210
21302
  }), ConsumablesStatusSchema.extend({ lastFetchedAt: number() });
21211
- object({
21303
+ /**
21304
+ * Door / window / opening / garage / valve contact sensor. Boolean
21305
+ * "is the entry currently open" with the timestamp of the last
21306
+ * transition. Drives Home Assistant `binary_sensor` entries whose
21307
+ * `device_class` is `door`, `window`, `opening`, `garage`, or
21308
+ * `garage_door` — and any future native integration that needs
21309
+ * the same semantics.
21310
+ *
21311
+ * Push-driven: providers update the slice on transition events from
21312
+ * the upstream source (HA WebSocket `state_changed`, ZWave
21313
+ * `notification` …). Consumers read the slice; no polling.
21314
+ */
21315
+ var ContactStatusSchema = object({
21212
21316
  /** True when the entry is open; false when closed. */
21213
21317
  entryOpen: boolean(),
21214
21318
  /** Ms epoch of the last open↔closed transition. 0 if never observed. */
@@ -21807,7 +21911,15 @@ object({
21807
21911
  deviceId: number(),
21808
21912
  status: FeatureProbeStatusSchema
21809
21913
  });
21810
- object({
21914
+ /**
21915
+ * Water leak / moisture sensor. Boolean "is liquid currently
21916
+ * detected" with the timestamp of the last transition. Drives Home
21917
+ * Assistant `binary_sensor` entries with `device_class: moisture`,
21918
+ * and any future native flood sensor.
21919
+ *
21920
+ * Push-driven from the upstream source.
21921
+ */
21922
+ var FloodStatusSchema = object({
21811
21923
  /** True when leak is currently detected. */
21812
21924
  flooded: boolean(),
21813
21925
  /** Ms epoch of the last flooded↔dry transition. 0 if never observed. */
@@ -21861,7 +21973,15 @@ DeviceType.Humidifier, method(object({
21861
21973
  kind: "mutation",
21862
21974
  auth: "admin"
21863
21975
  });
21864
- object({
21976
+ /**
21977
+ * Single-metric humidity reading. Drives Home Assistant `sensor`
21978
+ * entries with `device_class: humidity`.
21979
+ *
21980
+ * Unit normalisation: percent. The canonical display unit (`%`) is a
21981
+ * descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
21982
+ * `sourceInfo`.
21983
+ */
21984
+ var HumiditySensorStatusSchema = object({
21865
21985
  /** Current relative humidity, 0..100. */
21866
21986
  percent: number().min(0).max(100),
21867
21987
  /** Ms epoch when the slice was last updated. */
@@ -22483,7 +22603,7 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
22483
22603
  * tunnel always emits `https://` regardless. */
22484
22604
  scheme: _enum(["http", "https"]).optional()
22485
22605
  }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
22486
- object({
22606
+ var LockControlStatusSchema = object({
22487
22607
  /** Lifecycle state of the lock. `jammed` means the motor reported
22488
22608
  * failure to reach the target — operator intervention required. */
22489
22609
  state: _enum([
@@ -22821,7 +22941,19 @@ authKey: string().optional() }), object({
22821
22941
  /** Human-readable error when `ok: false`. */
22822
22942
  error: string().optional()
22823
22943
  }), { kind: "mutation" });
22824
- object({
22944
+ /**
22945
+ * Hardware / firmware motion sensor cap — binary detected state plus
22946
+ * a timestamp of the last observation. Distinct from
22947
+ * `motion-detection.cap.ts` which owns the LOCAL ML motion pipeline;
22948
+ * `motion` is the lightweight readout from on-camera motion (Reolink
22949
+ * `GetMdState`, Baichuan push `type: motion`, ONVIF analytics).
22950
+ *
22951
+ * Native-motion providers also fan out to `detection.camera-native`
22952
+ * with `source: 'onboard'` so cross-cutting system services
22953
+ * (alert-center, advanced-notifier) can subscribe once and receive
22954
+ * motion from every camera.
22955
+ */
22956
+ var MotionStatusSchema = object({
22825
22957
  detected: boolean(),
22826
22958
  /** Ms epoch of the last detected-true observation. Null if never detected. */
22827
22959
  lastDetectedAt: number().nullable(),
@@ -23933,7 +24065,7 @@ var GpsLocationSchema = object({
23933
24065
  /** Reported accuracy in meters (lower = better). */
23934
24066
  accuracyMeters: number().nonnegative()
23935
24067
  });
23936
- object({
24068
+ var PresenceStatusSchema = object({
23937
24069
  /** `home` / `not_home` / any user-defined zone name. */
23938
24070
  state: string(),
23939
24071
  /** Optional textual location label (zone name, city, address). Null
@@ -24349,7 +24481,7 @@ method(object({
24349
24481
  toMs: number()
24350
24482
  }), RecordingAvailabilitySchema, {
24351
24483
  kind: "query",
24352
- auth: "admin"
24484
+ auth: "protected"
24353
24485
  }), method(object({
24354
24486
  deviceId: number(),
24355
24487
  fromMs: number(),
@@ -24357,14 +24489,14 @@ method(object({
24357
24489
  tzOffsetMinutes: number()
24358
24490
  }), RecordingDaysSchema, {
24359
24491
  kind: "query",
24360
- auth: "admin"
24492
+ auth: "protected"
24361
24493
  }), method(object({
24362
24494
  deviceId: number(),
24363
24495
  fromMs: number(),
24364
24496
  toMs: number()
24365
24497
  }), RecordingManifestSchema, {
24366
24498
  kind: "query",
24367
- auth: "admin"
24499
+ auth: "protected"
24368
24500
  }), method(object({}), RecordingStorageUsageSchema, {
24369
24501
  kind: "query",
24370
24502
  auth: "admin"
@@ -24893,7 +25025,16 @@ DeviceType.Script, method(object({
24893
25025
  kind: "mutation",
24894
25026
  auth: "admin"
24895
25027
  });
24896
- object({
25028
+ /**
25029
+ * Smoke alarm sensor — boolean "is smoke currently detected" with
25030
+ * timestamp of the last transition. Drives Home Assistant
25031
+ * `binary_sensor` entries with `device_class: smoke`.
25032
+ *
25033
+ * Push-driven: a smoke event is critical, so the slice updates
25034
+ * immediately on the upstream signal. Auto-clearing back to false is
25035
+ * provider-controlled (some alarms latch until manually reset).
25036
+ */
25037
+ var SmokeStatusSchema = object({
24897
25038
  detected: boolean(),
24898
25039
  /** Ms epoch of the last transition. 0 if never observed. */
24899
25040
  lastChangedAt: number()
@@ -25102,7 +25243,23 @@ object({
25102
25243
  lastChangedAt: number()
25103
25244
  });
25104
25245
  DeviceType.Sensor;
25105
- object({
25246
+ /**
25247
+ * Single-metric temperature reading. Drives Home Assistant `sensor`
25248
+ * entries with `device_class: temperature` and any future native
25249
+ * thermometer.
25250
+ *
25251
+ * Unit normalisation: providers convert to Celsius before storing.
25252
+ * The slice value is always Celsius so cross-cap aggregators
25253
+ * (climate-control's `currentTemp`, energy analytics) can compose
25254
+ * without per-source unit fixups. The canonical display unit (`°C`) is
25255
+ * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
25256
+ * `sourceInfo`.
25257
+ *
25258
+ * Status `lastFetchedAt` lets staleness-aware consumers detect a
25259
+ * frozen feed (provider hung) distinct from a "temperature hasn't
25260
+ * changed" steady state.
25261
+ */
25262
+ var TemperatureSensorStatusSchema = object({
25106
25263
  /** Current temperature in Celsius. */
25107
25264
  celsius: number(),
25108
25265
  /** Ms epoch when the slice was last updated (push or poll). */
@@ -31363,6 +31520,1678 @@ Object.freeze({
31363
31520
  access: "create"
31364
31521
  }
31365
31522
  });
31523
+ Object.freeze({
31524
+ "accessories.setChildHidden": [{
31525
+ name: "childDeviceId",
31526
+ form: "single",
31527
+ optional: false
31528
+ }, {
31529
+ name: "deviceId",
31530
+ form: "single",
31531
+ optional: false
31532
+ }],
31533
+ "addonSettings.getDeviceSettings": [{
31534
+ name: "deviceId",
31535
+ form: "single",
31536
+ optional: false
31537
+ }],
31538
+ "addonSettings.updateDeviceSettings": [{
31539
+ name: "deviceId",
31540
+ form: "single",
31541
+ optional: false
31542
+ }],
31543
+ "alarmPanel.arm": [{
31544
+ name: "deviceId",
31545
+ form: "single",
31546
+ optional: false
31547
+ }],
31548
+ "alarmPanel.disarm": [{
31549
+ name: "deviceId",
31550
+ form: "single",
31551
+ optional: false
31552
+ }],
31553
+ "alarmPanel.trigger": [{
31554
+ name: "deviceId",
31555
+ form: "single",
31556
+ optional: false
31557
+ }],
31558
+ "audioAnalysis.resolveDeviceSettings": [{
31559
+ name: "deviceId",
31560
+ form: "single",
31561
+ optional: false
31562
+ }],
31563
+ "audioAnalyzer.classify": [{
31564
+ name: "deviceId",
31565
+ form: "single",
31566
+ optional: true
31567
+ }],
31568
+ "audioMetrics.getCurrentSnapshot": [{
31569
+ name: "deviceId",
31570
+ form: "single",
31571
+ optional: false
31572
+ }],
31573
+ "audioMetrics.getHistory": [{
31574
+ name: "deviceId",
31575
+ form: "single",
31576
+ optional: false
31577
+ }],
31578
+ "automationControl.disable": [{
31579
+ name: "deviceId",
31580
+ form: "single",
31581
+ optional: false
31582
+ }],
31583
+ "automationControl.enable": [{
31584
+ name: "deviceId",
31585
+ form: "single",
31586
+ optional: false
31587
+ }],
31588
+ "automationControl.trigger": [{
31589
+ name: "deviceId",
31590
+ form: "single",
31591
+ optional: false
31592
+ }],
31593
+ "battery.wakeForStream": [{
31594
+ name: "deviceId",
31595
+ form: "single",
31596
+ optional: false
31597
+ }],
31598
+ "brightness.setBrightness": [{
31599
+ name: "deviceId",
31600
+ form: "single",
31601
+ optional: false
31602
+ }],
31603
+ "button.press": [{
31604
+ name: "deviceId",
31605
+ form: "single",
31606
+ optional: false
31607
+ }],
31608
+ "cameraCredentials.getCredentials": [{
31609
+ name: "deviceId",
31610
+ form: "single",
31611
+ optional: false
31612
+ }],
31613
+ "cameraStreams.getBrokerStreams": [{
31614
+ name: "deviceId",
31615
+ form: "single",
31616
+ optional: false
31617
+ }],
31618
+ "cameraStreams.getCameraStreams": [{
31619
+ name: "deviceId",
31620
+ form: "single",
31621
+ optional: false
31622
+ }],
31623
+ "cameraStreams.getProfileRtspEntries": [{
31624
+ name: "deviceId",
31625
+ form: "single",
31626
+ optional: false
31627
+ }],
31628
+ "cameraStreams.getRtspEntries": [{
31629
+ name: "deviceId",
31630
+ form: "single",
31631
+ optional: false
31632
+ }],
31633
+ "cameraStreams.pickStream": [{
31634
+ name: "deviceId",
31635
+ form: "single",
31636
+ optional: false
31637
+ }],
31638
+ "climateControl.setFanMode": [{
31639
+ name: "deviceId",
31640
+ form: "single",
31641
+ optional: false
31642
+ }],
31643
+ "climateControl.setMode": [{
31644
+ name: "deviceId",
31645
+ form: "single",
31646
+ optional: false
31647
+ }],
31648
+ "climateControl.setPreset": [{
31649
+ name: "deviceId",
31650
+ form: "single",
31651
+ optional: false
31652
+ }],
31653
+ "climateControl.setSwingHorizontal": [{
31654
+ name: "deviceId",
31655
+ form: "single",
31656
+ optional: false
31657
+ }],
31658
+ "climateControl.setSwingVertical": [{
31659
+ name: "deviceId",
31660
+ form: "single",
31661
+ optional: false
31662
+ }],
31663
+ "climateControl.setTarget": [{
31664
+ name: "deviceId",
31665
+ form: "single",
31666
+ optional: false
31667
+ }],
31668
+ "climateControl.setTargetHumidity": [{
31669
+ name: "deviceId",
31670
+ form: "single",
31671
+ optional: false
31672
+ }],
31673
+ "climateControl.setTargetRange": [{
31674
+ name: "deviceId",
31675
+ form: "single",
31676
+ optional: false
31677
+ }],
31678
+ "color.setColor": [{
31679
+ name: "deviceId",
31680
+ form: "single",
31681
+ optional: false
31682
+ }],
31683
+ "consumables.reset": [{
31684
+ name: "deviceId",
31685
+ form: "single",
31686
+ optional: false
31687
+ }],
31688
+ "control.setValue": [{
31689
+ name: "deviceId",
31690
+ form: "single",
31691
+ optional: false
31692
+ }],
31693
+ "cover.close": [{
31694
+ name: "deviceId",
31695
+ form: "single",
31696
+ optional: false
31697
+ }],
31698
+ "cover.open": [{
31699
+ name: "deviceId",
31700
+ form: "single",
31701
+ optional: false
31702
+ }],
31703
+ "cover.setPosition": [{
31704
+ name: "deviceId",
31705
+ form: "single",
31706
+ optional: false
31707
+ }],
31708
+ "cover.setTiltPosition": [{
31709
+ name: "deviceId",
31710
+ form: "single",
31711
+ optional: false
31712
+ }],
31713
+ "cover.stop": [{
31714
+ name: "deviceId",
31715
+ form: "single",
31716
+ optional: false
31717
+ }],
31718
+ "dayNight.getOptions": [{
31719
+ name: "deviceId",
31720
+ form: "single",
31721
+ optional: false
31722
+ }],
31723
+ "dayNight.setSettings": [{
31724
+ name: "deviceId",
31725
+ form: "single",
31726
+ optional: false
31727
+ }],
31728
+ "decoder.createSession": [{
31729
+ name: "deviceId",
31730
+ form: "single",
31731
+ optional: true
31732
+ }],
31733
+ "deviceAdoption.release": [{
31734
+ name: "camDeviceId",
31735
+ form: "single",
31736
+ optional: false
31737
+ }],
31738
+ "deviceAdoption.resync": [{
31739
+ name: "camDeviceId",
31740
+ form: "single",
31741
+ optional: false
31742
+ }],
31743
+ "deviceDiscovery.adoptDevice": [{
31744
+ name: "deviceId",
31745
+ form: "single",
31746
+ optional: false
31747
+ }],
31748
+ "deviceDiscovery.listDiscovered": [{
31749
+ name: "deviceId",
31750
+ form: "single",
31751
+ optional: false
31752
+ }],
31753
+ "deviceDiscovery.refreshDiscovery": [{
31754
+ name: "deviceId",
31755
+ form: "single",
31756
+ optional: false
31757
+ }],
31758
+ "deviceDiscovery.releaseDevice": [{
31759
+ name: "childDeviceId",
31760
+ form: "single",
31761
+ optional: false
31762
+ }, {
31763
+ name: "deviceId",
31764
+ form: "single",
31765
+ optional: false
31766
+ }],
31767
+ "deviceManager.adoptionRelease": [{
31768
+ name: "camDeviceId",
31769
+ form: "single",
31770
+ optional: false
31771
+ }],
31772
+ "deviceManager.adoptionResync": [{
31773
+ name: "camDeviceId",
31774
+ form: "single",
31775
+ optional: false
31776
+ }],
31777
+ "deviceManager.applyInitialMeta": [{
31778
+ name: "deviceId",
31779
+ form: "single",
31780
+ optional: false
31781
+ }, {
31782
+ name: "linkDeviceId",
31783
+ form: "single",
31784
+ optional: true
31785
+ }],
31786
+ "deviceManager.disable": [{
31787
+ name: "deviceId",
31788
+ form: "single",
31789
+ optional: false
31790
+ }],
31791
+ "deviceManager.enable": [{
31792
+ name: "deviceId",
31793
+ form: "single",
31794
+ optional: false
31795
+ }],
31796
+ "deviceManager.getBindings": [{
31797
+ name: "deviceId",
31798
+ form: "single",
31799
+ optional: false
31800
+ }],
31801
+ "deviceManager.getChildren": [{
31802
+ name: "parentDeviceId",
31803
+ form: "single",
31804
+ optional: false
31805
+ }],
31806
+ "deviceManager.getConfigSchema": [{
31807
+ name: "deviceId",
31808
+ form: "single",
31809
+ optional: false
31810
+ }],
31811
+ "deviceManager.getDevice": [{
31812
+ name: "deviceId",
31813
+ form: "single",
31814
+ optional: false
31815
+ }],
31816
+ "deviceManager.getDeviceAggregate": [{
31817
+ name: "deviceId",
31818
+ form: "single",
31819
+ optional: false
31820
+ }],
31821
+ "deviceManager.getDeviceLiveInfoAggregate": [{
31822
+ name: "deviceId",
31823
+ form: "single",
31824
+ optional: false
31825
+ }],
31826
+ "deviceManager.getDeviceSettingsAggregate": [{
31827
+ name: "deviceId",
31828
+ form: "single",
31829
+ optional: false
31830
+ }],
31831
+ "deviceManager.getDeviceStatusAggregate": [{
31832
+ name: "deviceId",
31833
+ form: "single",
31834
+ optional: false
31835
+ }],
31836
+ "deviceManager.getDeviceStatusAggregateBatch": [{
31837
+ name: "deviceIds",
31838
+ form: "array",
31839
+ optional: false
31840
+ }],
31841
+ "deviceManager.getLinkedDevices": [{
31842
+ name: "deviceId",
31843
+ form: "single",
31844
+ optional: false
31845
+ }],
31846
+ "deviceManager.getSettingsSchema": [{
31847
+ name: "deviceId",
31848
+ form: "single",
31849
+ optional: false
31850
+ }],
31851
+ "deviceManager.getStreamProfileMap": [{
31852
+ name: "deviceId",
31853
+ form: "single",
31854
+ optional: false
31855
+ }],
31856
+ "deviceManager.getStreamSources": [{
31857
+ name: "deviceId",
31858
+ form: "single",
31859
+ optional: false
31860
+ }],
31861
+ "deviceManager.getWireableFields": [{
31862
+ name: "deviceId",
31863
+ form: "single",
31864
+ optional: false
31865
+ }],
31866
+ "deviceManager.loadConfig": [{
31867
+ name: "deviceId",
31868
+ form: "single",
31869
+ optional: false
31870
+ }],
31871
+ "deviceManager.loadMeta": [{
31872
+ name: "deviceId",
31873
+ form: "single",
31874
+ optional: false
31875
+ }],
31876
+ "deviceManager.loadRuntimeState": [{
31877
+ name: "deviceId",
31878
+ form: "single",
31879
+ optional: false
31880
+ }],
31881
+ "deviceManager.persistConfig": [{
31882
+ name: "deviceId",
31883
+ form: "single",
31884
+ optional: false
31885
+ }],
31886
+ "deviceManager.probeStreams": [{
31887
+ name: "deviceId",
31888
+ form: "single",
31889
+ optional: false
31890
+ }],
31891
+ "deviceManager.registerDevice": [{
31892
+ name: "parentDeviceId",
31893
+ form: "single",
31894
+ optional: true
31895
+ }],
31896
+ "deviceManager.remove": [{
31897
+ name: "deviceId",
31898
+ form: "single",
31899
+ optional: false
31900
+ }],
31901
+ "deviceManager.removeDevice": [{
31902
+ name: "deviceId",
31903
+ form: "single",
31904
+ optional: false
31905
+ }],
31906
+ "deviceManager.runDeviceAction": [{
31907
+ name: "deviceId",
31908
+ form: "single",
31909
+ optional: false
31910
+ }],
31911
+ "deviceManager.setChildLayout": [{
31912
+ name: "deviceId",
31913
+ form: "single",
31914
+ optional: false
31915
+ }],
31916
+ "deviceManager.setDisabled": [{
31917
+ name: "deviceId",
31918
+ form: "single",
31919
+ optional: false
31920
+ }],
31921
+ "deviceManager.setDisplay": [{
31922
+ name: "deviceId",
31923
+ form: "single",
31924
+ optional: false
31925
+ }],
31926
+ "deviceManager.setIntegrationId": [{
31927
+ name: "deviceId",
31928
+ form: "single",
31929
+ optional: false
31930
+ }],
31931
+ "deviceManager.setLinkDeviceId": [{
31932
+ name: "deviceId",
31933
+ form: "single",
31934
+ optional: false
31935
+ }, {
31936
+ name: "linkDeviceId",
31937
+ form: "single",
31938
+ optional: true
31939
+ }],
31940
+ "deviceManager.setLocation": [{
31941
+ name: "deviceId",
31942
+ form: "single",
31943
+ optional: false
31944
+ }],
31945
+ "deviceManager.setMetadata": [{
31946
+ name: "deviceId",
31947
+ form: "single",
31948
+ optional: false
31949
+ }],
31950
+ "deviceManager.setName": [{
31951
+ name: "deviceId",
31952
+ form: "single",
31953
+ optional: false
31954
+ }],
31955
+ "deviceManager.setPrimaryChildEntityId": [{
31956
+ name: "deviceId",
31957
+ form: "single",
31958
+ optional: false
31959
+ }],
31960
+ "deviceManager.setRole": [{
31961
+ name: "deviceId",
31962
+ form: "single",
31963
+ optional: false
31964
+ }],
31965
+ "deviceManager.setStreamProfileMap": [{
31966
+ name: "deviceId",
31967
+ form: "single",
31968
+ optional: false
31969
+ }],
31970
+ "deviceManager.setType": [{
31971
+ name: "deviceId",
31972
+ form: "single",
31973
+ optional: false
31974
+ }],
31975
+ "deviceManager.setWrapperActive": [{
31976
+ name: "deviceId",
31977
+ form: "single",
31978
+ optional: false
31979
+ }],
31980
+ "deviceManager.testField": [{
31981
+ name: "deviceId",
31982
+ form: "single",
31983
+ optional: false
31984
+ }],
31985
+ "deviceManager.updateConfig": [{
31986
+ name: "deviceId",
31987
+ form: "single",
31988
+ optional: false
31989
+ }],
31990
+ "deviceManager.updateDeviceField": [{
31991
+ name: "deviceId",
31992
+ form: "single",
31993
+ optional: false
31994
+ }],
31995
+ "deviceManager.updateDeviceFieldsBatch": [{
31996
+ name: "deviceId",
31997
+ form: "single",
31998
+ optional: false
31999
+ }],
32000
+ "deviceOps.getConfigEntries": [{
32001
+ name: "deviceId",
32002
+ form: "single",
32003
+ optional: false
32004
+ }],
32005
+ "deviceOps.getRawState": [{
32006
+ name: "deviceId",
32007
+ form: "single",
32008
+ optional: false
32009
+ }],
32010
+ "deviceOps.getSettingsSchema": [{
32011
+ name: "deviceId",
32012
+ form: "single",
32013
+ optional: false
32014
+ }],
32015
+ "deviceOps.getStreamSources": [{
32016
+ name: "deviceId",
32017
+ form: "single",
32018
+ optional: false
32019
+ }],
32020
+ "deviceOps.removeDevice": [{
32021
+ name: "deviceId",
32022
+ form: "single",
32023
+ optional: false
32024
+ }],
32025
+ "deviceOps.runAction": [{
32026
+ name: "deviceId",
32027
+ form: "single",
32028
+ optional: false
32029
+ }],
32030
+ "deviceOps.setConfig": [{
32031
+ name: "deviceId",
32032
+ form: "single",
32033
+ optional: false
32034
+ }],
32035
+ "deviceState.getCapSlice": [{
32036
+ name: "deviceId",
32037
+ form: "single",
32038
+ optional: false
32039
+ }],
32040
+ "deviceState.getSnapshot": [{
32041
+ name: "deviceId",
32042
+ form: "single",
32043
+ optional: false
32044
+ }],
32045
+ "deviceState.setCapSlice": [{
32046
+ name: "deviceId",
32047
+ form: "single",
32048
+ optional: false
32049
+ }],
32050
+ "events.getEventClipUrl": [{
32051
+ name: "deviceId",
32052
+ form: "single",
32053
+ optional: false
32054
+ }],
32055
+ "events.getEvents": [{
32056
+ name: "deviceId",
32057
+ form: "single",
32058
+ optional: false
32059
+ }],
32060
+ "events.getEventThumbnail": [{
32061
+ name: "deviceId",
32062
+ form: "single",
32063
+ optional: false
32064
+ }],
32065
+ "faceGallery.getFaceByTrack": [{
32066
+ name: "deviceId",
32067
+ form: "single",
32068
+ optional: false
32069
+ }],
32070
+ "faceGallery.listRecentFaces": [{
32071
+ name: "deviceId",
32072
+ form: "single",
32073
+ optional: true
32074
+ }],
32075
+ "fanControl.setDirection": [{
32076
+ name: "deviceId",
32077
+ form: "single",
32078
+ optional: false
32079
+ }],
32080
+ "fanControl.setOscillating": [{
32081
+ name: "deviceId",
32082
+ form: "single",
32083
+ optional: false
32084
+ }],
32085
+ "fanControl.setPercentage": [{
32086
+ name: "deviceId",
32087
+ form: "single",
32088
+ optional: false
32089
+ }],
32090
+ "fanControl.setPreset": [{
32091
+ name: "deviceId",
32092
+ form: "single",
32093
+ optional: false
32094
+ }],
32095
+ "humidifier.setMode": [{
32096
+ name: "deviceId",
32097
+ form: "single",
32098
+ optional: false
32099
+ }],
32100
+ "humidifier.setOn": [{
32101
+ name: "deviceId",
32102
+ form: "single",
32103
+ optional: false
32104
+ }],
32105
+ "humidifier.setTargetHumidity": [{
32106
+ name: "deviceId",
32107
+ form: "single",
32108
+ optional: false
32109
+ }],
32110
+ "imageSettings.getOptions": [{
32111
+ name: "deviceId",
32112
+ form: "single",
32113
+ optional: false
32114
+ }],
32115
+ "imageSettings.setSettings": [{
32116
+ name: "deviceId",
32117
+ form: "single",
32118
+ optional: false
32119
+ }],
32120
+ "intercom.endTalkSession": [{
32121
+ name: "deviceId",
32122
+ form: "single",
32123
+ optional: false
32124
+ }],
32125
+ "intercom.handleAnswer": [{
32126
+ name: "deviceId",
32127
+ form: "single",
32128
+ optional: false
32129
+ }],
32130
+ "intercom.pushTalkAudio": [{
32131
+ name: "deviceId",
32132
+ form: "single",
32133
+ optional: false
32134
+ }],
32135
+ "intercom.startSession": [{
32136
+ name: "deviceId",
32137
+ form: "single",
32138
+ optional: false
32139
+ }],
32140
+ "intercom.startTalkSession": [{
32141
+ name: "deviceId",
32142
+ form: "single",
32143
+ optional: false
32144
+ }],
32145
+ "intercom.stopSession": [{
32146
+ name: "deviceId",
32147
+ form: "single",
32148
+ optional: false
32149
+ }],
32150
+ "lawnMowerControl.dock": [{
32151
+ name: "deviceId",
32152
+ form: "single",
32153
+ optional: false
32154
+ }],
32155
+ "lawnMowerControl.pause": [{
32156
+ name: "deviceId",
32157
+ form: "single",
32158
+ optional: false
32159
+ }],
32160
+ "lawnMowerControl.startMowing": [{
32161
+ name: "deviceId",
32162
+ form: "single",
32163
+ optional: false
32164
+ }],
32165
+ "lockControl.lock": [{
32166
+ name: "deviceId",
32167
+ form: "single",
32168
+ optional: false
32169
+ }],
32170
+ "lockControl.open": [{
32171
+ name: "deviceId",
32172
+ form: "single",
32173
+ optional: false
32174
+ }],
32175
+ "lockControl.unlock": [{
32176
+ name: "deviceId",
32177
+ form: "single",
32178
+ optional: false
32179
+ }],
32180
+ "mediaPlayer.next": [{
32181
+ name: "deviceId",
32182
+ form: "single",
32183
+ optional: false
32184
+ }],
32185
+ "mediaPlayer.pause": [{
32186
+ name: "deviceId",
32187
+ form: "single",
32188
+ optional: false
32189
+ }],
32190
+ "mediaPlayer.play": [{
32191
+ name: "deviceId",
32192
+ form: "single",
32193
+ optional: false
32194
+ }],
32195
+ "mediaPlayer.playMedia": [{
32196
+ name: "deviceId",
32197
+ form: "single",
32198
+ optional: false
32199
+ }],
32200
+ "mediaPlayer.previous": [{
32201
+ name: "deviceId",
32202
+ form: "single",
32203
+ optional: false
32204
+ }],
32205
+ "mediaPlayer.seek": [{
32206
+ name: "deviceId",
32207
+ form: "single",
32208
+ optional: false
32209
+ }],
32210
+ "mediaPlayer.selectSource": [{
32211
+ name: "deviceId",
32212
+ form: "single",
32213
+ optional: false
32214
+ }],
32215
+ "mediaPlayer.setMute": [{
32216
+ name: "deviceId",
32217
+ form: "single",
32218
+ optional: false
32219
+ }],
32220
+ "mediaPlayer.setRepeat": [{
32221
+ name: "deviceId",
32222
+ form: "single",
32223
+ optional: false
32224
+ }],
32225
+ "mediaPlayer.setShuffle": [{
32226
+ name: "deviceId",
32227
+ form: "single",
32228
+ optional: false
32229
+ }],
32230
+ "mediaPlayer.setVolume": [{
32231
+ name: "deviceId",
32232
+ form: "single",
32233
+ optional: false
32234
+ }],
32235
+ "mediaPlayer.stop": [{
32236
+ name: "deviceId",
32237
+ form: "single",
32238
+ optional: false
32239
+ }],
32240
+ "motion.isDetected": [{
32241
+ name: "deviceId",
32242
+ form: "single",
32243
+ optional: false
32244
+ }],
32245
+ "motionDetection.analyze": [{
32246
+ name: "deviceId",
32247
+ form: "single",
32248
+ optional: false
32249
+ }],
32250
+ "motionDetection.removeCamera": [{
32251
+ name: "deviceId",
32252
+ form: "single",
32253
+ optional: false
32254
+ }],
32255
+ "motionTrigger.setMotionTrigger": [{
32256
+ name: "deviceId",
32257
+ form: "single",
32258
+ optional: false
32259
+ }],
32260
+ "motionZones.getOptions": [{
32261
+ name: "deviceId",
32262
+ form: "single",
32263
+ optional: false
32264
+ }],
32265
+ "motionZones.setZone": [{
32266
+ name: "deviceId",
32267
+ form: "single",
32268
+ optional: false
32269
+ }],
32270
+ "nativeObjectDetection.setEnabled": [{
32271
+ name: "deviceId",
32272
+ form: "single",
32273
+ optional: false
32274
+ }],
32275
+ "networkQuality.getDeviceStats": [{
32276
+ name: "deviceId",
32277
+ form: "single",
32278
+ optional: false
32279
+ }],
32280
+ "networkQuality.reportClientStats": [{
32281
+ name: "deviceId",
32282
+ form: "single",
32283
+ optional: false
32284
+ }],
32285
+ "notificationRules.setDeviceMuted": [{
32286
+ name: "deviceId",
32287
+ form: "single",
32288
+ optional: false
32289
+ }],
32290
+ "notifier.cancel": [{
32291
+ name: "deviceId",
32292
+ form: "single",
32293
+ optional: false
32294
+ }],
32295
+ "notifier.send": [{
32296
+ name: "deviceId",
32297
+ form: "single",
32298
+ optional: false
32299
+ }],
32300
+ "osd.setOverlay": [{
32301
+ name: "deviceId",
32302
+ form: "single",
32303
+ optional: false
32304
+ }],
32305
+ "osdManager.clearSlotBinding": [{
32306
+ name: "deviceId",
32307
+ form: "single",
32308
+ optional: false
32309
+ }],
32310
+ "osdManager.copyDeviceConfiguration": [{
32311
+ name: "sourceDeviceId",
32312
+ form: "single",
32313
+ optional: false
32314
+ }, {
32315
+ name: "targetDeviceId",
32316
+ form: "single",
32317
+ optional: false
32318
+ }],
32319
+ "osdManager.getDeviceOsd": [{
32320
+ name: "deviceId",
32321
+ form: "single",
32322
+ optional: false
32323
+ }],
32324
+ "osdManager.getSourceCatalog": [{
32325
+ name: "deviceId",
32326
+ form: "single",
32327
+ optional: false
32328
+ }],
32329
+ "osdManager.previewSlot": [{
32330
+ name: "deviceId",
32331
+ form: "single",
32332
+ optional: false
32333
+ }],
32334
+ "osdManager.renderDevice": [{
32335
+ name: "deviceId",
32336
+ form: "single",
32337
+ optional: false
32338
+ }],
32339
+ "osdManager.setSlotBinding": [{
32340
+ name: "deviceId",
32341
+ form: "single",
32342
+ optional: false
32343
+ }],
32344
+ "petFeeder.callPet": [{
32345
+ name: "deviceId",
32346
+ form: "single",
32347
+ optional: false
32348
+ }],
32349
+ "petFeeder.cancelFeed": [{
32350
+ name: "deviceId",
32351
+ form: "single",
32352
+ optional: false
32353
+ }],
32354
+ "petFeeder.feed": [{
32355
+ name: "deviceId",
32356
+ form: "single",
32357
+ optional: false
32358
+ }],
32359
+ "petFeeder.markFoodReplenished": [{
32360
+ name: "deviceId",
32361
+ form: "single",
32362
+ optional: false
32363
+ }],
32364
+ "petFeeder.playSound": [{
32365
+ name: "deviceId",
32366
+ form: "single",
32367
+ optional: false
32368
+ }],
32369
+ "petFeeder.resetDesiccant": [{
32370
+ name: "deviceId",
32371
+ form: "single",
32372
+ optional: false
32373
+ }],
32374
+ "petFeeder.setChildLock": [{
32375
+ name: "deviceId",
32376
+ form: "single",
32377
+ optional: false
32378
+ }],
32379
+ "petFeeder.setFeedSound": [{
32380
+ name: "deviceId",
32381
+ form: "single",
32382
+ optional: false
32383
+ }],
32384
+ "petFeeder.setIndicatorLight": [{
32385
+ name: "deviceId",
32386
+ form: "single",
32387
+ optional: false
32388
+ }],
32389
+ "petFeeder.setVolume": [{
32390
+ name: "deviceId",
32391
+ form: "single",
32392
+ optional: false
32393
+ }],
32394
+ "pipelineAnalytics.clearTracks": [{
32395
+ name: "deviceId",
32396
+ form: "single",
32397
+ optional: false
32398
+ }],
32399
+ "pipelineAnalytics.completeRetrainTrack": [{
32400
+ name: "deviceId",
32401
+ form: "single",
32402
+ optional: false
32403
+ }],
32404
+ "pipelineAnalytics.deleteDeviceEvents": [{
32405
+ name: "deviceId",
32406
+ form: "single",
32407
+ optional: false
32408
+ }],
32409
+ "pipelineAnalytics.deleteTracks": [{
32410
+ name: "deviceId",
32411
+ form: "single",
32412
+ optional: false
32413
+ }],
32414
+ "pipelineAnalytics.deselectRetrainFrame": [{
32415
+ name: "deviceId",
32416
+ form: "single",
32417
+ optional: false
32418
+ }],
32419
+ "pipelineAnalytics.getActiveTracks": [{
32420
+ name: "deviceId",
32421
+ form: "single",
32422
+ optional: false
32423
+ }],
32424
+ "pipelineAnalytics.getAudioEvents": [{
32425
+ name: "deviceId",
32426
+ form: "single",
32427
+ optional: false
32428
+ }],
32429
+ "pipelineAnalytics.getEventDensity": [{
32430
+ name: "deviceId",
32431
+ form: "single",
32432
+ optional: false
32433
+ }],
32434
+ "pipelineAnalytics.getKeyEvents": [{
32435
+ name: "deviceId",
32436
+ form: "single",
32437
+ optional: false
32438
+ }],
32439
+ "pipelineAnalytics.getMotionEvents": [{
32440
+ name: "deviceId",
32441
+ form: "single",
32442
+ optional: false
32443
+ }],
32444
+ "pipelineAnalytics.getObjectEvents": [{
32445
+ name: "deviceId",
32446
+ form: "single",
32447
+ optional: false
32448
+ }],
32449
+ "pipelineAnalytics.getRetrainExportUrl": [{
32450
+ name: "deviceIds",
32451
+ form: "array",
32452
+ optional: true
32453
+ }],
32454
+ "pipelineAnalytics.getSensorEvents": [{
32455
+ name: "deviceId",
32456
+ form: "single",
32457
+ optional: false
32458
+ }],
32459
+ "pipelineAnalytics.getTrack": [{
32460
+ name: "deviceId",
32461
+ form: "single",
32462
+ optional: false
32463
+ }],
32464
+ "pipelineAnalytics.getTrainingExportSummary": [{
32465
+ name: "deviceIds",
32466
+ form: "array",
32467
+ optional: true
32468
+ }],
32469
+ "pipelineAnalytics.getTrainingExportUrl": [{
32470
+ name: "deviceIds",
32471
+ form: "array",
32472
+ optional: true
32473
+ }],
32474
+ "pipelineAnalytics.listEventKinds": [{
32475
+ name: "deviceId",
32476
+ form: "single",
32477
+ optional: false
32478
+ }],
32479
+ "pipelineAnalytics.listEventKindsBatch": [{
32480
+ name: "deviceIds",
32481
+ form: "array",
32482
+ optional: false
32483
+ }],
32484
+ "pipelineAnalytics.listOpsLog": [{
32485
+ name: "deviceId",
32486
+ form: "single",
32487
+ optional: true
32488
+ }],
32489
+ "pipelineAnalytics.listRecentTracks": [{
32490
+ name: "deviceIds",
32491
+ form: "array",
32492
+ optional: false
32493
+ }],
32494
+ "pipelineAnalytics.listRetrainStaging": [{
32495
+ name: "deviceIds",
32496
+ form: "array",
32497
+ optional: true
32498
+ }],
32499
+ "pipelineAnalytics.listTracks": [{
32500
+ name: "deviceId",
32501
+ form: "single",
32502
+ optional: false
32503
+ }],
32504
+ "pipelineAnalytics.proposeRetrainAnnotations": [{
32505
+ name: "deviceId",
32506
+ form: "single",
32507
+ optional: false
32508
+ }],
32509
+ "pipelineAnalytics.pruneEventsBefore": [{
32510
+ name: "deviceId",
32511
+ form: "single",
32512
+ optional: false
32513
+ }],
32514
+ "pipelineAnalytics.pruneTracksBefore": [{
32515
+ name: "deviceId",
32516
+ form: "single",
32517
+ optional: false
32518
+ }],
32519
+ "pipelineAnalytics.rebuildObjectEmbeddings": [{
32520
+ name: "deviceId",
32521
+ form: "single",
32522
+ optional: true
32523
+ }],
32524
+ "pipelineAnalytics.restageRetrainTrack": [{
32525
+ name: "deviceId",
32526
+ form: "single",
32527
+ optional: false
32528
+ }],
32529
+ "pipelineAnalytics.saveRetrainAnnotations": [{
32530
+ name: "deviceId",
32531
+ form: "single",
32532
+ optional: false
32533
+ }],
32534
+ "pipelineAnalytics.searchObjectEvents": [{
32535
+ name: "deviceId",
32536
+ form: "single",
32537
+ optional: true
32538
+ }],
32539
+ "pipelineAnalytics.selectRetrainFrames": [{
32540
+ name: "deviceId",
32541
+ form: "single",
32542
+ optional: false
32543
+ }],
32544
+ "pipelineAnalytics.setTrackFlags": [{
32545
+ name: "deviceId",
32546
+ form: "single",
32547
+ optional: false
32548
+ }],
32549
+ "pipelineAnalytics.wipeAllAnalytics": [{
32550
+ name: "deviceId",
32551
+ form: "single",
32552
+ optional: false
32553
+ }],
32554
+ "pipelineExecutor.runPipeline": [{
32555
+ name: "deviceId",
32556
+ form: "single",
32557
+ optional: true
32558
+ }],
32559
+ "pipelineExecutor.runPipelineBatch": [{
32560
+ name: "deviceId",
32561
+ form: "single",
32562
+ optional: true
32563
+ }],
32564
+ "pipelineOrchestrator.assignAudio": [{
32565
+ name: "deviceId",
32566
+ form: "single",
32567
+ optional: false
32568
+ }],
32569
+ "pipelineOrchestrator.assignPipeline": [{
32570
+ name: "deviceId",
32571
+ form: "single",
32572
+ optional: false
32573
+ }],
32574
+ "pipelineOrchestrator.getAudioAssignment": [{
32575
+ name: "deviceId",
32576
+ form: "single",
32577
+ optional: false
32578
+ }],
32579
+ "pipelineOrchestrator.getCameraMetrics": [{
32580
+ name: "deviceId",
32581
+ form: "single",
32582
+ optional: false
32583
+ }],
32584
+ "pipelineOrchestrator.getCameraSettings": [{
32585
+ name: "deviceId",
32586
+ form: "single",
32587
+ optional: false
32588
+ }],
32589
+ "pipelineOrchestrator.getCameraStatus": [{
32590
+ name: "deviceId",
32591
+ form: "single",
32592
+ optional: false
32593
+ }],
32594
+ "pipelineOrchestrator.getCameraStatuses": [{
32595
+ name: "deviceIds",
32596
+ form: "array",
32597
+ optional: true
32598
+ }],
32599
+ "pipelineOrchestrator.getCameraStepOverrides": [{
32600
+ name: "deviceId",
32601
+ form: "single",
32602
+ optional: false
32603
+ }],
32604
+ "pipelineOrchestrator.getCameraSwitches": [{
32605
+ name: "deviceId",
32606
+ form: "single",
32607
+ optional: false
32608
+ }],
32609
+ "pipelineOrchestrator.getPipelineAssignment": [{
32610
+ name: "deviceId",
32611
+ form: "single",
32612
+ optional: false
32613
+ }],
32614
+ "pipelineOrchestrator.getPipelineDevicePin": [{
32615
+ name: "deviceId",
32616
+ form: "single",
32617
+ optional: false
32618
+ }],
32619
+ "pipelineOrchestrator.resolvePipeline": [{
32620
+ name: "deviceId",
32621
+ form: "single",
32622
+ optional: false
32623
+ }],
32624
+ "pipelineOrchestrator.setCameraPipelineForAgent": [{
32625
+ name: "deviceId",
32626
+ form: "single",
32627
+ optional: false
32628
+ }],
32629
+ "pipelineOrchestrator.setCameraStepOverride": [{
32630
+ name: "deviceId",
32631
+ form: "single",
32632
+ optional: false
32633
+ }],
32634
+ "pipelineOrchestrator.setCameraStepToggle": [{
32635
+ name: "deviceId",
32636
+ form: "single",
32637
+ optional: false
32638
+ }],
32639
+ "pipelineOrchestrator.setCameraSwitch": [{
32640
+ name: "deviceId",
32641
+ form: "single",
32642
+ optional: false
32643
+ }],
32644
+ "pipelineOrchestrator.setPipelineDevicePin": [{
32645
+ name: "deviceId",
32646
+ form: "single",
32647
+ optional: false
32648
+ }],
32649
+ "pipelineOrchestrator.unassignAudio": [{
32650
+ name: "deviceId",
32651
+ form: "single",
32652
+ optional: false
32653
+ }],
32654
+ "pipelineOrchestrator.unassignPipeline": [{
32655
+ name: "deviceId",
32656
+ form: "single",
32657
+ optional: false
32658
+ }],
32659
+ "pipelineRunner.attachCamera": [{
32660
+ name: "deviceId",
32661
+ form: "single",
32662
+ optional: false
32663
+ }],
32664
+ "pipelineRunner.detachCamera": [{
32665
+ name: "deviceId",
32666
+ form: "single",
32667
+ optional: false
32668
+ }],
32669
+ "pipelineRunner.getCameraMetrics": [{
32670
+ name: "deviceId",
32671
+ form: "single",
32672
+ optional: false
32673
+ }],
32674
+ "pipelineRunner.reportMotion": [{
32675
+ name: "deviceId",
32676
+ form: "single",
32677
+ optional: false
32678
+ }],
32679
+ "pipelineRunner.runDetailSubtree": [{
32680
+ name: "deviceId",
32681
+ form: "single",
32682
+ optional: false
32683
+ }],
32684
+ "pipelineRunner.runStatelessStep": [{
32685
+ name: "sourceDeviceId",
32686
+ form: "single",
32687
+ optional: false
32688
+ }],
32689
+ "plateGallery.getPlateByTrack": [{
32690
+ name: "deviceId",
32691
+ form: "single",
32692
+ optional: false
32693
+ }],
32694
+ "plateGallery.listPlates": [{
32695
+ name: "deviceId",
32696
+ form: "single",
32697
+ optional: true
32698
+ }],
32699
+ "privacyMask.getOptions": [{
32700
+ name: "deviceId",
32701
+ form: "single",
32702
+ optional: false
32703
+ }],
32704
+ "privacyMask.setAudioEnabled": [{
32705
+ name: "deviceId",
32706
+ form: "single",
32707
+ optional: false
32708
+ }],
32709
+ "privacyMask.setMask": [{
32710
+ name: "deviceId",
32711
+ form: "single",
32712
+ optional: false
32713
+ }],
32714
+ "ptz.continuousMove": [{
32715
+ name: "deviceId",
32716
+ form: "single",
32717
+ optional: false
32718
+ }],
32719
+ "ptz.deletePreset": [{
32720
+ name: "deviceId",
32721
+ form: "single",
32722
+ optional: false
32723
+ }],
32724
+ "ptz.getOptions": [{
32725
+ name: "deviceId",
32726
+ form: "single",
32727
+ optional: false
32728
+ }],
32729
+ "ptz.getPosition": [{
32730
+ name: "deviceId",
32731
+ form: "single",
32732
+ optional: false
32733
+ }],
32734
+ "ptz.getPresets": [{
32735
+ name: "deviceId",
32736
+ form: "single",
32737
+ optional: false
32738
+ }],
32739
+ "ptz.goHome": [{
32740
+ name: "deviceId",
32741
+ form: "single",
32742
+ optional: false
32743
+ }],
32744
+ "ptz.goToPreset": [{
32745
+ name: "deviceId",
32746
+ form: "single",
32747
+ optional: false
32748
+ }],
32749
+ "ptz.move": [{
32750
+ name: "deviceId",
32751
+ form: "single",
32752
+ optional: false
32753
+ }],
32754
+ "ptz.savePreset": [{
32755
+ name: "deviceId",
32756
+ form: "single",
32757
+ optional: false
32758
+ }],
32759
+ "ptz.setAutofocus": [{
32760
+ name: "deviceId",
32761
+ form: "single",
32762
+ optional: false
32763
+ }],
32764
+ "ptz.stop": [{
32765
+ name: "deviceId",
32766
+ form: "single",
32767
+ optional: false
32768
+ }],
32769
+ "ptzAutotrack.getSettings": [{
32770
+ name: "deviceId",
32771
+ form: "single",
32772
+ optional: false
32773
+ }],
32774
+ "ptzAutotrack.getStatus": [{
32775
+ name: "deviceId",
32776
+ form: "single",
32777
+ optional: false
32778
+ }],
32779
+ "ptzAutotrack.setEnabled": [{
32780
+ name: "deviceId",
32781
+ form: "single",
32782
+ optional: false
32783
+ }],
32784
+ "ptzAutotrack.setSettings": [{
32785
+ name: "deviceId",
32786
+ form: "single",
32787
+ optional: false
32788
+ }],
32789
+ "reboot.reboot": [{
32790
+ name: "deviceId",
32791
+ form: "single",
32792
+ optional: false
32793
+ }],
32794
+ "recording.deleteFootprint": [{
32795
+ name: "deviceId",
32796
+ form: "single",
32797
+ optional: false
32798
+ }],
32799
+ "recording.getAvailability": [{
32800
+ name: "deviceId",
32801
+ form: "single",
32802
+ optional: false
32803
+ }],
32804
+ "recording.getDaysWithRecordings": [{
32805
+ name: "deviceId",
32806
+ form: "single",
32807
+ optional: false
32808
+ }],
32809
+ "recording.getDeviceConfig": [{
32810
+ name: "deviceId",
32811
+ form: "single",
32812
+ optional: false
32813
+ }],
32814
+ "recording.getPlaybackManifest": [{
32815
+ name: "deviceId",
32816
+ form: "single",
32817
+ optional: false
32818
+ }],
32819
+ "recording.listOpsLog": [{
32820
+ name: "deviceId",
32821
+ form: "single",
32822
+ optional: true
32823
+ }],
32824
+ "recording.locateSegment": [{
32825
+ name: "deviceId",
32826
+ form: "single",
32827
+ optional: false
32828
+ }],
32829
+ "recording.pruneFootage": [{
32830
+ name: "deviceId",
32831
+ form: "single",
32832
+ optional: false
32833
+ }],
32834
+ "recording.readGopBytes": [{
32835
+ name: "deviceId",
32836
+ form: "single",
32837
+ optional: false
32838
+ }],
32839
+ "recording.readSegmentBytes": [{
32840
+ name: "deviceId",
32841
+ form: "single",
32842
+ optional: false
32843
+ }],
32844
+ "recording.relocateFootage": [{
32845
+ name: "deviceId",
32846
+ form: "single",
32847
+ optional: true
32848
+ }],
32849
+ "recording.renderClip": [{
32850
+ name: "deviceId",
32851
+ form: "single",
32852
+ optional: false
32853
+ }],
32854
+ "recording.renderGif": [{
32855
+ name: "deviceId",
32856
+ form: "single",
32857
+ optional: false
32858
+ }],
32859
+ "recording.rescanStorage": [{
32860
+ name: "deviceId",
32861
+ form: "single",
32862
+ optional: false
32863
+ }],
32864
+ "recording.setDeviceConfig": [{
32865
+ name: "deviceId",
32866
+ form: "single",
32867
+ optional: false
32868
+ }],
32869
+ "recording.startStorageMigrationMove": [{
32870
+ name: "deviceId",
32871
+ form: "single",
32872
+ optional: true
32873
+ }],
32874
+ "recordingExport.createExport": [{
32875
+ name: "deviceId",
32876
+ form: "single",
32877
+ optional: false
32878
+ }],
32879
+ "recordingExport.listExports": [{
32880
+ name: "deviceId",
32881
+ form: "single",
32882
+ optional: true
32883
+ }],
32884
+ "sceneMonitor.captureReference": [{
32885
+ name: "deviceId",
32886
+ form: "single",
32887
+ optional: false
32888
+ }],
32889
+ "sceneMonitor.createScene": [{
32890
+ name: "deviceId",
32891
+ form: "single",
32892
+ optional: false
32893
+ }],
32894
+ "sceneMonitor.deleteReference": [{
32895
+ name: "deviceId",
32896
+ form: "single",
32897
+ optional: false
32898
+ }],
32899
+ "sceneMonitor.deleteScene": [{
32900
+ name: "deviceId",
32901
+ form: "single",
32902
+ optional: false
32903
+ }],
32904
+ "sceneMonitor.listScenes": [{
32905
+ name: "deviceId",
32906
+ form: "single",
32907
+ optional: false
32908
+ }],
32909
+ "sceneMonitor.recheckNow": [{
32910
+ name: "deviceId",
32911
+ form: "single",
32912
+ optional: false
32913
+ }],
32914
+ "sceneMonitor.updateScene": [{
32915
+ name: "deviceId",
32916
+ form: "single",
32917
+ optional: false
32918
+ }],
32919
+ "scriptRunner.run": [{
32920
+ name: "deviceId",
32921
+ form: "single",
32922
+ optional: false
32923
+ }],
32924
+ "scriptRunner.stop": [{
32925
+ name: "deviceId",
32926
+ form: "single",
32927
+ optional: false
32928
+ }],
32929
+ "snapshot.getSnapshot": [{
32930
+ name: "deviceId",
32931
+ form: "single",
32932
+ optional: false
32933
+ }],
32934
+ "snapshot.getSnapshotOverview": [{
32935
+ name: "deviceIds",
32936
+ form: "array",
32937
+ optional: false
32938
+ }],
32939
+ "snapshot.invalidateCache": [{
32940
+ name: "deviceId",
32941
+ form: "single",
32942
+ optional: false
32943
+ }],
32944
+ "streamBroker.acquireEgressTranscode": [{
32945
+ name: "deviceId",
32946
+ form: "single",
32947
+ optional: false
32948
+ }],
32949
+ "streamBroker.assignProfile": [{
32950
+ name: "deviceId",
32951
+ form: "single",
32952
+ optional: false
32953
+ }],
32954
+ "streamBroker.getDeviceAudioMute": [{
32955
+ name: "deviceId",
32956
+ form: "single",
32957
+ optional: false
32958
+ }],
32959
+ "streamBroker.getStreamWithCodec": [{
32960
+ name: "deviceId",
32961
+ form: "single",
32962
+ optional: false
32963
+ }],
32964
+ "streamBroker.produceEventMedia": [{
32965
+ name: "deviceId",
32966
+ form: "single",
32967
+ optional: false
32968
+ }],
32969
+ "streamBroker.publishCameraStream": [{
32970
+ name: "deviceId",
32971
+ form: "single",
32972
+ optional: false
32973
+ }],
32974
+ "streamBroker.renderPreBufferClip": [{
32975
+ name: "deviceId",
32976
+ form: "single",
32977
+ optional: false
32978
+ }],
32979
+ "streamBroker.restartProfile": [{
32980
+ name: "deviceId",
32981
+ form: "single",
32982
+ optional: false
32983
+ }],
32984
+ "streamBroker.retractCameraStream": [{
32985
+ name: "deviceId",
32986
+ form: "single",
32987
+ optional: false
32988
+ }],
32989
+ "streamBroker.setDeviceAudioMute": [{
32990
+ name: "deviceId",
32991
+ form: "single",
32992
+ optional: false
32993
+ }],
32994
+ "streamBroker.unassignProfile": [{
32995
+ name: "deviceId",
32996
+ form: "single",
32997
+ optional: false
32998
+ }],
32999
+ "streamCatalog.getCatalog": [{
33000
+ name: "deviceId",
33001
+ form: "single",
33002
+ optional: false
33003
+ }],
33004
+ "streamParams.getConfigSchema": [{
33005
+ name: "deviceId",
33006
+ form: "single",
33007
+ optional: false
33008
+ }],
33009
+ "streamParams.getOptions": [{
33010
+ name: "deviceId",
33011
+ form: "single",
33012
+ optional: false
33013
+ }],
33014
+ "streamParams.setProfile": [{
33015
+ name: "deviceId",
33016
+ form: "single",
33017
+ optional: false
33018
+ }],
33019
+ "switch.setState": [{
33020
+ name: "deviceId",
33021
+ form: "single",
33022
+ optional: false
33023
+ }],
33024
+ "vacuumControl.locate": [{
33025
+ name: "deviceId",
33026
+ form: "single",
33027
+ optional: false
33028
+ }],
33029
+ "vacuumControl.pause": [{
33030
+ name: "deviceId",
33031
+ form: "single",
33032
+ optional: false
33033
+ }],
33034
+ "vacuumControl.returnToBase": [{
33035
+ name: "deviceId",
33036
+ form: "single",
33037
+ optional: false
33038
+ }],
33039
+ "vacuumControl.setFanSpeed": [{
33040
+ name: "deviceId",
33041
+ form: "single",
33042
+ optional: false
33043
+ }],
33044
+ "vacuumControl.start": [{
33045
+ name: "deviceId",
33046
+ form: "single",
33047
+ optional: false
33048
+ }],
33049
+ "vacuumControl.stop": [{
33050
+ name: "deviceId",
33051
+ form: "single",
33052
+ optional: false
33053
+ }],
33054
+ "valve.close": [{
33055
+ name: "deviceId",
33056
+ form: "single",
33057
+ optional: false
33058
+ }],
33059
+ "valve.open": [{
33060
+ name: "deviceId",
33061
+ form: "single",
33062
+ optional: false
33063
+ }],
33064
+ "valve.setPosition": [{
33065
+ name: "deviceId",
33066
+ form: "single",
33067
+ optional: false
33068
+ }],
33069
+ "valve.stop": [{
33070
+ name: "deviceId",
33071
+ form: "single",
33072
+ optional: false
33073
+ }],
33074
+ "videoclips.getClipPlayback": [{
33075
+ name: "deviceId",
33076
+ form: "single",
33077
+ optional: false
33078
+ }],
33079
+ "videoclips.listClips": [{
33080
+ name: "deviceId",
33081
+ form: "single",
33082
+ optional: false
33083
+ }],
33084
+ "waterHeater.setAway": [{
33085
+ name: "deviceId",
33086
+ form: "single",
33087
+ optional: false
33088
+ }],
33089
+ "waterHeater.setOperationMode": [{
33090
+ name: "deviceId",
33091
+ form: "single",
33092
+ optional: false
33093
+ }],
33094
+ "waterHeater.setTargetTemp": [{
33095
+ name: "deviceId",
33096
+ form: "single",
33097
+ optional: false
33098
+ }],
33099
+ "webrtcSession.addIceCandidate": [{
33100
+ name: "deviceId",
33101
+ form: "single",
33102
+ optional: false
33103
+ }],
33104
+ "webrtcSession.closeSession": [{
33105
+ name: "deviceId",
33106
+ form: "single",
33107
+ optional: false
33108
+ }],
33109
+ "webrtcSession.createSession": [{
33110
+ name: "deviceId",
33111
+ form: "single",
33112
+ optional: false
33113
+ }],
33114
+ "webrtcSession.getIceCandidates": [{
33115
+ name: "deviceId",
33116
+ form: "single",
33117
+ optional: false
33118
+ }],
33119
+ "webrtcSession.getSessionState": [{
33120
+ name: "deviceId",
33121
+ form: "single",
33122
+ optional: false
33123
+ }],
33124
+ "webrtcSession.handleAnswer": [{
33125
+ name: "deviceId",
33126
+ form: "single",
33127
+ optional: false
33128
+ }],
33129
+ "webrtcSession.handleOffer": [{
33130
+ name: "deviceId",
33131
+ form: "single",
33132
+ optional: false
33133
+ }],
33134
+ "webrtcSession.hasAdaptiveBitrate": [{
33135
+ name: "deviceId",
33136
+ form: "single",
33137
+ optional: false
33138
+ }],
33139
+ "webrtcSession.listStreams": [{
33140
+ name: "deviceId",
33141
+ form: "single",
33142
+ optional: false
33143
+ }],
33144
+ "zoneAnalytics.getCameraHistory": [{
33145
+ name: "deviceId",
33146
+ form: "single",
33147
+ optional: false
33148
+ }],
33149
+ "zoneAnalytics.getCurrentSnapshot": [{
33150
+ name: "deviceId",
33151
+ form: "single",
33152
+ optional: false
33153
+ }],
33154
+ "zoneAnalytics.getUnzonedHistory": [{
33155
+ name: "deviceId",
33156
+ form: "single",
33157
+ optional: false
33158
+ }],
33159
+ "zoneAnalytics.getZoneHistory": [{
33160
+ name: "deviceId",
33161
+ form: "single",
33162
+ optional: false
33163
+ }],
33164
+ "zoneRules.listRules": [{
33165
+ name: "deviceId",
33166
+ form: "single",
33167
+ optional: false
33168
+ }],
33169
+ "zoneRules.setRules": [{
33170
+ name: "deviceId",
33171
+ form: "single",
33172
+ optional: false
33173
+ }],
33174
+ "zones.addZone": [{
33175
+ name: "deviceId",
33176
+ form: "single",
33177
+ optional: false
33178
+ }],
33179
+ "zones.listZones": [{
33180
+ name: "deviceId",
33181
+ form: "single",
33182
+ optional: false
33183
+ }],
33184
+ "zones.removeZone": [{
33185
+ name: "deviceId",
33186
+ form: "single",
33187
+ optional: false
33188
+ }],
33189
+ "zones.updateZone": [{
33190
+ name: "deviceId",
33191
+ form: "single",
33192
+ optional: false
33193
+ }]
33194
+ });
31366
33195
  Object.freeze({
31367
33196
  "broker": "broker",
31368
33197
  "device-export": "device-export",
@@ -32082,7 +33911,7 @@ var Fmp4FragmentChild = class {
32082
33911
  meta: {
32083
33912
  sourceId: this.args.sourceId,
32084
33913
  decodeHwAccel: requested,
32085
- error: errMsg$12(err)
33914
+ error: errMsg$15(err)
32086
33915
  }
32087
33916
  });
32088
33917
  this.killChild();
@@ -32261,7 +34090,7 @@ var Fmp4FragmentChild = class {
32261
34090
  tags: { deviceId: this.args.deviceId },
32262
34091
  meta: {
32263
34092
  sourceId: this.args.sourceId,
32264
- error: errMsg$12(err)
34093
+ error: errMsg$15(err)
32265
34094
  }
32266
34095
  });
32267
34096
  }
@@ -78606,7 +80435,7 @@ function clearPairingFiles(accessoryUuid, logger) {
78606
80435
  }
78607
80436
  //#endregion
78608
80437
  //#region src/hap-setup-uri.ts
78609
- function errMsg$11(e) {
80438
+ function errMsg$14(e) {
78610
80439
  return e instanceof Error ? e.message : String(e);
78611
80440
  }
78612
80441
  /**
@@ -78633,11 +80462,79 @@ function firstExposedAccessorySetupUri(exposed, logger) {
78633
80462
  try {
78634
80463
  return first.setupURI();
78635
80464
  } catch (err) {
78636
- logger.debug("export-hap: setupURI failed on first exposed accessory", { meta: { error: errMsg$11(err) } });
80465
+ logger.debug("export-hap: setupURI failed on first exposed accessory", { meta: { error: errMsg$14(err) } });
78637
80466
  return;
78638
80467
  }
78639
80468
  }
78640
80469
  }
80470
+ //#endregion
80471
+ //#region src/mappers/builders/accessory-info.ts
80472
+ /**
80473
+ * `Service.AccessoryInformation` — the manufacturer / model / firmware /
80474
+ * serial block every HomeKit accessory carries.
80475
+ *
80476
+ * One implementation for both accessory shapes (camera and generic): the
80477
+ * fields come from the device's own metadata either way, and a second copy
80478
+ * would be a second answer to "what serial does this device publish".
80479
+ * Metadata is best-effort — a device that cannot answer still publishes.
80480
+ */
80481
+ async function populateAccessoryInfo(accessory, proxy, displayName, modelFallback) {
80482
+ const info = accessory.getService(import_dist.Service.AccessoryInformation);
80483
+ if (!info) return;
80484
+ try {
80485
+ const device = await proxy.deviceManager?.getDevice({});
80486
+ const metadata = readMetadata(device);
80487
+ info.setCharacteristic(import_dist.Characteristic.Name, device?.name ?? displayName);
80488
+ info.setCharacteristic(import_dist.Characteristic.Manufacturer, stringOr(metadata?.manufacturer, "CamStack"));
80489
+ info.setCharacteristic(import_dist.Characteristic.Model, stringOr(metadata?.model, modelFallback));
80490
+ info.setCharacteristic(import_dist.Characteristic.FirmwareRevision, stringOr(metadata?.firmware, "0.0.0"));
80491
+ info.setCharacteristic(import_dist.Characteristic.SerialNumber, stringOr(metadata?.sn, `camstack-${proxy.deviceId}`));
80492
+ } catch {}
80493
+ }
80494
+ /** The four fields this module reads, or `null` — the device record's
80495
+ * `metadata` is an open bag and nothing else here depends on its shape. */
80496
+ function readMetadata(device) {
80497
+ const metadata = device?.metadata;
80498
+ if (metadata === null || typeof metadata !== "object") return {};
80499
+ const entries = new Map(Object.entries(metadata));
80500
+ return {
80501
+ model: stringOrNull(entries.get("model")),
80502
+ manufacturer: stringOrNull(entries.get("manufacturer")),
80503
+ firmware: stringOrNull(entries.get("firmware")),
80504
+ sn: stringOrNull(entries.get("sn"))
80505
+ };
80506
+ }
80507
+ function stringOrNull(value) {
80508
+ return typeof value === "string" ? value : null;
80509
+ }
80510
+ function stringOr(value, fallback) {
80511
+ return typeof value === "string" && value.length > 0 ? value : fallback;
80512
+ }
80513
+ //#endregion
80514
+ //#region src/mappers/builders/generic/characteristic-update.ts
80515
+ /**
80516
+ * Parse `status` with the capability's OWN Zod schema and turn it into
80517
+ * characteristic writes.
80518
+ *
80519
+ * Duck-typing the payload here would be the second source of truth about what a
80520
+ * cap reports; the schema is the first and only one. A payload that does not
80521
+ * match yields NO updates — never a partial or invented value — and the caller
80522
+ * logs the drop, because a sensor that silently stops moving is
80523
+ * indistinguishable from a sensor that never changed.
80524
+ *
80525
+ * Rows `.pick()` only the fields they read, so a provider omitting a timestamp
80526
+ * cannot silence a sensor.
80527
+ */
80528
+ function reader(schema, toUpdates) {
80529
+ return (status) => {
80530
+ const parsed = schema.safeParse(status);
80531
+ return parsed.success ? toUpdates(parsed.data) : [];
80532
+ };
80533
+ }
80534
+ /** Push every update onto `service`. */
80535
+ function applyUpdates(service, updates) {
80536
+ for (const update of updates) service.updateCharacteristic(update.characteristic, update.value);
80537
+ }
78641
80538
  /**
78642
80539
  * hap-nodejs' `checkName` regex, verbatim.
78643
80540
  *
@@ -78774,6 +80671,92 @@ function titleCase(raw) {
78774
80671
  return raw.split(/[-_\s]+/u).filter((part) => part.length > 0).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
78775
80672
  }
78776
80673
  //#endregion
80674
+ //#region src/mappers/builders/service-label.ts
80675
+ /**
80676
+ * The ONE place a secondary service on the camera accessory gets its label.
80677
+ *
80678
+ * A "secondary service" here is a Switch or Lightbulb published alongside the
80679
+ * camera on the same accessory — the privacy switch, each accessory child
80680
+ * (siren, floodlight), each PTZ action. iOS Home renders these as their own
80681
+ * controls, and the operator has seen them as "Interruttore 1", "Interruttore
80682
+ * 2" through three separate rounds of fixes.
80683
+ *
80684
+ * ## Why `Name` alone cannot rename anything
80685
+ *
80686
+ * Two facts about hap-nodejs 2.1.7, both measured against the installed copy
80687
+ * rather than reasoned about:
80688
+ *
80689
+ * 1. `accessory.addService(Type, displayName, subtype)` ALREADY writes
80690
+ * `displayName` to `Characteristic.Name` (`Service` constructor). So every
80691
+ * round of this bug — including the one that moved the label onto
80692
+ * `ConfiguredName` — shipped with `Name` correctly set. "iOS had no name
80693
+ * to render" was never true.
80694
+ * 2. The mDNS configuration number (`c#`) is a sha1 over
80695
+ * `internalHAPRepresentation(false)`, which OMITS characteristic VALUES.
80696
+ * Changing the string in `Name` therefore does not bump `c#`, a paired
80697
+ * controller gets no signal to re-read `/accessories`, and the name it
80698
+ * cached at first enumeration stands forever.
80699
+ *
80700
+ * `Name` is also declared `pr` only — paired read, no write, no notify. It is
80701
+ * the seed a controller seeds its database from once; it is not a channel.
80702
+ *
80703
+ * ## Why `ConfiguredName`
80704
+ *
80705
+ * `ConfiguredName` (`000000E3`) is declared `pr | pw | ev` — the only name
80706
+ * characteristic a controller may write and may subscribe to. It is what iOS
80707
+ * 16+ reads for a service the user can rename, and adding it CHANGES the
80708
+ * accessory structure, so `c#` does bump and the controller re-reads.
80709
+ *
80710
+ * It was removed once because hap-nodejs logged
80711
+ *
80712
+ * ```
80713
+ * Characteristic not in required or optional characteristic section for
80714
+ * service Switch. Adding anyway.
80715
+ * ```
80716
+ *
80717
+ * That line is a WARNING, not a rejection: `Service.getCharacteristic` calls
80718
+ * `addCharacteristic` unconditionally and only then emits the warning. The
80719
+ * characteristic was always present and always published. hap-nodejs'
80720
+ * per-service optional lists simply predate `ConfiguredName` being valid on
80721
+ * any service.
80722
+ *
80723
+ * Registering it with {@link Service.addOptionalCharacteristic} first takes
80724
+ * the branch above the warning, so the accessory still builds with ZERO
80725
+ * characteristic warnings — which is what `service-naming.spec.ts` asserts.
80726
+ *
80727
+ * ## Scope: EVERY service, including the sensors
80728
+ *
80729
+ * An earlier round applied this to Switch- and Lightbulb-shaped services only,
80730
+ * on the theory that `Service.MotionSensor` and `Service.Battery` are not
80731
+ * separately named tiles in iOS Home and that naming them would be a guess.
80732
+ *
80733
+ * That theory was never measured, and it had a cost: it left services on the
80734
+ * accessory whose name a paired controller could never be told about, and it
80735
+ * made "did `ConfiguredName` fix the operator's 'Interruttore N'?" unanswerable
80736
+ * — a negative result on a partial application proves nothing about the
80737
+ * mechanism. Every service this addon publishes now carries both
80738
+ * characteristics, on the camera accessory and on the generic one.
80739
+ *
80740
+ * The reasoning above is mechanism, not measurement: it says why `Name` alone
80741
+ * CANNOT work and why `ConfiguredName` is the only characteristic that can. It
80742
+ * does not prove iOS renders it on every service shape. That is an observation
80743
+ * only a re-paired controller can make — and after a change like this one, the
80744
+ * controller must be re-paired, because a cached accessory database is exactly
80745
+ * what the whole mechanism is about.
80746
+ */
80747
+ /**
80748
+ * Publish `name` as both the immutable `Name` and the controller-visible
80749
+ * `ConfiguredName` of `service`.
80750
+ *
80751
+ * `name` must already be HAP-valid — build it with `service-names.ts`, which
80752
+ * cannot return a string hap-nodejs' `checkName` would warn about.
80753
+ */
80754
+ function applyServiceLabel(service, name) {
80755
+ service.setCharacteristic(import_dist.Characteristic.Name, name);
80756
+ if (!service.optionalCharacteristics.some((characteristic) => characteristic.UUID === import_dist.Characteristic.ConfiguredName.UUID)) service.addOptionalCharacteristic(import_dist.Characteristic.ConfiguredName);
80757
+ service.setCharacteristic(import_dist.Characteristic.ConfiguredName, name);
80758
+ }
80759
+ //#endregion
78777
80760
  //#region src/mappers/builders/battery.ts
78778
80761
  /**
78779
80762
  * Battery builder — surfaces a battery-operated camera's power state
@@ -78783,34 +80766,56 @@ function titleCase(raw) {
78783
80766
  * subscribes to the runtime-state slice so iOS Home reflects level /
78784
80767
  * charging changes pushed by the firmware without a poll loop.
78785
80768
  *
80769
+ * The status→characteristic mapping is {@link batteryCharacteristicUpdates},
80770
+ * exported because the generic (non-camera) export path publishes the same
80771
+ * `Service.Battery` from the same cap — one derivation, two accessory shapes.
80772
+ *
80773
+ * Skipped silently when the `battery` cap is not bound — caller checks
80774
+ * cap presence before invoking this builder (see `camera-accessory.ts`).
80775
+ */
80776
+ var LOW_BATTERY_THRESHOLD_PCT = 20;
80777
+ /**
78786
80778
  * Mapping:
78787
80779
  * - `BatteryStatus.percentage` (0..100) → `Characteristic.BatteryLevel`
78788
80780
  * - `BatteryStatus.charging`:
78789
80781
  * `'none'` → `ChargingState.NOT_CHARGING`
78790
80782
  * `'dc' | 'solar'` → `ChargingState.CHARGING`
78791
80783
  * - `percentage <= LOW_BATTERY_THRESHOLD_PCT` → `StatusLowBattery.LOW`
78792
- *
78793
- * Skipped silently when the `battery` cap is not bound — caller checks
78794
- * cap presence before invoking this builder (see `camera-accessory.ts`).
78795
80784
  */
78796
- var LOW_BATTERY_THRESHOLD_PCT = 20;
80785
+ var batteryCharacteristicUpdates = reader(BatteryStatusSchema.pick({ percentage: true }).extend({ charging: BatteryStatusSchema.shape.charging.optional() }), (status) => {
80786
+ const pct = Math.max(0, Math.min(100, Math.round(status.percentage)));
80787
+ return [
80788
+ {
80789
+ characteristic: import_dist.Characteristic.BatteryLevel,
80790
+ value: pct
80791
+ },
80792
+ ...status.charging === void 0 ? [] : [{
80793
+ characteristic: import_dist.Characteristic.ChargingState,
80794
+ value: status.charging === "none" ? import_dist.Characteristic.ChargingState.NOT_CHARGING : import_dist.Characteristic.ChargingState.CHARGING
80795
+ }],
80796
+ {
80797
+ characteristic: import_dist.Characteristic.StatusLowBattery,
80798
+ value: pct <= LOW_BATTERY_THRESHOLD_PCT ? import_dist.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : import_dist.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL
80799
+ }
80800
+ ];
80801
+ });
78797
80802
  async function buildBattery(bctx) {
78798
80803
  const { ctx, accessory, proxy, numericDeviceId, displayName } = bctx;
78799
80804
  const log = ctx.logger.withTags({ deviceId: numericDeviceId });
78800
- const service = accessory.addService(import_dist.Service.Battery, hapServiceName([displayName], `Camera ${numericDeviceId}`));
80805
+ const label = hapServiceName([displayName], `Camera ${numericDeviceId}`);
80806
+ const service = accessory.addService(import_dist.Service.Battery, label);
80807
+ applyServiceLabel(service, label);
78801
80808
  try {
78802
80809
  const status = await proxy.battery?.getStatus({});
78803
- if (status) applyToService(service, status);
80810
+ if (status !== void 0 && status !== null) applyToService(service, status);
78804
80811
  } catch (err) {
78805
- log.debug("export-hap: battery getStatus hydrate failed (non-fatal)", { meta: { error: errMsg$10(err) } });
80812
+ log.debug("export-hap: battery getStatus hydrate failed (non-fatal)", { meta: { error: errMsg$13(err) } });
78806
80813
  }
78807
80814
  const unsubscribes = [];
78808
80815
  if (proxy.state.battery) {
78809
80816
  const unsub = proxy.state.battery.subscribe((value) => {
78810
80817
  if (!value) return;
78811
- const status = value;
78812
- if (typeof status.percentage !== "number") return;
78813
- applyToService(service, status);
80818
+ applyToService(service, value);
78814
80819
  });
78815
80820
  unsubscribes.push(unsub);
78816
80821
  }
@@ -78821,14 +80826,9 @@ async function buildBattery(bctx) {
78821
80826
  } };
78822
80827
  }
78823
80828
  function applyToService(service, status) {
78824
- const pct = Math.max(0, Math.min(100, Math.round(status.percentage)));
78825
- service.updateCharacteristic(import_dist.Characteristic.BatteryLevel, pct);
78826
- const chargingState = status.charging === "none" ? import_dist.Characteristic.ChargingState.NOT_CHARGING : import_dist.Characteristic.ChargingState.CHARGING;
78827
- service.updateCharacteristic(import_dist.Characteristic.ChargingState, chargingState);
78828
- const lowBattery = pct <= LOW_BATTERY_THRESHOLD_PCT ? import_dist.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : import_dist.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL;
78829
- service.updateCharacteristic(import_dist.Characteristic.StatusLowBattery, lowBattery);
80829
+ applyUpdates(service, batteryCharacteristicUpdates(status));
78830
80830
  }
78831
- function errMsg$10(err) {
80831
+ function errMsg$13(err) {
78832
80832
  return err instanceof Error ? err.message : String(err);
78833
80833
  }
78834
80834
  //#endregion
@@ -89580,7 +91580,7 @@ var VIDEO_LOOPBACK_RCVBUF_BYTES = 8 * 1024 * 1024;
89580
91580
  * not burst size.
89581
91581
  */
89582
91582
  var AUDIO_LOOPBACK_RCVBUF_BYTES = 1024 * 1024;
89583
- function errMsg$9(err) {
91583
+ function errMsg$12(err) {
89584
91584
  return err instanceof Error ? err.message : String(err);
89585
91585
  }
89586
91586
  /**
@@ -89595,13 +91595,13 @@ function applyReceiveBuffer(socket, requestedBytes) {
89595
91595
  try {
89596
91596
  socket.setRecvBufferSize(requestedBytes);
89597
91597
  } catch (err) {
89598
- error = errMsg$9(err);
91598
+ error = errMsg$12(err);
89599
91599
  }
89600
91600
  let effectiveBytes = null;
89601
91601
  try {
89602
91602
  effectiveBytes = socket.getRecvBufferSize();
89603
91603
  } catch (err) {
89604
- if (error === null) error = errMsg$9(err);
91604
+ if (error === null) error = errMsg$12(err);
89605
91605
  }
89606
91606
  return {
89607
91607
  requestedBytes,
@@ -89861,20 +91861,20 @@ function buildCameraStreamingDelegate(bctx, advertised) {
89861
91861
  delegate: {
89862
91862
  handleSnapshotRequest(request, callback) {
89863
91863
  handleSnapshot(bctx, request).then((buf) => callback(void 0, buf)).catch((err) => {
89864
- log.warn("export-hap: snapshot failed", { meta: { error: errMsg$8(err) } });
89865
- callback(err instanceof Error ? err : new Error(errMsg$8(err)));
91864
+ log.warn("export-hap: snapshot failed", { meta: { error: errMsg$11(err) } });
91865
+ callback(err instanceof Error ? err : new Error(errMsg$11(err)));
89866
91866
  });
89867
91867
  },
89868
91868
  prepareStream(request, callback) {
89869
91869
  prepareStream(request, sessions, bctx).then((resp) => callback(void 0, resp)).catch((err) => {
89870
- log.warn("export-hap: prepareStream failed", { meta: { error: errMsg$8(err) } });
89871
- callback(err instanceof Error ? err : new Error(errMsg$8(err)));
91870
+ log.warn("export-hap: prepareStream failed", { meta: { error: errMsg$11(err) } });
91871
+ callback(err instanceof Error ? err : new Error(errMsg$11(err)));
89872
91872
  });
89873
91873
  },
89874
91874
  handleStreamRequest(request, callback) {
89875
91875
  handleStreamRequest(request, sessions, bctx, advertised).then(() => callback()).catch((err) => {
89876
- log.warn("export-hap: handleStreamRequest failed", { meta: { error: errMsg$8(err) } });
89877
- callback(err instanceof Error ? err : new Error(errMsg$8(err)));
91876
+ log.warn("export-hap: handleStreamRequest failed", { meta: { error: errMsg$11(err) } });
91877
+ callback(err instanceof Error ? err : new Error(errMsg$11(err)));
89878
91878
  });
89879
91879
  }
89880
91880
  },
@@ -89990,7 +91990,7 @@ async function prepareStream(request, sessions, bctx) {
89990
91990
  closeSocket(audioUdp);
89991
91991
  closeSocket(videoLoopUdp);
89992
91992
  closeSocket(audioLoopUdp);
89993
- throw new Error(`export-hap: outbound SrtpSession init failed: ${errMsg$8(err)}`, { cause: err });
91993
+ throw new Error(`export-hap: outbound SrtpSession init failed: ${errMsg$11(err)}`, { cause: err });
89994
91994
  }
89995
91995
  let upstreamAudioSrtp = null;
89996
91996
  try {
@@ -90004,7 +92004,7 @@ async function prepareStream(request, sessions, bctx) {
90004
92004
  profile: import_src.ProtectionProfileAes128CmHmacSha1_80
90005
92005
  });
90006
92006
  } catch (err) {
90007
- bctx.ctx.logger.withTags({ deviceId: bctx.numericDeviceId }).warn("export-hap: SrtpSession init failed (upstream audio decrypt disabled)", { meta: { error: errMsg$8(err) } });
92007
+ bctx.ctx.logger.withTags({ deviceId: bctx.numericDeviceId }).warn("export-hap: SrtpSession init failed (upstream audio decrypt disabled)", { meta: { error: errMsg$11(err) } });
90008
92008
  }
90009
92009
  const videoSsrc = randomSsrc();
90010
92010
  const audioSsrc = randomSsrc();
@@ -90121,7 +92121,7 @@ async function prepareStream(request, sessions, bctx) {
90121
92121
  });
90122
92122
  audioUdp.on("message", (packet, rinfo) => {
90123
92123
  handleIncomingAudioRtp(session, packet, rinfo.address, bctx).catch((err) => {
90124
- bctx.ctx.logger.withTags({ deviceId: bctx.numericDeviceId }).debug("export-hap: incoming-audio handler error (dropped)", { meta: { error: errMsg$8(err) } });
92124
+ bctx.ctx.logger.withTags({ deviceId: bctx.numericDeviceId }).debug("export-hap: incoming-audio handler error (dropped)", { meta: { error: errMsg$11(err) } });
90125
92125
  });
90126
92126
  });
90127
92127
  logLoopbackBuffer(tagLog, request.sessionID, "video", videoLoop.buffer);
@@ -90317,7 +92317,7 @@ function readControllerRtcp(session, leg, packet, log) {
90317
92317
  } catch (err) {
90318
92318
  drop(session, "inbound-rtcp-decrypt-failed");
90319
92319
  storeReceiverReports(session, leg, recordUnreadableRtcp(tally));
90320
- logUnreadableRtcp(session, leg, `decrypt: ${errMsg$8(err)}`, log);
92320
+ logUnreadableRtcp(session, leg, `decrypt: ${errMsg$11(err)}`, log);
90321
92321
  return;
90322
92322
  }
90323
92323
  const outcome = ingestDecryptedRtcp(plaintext, tally);
@@ -91083,7 +93083,7 @@ async function handleIncomingAudioRtp(session, packet, sourceAddress, bctx) {
91083
93083
  session.upstreamRtpDecryptFailures += 1;
91084
93084
  if (session.upstreamRtpDecryptFailures % 100 === 1) log.debug("export-hap: SRTP decrypt failed (rate-limited)", { meta: {
91085
93085
  failures: session.upstreamRtpDecryptFailures,
91086
- error: errMsg$8(err)
93086
+ error: errMsg$11(err)
91087
93087
  } });
91088
93088
  return;
91089
93089
  }
@@ -91097,7 +93097,7 @@ async function handleIncomingAudioRtp(session, packet, sourceAddress, bctx) {
91097
93097
  rtpPayloadType = parsedRtp.header.payloadType;
91098
93098
  } catch (err) {
91099
93099
  drop(session, "upstream-parse-failed");
91100
- log.debug("export-hap: RTP parse failed after decrypt", { meta: { error: errMsg$8(err) } });
93100
+ log.debug("export-hap: RTP parse failed after decrypt", { meta: { error: errMsg$11(err) } });
91101
93101
  return;
91102
93102
  }
91103
93103
  const negotiatedAudioPt = session.lastStartParams?.audioPt;
@@ -91121,7 +93121,7 @@ async function handleIncomingAudioRtp(session, packet, sourceAddress, bctx) {
91121
93121
  if (session.intercomTalkSessionId === null) {
91122
93122
  session.intercomTalkSessionId = "";
91123
93123
  const opened = await openIntercomTalkSession(bctx).catch((err) => {
91124
- log.warn("export-hap: intercom.startTalkSession failed", { meta: { error: errMsg$8(err) } });
93124
+ log.warn("export-hap: intercom.startTalkSession failed", { meta: { error: errMsg$11(err) } });
91125
93125
  return null;
91126
93126
  });
91127
93127
  if (opened) {
@@ -91148,7 +93148,7 @@ async function handleIncomingAudioRtp(session, packet, sourceAddress, bctx) {
91148
93148
  drop(session, "upstream-push-failed");
91149
93149
  log.debug("export-hap: intercom.pushTalkAudio failed (will re-open on next frame)", { meta: {
91150
93150
  sequenceNumber: session.intercomPcmSequence,
91151
- error: errMsg$8(err)
93151
+ error: errMsg$11(err)
91152
93152
  } });
91153
93153
  session.intercomTalkSessionId = null;
91154
93154
  }
@@ -91180,7 +93180,7 @@ async function closeIntercomTalkSession(session, bctx) {
91180
93180
  } catch (err) {
91181
93181
  log.debug("export-hap: intercom.endTalkSession failed (continuing)", { meta: {
91182
93182
  sessionId,
91183
- error: errMsg$8(err)
93183
+ error: errMsg$11(err)
91184
93184
  } });
91185
93185
  }
91186
93186
  }
@@ -91205,7 +93205,7 @@ function closeSocket(udp) {
91205
93205
  function randomSsrc() {
91206
93206
  return Math.floor(Math.random() * 2147483646) + 1 | 0;
91207
93207
  }
91208
- function errMsg$8(err) {
93208
+ function errMsg$11(err) {
91209
93209
  return err instanceof Error ? err.message : String(err);
91210
93210
  }
91211
93211
  //#endregion
@@ -91289,14 +93289,14 @@ async function buildDoorbell(input) {
91289
93289
  }
91290
93290
  log.info("export-hap: doorbell SINGLE_PRESS pushed to HomeKit", { meta: { ...delivery } });
91291
93291
  } catch (err) {
91292
- log.warn("export-hap: ringDoorbell() failed", { meta: { error: errMsg$7(err) } });
93292
+ log.warn("export-hap: ringDoorbell() failed", { meta: { error: errMsg$10(err) } });
91293
93293
  }
91294
93294
  });
91295
93295
  return { async dispose() {
91296
93296
  unsubscribe();
91297
93297
  } };
91298
93298
  }
91299
- function errMsg$7(err) {
93299
+ function errMsg$10(err) {
91300
93300
  return err instanceof Error ? err.message : String(err);
91301
93301
  }
91302
93302
  //#endregion
@@ -91336,13 +93336,15 @@ var RESET_DEBOUNCE_MS = 5e3;
91336
93336
  */
91337
93337
  async function buildMotionSensor(bctx, existing = null) {
91338
93338
  const { ctx, accessory, proxy, numericDeviceId, displayName } = bctx;
91339
- const motionService = existing ?? accessory.addService(import_dist.Service.MotionSensor, hapServiceName([displayName], `Camera ${numericDeviceId}`));
93339
+ const label = hapServiceName([displayName], `Camera ${numericDeviceId}`);
93340
+ const motionService = existing ?? accessory.addService(import_dist.Service.MotionSensor, label);
93341
+ applyServiceLabel(motionService, label);
91340
93342
  motionService.setCharacteristic(import_dist.Characteristic.MotionDetected, false);
91341
93343
  try {
91342
93344
  const detected = await proxy.motion?.isDetected({});
91343
93345
  if (typeof detected === "boolean") motionService.updateCharacteristic(import_dist.Characteristic.MotionDetected, detected);
91344
93346
  } catch (err) {
91345
- ctx.logger.withTags({ deviceId: numericDeviceId }).debug("export-hap: initial motion hydrate failed (non-fatal)", { meta: { error: errMsg$6(err) } });
93347
+ ctx.logger.withTags({ deviceId: numericDeviceId }).debug("export-hap: initial motion hydrate failed (non-fatal)", { meta: { error: errMsg$9(err) } });
91346
93348
  }
91347
93349
  let resetTimer = null;
91348
93350
  const armReset = () => {
@@ -91376,82 +93378,10 @@ async function buildMotionSensor(bctx, existing = null) {
91376
93378
  }
91377
93379
  } };
91378
93380
  }
91379
- function errMsg$6(err) {
93381
+ function errMsg$9(err) {
91380
93382
  return err instanceof Error ? err.message : String(err);
91381
93383
  }
91382
93384
  //#endregion
91383
- //#region src/mappers/builders/service-label.ts
91384
- /**
91385
- * The ONE place a secondary service on the camera accessory gets its label.
91386
- *
91387
- * A "secondary service" here is a Switch or Lightbulb published alongside the
91388
- * camera on the same accessory — the privacy switch, each accessory child
91389
- * (siren, floodlight), each PTZ action. iOS Home renders these as their own
91390
- * controls, and the operator has seen them as "Interruttore 1", "Interruttore
91391
- * 2" through three separate rounds of fixes.
91392
- *
91393
- * ## Why `Name` alone cannot rename anything
91394
- *
91395
- * Two facts about hap-nodejs 2.1.7, both measured against the installed copy
91396
- * rather than reasoned about:
91397
- *
91398
- * 1. `accessory.addService(Type, displayName, subtype)` ALREADY writes
91399
- * `displayName` to `Characteristic.Name` (`Service` constructor). So every
91400
- * round of this bug — including the one that moved the label onto
91401
- * `ConfiguredName` — shipped with `Name` correctly set. "iOS had no name
91402
- * to render" was never true.
91403
- * 2. The mDNS configuration number (`c#`) is a sha1 over
91404
- * `internalHAPRepresentation(false)`, which OMITS characteristic VALUES.
91405
- * Changing the string in `Name` therefore does not bump `c#`, a paired
91406
- * controller gets no signal to re-read `/accessories`, and the name it
91407
- * cached at first enumeration stands forever.
91408
- *
91409
- * `Name` is also declared `pr` only — paired read, no write, no notify. It is
91410
- * the seed a controller seeds its database from once; it is not a channel.
91411
- *
91412
- * ## Why `ConfiguredName`
91413
- *
91414
- * `ConfiguredName` (`000000E3`) is declared `pr | pw | ev` — the only name
91415
- * characteristic a controller may write and may subscribe to. It is what iOS
91416
- * 16+ reads for a service the user can rename, and adding it CHANGES the
91417
- * accessory structure, so `c#` does bump and the controller re-reads.
91418
- *
91419
- * It was removed once because hap-nodejs logged
91420
- *
91421
- * ```
91422
- * Characteristic not in required or optional characteristic section for
91423
- * service Switch. Adding anyway.
91424
- * ```
91425
- *
91426
- * That line is a WARNING, not a rejection: `Service.getCharacteristic` calls
91427
- * `addCharacteristic` unconditionally and only then emits the warning. The
91428
- * characteristic was always present and always published. hap-nodejs'
91429
- * per-service optional lists simply predate `ConfiguredName` being valid on
91430
- * any service.
91431
- *
91432
- * Registering it with {@link Service.addOptionalCharacteristic} first takes
91433
- * the branch above the warning, so the accessory still builds with ZERO
91434
- * characteristic warnings — which is what `service-naming.spec.ts` asserts.
91435
- *
91436
- * ## Scope
91437
- *
91438
- * Switch- and Lightbulb-shaped services only. `Service.MotionSensor` on a
91439
- * camera accessory is not a separately named tile in iOS Home, so giving it a
91440
- * writable name would be a guess, and this module does not guess.
91441
- */
91442
- /**
91443
- * Publish `name` as both the immutable `Name` and the controller-visible
91444
- * `ConfiguredName` of `service`.
91445
- *
91446
- * `name` must already be HAP-valid — build it with `service-names.ts`, which
91447
- * cannot return a string hap-nodejs' `checkName` would warn about.
91448
- */
91449
- function applyServiceLabel(service, name) {
91450
- service.setCharacteristic(import_dist.Characteristic.Name, name);
91451
- if (!service.optionalCharacteristics.some((characteristic) => characteristic.UUID === import_dist.Characteristic.ConfiguredName.UUID)) service.addOptionalCharacteristic(import_dist.Characteristic.ConfiguredName);
91452
- service.setCharacteristic(import_dist.Characteristic.ConfiguredName, name);
91453
- }
91454
- //#endregion
91455
93385
  //#region src/mappers/builders/privacy-switch.ts
91456
93386
  /**
91457
93387
  * Privacy-mask switch builder — turns the camstack `privacy-mask` cap's
@@ -91479,7 +93409,7 @@ async function buildPrivacySwitch(bctx) {
91479
93409
  const status = await proxy.privacyMask?.getStatus({});
91480
93410
  if (status && typeof status.enabled === "boolean") service.updateCharacteristic(import_dist.Characteristic.On, status.enabled);
91481
93411
  } catch (err) {
91482
- log.debug("export-hap: privacy-mask getStatus hydrate failed (non-fatal)", { meta: { error: errMsg$5(err) } });
93412
+ log.debug("export-hap: privacy-mask getStatus hydrate failed (non-fatal)", { meta: { error: errMsg$8(err) } });
91483
93413
  }
91484
93414
  service.getCharacteristic(import_dist.Characteristic.On).onSet(async (value) => {
91485
93415
  const enabled = value === true;
@@ -91488,7 +93418,7 @@ async function buildPrivacySwitch(bctx) {
91488
93418
  } catch (err) {
91489
93419
  log.warn("export-hap: privacy-mask setMask failed", { meta: {
91490
93420
  enabled,
91491
- error: errMsg$5(err)
93421
+ error: errMsg$8(err)
91492
93422
  } });
91493
93423
  }
91494
93424
  });
@@ -91506,7 +93436,7 @@ async function buildPrivacySwitch(bctx) {
91506
93436
  } catch {}
91507
93437
  } };
91508
93438
  }
91509
- function errMsg$5(err) {
93439
+ function errMsg$8(err) {
91510
93440
  return err instanceof Error ? err.message : String(err);
91511
93441
  }
91512
93442
  //#endregion
@@ -91597,7 +93527,7 @@ async function buildPtz(bctx) {
91597
93527
  } catch (err) {
91598
93528
  log.warn("export-hap: ptz.goToPreset failed", { meta: {
91599
93529
  presetId: preset.id,
91600
- error: errMsg$4(err)
93530
+ error: errMsg$7(err)
91601
93531
  } });
91602
93532
  }
91603
93533
  armReset(() => service.updateCharacteristic(import_dist.Characteristic.On, false), MOMENTARY_RESET_MS);
@@ -91615,14 +93545,14 @@ async function buildPtz(bctx) {
91615
93545
  try {
91616
93546
  await proxy.ptz?.stop({});
91617
93547
  } catch (err) {
91618
- log.debug("export-hap: ptz.stop failed (non-fatal)", { meta: { error: errMsg$4(err) } });
93548
+ log.debug("export-hap: ptz.stop failed (non-fatal)", { meta: { error: errMsg$7(err) } });
91619
93549
  }
91620
93550
  service.updateCharacteristic(import_dist.Characteristic.On, false);
91621
93551
  }, options.ptzPulseMs);
91622
93552
  } catch (err) {
91623
93553
  log.warn("export-hap: ptz.continuousMove failed", { meta: {
91624
93554
  dir: dir.label,
91625
- error: errMsg$4(err)
93555
+ error: errMsg$7(err)
91626
93556
  } });
91627
93557
  service.updateCharacteristic(import_dist.Characteristic.On, false);
91628
93558
  }
@@ -91645,7 +93575,7 @@ async function readPresets(bctx) {
91645
93575
  name: p.name
91646
93576
  }));
91647
93577
  } catch (err) {
91648
- ctx.logger.withTags({ deviceId: numericDeviceId }).debug("export-hap: ptz.getPresets failed (non-fatal)", { meta: { error: errMsg$4(err) } });
93578
+ ctx.logger.withTags({ deviceId: numericDeviceId }).debug("export-hap: ptz.getPresets failed (non-fatal)", { meta: { error: errMsg$7(err) } });
91649
93579
  return [];
91650
93580
  }
91651
93581
  }
@@ -91660,7 +93590,7 @@ async function tryBuildAutotrack(bctx) {
91660
93590
  const status = await proxy.ptzAutotrack.getStatus({});
91661
93591
  if (status && typeof status.enabled === "boolean") service.updateCharacteristic(import_dist.Characteristic.On, status.enabled);
91662
93592
  } catch (err) {
91663
- log.debug("export-hap: ptzAutotrack.getStatus failed (non-fatal)", { meta: { error: errMsg$4(err) } });
93593
+ log.debug("export-hap: ptzAutotrack.getStatus failed (non-fatal)", { meta: { error: errMsg$7(err) } });
91664
93594
  }
91665
93595
  service.getCharacteristic(import_dist.Characteristic.On).onSet(async (value) => {
91666
93596
  const enabled = value === true;
@@ -91669,13 +93599,13 @@ async function tryBuildAutotrack(bctx) {
91669
93599
  } catch (err) {
91670
93600
  log.warn("export-hap: ptzAutotrack.setEnabled failed", { meta: {
91671
93601
  enabled,
91672
- error: errMsg$4(err)
93602
+ error: errMsg$7(err)
91673
93603
  } });
91674
93604
  }
91675
93605
  });
91676
93606
  return { async dispose() {} };
91677
93607
  }
91678
- function errMsg$4(err) {
93608
+ function errMsg$7(err) {
91679
93609
  return err instanceof Error ? err.message : String(err);
91680
93610
  }
91681
93611
  //#endregion
@@ -92706,13 +94636,13 @@ async function buildChildSwitch(bctx, subtype, deviceType) {
92706
94636
  const switchStatus = await proxy.switch?.getStatus({});
92707
94637
  if (switchStatus && typeof switchStatus.on === "boolean") service.updateCharacteristic(import_dist.Characteristic.On, switchStatus.on);
92708
94638
  } catch (err) {
92709
- log.debug("export-hap: child switch.getStatus failed (non-fatal)", { meta: { error: errMsg$3(err) } });
94639
+ log.debug("export-hap: child switch.getStatus failed (non-fatal)", { meta: { error: errMsg$6(err) } });
92710
94640
  }
92711
94641
  if (useLightbulb) try {
92712
94642
  const status = await proxy.brightness?.getStatus({});
92713
94643
  if (status && typeof status.percentage === "number") service.updateCharacteristic(import_dist.Characteristic.Brightness, status.percentage);
92714
94644
  } catch (err) {
92715
- log.debug("export-hap: child brightness.getStatus failed (non-fatal)", { meta: { error: errMsg$3(err) } });
94645
+ log.debug("export-hap: child brightness.getStatus failed (non-fatal)", { meta: { error: errMsg$6(err) } });
92716
94646
  }
92717
94647
  if (hasSwitch) service.getCharacteristic(import_dist.Characteristic.On).onSet(async (value) => {
92718
94648
  const on = value === true;
@@ -92721,7 +94651,7 @@ async function buildChildSwitch(bctx, subtype, deviceType) {
92721
94651
  } catch (err) {
92722
94652
  log.warn("export-hap: child switch.setState failed", { meta: {
92723
94653
  on,
92724
- error: errMsg$3(err)
94654
+ error: errMsg$6(err)
92725
94655
  } });
92726
94656
  }
92727
94657
  });
@@ -92733,7 +94663,7 @@ async function buildChildSwitch(bctx, subtype, deviceType) {
92733
94663
  } catch (err) {
92734
94664
  log.warn("export-hap: child brightness.setBrightness failed", { meta: {
92735
94665
  percentage,
92736
- error: errMsg$3(err)
94666
+ error: errMsg$6(err)
92737
94667
  } });
92738
94668
  }
92739
94669
  });
@@ -92756,7 +94686,7 @@ async function buildChildSwitch(bctx, subtype, deviceType) {
92756
94686
  } catch {}
92757
94687
  } };
92758
94688
  }
92759
- function errMsg$3(err) {
94689
+ function errMsg$6(err) {
92760
94690
  return err instanceof Error ? err.message : String(err);
92761
94691
  }
92762
94692
  //#endregion
@@ -92783,7 +94713,7 @@ async function buildChildServicesFor(input) {
92783
94713
  for (const h of handles) try {
92784
94714
  await h.dispose();
92785
94715
  } catch (err) {
92786
- ctx.logger.withTags({ deviceId: parentNumericId }).debug("export-hap: child service dispose failed (continuing)", { meta: { error: errMsg$2(err) } });
94716
+ ctx.logger.withTags({ deviceId: parentNumericId }).debug("export-hap: child service dispose failed (continuing)", { meta: { error: errMsg$5(err) } });
92787
94717
  }
92788
94718
  } };
92789
94719
  }
@@ -92799,7 +94729,7 @@ async function listChildren(ctx, parentNumericId) {
92799
94729
  features: Array.isArray(c.features) ? c.features.filter((f) => typeof f === "string") : []
92800
94730
  }));
92801
94731
  } catch (err) {
92802
- ctx.logger.withTags({ deviceId: parentNumericId }).debug("export-hap: deviceManager.getChildren failed (non-fatal)", { meta: { error: errMsg$2(err) } });
94732
+ ctx.logger.withTags({ deviceId: parentNumericId }).debug("export-hap: deviceManager.getChildren failed (non-fatal)", { meta: { error: errMsg$5(err) } });
92803
94733
  return [];
92804
94734
  }
92805
94735
  }
@@ -92815,10 +94745,76 @@ function asDeviceType(raw) {
92815
94745
  for (const value of Object.values(DeviceType)) if (value === lower) return value;
92816
94746
  return DeviceType.Generic;
92817
94747
  }
92818
- function errMsg$2(err) {
94748
+ function errMsg$5(err) {
92819
94749
  return err instanceof Error ? err.message : String(err);
92820
94750
  }
92821
94751
  //#endregion
94752
+ //#region src/mappers/kind.ts
94753
+ /**
94754
+ * Which orchestrator a device gets, which device types the picker offers, and
94755
+ * what a device's HomeKit accessory is called on the wire.
94756
+ *
94757
+ * Separate from `index.ts` (the factory registry) only so the orchestrators can
94758
+ * import the uuid convention without importing the registry that imports them.
94759
+ */
94760
+ var SUPPORTED_MAPPER_KINDS = ["camera", "generic"];
94761
+ /**
94762
+ * The device types the Export picker offers, and the answer to the cap's
94763
+ * `listSupportedDeviceKinds`.
94764
+ *
94765
+ * A UI FILTER, nothing more. Whether a device actually exports anything is
94766
+ * decided by its capabilities (`rowsForCaps` in the capability table) — the
94767
+ * doctrine the HA exporter writes down, and the thing that confined the
94768
+ * previous exporter to cameras when it was ignored.
94769
+ *
94770
+ * Tier A: the types whose HomeKit services exist today and need no
94771
+ * feature-dependent shape. `cover`, `climate`, `fan`, `valve`, `humidifier`,
94772
+ * `water-heater`, `alarm-panel`, `button` and `media-player` are deliberately
94773
+ * absent — each needs a service whose semantics the table cannot yet honour,
94774
+ * and offering the tab before the mapping exists is how an operator gets an
94775
+ * accessory that pairs and does nothing.
94776
+ */
94777
+ var HAP_EXPORTABLE_DEVICE_TYPES = [
94778
+ DeviceType.Camera,
94779
+ DeviceType.Light,
94780
+ DeviceType.Switch,
94781
+ DeviceType.Siren,
94782
+ DeviceType.Sensor,
94783
+ DeviceType.Lock,
94784
+ DeviceType.Presence,
94785
+ DeviceType.Generic
94786
+ ];
94787
+ /**
94788
+ * Resolve the orchestrator for a device from its TYPE.
94789
+ *
94790
+ * `null` means "no Export tab": the picker must not offer a type no
94791
+ * orchestrator can build. An UNKNOWN type (the device-manager read failed) maps
94792
+ * to `camera` — that is what every entry persisted before this function existed
94793
+ * carries, and a transient API failure must never re-shape an exposed camera.
94794
+ */
94795
+ function pickMapperKind(deviceType) {
94796
+ if (deviceType === null || deviceType === void 0 || deviceType.length === 0) return "camera";
94797
+ if (deviceType === DeviceType.Camera) return "camera";
94798
+ return HAP_EXPORTABLE_DEVICE_TYPES.find((type) => type === deviceType) === void 0 ? null : "generic";
94799
+ }
94800
+ /**
94801
+ * The deterministic HAP accessory UUID for a device.
94802
+ *
94803
+ * One function, because three call sites depend on the answer agreeing: the
94804
+ * orchestrator that builds the accessory, `unexposeDevice` (which wipes
94805
+ * hap-nodejs' pairing blobs by uuid, on a device whose mapper is already gone)
94806
+ * and the export sync state that records it.
94807
+ *
94808
+ * The camera namespace is FROZEN — every camera paired to date derives its MAC
94809
+ * from `sha256("camstack:hap:" + uuid)` of this exact string, and changing it
94810
+ * would make every paired camera a stranger. Generic devices get their own
94811
+ * namespace so a switch and a camera that happen to share a device id are not
94812
+ * the same HomeKit accessory.
94813
+ */
94814
+ function accessoryUuidFor(kind, deviceId) {
94815
+ return import_dist.uuid.generate(kind === "camera" ? `camstack:camera:${deviceId}` : `camstack:device:${deviceId}`);
94816
+ }
94817
+ //#endregion
92822
94818
  //#region src/mappers/camera-accessory.ts
92823
94819
  /**
92824
94820
  * Camera accessory orchestrator — given a camstack deviceId, builds one
@@ -92851,9 +94847,9 @@ async function buildCameraAccessory(input) {
92851
94847
  const capNames = new Set(proxy.binding?.entries.map((e) => e.capName) ?? []);
92852
94848
  const isDoorbell = capNames.has("doorbell");
92853
94849
  const category = isDoorbell ? import_dist.Categories.VIDEO_DOORBELL : import_dist.Categories.IP_CAMERA;
92854
- const accessory = new import_dist.Accessory(displayName, import_dist.uuid.generate(`camstack:camera:${numericId}`));
94850
+ const accessory = new import_dist.Accessory(displayName, accessoryUuidFor("camera", numericId));
92855
94851
  accessory.category = category;
92856
- await populateAccessoryInfo(accessory, proxy, displayName);
94852
+ await populateAccessoryInfo(accessory, proxy, displayName, "Camera");
92857
94853
  const bctx = {
92858
94854
  ctx,
92859
94855
  accessory,
@@ -92911,55 +94907,462 @@ async function buildCameraAccessory(input) {
92911
94907
  for (const h of handles) try {
92912
94908
  await h.dispose();
92913
94909
  } catch (err) {
92914
- log.debug("export-hap: builder dispose failed (continuing)", { meta: { error: errMsg$1(err) } });
94910
+ log.debug("export-hap: builder dispose failed (continuing)", { meta: { error: errMsg$4(err) } });
92915
94911
  }
92916
94912
  try {
92917
94913
  await streams.dispose();
92918
94914
  } catch (err) {
92919
- log.debug("export-hap: streams dispose failed", { meta: { error: errMsg$1(err) } });
94915
+ log.debug("export-hap: streams dispose failed", { meta: { error: errMsg$4(err) } });
92920
94916
  }
92921
94917
  }
92922
94918
  };
92923
94919
  }
92924
- async function populateAccessoryInfo(accessory, proxy, displayName) {
92925
- const info = accessory.getService(import_dist.Service.AccessoryInformation);
92926
- if (!info) return;
94920
+ function errMsg$4(err) {
94921
+ return err instanceof Error ? err.message : String(err);
94922
+ }
94923
+ //#endregion
94924
+ //#region src/mappers/builders/generic/lock.ts
94925
+ /**
94926
+ * `lock-control` → `Service.LockMechanism`.
94927
+ *
94928
+ * Not a sensor row: HomeKit models a lock as two characteristics, a CURRENT
94929
+ * state the accessory owns and a TARGET state the controller writes, and the
94930
+ * cap's five states do not map onto either one alone.
94931
+ *
94932
+ * ```
94933
+ * cap state LockCurrentState LockTargetState
94934
+ * locked SECURED SECURED
94935
+ * unlocked UNSECURED UNSECURED
94936
+ * locking UNKNOWN SECURED (in flight — do not claim SECURED)
94937
+ * unlocking UNKNOWN UNSECURED
94938
+ * jammed JAMMED unchanged (the controller's intent stands)
94939
+ * ```
94940
+ *
94941
+ * Reporting SECURED while the bolt is still moving is the failure worth naming:
94942
+ * iOS renders the lock as closed, the operator walks away, and the motor stalls
94943
+ * behind them. `UNKNOWN` is the honest answer for an in-flight transition.
94944
+ *
94945
+ * `open` (the cap's third method, for locks with a latch/buzzer) has no HomeKit
94946
+ * counterpart on this service and is deliberately not wired — a Switch that
94947
+ * silently buzzes a door open would be a second knob nobody asked for.
94948
+ */
94949
+ var SUBTYPE = "lock-control";
94950
+ var readLock = reader(LockControlStatusSchema.pick({ state: true }), (status) => {
94951
+ const current = status.state === "locked" ? import_dist.Characteristic.LockCurrentState.SECURED : status.state === "unlocked" ? import_dist.Characteristic.LockCurrentState.UNSECURED : status.state === "jammed" ? import_dist.Characteristic.LockCurrentState.JAMMED : import_dist.Characteristic.LockCurrentState.UNKNOWN;
94952
+ const target = status.state === "locked" || status.state === "locking" ? import_dist.Characteristic.LockTargetState.SECURED : status.state === "unlocked" || status.state === "unlocking" ? import_dist.Characteristic.LockTargetState.UNSECURED : null;
94953
+ return [{
94954
+ characteristic: import_dist.Characteristic.LockCurrentState,
94955
+ value: current
94956
+ }, ...target === null ? [] : [{
94957
+ characteristic: import_dist.Characteristic.LockTargetState,
94958
+ value: target
94959
+ }]];
94960
+ });
94961
+ async function buildLockMechanism(bctx) {
94962
+ const { ctx, accessory, proxy, numericDeviceId, displayName } = bctx;
94963
+ const log = ctx.logger.withTags({ deviceId: numericDeviceId });
94964
+ const label = hapServiceName([displayName], `Lock ${numericDeviceId}`);
94965
+ const service = accessory.addService(import_dist.Service.LockMechanism, label, SUBTYPE);
94966
+ applyServiceLabel(service, label);
94967
+ const apply = (status, source) => {
94968
+ const updates = readLock(status);
94969
+ if (updates.length === 0) {
94970
+ log.debug("export-hap: lock status did not match the cap schema — no update", { meta: { source } });
94971
+ return;
94972
+ }
94973
+ applyUpdates(service, updates);
94974
+ };
92927
94975
  try {
92928
- const device = await proxy.deviceManager?.getDevice({});
92929
- const metadata = device?.metadata ?? null;
92930
- info.setCharacteristic(import_dist.Characteristic.Name, device?.name ?? displayName);
92931
- info.setCharacteristic(import_dist.Characteristic.Manufacturer, stringOr(metadata?.manufacturer, "CamStack"));
92932
- info.setCharacteristic(import_dist.Characteristic.Model, stringOr(metadata?.model, "Camera"));
92933
- info.setCharacteristic(import_dist.Characteristic.FirmwareRevision, stringOr(metadata?.firmware, "0.0.0"));
92934
- info.setCharacteristic(import_dist.Characteristic.SerialNumber, stringOr(metadata?.sn, `camstack-${proxy.deviceId}`));
92935
- } catch {}
94976
+ const status = await proxy.lockControl?.getStatus({});
94977
+ if (status !== void 0 && status !== null) apply(status, "getStatus");
94978
+ } catch (err) {
94979
+ log.debug("export-hap: lock getStatus hydrate failed (non-fatal)", { meta: { error: errMsg$3(err) } });
94980
+ }
94981
+ service.getCharacteristic(import_dist.Characteristic.LockTargetState).onSet(async (value) => {
94982
+ const secure = value === import_dist.Characteristic.LockTargetState.SECURED;
94983
+ try {
94984
+ if (secure) await proxy.lockControl?.lock({});
94985
+ else await proxy.lockControl?.unlock({});
94986
+ } catch (err) {
94987
+ log.warn("export-hap: lock command failed", { meta: {
94988
+ secure,
94989
+ error: errMsg$3(err)
94990
+ } });
94991
+ }
94992
+ });
94993
+ const unsubscribe = proxy.state.lockControl?.subscribe((value) => {
94994
+ if (value === void 0 || value === null) return;
94995
+ apply(value, "slice");
94996
+ }) ?? null;
94997
+ return { async dispose() {
94998
+ try {
94999
+ unsubscribe?.();
95000
+ } catch {}
95001
+ } };
92936
95002
  }
92937
- function stringOr(value, fallback) {
92938
- return typeof value === "string" && value.length > 0 ? value : fallback;
95003
+ function errMsg$3(err) {
95004
+ return err instanceof Error ? err.message : String(err);
95005
+ }
95006
+ //#endregion
95007
+ //#region src/mappers/builders/generic/sensor-service.ts
95008
+ async function buildSensorService(input) {
95009
+ const { bctx, spec, name, subtype } = input;
95010
+ const { ctx, accessory, proxy, numericDeviceId } = bctx;
95011
+ const log = ctx.logger.withTags({ deviceId: numericDeviceId });
95012
+ const label = hapServiceName([name], spec.label);
95013
+ const service = spec.addService(accessory, label, subtype);
95014
+ applyServiceLabel(service, label);
95015
+ const apply = (status, source) => {
95016
+ const updates = spec.read(status);
95017
+ if (updates.length === 0) {
95018
+ log.debug("export-hap: sensor status did not match the cap schema — no update", { meta: {
95019
+ subtype,
95020
+ source
95021
+ } });
95022
+ return;
95023
+ }
95024
+ applyUpdates(service, updates);
95025
+ };
95026
+ try {
95027
+ const status = await spec.getStatus(proxy);
95028
+ if (status !== void 0 && status !== null) apply(status, "getStatus");
95029
+ } catch (err) {
95030
+ log.debug("export-hap: sensor getStatus hydrate failed (non-fatal)", { meta: {
95031
+ subtype,
95032
+ error: errMsg$2(err)
95033
+ } });
95034
+ }
95035
+ const unsubscribe = spec.subscribe(proxy, (value) => {
95036
+ if (value === void 0 || value === null) return;
95037
+ apply(value, "slice");
95038
+ });
95039
+ return { async dispose() {
95040
+ try {
95041
+ unsubscribe?.();
95042
+ } catch {}
95043
+ } };
95044
+ }
95045
+ function errMsg$2(err) {
95046
+ return err instanceof Error ? err.message : String(err);
95047
+ }
95048
+ //#endregion
95049
+ //#region src/mappers/builders/generic/cap-service-table.ts
95050
+ /**
95051
+ * The capability→HomeKit-service TABLE for non-camera devices.
95052
+ *
95053
+ * This is the whole coverage decision for the generic export path, and it is
95054
+ * deliberately a table rather than a `switch` on `DeviceType`. The doctrine is
95055
+ * the one the Home Assistant exporter already writes down: *coverage is decided
95056
+ * by a device's capabilities, not its type — restricting the picker by type is
95057
+ * what confined the previous exporter to cameras.* A `DeviceType` here is only
95058
+ * ever a UI filter (`HAP_EXPORTABLE_DEVICE_TYPES`) or an icon
95059
+ * (`ACCESSORY_CATEGORY_BY_TYPE`); it never decides whether something exports.
95060
+ *
95061
+ * The shape mirrors `CAP_ENTITY_MAP` in
95062
+ * `packages/addon-provider-homeassistant/src/ha-export/entity-catalog.ts` so
95063
+ * the two can be unified later — one row per capability, keyed by the cap name
95064
+ * exactly as it appears in `proxy.binding.entries[].capName`.
95065
+ *
95066
+ * ## Reading a status
95067
+ *
95068
+ * Every row parses the cap's OWN Zod status schema rather than duck-typing the
95069
+ * payload, and it `.pick()`s only the fields it reads: a provider that omits a
95070
+ * timestamp must not silence a sensor, and a provider that sends the wrong
95071
+ * shape must not write a garbage characteristic. A parse that fails yields no
95072
+ * updates, and the builder that owns the service logs the drop — silence reads
95073
+ * as "the sensor never changed".
95074
+ *
95075
+ * ## Tier
95076
+ *
95077
+ * Tier A only: the caps whose HomeKit service exists today and needs no
95078
+ * feature-dependent shape (`cover` needs `cover-positionable`, `climate-control`
95079
+ * needs the dual-setpoint split, `alarm-panel` must survive a refused `arm`).
95080
+ * Those are a separate task; adding a row here must never mean adding a service
95081
+ * whose semantics this file cannot fully honour.
95082
+ */
95083
+ /** HomeKit's floor for `CurrentAmbientLightLevel`; 0 lux is out of range. */
95084
+ var MIN_LUX = 1e-4;
95085
+ var SENSOR_SPECS = {
95086
+ contact: {
95087
+ label: "Contact",
95088
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.ContactSensor, name, subtype),
95089
+ getStatus: (proxy) => proxy.contact?.getStatus({}),
95090
+ subscribe: (proxy, onValue) => proxy.state.contact?.subscribe(onValue) ?? null,
95091
+ read: reader(ContactStatusSchema.pick({ entryOpen: true }), (status) => [{
95092
+ characteristic: import_dist.Characteristic.ContactSensorState,
95093
+ value: status.entryOpen ? import_dist.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : import_dist.Characteristic.ContactSensorState.CONTACT_DETECTED
95094
+ }])
95095
+ },
95096
+ motion: {
95097
+ label: "Motion",
95098
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.MotionSensor, name, subtype),
95099
+ getStatus: (proxy) => proxy.motion?.getStatus({}),
95100
+ subscribe: (proxy, onValue) => proxy.state.motion?.subscribe(onValue) ?? null,
95101
+ read: reader(MotionStatusSchema.pick({ detected: true }), (status) => [{
95102
+ characteristic: import_dist.Characteristic.MotionDetected,
95103
+ value: status.detected
95104
+ }])
95105
+ },
95106
+ presence: {
95107
+ label: "Presence",
95108
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.OccupancySensor, name, subtype),
95109
+ getStatus: (proxy) => proxy.presence?.getStatus({}),
95110
+ subscribe: (proxy, onValue) => proxy.state.presence?.subscribe(onValue) ?? null,
95111
+ read: reader(PresenceStatusSchema.pick({ state: true }), (status) => [{
95112
+ characteristic: import_dist.Characteristic.OccupancyDetected,
95113
+ value: status.state === "home" ? import_dist.Characteristic.OccupancyDetected.OCCUPANCY_DETECTED : import_dist.Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED
95114
+ }])
95115
+ },
95116
+ smoke: {
95117
+ label: "Smoke",
95118
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.SmokeSensor, name, subtype),
95119
+ getStatus: (proxy) => proxy.smoke?.getStatus({}),
95120
+ subscribe: (proxy, onValue) => proxy.state.smoke?.subscribe(onValue) ?? null,
95121
+ read: reader(SmokeStatusSchema.pick({ detected: true }), (status) => [{
95122
+ characteristic: import_dist.Characteristic.SmokeDetected,
95123
+ value: status.detected ? import_dist.Characteristic.SmokeDetected.SMOKE_DETECTED : import_dist.Characteristic.SmokeDetected.SMOKE_NOT_DETECTED
95124
+ }])
95125
+ },
95126
+ "carbon-monoxide": {
95127
+ label: "Carbon monoxide",
95128
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.CarbonMonoxideSensor, name, subtype),
95129
+ getStatus: (proxy) => proxy.carbonMonoxide?.getStatus({}),
95130
+ subscribe: (proxy, onValue) => proxy.state.carbonMonoxide?.subscribe(onValue) ?? null,
95131
+ read: reader(CarbonMonoxideStatusSchema.pick({ detected: true }), (status) => [{
95132
+ characteristic: import_dist.Characteristic.CarbonMonoxideDetected,
95133
+ value: status.detected ? import_dist.Characteristic.CarbonMonoxideDetected.CO_LEVELS_ABNORMAL : import_dist.Characteristic.CarbonMonoxideDetected.CO_LEVELS_NORMAL
95134
+ }])
95135
+ },
95136
+ flood: {
95137
+ label: "Leak",
95138
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.LeakSensor, name, subtype),
95139
+ getStatus: (proxy) => proxy.flood?.getStatus({}),
95140
+ subscribe: (proxy, onValue) => proxy.state.flood?.subscribe(onValue) ?? null,
95141
+ read: reader(FloodStatusSchema.pick({ flooded: true }), (status) => [{
95142
+ characteristic: import_dist.Characteristic.LeakDetected,
95143
+ value: status.flooded ? import_dist.Characteristic.LeakDetected.LEAK_DETECTED : import_dist.Characteristic.LeakDetected.LEAK_NOT_DETECTED
95144
+ }])
95145
+ },
95146
+ "temperature-sensor": {
95147
+ label: "Temperature",
95148
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.TemperatureSensor, name, subtype),
95149
+ getStatus: (proxy) => proxy.temperatureSensor?.getStatus({}),
95150
+ subscribe: (proxy, onValue) => proxy.state.temperatureSensor?.subscribe(onValue) ?? null,
95151
+ read: reader(TemperatureSensorStatusSchema.pick({ celsius: true }), (status) => [{
95152
+ characteristic: import_dist.Characteristic.CurrentTemperature,
95153
+ value: status.celsius
95154
+ }])
95155
+ },
95156
+ "humidity-sensor": {
95157
+ label: "Humidity",
95158
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.HumiditySensor, name, subtype),
95159
+ getStatus: (proxy) => proxy.humiditySensor?.getStatus({}),
95160
+ subscribe: (proxy, onValue) => proxy.state.humiditySensor?.subscribe(onValue) ?? null,
95161
+ read: reader(HumiditySensorStatusSchema.pick({ percent: true }), (status) => [{
95162
+ characteristic: import_dist.Characteristic.CurrentRelativeHumidity,
95163
+ value: status.percent
95164
+ }])
95165
+ },
95166
+ "ambient-light-sensor": {
95167
+ label: "Light level",
95168
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.LightSensor, name, subtype),
95169
+ getStatus: (proxy) => proxy.ambientLightSensor?.getStatus({}),
95170
+ subscribe: (proxy, onValue) => proxy.state.ambientLightSensor?.subscribe(onValue) ?? null,
95171
+ read: reader(AmbientLightSensorStatusSchema.pick({ lux: true }), (status) => [{
95172
+ characteristic: import_dist.Characteristic.CurrentAmbientLightLevel,
95173
+ value: Math.max(MIN_LUX, status.lux)
95174
+ }])
95175
+ }
95176
+ };
95177
+ /**
95178
+ * The `battery` row reads like a sensor but writes three characteristics from
95179
+ * one status, and the camera path publishes the SAME service from the same cap.
95180
+ * The derivation therefore lives once, in `battery.ts`, and both callers read
95181
+ * it from there.
95182
+ */
95183
+ var BATTERY_SPEC = {
95184
+ label: "Battery",
95185
+ addService: (accessory, name, subtype) => accessory.addService(import_dist.Service.Battery, name, subtype),
95186
+ getStatus: (proxy) => proxy.battery?.getStatus({}),
95187
+ subscribe: (proxy, onValue) => proxy.state.battery?.subscribe(onValue) ?? null,
95188
+ read: batteryCharacteristicUpdates
95189
+ };
95190
+ function sensorRow(capName, spec) {
95191
+ return {
95192
+ caps: [capName],
95193
+ label: spec.label,
95194
+ build: ({ bctx, name }) => buildSensorService({
95195
+ bctx,
95196
+ spec,
95197
+ name,
95198
+ subtype: capName
95199
+ })
95200
+ };
95201
+ }
95202
+ /**
95203
+ * THE table. Order matters only for one thing: the first row that matches a
95204
+ * device decides the accessory's category when the device's own type does not.
95205
+ */
95206
+ var HAP_CAP_SERVICES = [
95207
+ {
95208
+ caps: ["switch", "brightness"],
95209
+ label: "Power",
95210
+ build: ({ bctx, name, deviceType }) => buildChildSwitch({
95211
+ ...bctx,
95212
+ displayName: name
95213
+ }, "switch", deviceType)
95214
+ },
95215
+ {
95216
+ caps: ["lock-control"],
95217
+ label: "Lock",
95218
+ build: ({ bctx, name }) => buildLockMechanism({
95219
+ ...bctx,
95220
+ displayName: name
95221
+ })
95222
+ },
95223
+ {
95224
+ caps: ["battery"],
95225
+ label: BATTERY_SPEC.label,
95226
+ build: ({ bctx, name }) => buildSensorService({
95227
+ bctx,
95228
+ spec: BATTERY_SPEC,
95229
+ name,
95230
+ subtype: "battery"
95231
+ })
95232
+ },
95233
+ ...Object.entries(SENSOR_SPECS).map(([capName, spec]) => sensorRow(capName, spec))
95234
+ ];
95235
+ /**
95236
+ * The rows a device's bound capabilities select, in table order.
95237
+ *
95238
+ * A row matches when ANY of its caps is bound: a lamp with `switch` but no
95239
+ * `brightness` is still a Lightbulb-shaped row, and the builder degrades on its
95240
+ * own.
95241
+ */
95242
+ function rowsForCaps(capNames) {
95243
+ return HAP_CAP_SERVICES.filter((row) => row.caps.some((cap) => capNames.has(cap)));
95244
+ }
95245
+ /**
95246
+ * The HomeKit accessory category per camstack device type — the icon iOS Home
95247
+ * draws and nothing else. Absent = `OTHER`, which is a valid accessory that
95248
+ * simply gets the generic tile.
95249
+ *
95250
+ * This is NOT a coverage decision. `HAP_EXPORTABLE_DEVICE_TYPES` (in
95251
+ * `mappers/index.ts`) filters the picker; whether a device exports anything is
95252
+ * decided by `rowsForCaps`.
95253
+ */
95254
+ var ACCESSORY_CATEGORY_BY_TYPE = {
95255
+ [DeviceType.Switch]: import_dist.Categories.SWITCH,
95256
+ [DeviceType.Light]: import_dist.Categories.LIGHTBULB,
95257
+ [DeviceType.Siren]: import_dist.Categories.SWITCH,
95258
+ [DeviceType.Lock]: import_dist.Categories.DOOR_LOCK,
95259
+ [DeviceType.Sensor]: import_dist.Categories.SENSOR,
95260
+ [DeviceType.Presence]: import_dist.Categories.SENSOR
95261
+ };
95262
+ function accessoryCategoryFor(deviceType) {
95263
+ return ACCESSORY_CATEGORY_BY_TYPE[deviceType] ?? import_dist.Categories.OTHER;
95264
+ }
95265
+ //#endregion
95266
+ //#region src/mappers/generic-accessory.ts
95267
+ /**
95268
+ * Generic accessory orchestrator — every camstack device that is NOT a camera.
95269
+ *
95270
+ * Same three steps as the camera orchestrator, and deliberately nothing more:
95271
+ * 1. resolve the typed `DeviceProxy`,
95272
+ * 2. select rows from the capability→service table with the device's BOUND
95273
+ * capabilities (never its type — see `builders/generic/cap-service-table.ts`),
95274
+ * 3. let each row add its service to one `Accessory`.
95275
+ *
95276
+ * It publishes standalone, like the camera path: one accessory, one mDNS
95277
+ * advertisement, one pairing, the shared setup code. This addon is not a
95278
+ * bridge — that decision predates this file and is not re-litigated here.
95279
+ *
95280
+ * ## It REFUSES rather than publishing an empty tile
95281
+ *
95282
+ * A device whose capabilities select no row throws. The alternative is an
95283
+ * accessory carrying nothing but `AccessoryInformation`: it pairs, it appears
95284
+ * in the Home app, it does nothing, and the operator has no way to tell that
95285
+ * from a broken integration. The Export tab is gated on the same question
95286
+ * (`hap-export.addon.ts`), so reaching this throw means the device changed
95287
+ * shape between the tab rendering and the toggle landing.
95288
+ */
95289
+ async function buildGenericAccessory(input) {
95290
+ const { ctx, deviceId, displayName, options } = input;
95291
+ const numericId = Number.parseInt(deviceId, 10);
95292
+ if (!Number.isFinite(numericId)) throw new Error(`export-hap: cannot map device '${deviceId}' — id is not numeric`);
95293
+ const log = ctx.logger.withTags({ deviceId: numericId });
95294
+ const proxy = await ctx.fetchDevice(numericId);
95295
+ const capNames = new Set(proxy.binding?.entries.map((e) => e.capName) ?? []);
95296
+ const rows = rowsForCaps(capNames);
95297
+ if (rows.length === 0) throw new Error(`export-hap: device ${numericId} carries no capability HomeKit can export (bound: ${[...capNames].toSorted().join(", ") || "none"})`);
95298
+ const deviceType = await resolveDeviceType(proxy);
95299
+ const accessory = new import_dist.Accessory(displayName, accessoryUuidFor("generic", numericId));
95300
+ accessory.category = accessoryCategoryFor(deviceType);
95301
+ await populateAccessoryInfo(accessory, proxy, displayName, "Accessory");
95302
+ const bctx = {
95303
+ ctx,
95304
+ accessory,
95305
+ proxy,
95306
+ numericDeviceId: numericId,
95307
+ displayName,
95308
+ options
95309
+ };
95310
+ const handles = [];
95311
+ for (const row of rows) {
95312
+ const name = rows.length === 1 ? displayName : row.label;
95313
+ handles.push(await row.build({
95314
+ bctx,
95315
+ name,
95316
+ deviceType
95317
+ }));
95318
+ }
95319
+ log.info("export-hap: built generic accessory", { meta: {
95320
+ deviceType,
95321
+ services: rows.map((row) => row.caps[0]).join(",")
95322
+ } });
95323
+ return {
95324
+ accessory,
95325
+ accessories: [accessory],
95326
+ async dispose() {
95327
+ for (const handle of handles) try {
95328
+ await handle.dispose();
95329
+ } catch (err) {
95330
+ log.debug("export-hap: generic builder dispose failed (continuing)", { meta: { error: errMsg$1(err) } });
95331
+ }
95332
+ }
95333
+ };
95334
+ }
95335
+ /**
95336
+ * The device's own type, or `Generic`.
95337
+ *
95338
+ * Used ONLY where one capability has two HomeKit shapes — a siren's
95339
+ * `brightness` is alarm volume, not luminosity — and for the accessory's icon.
95340
+ * `Generic` is the safe reading: it is the shape `child-switch.ts` already
95341
+ * treats as "a lamp if it dims, a switch otherwise".
95342
+ */
95343
+ async function resolveDeviceType(proxy) {
95344
+ try {
95345
+ const device = await proxy.deviceManager?.getDevice({});
95346
+ const raw = typeof device?.type === "string" ? device.type.toLowerCase() : "";
95347
+ return Object.values(DeviceType).find((value) => value === raw) ?? DeviceType.Generic;
95348
+ } catch {
95349
+ return DeviceType.Generic;
95350
+ }
92939
95351
  }
92940
95352
  function errMsg$1(err) {
92941
95353
  return err instanceof Error ? err.message : String(err);
92942
95354
  }
92943
95355
  //#endregion
92944
95356
  //#region src/mappers/index.ts
92945
- var SUPPORTED_MAPPER_KINDS = ["camera"];
92946
- var REGISTRY = { camera: buildCameraAccessory };
95357
+ var REGISTRY = {
95358
+ camera: buildCameraAccessory,
95359
+ generic: buildGenericAccessory
95360
+ };
92947
95361
  function getMapperFactory(kind) {
92948
95362
  const factory = REGISTRY[kind];
92949
95363
  if (!factory) throw new Error(`export-hap: no mapper registered for kind '${kind}'`);
92950
95364
  return factory;
92951
95365
  }
92952
- /**
92953
- * Resolve the best-fit mapper kind. With Round 2, the operator just
92954
- * picks a camera and the orchestrator does the rest — the single
92955
- * `camera` kind is returned unconditionally. We keep the function
92956
- * signature for backwards-compat with the addon's existing
92957
- * `exposeDevice` flow and to leave room for future device types (NVR,
92958
- * climate sensor, ...).
92959
- */
92960
- function pickMapperKind(_capabilities) {
92961
- return "camera";
92962
- }
92963
95366
  //#endregion
92964
95367
  //#region src/mappers/builders/stream-hwaccel-memo.ts
92965
95368
  /**
@@ -93097,8 +95500,8 @@ function syncStateToJson(map) {
93097
95500
  //#endregion
93098
95501
  //#region src/hap-export.addon.ts
93099
95502
  /**
93100
- * HomeKit (HAP) export addon — publishes a single HAP bridge process
93101
- * that exposes selected camstack devices as HomeKit accessories.
95503
+ * HomeKit (HAP) export addon — publishes selected camstack devices as
95504
+ * standalone HomeKit accessories.
93102
95505
  *
93103
95506
  * Operator flow:
93104
95507
  * 1. Install + enable the addon (hub-only, group `export-hap`).
@@ -93109,10 +95512,10 @@ function syncStateToJson(map) {
93109
95512
  * 3. The setup URI (`X-HM://…`) is logged AND surfaced via the
93110
95513
  * `getStatus` cap method so the wizard UI can render the QR.
93111
95514
  * 4. Operator opens iOS Home → + → scan QR → enter pincode.
93112
- * 5. Operator hits "Expose to HomeKit" on individual camstack
93113
- * devices; the addon attaches a MotionSensor accessory (MVP)
93114
- * to the bridge and persists the choice via the same
93115
- * `updateGlobalSettings` path.
95515
+ * 5. Operator hits "Expose to HomeKit" on individual camstack devices.
95516
+ * Each exposed device is published as its OWN accessory with its own
95517
+ * mDNS advertisement, sharing one setup code cameras cannot be
95518
+ * bridged, so nothing is.
93116
95519
  *
93117
95520
  * Exception — the hap-nodejs library's own `HAPStorage` lives under
93118
95521
  * `ctx.dataDir/hap-store/`. That directory is library-internal: the
@@ -93121,23 +95524,20 @@ function syncStateToJson(map) {
93121
95524
  * I/O for addon-owned data") explicitly scopes itself to OUR own data;
93122
95525
  * library-managed blobs are out of scope.
93123
95526
  *
93124
- * Round 2 scope: full camera bridge. `exposeDevice({deviceId})` builds
93125
- * one HomeKit Accessory per camstack camera with auto-detected feature
93126
- * services driven by the device's capability binding:
93127
- * - `camera-streams` cap CameraRTPStreamManagement via ffmpeg
93128
- * - `intercom` cap → Microphone + Speaker (audio bridge wiring is a
93129
- * Round 3 follow-up services are declared so iOS Home shows the
93130
- * talk-back button, but PCM upload is not yet routed)
93131
- * - `doorbell` cap DoorbellController + Doorbell service
93132
- * - `motion-detection` cap MotionSensor service
93133
- * - `ptz` cap preset switches + 4 directional momentary switches
93134
- * - `ptz-autotrack` cap → stateful "Autotrack" switch
93135
- * Children (siren, floodlight, spotlight, …) become independent
93136
- * accessories under the same Bridge see `mappers/child-accessory.ts`.
93137
- *
93138
- * What's deferred to Round 3+: cam→browser audio Opus transcode,
93139
- * intercom upload bridge, HomeKit Secure Video, recording, native
93140
- * H.264 stream tap (currently uses RTSP + ffmpeg copy).
95527
+ * ## Two accessory shapes, one rule about coverage
95528
+ *
95529
+ * `exposeDevice({deviceId})` resolves the device's TYPE to a mapper kind
95530
+ * (`mappers/kind.ts`) and hands off:
95531
+ *
95532
+ * - a camera `mappers/camera-accessory.ts`: streams, HKSV recording,
95533
+ * doorbell, motion, intercom, PTZ, battery, privacy and every accessory
95534
+ * child, all as services on one accessory;
95535
+ * - anything else → `mappers/generic-accessory.ts`, driven by the
95536
+ * capabilityservice table in `mappers/builders/generic/`.
95537
+ *
95538
+ * The type only picks the SHAPE. What a device publishes is decided by the
95539
+ * capabilities bound to itrestricting coverage by type is precisely what
95540
+ * confined this exporter to cameras for its first three rounds.
93141
95541
  */
93142
95542
  var DEFAULT_DEVICE_SETTINGS = {
93143
95543
  streamPreference: "auto",
@@ -93180,7 +95580,6 @@ var DEFAULT_CONFIG = {
93180
95580
  fixedPin: "",
93181
95581
  interfaceName: "",
93182
95582
  ptzPulseMs: 400,
93183
- hksvPreview: false,
93184
95583
  identity: {
93185
95584
  username: "",
93186
95585
  pincode: "",
@@ -93296,7 +95695,7 @@ var ExportHapAddon = class extends BaseAddon {
93296
95695
  ...setup ? { setup } : {}
93297
95696
  };
93298
95697
  },
93299
- listSupportedDeviceKinds: async () => [...SUPPORTED_MAPPER_KINDS],
95698
+ listSupportedDeviceKinds: async () => [...HAP_EXPORTABLE_DEVICE_TYPES],
93300
95699
  listExposedDevices: async () => Array.from(this.exposed.entries()).map(([deviceId, m]) => {
93301
95700
  const entry = this.config.exposed.find((e) => e.deviceId === deviceId);
93302
95701
  return {
@@ -93381,9 +95780,10 @@ var ExportHapAddon = class extends BaseAddon {
93381
95780
  log.debug("export-hap: device already exposed — refreshing capabilities");
93382
95781
  await this.detachMapper(deviceId);
93383
95782
  }
93384
- const mapperKind = pickMapperKind(capabilities);
93385
- if (!mapperKind) throw new Error(`export-hap: no mapper for capabilities ${JSON.stringify(capabilities ?? [])}`);
93386
- const displayName = await this.resolveDisplayName(deviceId);
95783
+ const summary = await this.fetchDeviceSummary(numericId);
95784
+ const mapperKind = pickMapperKind(summary?.type);
95785
+ if (!mapperKind) throw new Error(`export-hap: device ${numericId} has type '${summary?.type ?? "unknown"}', which HomeKit export does not support`);
95786
+ const displayName = summary?.name ?? `Device ${deviceId}`;
93387
95787
  const previous = this.config.exposed.find((e) => e.deviceId === deviceId);
93388
95788
  const baseEntry = carryForward({
93389
95789
  deviceId,
@@ -93413,10 +95813,11 @@ var ExportHapAddon = class extends BaseAddon {
93413
95813
  async unexposeDevice(deviceId, options = {}) {
93414
95814
  const numericId = Number.parseInt(deviceId, 10);
93415
95815
  const log = this.ctx.logger.withTags({ deviceId: numericId });
95816
+ const mapperKind = this.findEntry(numericId)?.mapperKind ?? "camera";
93416
95817
  await this.detachMapper(deviceId);
93417
95818
  const next = this.config.exposed.filter((e) => e.deviceId !== deviceId);
93418
95819
  if (next.length !== this.config.exposed.length) await this.updateGlobalSettings({ exposed: next });
93419
- if (options.clearPairing !== false) clearPairingFiles(import_dist.uuid.generate(`camstack:camera:${numericId}`), this.ctx.logger);
95820
+ if (options.clearPairing !== false) clearPairingFiles(accessoryUuidFor(mapperKind, numericId), this.ctx.logger);
93420
95821
  await this.forgetFingerprint(numericId);
93421
95822
  log.info("export-hap: unexposed device");
93422
95823
  }
@@ -93466,10 +95867,10 @@ var ExportHapAddon = class extends BaseAddon {
93466
95867
  /** Pending rebuild timers per deviceId — used to debounce. Cleared
93467
95868
  * on detach so stale timers can't republish a removed accessory. */
93468
95869
  pendingRebuildTimers = /* @__PURE__ */ new Map();
93469
- /** Deterministic HAP accessory UUID for a device (matches `buildCameraAccessory`
93470
- * and `unexposeDevice`). */
95870
+ /** Deterministic HAP accessory UUID for a device the same one the
95871
+ * orchestrator built it with (`mappers/kind.ts`). */
93471
95872
  hapAccessoryUuid(deviceId) {
93472
- return import_dist.uuid.generate(`camstack:camera:${deviceId}`);
95873
+ return accessoryUuidFor(this.findEntry(deviceId)?.mapperKind ?? "camera", deviceId);
93473
95874
  }
93474
95875
  /**
93475
95876
  * Export fingerprint of a device from its PERSISTED features + type
@@ -93628,31 +96029,79 @@ var ExportHapAddon = class extends BaseAddon {
93628
96029
  log.warn("export-hap: reconcile rebuild failed", { meta: { error: errMsg(err) } });
93629
96030
  }
93630
96031
  }
93631
- async resolveDisplayName(deviceId) {
93632
- const numeric = Number.parseInt(deviceId, 10);
93633
- if (!Number.isFinite(numeric)) return `Device ${deviceId}`;
96032
+ /**
96033
+ * The device's name and type, or `null` when the registry cannot answer.
96034
+ *
96035
+ * `null` is NOT "no such device" — it also covers a transient API failure,
96036
+ * and every caller treats it as "keep doing what was already being done"
96037
+ * rather than re-shaping an exposed accessory on incomplete information.
96038
+ */
96039
+ async fetchDeviceSummary(deviceId) {
96040
+ if (!Number.isFinite(deviceId)) return null;
96041
+ try {
96042
+ const device = await this.ctx.api.deviceManager?.getDevice.query({ deviceId });
96043
+ if (!device) return null;
96044
+ return {
96045
+ name: device.name,
96046
+ type: device.type
96047
+ };
96048
+ } catch (err) {
96049
+ this.ctx.logger.withTags({ deviceId }).debug("export-hap: deviceManager.getDevice failed", { meta: { error: errMsg(err) } });
96050
+ return null;
96051
+ }
96052
+ }
96053
+ /**
96054
+ * Which accessory shape this device would get, or `null` for "no Export tab".
96055
+ *
96056
+ * Two questions, in this order, because they fail differently:
96057
+ * 1. the TYPE — a type with no orchestrator is refused outright;
96058
+ * 2. for a non-camera, the CAPABILITIES — a device carrying nothing the
96059
+ * table maps would publish an accessory that pairs and does nothing, and
96060
+ * the operator cannot tell that from a broken integration.
96061
+ *
96062
+ * A camera skips (2): the camera orchestrator has always published on type
96063
+ * alone, and adding a cap gate here would be a new way for an existing camera
96064
+ * to lose its Export tab. A registry that cannot answer at all also resolves
96065
+ * to `camera`, which is what every persisted entry predating this method
96066
+ * carries.
96067
+ */
96068
+ async resolveExportKind(deviceId) {
96069
+ const summary = await this.fetchDeviceSummary(deviceId);
96070
+ const kind = pickMapperKind(summary?.type);
96071
+ if (kind === null) return null;
96072
+ if (kind === "camera") return "camera";
93634
96073
  try {
93635
- const device = await this.ctx.api.deviceManager?.getDevice.query({ deviceId: numeric });
93636
- if (device?.name) return device.name;
96074
+ const proxy = await this.ctx.fetchDevice(deviceId);
96075
+ const capNames = new Set(proxy.binding?.entries.map((entry) => entry.capName) ?? []);
96076
+ if (rowsForCaps(capNames).length > 0) return "generic";
96077
+ this.ctx.logger.withTags({ deviceId }).debug("export-hap: no Export tab — no mapped caps", { meta: {
96078
+ type: summary?.type,
96079
+ caps: [...capNames].toSorted().join(",")
96080
+ } });
96081
+ return null;
93637
96082
  } catch (err) {
93638
- this.ctx.logger.withTags({ deviceId: numeric }).debug("export-hap: deviceManager.getDevice failed", { meta: { error: errMsg(err) } });
96083
+ this.ctx.logger.withTags({ deviceId }).debug("export-hap: no Export tab — capability read failed", { meta: {
96084
+ type: summary?.type,
96085
+ error: errMsg(err)
96086
+ } });
96087
+ return null;
93639
96088
  }
93640
- return `Device ${deviceId}`;
93641
96089
  }
93642
96090
  globalSettingsSchema() {
93643
96091
  return this.schema({ sections: [{
93644
96092
  id: "export-hap",
93645
96093
  title: "HomeKit Export",
93646
- description: "Publishes a HomeKit bridge. After the addon boots, scan the pairing QR (or enter the setup code) from the Exported devices panel in the iOS Home app. After pairing, mark individual devices as \"Expose to HomeKit\" on each device's settings page.",
96094
+ description: "Publishes each exposed device as its own HomeKit accessory, sharing one setup code. Mark a device \"Expose to HomeKit\" on its settings page, then add it in the iOS Home app (+ Add Accessory) and enter the setup code shown there.",
93647
96095
  columns: 1,
93648
96096
  fields: [
93649
96097
  this.field({
93650
96098
  type: "text",
93651
96099
  key: "bridgeName",
93652
- label: "Bridge name",
93653
- description: "Name shown in iOS Home and in the Bonjour advertisement.",
96100
+ label: "Installation name",
96101
+ description: "Legacy. Every exposed device is published STANDALONE, under its own camstack device name — HomeKit cameras cannot be bridged, so nothing is bridged. This string only seeds the identity generated on first boot and is not shown in iOS Home.",
93654
96102
  default: DEFAULT_CONFIG.bridgeName,
93655
- requiresRestart: true
96103
+ requiresRestart: true,
96104
+ placement: { tab: "advanced" }
93656
96105
  }),
93657
96106
  this.field({
93658
96107
  type: "number",
@@ -93688,21 +96137,13 @@ var ExportHapAddon = class extends BaseAddon {
93688
96137
  description: "Duration of a single pan/tilt momentary command issued by the PTZ direction switches in Apple Home. Lower = finer steps; higher = bigger sweeps per tap. Defaults to 400ms.",
93689
96138
  default: DEFAULT_CONFIG.ptzPulseMs,
93690
96139
  placement: { tab: "advanced" }
93691
- }),
93692
- this.field({
93693
- type: "boolean",
93694
- key: "hksvPreview",
93695
- label: "HKSV Developer Preview (experimental)",
93696
- 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.",
93697
- default: DEFAULT_CONFIG.hksvPreview,
93698
- requiresRestart: true,
93699
- placement: { tab: "advanced" }
93700
96140
  })
93701
96141
  ]
93702
96142
  }] });
93703
96143
  }
93704
96144
  async buildDeviceSettingsContribution(deviceId) {
93705
- if (!await this.isCameraDevice(deviceId)) return null;
96145
+ const kind = await this.resolveExportKind(deviceId);
96146
+ if (kind === null) return null;
93706
96147
  const entry = this.findEntry(deviceId);
93707
96148
  const settings = entry?.settings ?? DEFAULT_DEVICE_SETTINGS;
93708
96149
  const enabled = entry !== null;
@@ -93719,6 +96160,32 @@ var ExportHapAddon = class extends BaseAddon {
93719
96160
  } catch (err) {
93720
96161
  this.ctx.logger.withTags({ deviceId }).debug("export-hap: setupURI failed for per-device contribution", { meta: { error: errMsg(err) } });
93721
96162
  }
96163
+ const cameraFields = kind !== "camera" ? [] : [{
96164
+ type: "select",
96165
+ key: streamPreferenceKey,
96166
+ label: "Source stream (HomeKit)",
96167
+ description: "Which camstack profile slot HomeKit pulls. Auto = the broker picks the slot closest to 1080p at session start.",
96168
+ options: HAP_STREAM_PREFERENCE_OPTIONS,
96169
+ required: true,
96170
+ value: settings.streamPreference,
96171
+ showWhen: {
96172
+ field: enabledKey,
96173
+ equals: true
96174
+ },
96175
+ immediate: true
96176
+ }, {
96177
+ type: "boolean",
96178
+ key: hksvKey,
96179
+ label: "HomeKit recording (Secure Video)",
96180
+ description: "Offer “Stream and Allow Recording” in iOS Home. Requires iCloud+ and a home hub. Keeps a continuous 8s prebuffer for this camera (~0.7% of one CPU core, H.264 sources only).",
96181
+ style: "switch",
96182
+ value: resolveHksvRecording(settings),
96183
+ showWhen: {
96184
+ field: enabledKey,
96185
+ equals: true
96186
+ },
96187
+ immediate: true
96188
+ }];
93722
96189
  return {
93723
96190
  tabs: [{
93724
96191
  id: "export",
@@ -93729,7 +96196,7 @@ var ExportHapAddon = class extends BaseAddon {
93729
96196
  sections: [{
93730
96197
  id: "export-hap",
93731
96198
  title: "HomeKit Export",
93732
- description: "Mirror this camera into the HomeKit bridge so iOS Home can pair with it.",
96199
+ description: kind === "camera" ? "Publish this camera as its own HomeKit accessory so iOS Home can pair with it." : "Publish this device as its own HomeKit accessory. Its capabilities decide which controls iOS Home shows.",
93733
96200
  tab: "export",
93734
96201
  columns: 1,
93735
96202
  order: 10,
@@ -93745,7 +96212,7 @@ var ExportHapAddon = class extends BaseAddon {
93745
96212
  type: "qr-code",
93746
96213
  key: "__hap-pair-qr",
93747
96214
  label: paired ? "Pairing QR (already paired)" : "Pairing QR",
93748
- caption: paired ? `Already paired with at least one device. Scan from another iPhone / iPad to add it there too. Setup code: ${this.config.identity.pincode}.` : `Scan with the iOS Camera app to pair this camera in HomeKit. Setup code: ${this.config.identity.pincode}.`,
96215
+ caption: paired ? `Already paired with at least one device. Scan from another iPhone / iPad to add it there too. Setup code: ${this.config.identity.pincode}.` : `Scan with the iOS Camera app to pair this accessory in HomeKit. Setup code: ${this.config.identity.pincode}.`,
93749
96216
  value: qrValue,
93750
96217
  size: 192,
93751
96218
  alt: `HomeKit pairing QR for ${name}`,
@@ -93758,38 +96225,12 @@ var ExportHapAddon = class extends BaseAddon {
93758
96225
  type: "boolean",
93759
96226
  key: enabledKey,
93760
96227
  label: "Expose to HomeKit",
93761
- description: "Toggle to publish this camera onto the HAP bridge.",
96228
+ description: "Toggle to publish this device as a HomeKit accessory.",
93762
96229
  style: "switch",
93763
96230
  value: enabled,
93764
96231
  immediate: true
93765
96232
  },
93766
- {
93767
- type: "select",
93768
- key: streamPreferenceKey,
93769
- label: "Source stream (HomeKit)",
93770
- description: "Which camstack profile slot HomeKit pulls. Auto = the broker picks the slot closest to 1080p at session start.",
93771
- options: HAP_STREAM_PREFERENCE_OPTIONS,
93772
- required: true,
93773
- value: settings.streamPreference,
93774
- showWhen: {
93775
- field: enabledKey,
93776
- equals: true
93777
- },
93778
- immediate: true
93779
- },
93780
- {
93781
- type: "boolean",
93782
- key: hksvKey,
93783
- label: "HomeKit recording (Secure Video)",
93784
- 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).",
93785
- style: "switch",
93786
- value: resolveHksvRecording(settings),
93787
- showWhen: {
93788
- field: enabledKey,
93789
- equals: true
93790
- },
93791
- immediate: true
93792
- }
96233
+ ...cameraFields
93793
96234
  ]
93794
96235
  }]
93795
96236
  };
@@ -93855,23 +96296,6 @@ var ExportHapAddon = class extends BaseAddon {
93855
96296
  }
93856
96297
  return { success: true };
93857
96298
  }
93858
- /**
93859
- * Camera-only gate — HomeKit export only knows how to mirror cameras
93860
- * today. Mirrors the snapshot addon's source-side filter so the
93861
- * device-details page doesn't render an "Export" tab on lights /
93862
- * switches / sensors.
93863
- */
93864
- async isCameraDevice(deviceId) {
93865
- const api = this.ctx.api;
93866
- if (!api.deviceManager) return true;
93867
- try {
93868
- const dev = await api.deviceManager.getDevice.query({ deviceId });
93869
- if (!dev) return true;
93870
- return dev.type === DeviceType.Camera;
93871
- } catch {
93872
- return true;
93873
- }
93874
- }
93875
96299
  findEntry(deviceId) {
93876
96300
  const id = String(deviceId);
93877
96301
  return this.config.exposed.find((e) => e.deviceId === id) ?? null;