@mega-yfue/eufy-sdk 0.6.0-beta.45 → 0.6.0-beta.47

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.
package/dist/index.js CHANGED
@@ -3471,11 +3471,11 @@ function propertiesOf(members, ctx) {
3471
3471
  }
3472
3472
  var BOOL_VALUES = /* @__PURE__ */ new Set([true, false, 0, 1, "0", "1", "true", "false"]);
3473
3473
  function memberWrite(name, m, value, ctx) {
3474
- if (!inDomain(m, value))
3475
- throw new Error(rejection(name, m, value));
3474
+ if (!inDomain(m, value, ctx))
3475
+ throw new Error(rejection(name, m, value, ctx));
3476
3476
  const cmd = m.write?.(value, ctx);
3477
3477
  if (!cmd)
3478
- throw new Error(rejection(name, m, value));
3478
+ throw new Error(rejection(name, m, value, ctx));
3479
3479
  const reflected = m.observation?.reflects(value, ctx);
3480
3480
  return m.observation && reflected ? observeCommand(cmd, {
3481
3481
  event: m.observation.event,
@@ -3487,10 +3487,11 @@ function memberWrite(name, m, value, ctx) {
3487
3487
  timeoutMs: m.observation.timeoutMs
3488
3488
  }) : cmd;
3489
3489
  }
3490
- function writeDomain(m) {
3491
- return m.args?.[0]?.values ?? m.decodedValues ?? (m.enumValues && Object.keys(m.enumValues).map(Number));
3490
+ function writeDomain(m, ctx) {
3491
+ const values = resolvedEnum(m, ctx).values;
3492
+ return m.args?.[0]?.values ?? m.decodedValues ?? (values && Object.keys(values).map(Number));
3492
3493
  }
3493
- function inDomain(m, value) {
3494
+ function inDomain(m, value, ctx) {
3494
3495
  if (m.type === "bool")
3495
3496
  return BOOL_VALUES.has(typeof value === "string" ? value.toLowerCase() : value);
3496
3497
  if (typeof value === "number") {
@@ -3499,11 +3500,11 @@ function inDomain(m, value) {
3499
3500
  if (m.max !== void 0 && value > m.max)
3500
3501
  return false;
3501
3502
  }
3502
- const domain = writeDomain(m)?.filter((v) => typeof v === typeof value);
3503
+ const domain = writeDomain(m, ctx)?.filter((v) => typeof v === typeof value);
3503
3504
  return !domain?.length || domain.includes(value);
3504
3505
  }
3505
- function rejection(name, m, value) {
3506
- const options = writeDomain(m);
3506
+ function rejection(name, m, value, ctx) {
3507
+ const options = writeDomain(m, ctx);
3507
3508
  const domain = options?.length ? `one of ${options.join("/")}` : m.min !== void 0 && m.max !== void 0 ? `in ${m.min}..${m.max}` : m.min !== void 0 ? `>= ${m.min}` : void 0;
3508
3509
  return `${name}: ${JSON.stringify(value)} is not a valid value` + (domain ? ` (must be ${domain})` : "");
3509
3510
  }
@@ -4817,8 +4818,10 @@ function cellGated(members) {
4817
4818
  return Object.fromEntries(gated);
4818
4819
  }
4819
4820
  var notSensor = (ctx) => ctx.codec !== "sensor";
4820
- function notOnSensor(members) {
4821
+ function notOnSensor(members, sensorOnly) {
4821
4822
  const gated = Object.entries(members).map(([name, m]) => {
4823
+ if (sensorOnly.includes(name))
4824
+ return [name, m];
4822
4825
  const own = m.available;
4823
4826
  const available = own ? (ctx) => notSensor(ctx) && own(ctx) : notSensor;
4824
4827
  return [name, { ...m, available }];
@@ -5054,12 +5057,26 @@ var CAMERA_BATTERY_MEMBERS = cellGated({
5054
5057
  provenance: "apk",
5055
5058
  unexposed: true,
5056
5059
  description: "Raw GET_CAMERA_INFO value (1103), meaning unevidenced \u2014 read live as the constant 5 on a T8170 at 92% and a T8171 at 27%, so it is NOT a low-battery flag."
5060
+ },
5061
+ /**
5062
+ * A sensor's battery is low. The app reads the entry sensor's 1552 and the motion sensor's 1601 into the
5063
+ * same field (`sensor_is_low_power`, `asBoolean`), so one member reads whichever the sensor reports.
5064
+ * Present on the `sensor` codec only: a camera or a lock reports a level instead.
5065
+ */
5066
+ batteryLow: {
5067
+ param: 1552,
5068
+ readAliases: [{ paramType: 1601 }],
5069
+ type: "bool",
5070
+ kind: "boolean",
5071
+ provenance: "verified",
5072
+ available: (ctx) => !notSensor(ctx),
5073
+ description: "Sensor battery low (1552 entry sensor, 1601 motion sensor; sensor_is_low_power)."
5057
5074
  }
5058
5075
  });
5059
- var BATTERY_MEMBERS = notOnSensor(CAMERA_BATTERY_MEMBERS);
5076
+ var BATTERY_MEMBERS = notOnSensor(CAMERA_BATTERY_MEMBERS, ["batteryLow"]);
5060
5077
  var BATTERY = {
5061
5078
  capability: "battery",
5062
- description: "Battery level, charging, health, temperature, and solar input.",
5079
+ description: "Battery level, charging, health, temperature and solar input; a sensor's low-battery flag.",
5063
5080
  members: BATTERY_MEMBERS,
5064
5081
  properties: propertiesOf(BATTERY_MEMBERS),
5065
5082
  /** A reported battery-level param (1101) is the verified proof the device is battery-powered. */
@@ -5104,186 +5121,6 @@ function resolveWorkingModeValue(model, name) {
5104
5121
  return hit ? Number(hit[0]) : void 0;
5105
5122
  }
5106
5123
 
5107
- // dist/model/capabilities/contact.js
5108
- var CONTACT_CMD = {
5109
- /** Alarm sound/tone selection (app `APP_CMD_ALARM_SOUND_TYPE`). See {@link EntryAlarmTone}. */
5110
- ALARM_SOUND_TYPE: 1507,
5111
- /** Alarm volume (app `APP_CMD_ALARM_VOLUME_VALUE`). A device level 1-26, not a percentage. */
5112
- ALARM_VOLUME: 1508
5113
- };
5114
- var EntryAlarmTone = {
5115
- /** Silent — no alarm tone. */
5116
- None: 0,
5117
- Water: 1,
5118
- Classic: 2,
5119
- Light: 3,
5120
- Ding: 4
5121
- };
5122
- var ALARM_VOLUME_MIN = 1;
5123
- var ALARM_VOLUME_MAX = 26;
5124
- var SENSOR_STATUS_CMD = 1829;
5125
- var CONTACT_PARAM = 1550;
5126
- function statusParam(signal, paramType) {
5127
- if (signal.source !== "p2p-frame" || signal.json?.cmd !== SENSOR_STATUS_CMD)
5128
- return void 0;
5129
- const params = signal.json.payload?.params;
5130
- if (!Array.isArray(params))
5131
- return void 0;
5132
- const hit = params.find((p) => p?.param_type === paramType);
5133
- return hit?.param_value == null ? void 0 : String(hit.param_value);
5134
- }
5135
- function contactOpen(raw) {
5136
- return raw == null || raw === "" ? {} : { open: String(raw) === "1" };
5137
- }
5138
- var CONTACT_MEMBERS = {
5139
- /**
5140
- * The headline read: `true` when the door or window is open. Polarity is the V6 app's own
5141
- * `"1".equals(e)` — see `contactOpen`, which normalises every inbound source through one
5142
- * predicate so the poll, the push and the station notify cannot disagree about what open means.
5143
- * Reporting 1550 is also this capability's detection evidence, so a device with this getter is
5144
- * confirmed to be a contact sensor.
5145
- */
5146
- open: {
5147
- param: CONTACT_PARAM,
5148
- property: "contact",
5149
- type: "bool",
5150
- kind: "boolean",
5151
- provenance: "verified",
5152
- description: "Contact state, true=open (verified: param 1550 entry-sensor contact)."
5153
- },
5154
- /**
5155
- * Unix seconds at which the sensor last checked in — the liveness read beside the contact state, and
5156
- * the way to tell a genuinely closed door from a sensor that stopped reporting. A `timestamp` kind
5157
- * takes no `unit`: it is an instant, not a duration.
5158
- */
5159
- lastSeen: {
5160
- param: 1551,
5161
- type: "number",
5162
- kind: "timestamp",
5163
- provenance: "verified",
5164
- description: "Last-seen unix timestamp, seconds (verified: param 1551)."
5165
- },
5166
- /**
5167
- * Link quality to whichever radio the sensor is paired over, in dBm as the device measures it. The
5168
- * same param 1141 the other sub-1G sensors report on, so the number is comparable across them —
5169
- * unlike a bars mapping, which this SDK never applies.
5170
- */
5171
- rssi: {
5172
- param: 1141,
5173
- type: "number",
5174
- unit: "dBm",
5175
- kind: "dbm",
5176
- provenance: "verified",
5177
- description: "Sub-1G/Wi-Fi signal strength (verified: param 1141 = RSSI)."
5178
- },
5179
- /**
5180
- * Rejected, not clamped, outside the tone enum: a fire-and-forget write of a bogus tone would look like
5181
- * it worked. The wire is a direct-binary scalar `[ch][value][acct]` at signCode 8 — the app's own
5182
- * captured frame.
5183
- */
5184
- alarmSoundType: {
5185
- param: CONTACT_CMD.ALARM_SOUND_TYPE,
5186
- type: "enum",
5187
- kind: "enum",
5188
- enumValues: enumLabels(EntryAlarmTone),
5189
- provenance: "verified",
5190
- requires: [CONTACT_CMD.ALARM_SOUND_TYPE],
5191
- args: [{ name: "tone", kind: "enum", description: "Silent (0) or one of the four chimes." }],
5192
- description: "Entry-sensor alarm tone (1507): 0=None,1=Water,2=Classic,3=Light,4=Ding. Write verified live on a T90E0 (direct-binary [ch][value][acct]). See CONTACT_CMD.ALARM_SOUND_TYPE.",
5193
- write: (v, ctx) => {
5194
- const tone = coerceEnumValue(EntryAlarmTone, v);
5195
- return tone === void 0 ? void 0 : setScalar(CONTACT_CMD.ALARM_SOUND_TYPE, tone, ctx, "auto");
5196
- }
5197
- },
5198
- /**
5199
- * `1350` SET_PAYLOAD, `mChannel` = the device channel, `mValue3` 0, payload carrying the channel and a
5200
- * transaction stamp — the byte-shape of the app's own captured frame. Out of range is refused rather
5201
- * than clamped: the app's slider has no values outside it, so one is a caller error, not a nudge.
5202
- *
5203
- * Level-2 only, though the `mValue3` 0 would allow `"auto"`: an entry sensor's session IS its
5204
- * HomeBase's, which always holds a key — the second of `setPayload`'s two conditions, not an oversight
5205
- * of the first.
5206
- */
5207
- alarmVolume: {
5208
- param: CONTACT_CMD.ALARM_VOLUME,
5209
- type: "number",
5210
- kind: "scalar",
5211
- provenance: "verified",
5212
- requires: [CONTACT_CMD.ALARM_VOLUME],
5213
- min: ALARM_VOLUME_MIN,
5214
- max: ALARM_VOLUME_MAX,
5215
- args: [
5216
- {
5217
- name: "level",
5218
- kind: "scalar",
5219
- min: ALARM_VOLUME_MIN,
5220
- max: ALARM_VOLUME_MAX,
5221
- description: "A device level, not a percentage."
5222
- }
5223
- ],
5224
- description: "Entry-sensor alarm volume (1508), a device level 1-26 (NOT a percentage). Write verified live on a T90E0 (1350 SET_PAYLOAD {channel,volume,transaction}). See CONTACT_CMD.ALARM_VOLUME.",
5225
- write: (v, ctx) => {
5226
- const level = Number(v);
5227
- return Number.isInteger(level) && level >= ALARM_VOLUME_MIN && level <= ALARM_VOLUME_MAX ? setPayload(CONTACT_CMD.ALARM_VOLUME, { channel: ctx.channel, volume: level, transaction: String(Date.now()) }, ctx, 0) : void 0;
5228
- }
5229
- }
5230
- };
5231
- var CONTACT = {
5232
- capability: "contact",
5233
- description: "Entry (door/window) sensor: contact state, last-seen, and the built-in alarm tone/volume.",
5234
- members: CONTACT_MEMBERS,
5235
- properties: propertiesOf(CONTACT_MEMBERS),
5236
- /** A reported entry-sensor contact param (1550) is the verified proof of a contact sensor. */
5237
- detection: { evidenceParams: [1550] },
5238
- /**
5239
- * Inbound `contactState`: the contact param (1550) changing on a cloud poll (open/closed), AND the FCM
5240
- * door-sensor push (`CusPushEvent.DOOR_SENSOR` = 3) — verified live on a T8900 (both fire).
5241
- *
5242
- * Both sources carry the state under a different key, so each normalises to `open`: the poll's `to` is
5243
- * the 1550 param value, and the push's is the wire's single-letter `e`. Polarity for the push is the
5244
- * V6 app's own `CusPushMode.isSensorOpen()`, which is exactly `"1".equals(e)` — anything else,
5245
- * including absent, is closed — read under its `a == PUSH_DOOR_SENSOR_EVT` branch to choose the
5246
- * "sensor is opened" vs "sensor is closed" notification string.
5247
- */
5248
- events: [
5249
- {
5250
- source: "poll",
5251
- match: 1550,
5252
- emit: "contactState",
5253
- derive: (s) => contactOpen(s.source === "poll" ? s.to : void 0)
5254
- },
5255
- {
5256
- source: "push",
5257
- match: CusPushEvent.DOOR_SENSOR,
5258
- emit: "contactState",
5259
- derive: (s) => contactOpen(s.source === "push" ? s.payload?.["e"] : void 0)
5260
- }
5261
- ],
5262
- /**
5263
- * The contact is a settled state reported by three transports, so one door movement would otherwise
5264
- * be announced up to three times — the station notify, then the same value as an FCM push ~2 s
5265
- * later, then again on the next cloud poll.
5266
- */
5267
- stateEvents: [{ event: "contactState", field: "open" }],
5268
- /**
5269
- * The station's own notify — the fastest contact signal there is, and the only one that needs no
5270
- * cloud at all (see {@link SENSOR_STATUS_CMD} for the measured lead over the push).
5271
- *
5272
- * A declarative `events` row can't express this: those match a push event id or a poll param id,
5273
- * while the state here is nested inside a frame's `params` array under an inner command id. Hence
5274
- * the decode hook, which is the documented escape hatch for exactly that.
5275
- *
5276
- * Emits nothing when the notify carries no contact param, so a status frame reporting only battery
5277
- * or signal strength doesn't fabricate a contact event.
5278
- */
5279
- decodeEvent(signal) {
5280
- const raw = statusParam(signal, CONTACT_PARAM);
5281
- if (raw === void 0)
5282
- return null;
5283
- return { event: "contactState", payload: contactOpen(raw) };
5284
- }
5285
- };
5286
-
5287
5124
  // dist/model/device-family.js
5288
5125
  var INDOOR_CAMERA_TYPES = /* @__PURE__ */ new Set([
5289
5126
  DeviceType.INDOOR_CAMERA,
@@ -5927,7 +5764,7 @@ var CAMERA_CMD = {
5927
5764
  * (1013). (Some models omit full colour; enum in {@link NightVision}.)
5928
5765
  */
5929
5766
  NIGHT_VISION_TYPE: 1277,
5930
- /** T8442 infrared night vision on/off: direct L1 int-string, hardware verified (0/1). */
5767
+ /** Night vision on a device reporting 2002: direct level-1 [channel][value]. */
5931
5768
  IC_NIGHT_VISION_TYPE: 1013,
5932
5769
  /**
5933
5770
  * Push-notification STYLE — how much a detection push carries: text alone, a thumbnail, or text
@@ -6038,10 +5875,12 @@ var SoundDetectionType = {
6038
5875
  var NightVision = {
6039
5876
  /** Off — never use infrared. */
6040
5877
  Off: 0,
6041
- /** Infrared / "B&W Auto" — black-and-white night vision. */
5878
+ /** Infrared / "B&W Auto" — black-and-white night vision, switched on when it gets dark. */
6042
5879
  Infrared: 1,
6043
5880
  /** Full colour night vision (models with a spotlight / starlight sensor). */
6044
- FullColor: 2
5881
+ FullColor: 2,
5882
+ /** Infrared held on — black-and-white night vision, not switched by the light level. */
5883
+ InfraredOn: 3
6045
5884
  };
6046
5885
  var RecordingQuality = {
6047
5886
  HD720: "HD (720P)",
@@ -6119,8 +5958,8 @@ function powerValue(on, ctx) {
6119
5958
  function isT8410HomeBase3(ctx) {
6120
5959
  return ctx.model === "T8410" && ctx.stationSerial?.startsWith("T8030") === true;
6121
5960
  }
6122
- function isT8442(ctx) {
6123
- return ctx.model === "T8442";
5961
+ function readsNightVisionOn2002(ctx) {
5962
+ return ctx.paramIds?.has(2002) === true && !ctx.paramIds.has(CAMERA_CMD.NIGHT_VISION_TYPE);
6124
5963
  }
6125
5964
  function usesHomeBasePowerPayload(ctx) {
6126
5965
  if (!isT8410HomeBase3(ctx) || !ctx.firmwareVersion || !/^\d+(?:\.\d+){2,3}$/.test(ctx.firmwareVersion))
@@ -6335,36 +6174,27 @@ var CAMERA_MEMBERS = {
6335
6174
  return style == null ? void 0 : setJson(CAMERA_CMD.PUSH_NOTIFY_TYPE, { value: style, transaction: String(Date.now()) }, ctx);
6336
6175
  }
6337
6176
  },
6338
- /**
6339
- * Three modes, and `FullColor` is the conditional one: a model without a spotlight or starlight
6340
- * sensor omits it, and the wire accepts the value regardless — the device's own reported set is the
6341
- * only statement of which of the three it has. `coerceEnumValue` rejects a value outside
6342
- * {@link NightVision} rather than coercing it. The payload key on the wire is `night_sion`.
6343
- *
6344
- * 1277 carries more than these three on some models: the V6 app's per-model mappings put modes on
6345
- * 3, 5, 6 and 8 as well, and which model takes which mapping is not in the app bundle. A reported value
6346
- * outside the published options reads as `undefined` rather than as an unnamed number.
6347
- */
6177
+ /** Modes come from the thing description (`thing`); unlisted values read as `undefined`. */
6348
6178
  nightVision: {
6349
6179
  param: CAMERA_CMD.NIGHT_VISION_TYPE,
6350
6180
  type: "enum",
6351
6181
  kind: "enum",
6352
- enumValues: { 0: "Off", 1: "Infrared", 2: "Full Color" },
6353
- enumValuesFor: (ctx) => isT8442(ctx) ? { 0: "Off", 1: "Infrared" } : void 0,
6182
+ enumValues: { 0: "Off", 1: "Infrared", 2: "Full Color", 3: "Infrared On" },
6183
+ thing: ["nightvision_type_new", "nightvision_type"],
6354
6184
  provenance: "verified",
6355
6185
  available: isCameraCodec,
6356
- readAvailable: (ctx) => !isT8442(ctx),
6357
- readAliases: [{ paramType: 2002, available: isT8442 }],
6358
- description: "Night-vision mode: 0 = off, 1 = infrared (B&W), 2 = full colour where supported. T8425 uses NIGHT_VISION_TYPE 1277 (1350 SET_PAYLOAD, night_sion); T8442 reports 2002 and writes direct IC_NIGHT_VISION_TYPE 1013 (int-string, 0/1). Both wires verified live.",
6186
+ readAvailable: (ctx) => !readsNightVisionOn2002(ctx),
6187
+ readAliases: [{ paramType: 2002, available: readsNightVisionOn2002 }],
6188
+ coerce: (raw) => raw === "true" ? 1 : raw === "false" ? 0 : Number(raw),
6189
+ description: "Night-vision mode: 0 = off, 1 = infrared by light level, 2 = full colour, 3 = infrared held on. Written on 1277 (1350 SET_PAYLOAD, night_sion), or on 1013 as a bare [channel][value] when the device reports 2002.",
6359
6190
  write: (v, ctx) => {
6360
6191
  const nv = coerceEnumValue(NightVision, v);
6361
6192
  if (nv == null)
6362
6193
  return void 0;
6363
- if (isT8442(ctx)) {
6364
- if (nv === NightVision.FullColor)
6365
- throw new Error("nightVision: Full Color is not supported on T8442 (only Off/Infrared)");
6366
- return setScalar(CAMERA_CMD.IC_NIGHT_VISION_TYPE, nv, ctx, "int-string");
6367
- }
6194
+ if (readsNightVisionOn2002(ctx))
6195
+ return setScalar(CAMERA_CMD.IC_NIGHT_VISION_TYPE, nv, ctx, "int-pair");
6196
+ if (nv === NightVision.InfraredOn)
6197
+ throw new Error("nightVision: Infrared On has no verified write on this model");
6368
6198
  return setPayload(CAMERA_CMD.NIGHT_VISION_TYPE, { channel: ctx.channel, night_sion: nv }, ctx, 0, 0, "auto");
6369
6199
  }
6370
6200
  },
@@ -7598,41 +7428,330 @@ var PTZ = {
7598
7428
  }
7599
7429
  };
7600
7430
 
7431
+ // dist/model/capabilities/contact.js
7432
+ var CONTACT_CMD = {
7433
+ /** Contact state (`APP_CMD_ENTRY_SENSOR_STATUS`, identifier `sensor_door_status`). */
7434
+ STATUS: 1550,
7435
+ /** Removed from its mount (`APP_CMD_DEV_DISASSEMBLE`, identifier `sensor_disassemble_status`). */
7436
+ TAMPER: 61015,
7437
+ /** Bypassed by the alarm (`APP_CMD_DEV_ENTRY_BYPASS`, identifier `sensor_door_bypass_status`). */
7438
+ BYPASS: 61016,
7439
+ /** Left-open alert (`APP_CMD_SENSOR_OPEN_STATUS_ALERT`, identifier `sensor_door_open_status_alert_config`). */
7440
+ OPEN_ALERT: 1290,
7441
+ /** Daily state check (`APP_CMD_SENSOR_DOOR_DAILY_CHECK`, identifier `sensor_door_daily_check_status_config`). */
7442
+ DAILY_CHECK: 1291,
7443
+ /** Alarm tone (`APP_CMD_ALARM_SOUND_TYPE`, identifier `device_alarm_sound_type`). */
7444
+ ALARM_SOUND_TYPE: 1507,
7445
+ /** Alarm volume (`APP_CMD_ALARM_VOLUME_VALUE`, identifier `alarm_volume_value`). */
7446
+ ALARM_VOLUME: 1508
7447
+ };
7448
+ var SENSOR_STATUS_CMD = 1829;
7449
+ var OPEN_ALERT_MINUTES = [5, 10, 15, 30, 60];
7450
+ var ALARM_TONES = { None: 0, Water: 1, Classics: 2, Light: 3, Ding: 4 };
7451
+ var ALARM_VOLUME_MIN = 1;
7452
+ var ALARM_VOLUME_MAX = 26;
7453
+ var described = (identifier) => (ctx) => !ctx.things || identifier in ctx.things.properties;
7454
+ function statusParam(signal, paramType) {
7455
+ if (signal.source !== "p2p-frame" || signal.json?.cmd !== SENSOR_STATUS_CMD)
7456
+ return void 0;
7457
+ const params = signal.json.payload?.params;
7458
+ if (!Array.isArray(params))
7459
+ return void 0;
7460
+ const hit = params.find((p) => p?.param_type === paramType);
7461
+ return hit?.param_value == null ? void 0 : String(hit.param_value);
7462
+ }
7463
+ function contactOpen(raw) {
7464
+ return raw == null || raw === "" ? {} : { open: String(raw) === "1" };
7465
+ }
7466
+ function config(raw) {
7467
+ if (typeof raw !== "string" || !raw)
7468
+ return void 0;
7469
+ try {
7470
+ const parsed = JSON.parse(Buffer.from(raw, "base64").toString("utf8"));
7471
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
7472
+ } catch {
7473
+ return void 0;
7474
+ }
7475
+ }
7476
+ var hhmm = (minutes) => `${String(Math.floor(minutes / 60)).padStart(2, "0")}:${String(minutes % 60).padStart(2, "0")}`;
7477
+ function minutesOf(time) {
7478
+ const m = /^(\d{1,2}):(\d{2})$/.exec(time);
7479
+ if (!m)
7480
+ return void 0;
7481
+ const [h, min] = [Number(m[1]), Number(m[2])];
7482
+ return h < 24 && min < 60 ? h * 60 + min : void 0;
7483
+ }
7484
+ var CONTACT_MEMBERS = {
7485
+ /**
7486
+ * The headline read: `true` when the door or window is open. Every inbound source normalises through
7487
+ * `contactOpen`, so the poll, the push and the station notify cannot disagree about what open means.
7488
+ * Reporting 1550 is also this capability's detection evidence.
7489
+ */
7490
+ open: {
7491
+ param: CONTACT_CMD.STATUS,
7492
+ property: "contact",
7493
+ type: "bool",
7494
+ kind: "boolean",
7495
+ provenance: "verified",
7496
+ description: "Contact state, true=open (1550, sensor_door_status)."
7497
+ },
7498
+ /** The sensor was removed from its mount. Reported by a T8900; the meaning of a set value is the product bundle's. */
7499
+ tampered: {
7500
+ param: CONTACT_CMD.TAMPER,
7501
+ type: "bool",
7502
+ kind: "boolean",
7503
+ provenance: "apk",
7504
+ description: "Removed from its mount (61015, sensor_disassemble_status)."
7505
+ },
7506
+ /** The alarm currently skips this sensor. Reported by a T8900; the meaning of a set value is the product bundle's. */
7507
+ bypassed: {
7508
+ param: CONTACT_CMD.BYPASS,
7509
+ type: "bool",
7510
+ kind: "boolean",
7511
+ provenance: "apk",
7512
+ description: "Bypassed by the alarm (61016, sensor_door_bypass_status)."
7513
+ },
7514
+ /**
7515
+ * The left-open alert config, whole as the sensor stores it (base64 JSON). Its two fields are the
7516
+ * members below; both are set together by `setOpenAlert`, since one frame carries both.
7517
+ */
7518
+ openAlertConfig: {
7519
+ param: CONTACT_CMD.OPEN_ALERT,
7520
+ type: "string",
7521
+ kind: "text",
7522
+ provenance: "verified",
7523
+ unexposed: true,
7524
+ writtenElsewhere: true,
7525
+ description: "Left-open alert config, base64 JSON {door_enable,door_time_point} (1290)."
7526
+ },
7527
+ /** Whether a notification fires when the door stays open longer than {@link CONTACT_MEMBERS.openAlertMinutes}. */
7528
+ openAlert: {
7529
+ readsFrom: "openAlertConfig",
7530
+ type: "bool",
7531
+ kind: "boolean",
7532
+ provenance: "verified",
7533
+ decode: (raw) => {
7534
+ const c = config(raw);
7535
+ return c === void 0 ? void 0 : asBool(c.door_enable);
7536
+ },
7537
+ description: "Left-open alert on (1290 door_enable). Set with setOpenAlert."
7538
+ },
7539
+ /** How long the door may stay open before the alert fires: one of 5, 10, 15, 30 or 60 minutes. */
7540
+ openAlertMinutes: {
7541
+ readsFrom: "openAlertConfig",
7542
+ type: "number",
7543
+ kind: "scalar",
7544
+ provenance: "verified",
7545
+ decode: (raw) => {
7546
+ const n = Number(config(raw)?.door_time_point);
7547
+ return Number.isInteger(n) ? n : void 0;
7548
+ },
7549
+ description: "Left-open alert delay, minutes: 5, 10, 15, 30 or 60 (1290 door_time_point)."
7550
+ },
7551
+ /** The daily state check config, whole (base64 JSON); its fields are the members below. */
7552
+ dailyCheckConfig: {
7553
+ param: CONTACT_CMD.DAILY_CHECK,
7554
+ type: "string",
7555
+ kind: "text",
7556
+ provenance: "verified",
7557
+ unexposed: true,
7558
+ writtenElsewhere: true,
7559
+ description: "Daily check config, base64 JSON {door_enable,door_time_interval} (1291)."
7560
+ },
7561
+ /** Whether the sensor's state is notified at each of {@link CONTACT_MEMBERS.dailyCheckTimes}. */
7562
+ dailyCheck: {
7563
+ readsFrom: "dailyCheckConfig",
7564
+ type: "bool",
7565
+ kind: "boolean",
7566
+ provenance: "verified",
7567
+ decode: (raw) => {
7568
+ const c = config(raw);
7569
+ return c === void 0 ? void 0 : asBool(c.door_enable);
7570
+ },
7571
+ description: "Daily state check on (1291 door_enable). Set with setDailyCheck."
7572
+ },
7573
+ /** The times of day the state is notified, `HH:MM` comma-separated in the order the sensor holds them. */
7574
+ dailyCheckTimes: {
7575
+ readsFrom: "dailyCheckConfig",
7576
+ type: "string",
7577
+ kind: "text",
7578
+ provenance: "verified",
7579
+ decode: (raw) => {
7580
+ const times = config(raw)?.door_time_interval;
7581
+ return Array.isArray(times) ? times.map((t) => hhmm(Number(t) || 0)).join(",") : void 0;
7582
+ },
7583
+ description: "Daily check times, HH:MM comma-separated (1291 door_time_interval, minutes since midnight)."
7584
+ },
7585
+ /**
7586
+ * Turn the left-open alert on or off, with its delay. One frame carries both: `1350` SET_PAYLOAD,
7587
+ * sub-command 1290, `{door_enable, door_time_point}` — the product's settings bundle's own frame.
7588
+ * Verified live on a T8900: param 1290 read back the written value and the app showed it.
7589
+ */
7590
+ setOpenAlert: method(({ ctx, sink }) => async (on, minutes) => {
7591
+ if (!OPEN_ALERT_MINUTES.includes(minutes)) {
7592
+ throw new Error(`openAlertMinutes: ${String(minutes)} is not one of ${OPEN_ALERT_MINUTES.join(", ")}`);
7593
+ }
7594
+ await sink.dispatch(setPayload(CONTACT_CMD.OPEN_ALERT, { door_enable: asBool(on) ? 1 : 0, door_time_point: minutes }, ctx, 0));
7595
+ }, "Set the left-open alert and its delay (5, 10, 15, 30 or 60 minutes), which share one wire."),
7596
+ /**
7597
+ * Turn the daily state check on or off, with its times of day. One frame carries both: `1350`
7598
+ * SET_PAYLOAD, sub-command 1291, `{door_enable, door_time_interval}` with each time as minutes since
7599
+ * midnight — the product's settings bundle's own frame.
7600
+ * Verified live on a T8900: param 1291 read back the written times and the app showed them.
7601
+ */
7602
+ setDailyCheck: method(({ ctx, sink }) => async (on, times) => {
7603
+ const minutes = times.map(minutesOf);
7604
+ if (minutes.some((m) => m === void 0)) {
7605
+ throw new Error(`dailyCheckTimes: ${times.join(",")} must all be HH:MM`);
7606
+ }
7607
+ await sink.dispatch(setPayload(CONTACT_CMD.DAILY_CHECK, { door_enable: asBool(on) ? 1 : 0, door_time_interval: minutes }, ctx, 0));
7608
+ }, "Set the daily state check and its times of day (HH:MM), which share one wire."),
7609
+ /**
7610
+ * The built-in alarm tone, offered where the product's description lists it: a sensor may report the
7611
+ * param while the app offers no such setting on it. Rejected, not clamped, outside the listed tones.
7612
+ * Write: a direct-binary scalar `[ch][value][acct]` at signCode 8, captured live on a T90E0.
7613
+ */
7614
+ alarmSoundType: {
7615
+ param: CONTACT_CMD.ALARM_SOUND_TYPE,
7616
+ type: "enum",
7617
+ kind: "enum",
7618
+ enumValues: enumLabels(ALARM_TONES),
7619
+ thing: ["device_alarm_sound_type"],
7620
+ provenance: "verified",
7621
+ available: described("device_alarm_sound_type"),
7622
+ args: [{ name: "tone", kind: "enum", description: "Silent (0) or one of the four tones." }],
7623
+ description: "Alarm tone (1507, device_alarm_sound_type): 0=None,1=Water,2=Classics,3=Light,4=Ding.",
7624
+ write: (v, ctx) => {
7625
+ const tone = coerceEnumValue(ALARM_TONES, v);
7626
+ return tone === void 0 ? void 0 : setScalar(CONTACT_CMD.ALARM_SOUND_TYPE, tone, ctx, "auto");
7627
+ }
7628
+ },
7629
+ /**
7630
+ * The built-in alarm volume, a device level 1-26, offered where the product's description lists it.
7631
+ * Write: `1350` SET_PAYLOAD with `mChannel` the device channel, `mValue3` 0, payload `{channel, volume,
7632
+ * transaction}` — the app's own captured frame on a T90E0. Out of range is refused, not clamped.
7633
+ */
7634
+ alarmVolume: {
7635
+ param: CONTACT_CMD.ALARM_VOLUME,
7636
+ type: "number",
7637
+ kind: "scalar",
7638
+ provenance: "verified",
7639
+ available: described("alarm_volume_value"),
7640
+ min: ALARM_VOLUME_MIN,
7641
+ max: ALARM_VOLUME_MAX,
7642
+ args: [
7643
+ { name: "level", kind: "scalar", min: ALARM_VOLUME_MIN, max: ALARM_VOLUME_MAX, description: "A device level." }
7644
+ ],
7645
+ description: "Alarm volume (1508, alarm_volume_value), a device level 1-26, not a percentage.",
7646
+ write: (v, ctx) => {
7647
+ const level = Number(v);
7648
+ return Number.isInteger(level) && level >= ALARM_VOLUME_MIN && level <= ALARM_VOLUME_MAX ? setPayload(CONTACT_CMD.ALARM_VOLUME, { channel: ctx.channel, volume: level, transaction: String(Date.now()) }, ctx, 0) : void 0;
7649
+ }
7650
+ }
7651
+ };
7652
+ var CONTACT = {
7653
+ capability: "contact",
7654
+ description: "Entry (door/window) sensor: contact state, tamper and bypass, the left-open alert and the daily state check, and the built-in alarm where the product has one.",
7655
+ members: CONTACT_MEMBERS,
7656
+ properties: propertiesOf(CONTACT_MEMBERS),
7657
+ /** A reported entry-sensor contact param (1550) is the verified proof of a contact sensor. */
7658
+ detection: { evidenceParams: [CONTACT_CMD.STATUS] },
7659
+ /**
7660
+ * Inbound `contactState`: the contact param changing on a cloud poll, and the FCM door-sensor push
7661
+ * (`CusPushEvent.DOOR_SENSOR`), whose state is the single-letter `e` — both verified live on a T8900.
7662
+ */
7663
+ events: [
7664
+ {
7665
+ source: "poll",
7666
+ match: CONTACT_CMD.STATUS,
7667
+ emit: "contactState",
7668
+ derive: (s) => contactOpen(s.source === "poll" ? s.to : void 0)
7669
+ },
7670
+ {
7671
+ source: "push",
7672
+ match: CusPushEvent.DOOR_SENSOR,
7673
+ emit: "contactState",
7674
+ derive: (s) => contactOpen(s.source === "push" ? s.payload?.["e"] : void 0)
7675
+ }
7676
+ ],
7677
+ /**
7678
+ * One door movement reaches the SDK up to three times — the station notify, the same value as a push
7679
+ * ~2 s later, then again on the next poll — so `contactState` is announced on a change only.
7680
+ */
7681
+ stateEvents: [{ event: "contactState", field: "open" }],
7682
+ /**
7683
+ * The station's own notify, the fastest contact signal and the only one that needs no cloud. A
7684
+ * declarative `events` row matches a push id or a poll param, not a param nested in a frame's
7685
+ * `params` array under an inner command, hence the decode hook. A notify without the contact param
7686
+ * (battery or signal only) emits nothing.
7687
+ */
7688
+ decodeEvent(signal) {
7689
+ const raw = statusParam(signal, CONTACT_CMD.STATUS);
7690
+ if (raw === void 0)
7691
+ return null;
7692
+ return { event: "contactState", payload: contactOpen(raw) };
7693
+ }
7694
+ };
7695
+
7601
7696
  // dist/model/capabilities/leak.js
7697
+ var LEAK_PARAM = {
7698
+ /** Water detected (`APP_CMD_WATER_SENSOR_WATER_STATE`, identifier `sensor_water_status`). */
7699
+ WATER: 1820,
7700
+ /** Temperature below the threshold (`APP_CMD_WATER_SENSOR_TEMPERATE_LOW`, `sensor_temperature_low_status`). */
7701
+ LOW_TEMPERATURE: 1822,
7702
+ /** Low-temperature alarm threshold, °F (`APP_CMD_WATER_SENSOR_TEMPERATURE_ALARM`). */
7703
+ LOW_TEMPERATURE_THRESHOLD: 1873
7704
+ };
7602
7705
  var LEAK_MEMBERS = {
7603
7706
  /**
7604
- * The alarm flag, modelled as state even though the sensor announces a leak as a PUSH EVENT rather
7605
- * than by holding a param — so 1560 is a `guessed` placeholder and the evidence gate will normally
7606
- * leave this getter uninstalled. The push event is the reliable channel for a leak; this read is
7607
- * here so the flag has a home once a capture pins a real id.
7707
+ * Water is detected. The app's own `event_sensor_water_detected_notify` is this param changing, so
7708
+ * the state and the alert are one fact.
7608
7709
  */
7609
7710
  leakDetected: {
7610
- param: 1560,
7711
+ param: LEAK_PARAM.WATER,
7611
7712
  type: "bool",
7612
7713
  kind: "boolean",
7613
- provenance: "guessed",
7614
- description: "Water-leak detected. UNVERIFIED: actually delivered as a push event, not a state param; placeholder id pending capture verification."
7714
+ provenance: "apk",
7715
+ description: "Water detected (1820, sensor_water_status)."
7716
+ },
7717
+ /** The temperature is below {@link LEAK_MEMBERS.lowTemperatureThreshold}: the freeze alarm. */
7718
+ lowTemperature: {
7719
+ param: LEAK_PARAM.LOW_TEMPERATURE,
7720
+ type: "bool",
7721
+ kind: "boolean",
7722
+ provenance: "apk",
7723
+ description: "Temperature below the alarm threshold (1822, sensor_temperature_low_status)."
7615
7724
  },
7616
7725
  /**
7617
- * Unix seconds at which the sensor last checked in — param 1551, the same last-seen id the other
7618
- * sensor capabilities read. A `timestamp` kind takes no `unit`: the number is an instant, not a
7619
- * duration, and declaring `unit: "s"` beside it fails the value-kind spec.
7726
+ * The temperature, in °F, below which the freeze alarm fires: one of 32, 33, 35, 37 or 40 as the
7727
+ * product's description lists them. The write is the bundle's `1350` SET_PAYLOAD on 1873, not yet run
7728
+ * on a device, so no setter is installed.
7620
7729
  */
7621
- lastSeen: {
7622
- param: 1551,
7730
+ lowTemperatureThreshold: {
7731
+ param: LEAK_PARAM.LOW_TEMPERATURE_THRESHOLD,
7623
7732
  type: "number",
7624
- kind: "timestamp",
7625
- provenance: "verified",
7626
- description: "Last-seen unix timestamp, seconds (verified: param 1551)."
7733
+ kind: "enum",
7734
+ enumValues: { 32: "32\xB0F", 33: "33\xB0F", 35: "35\xB0F", 37: "37\xB0F", 40: "40\xB0F" },
7735
+ thing: ["sensor_low_temperature_alarm_threshold"],
7736
+ provenance: "apk",
7737
+ unverified: true,
7738
+ description: "Freeze alarm threshold, \xB0F: 32, 33, 35, 37 or 40 (1873, sensor_low_temperature_alarm_threshold)."
7627
7739
  }
7628
7740
  };
7629
7741
  var LEAK = {
7630
7742
  capability: "leak",
7631
- description: "Water-leak / freeze sensor alarm state.",
7743
+ description: "Water-leak and freeze sensor: water and low-temperature state, and the freeze threshold.",
7632
7744
  members: LEAK_MEMBERS,
7633
7745
  properties: propertiesOf(LEAK_MEMBERS),
7634
- // Leak sensors expose no stable state param; the model/name (water/leak/freeze) is the signal.
7635
- detection: { modelHints: [/water|leak|freeze/i] }
7746
+ /**
7747
+ * A reported water-state param (1820) proves a water sensor, as does a product description listing
7748
+ * `sensor_water_status` — the app's own definition of one, which holds before the sensor has reported.
7749
+ * Never the device's name: that is what its owner typed.
7750
+ */
7751
+ detection: {
7752
+ evidenceParams: [LEAK_PARAM.WATER],
7753
+ detect: (rec) => rec.things !== void 0 && "sensor_water_status" in rec.things.properties
7754
+ }
7636
7755
  };
7637
7756
 
7638
7757
  // dist/model/capabilities/smoke.js
@@ -7649,18 +7768,6 @@ var SMOKE_MEMBERS = {
7649
7768
  kind: "boolean",
7650
7769
  provenance: "guessed",
7651
7770
  description: "Smoke detected. UNVERIFIED: placeholder id pending capture verification."
7652
- },
7653
- /**
7654
- * Unix seconds at which the detector last checked in — param 1551, the same last-seen id the other
7655
- * sensor capabilities read. A `timestamp` kind takes no `unit`: the number is an instant, not a
7656
- * duration, and declaring `unit: "s"` beside it fails the value-kind spec.
7657
- */
7658
- lastSeen: {
7659
- param: 1551,
7660
- type: "number",
7661
- kind: "timestamp",
7662
- provenance: "verified",
7663
- description: "Last-seen unix timestamp, seconds (verified: param 1551)."
7664
7771
  }
7665
7772
  };
7666
7773
  var SMOKE = {
@@ -7686,11 +7793,23 @@ var CO_MEMBERS = {
7686
7793
  kind: "boolean",
7687
7794
  provenance: "guessed",
7688
7795
  description: "Carbon-monoxide detected. UNVERIFIED: placeholder id pending capture verification."
7689
- },
7796
+ }
7797
+ };
7798
+ var CO = {
7799
+ capability: "co",
7800
+ description: "Carbon-monoxide (CO) detector alarm state.",
7801
+ members: CO_MEMBERS,
7802
+ properties: propertiesOf(CO_MEMBERS),
7803
+ // CO detectors expose no stable state param; the model/name (co/carbon) is the signal.
7804
+ detection: { modelHints: [/co\b|carbon/i] }
7805
+ };
7806
+
7807
+ // dist/model/capabilities/link.js
7808
+ var LINK_MEMBERS = {
7690
7809
  /**
7691
- * Unix seconds at which the detector last checked in — param 1551, the same last-seen id the other
7692
- * sensor capabilities read. A `timestamp` kind takes no `unit`: the number is an instant, not a
7693
- * duration, and declaring `unit: "s"` beside it fails the value-kind spec.
7810
+ * Unix seconds at which the device last checked in, as the device itself reports it — beside
7811
+ * `deviceState(sn).lastSeenMs`, which the SDK derives from the cloud's own update times. A
7812
+ * `timestamp` takes no `unit`: it is an instant, not a duration.
7694
7813
  */
7695
7814
  lastSeen: {
7696
7815
  param: 1551,
@@ -7698,15 +7817,23 @@ var CO_MEMBERS = {
7698
7817
  kind: "timestamp",
7699
7818
  provenance: "verified",
7700
7819
  description: "Last-seen unix timestamp, seconds (verified: param 1551)."
7820
+ },
7821
+ /** Link quality in dBm as the device measures it, never normalised to a bar count. */
7822
+ rssi: {
7823
+ param: 1141,
7824
+ type: "number",
7825
+ unit: "dBm",
7826
+ kind: "dbm",
7827
+ provenance: "verified",
7828
+ description: "Sub-1G/Wi-Fi signal strength (verified: param 1141 = RSSI)."
7701
7829
  }
7702
7830
  };
7703
- var CO = {
7704
- capability: "co",
7705
- description: "Carbon-monoxide (CO) detector alarm state.",
7706
- members: CO_MEMBERS,
7707
- properties: propertiesOf(CO_MEMBERS),
7708
- // CO detectors expose no stable state param; the model/name (co/carbon) is the signal.
7709
- detection: { modelHints: [/co\b|carbon/i] }
7831
+ var LINK = {
7832
+ capability: "link",
7833
+ description: "Check-in timestamp and signal strength, as the device reports them.",
7834
+ members: LINK_MEMBERS,
7835
+ properties: propertiesOf(LINK_MEMBERS),
7836
+ detection: { evidenceParams: [1551, 1141] }
7710
7837
  };
7711
7838
 
7712
7839
  // dist/model/capabilities/lock.js
@@ -7822,18 +7949,6 @@ var LOCK_MEMBERS = {
7822
7949
  provenance: "verified",
7823
7950
  description: "Lock battery level 0-100 (verified: param 1101, alias 6001)."
7824
7951
  },
7825
- /**
7826
- * Link quality in dBm as the lock measures it, on the shared param 1141. On the `ff09` wire the
7827
- * actuations are fire-and-forget, so a weak link is silent rather than an error.
7828
- */
7829
- rssi: {
7830
- param: 1141,
7831
- type: "number",
7832
- unit: "dBm",
7833
- kind: "dbm",
7834
- provenance: "verified",
7835
- description: "Lock signal strength (verified: param 1141 = RSSI)."
7836
- },
7837
7952
  /**
7838
7953
  * Lock the deadbolt/door. Resolves after sending, without confirming physical state — except on the
7839
7954
  * classic Wi-Fi lock, which answers: there it rejects on a device refusal or no reply as well.
@@ -8053,18 +8168,6 @@ var KEYPAD_MEMBERS = {
8053
8168
  provenance: "apk",
8054
8169
  coerce: isChargingState,
8055
8170
  description: "Keypad charging \u2014 derived from APP_CMD_KEYPAD_BATTERY_CHARGER_STATE (1655) as `value \u2209 {0,2}`, matching the app's own decode."
8056
- },
8057
- /**
8058
- * The keypad's link quality in dBm, on the same param 1141 the other sub-1G sensors report signal
8059
- * strength on. Reported as the device measures it: raw dBm, never normalised to a bar count.
8060
- */
8061
- rssi: {
8062
- param: 1141,
8063
- type: "number",
8064
- unit: "dBm",
8065
- kind: "dbm",
8066
- provenance: "verified",
8067
- description: "Keypad signal strength (verified: param 1141 = RSSI)."
8068
8171
  }
8069
8172
  };
8070
8173
  var KEYPAD = {
@@ -8206,16 +8309,16 @@ function armingCommand(mode, ctx) {
8206
8309
  }
8207
8310
  return setPayload(ARMING_CMD.SET_ARMING, { mode_type: ARMING_MODE_WIRE[mode], user_name: ctx.accountName }, ctx, 0, void 0, "auto");
8208
8311
  }
8209
- function alarmDelayCommand(mode, config, ctx) {
8312
+ function alarmDelayCommand(mode, config2, ctx) {
8210
8313
  const data = {
8211
8314
  mode_id: ARMING_MODE_WIRE[mode],
8212
8315
  count_down_alarm: {
8213
- channel_list: config.countDownAlarm.channelList,
8214
- delay_time: config.countDownAlarm.delaySeconds
8316
+ channel_list: config2.countDownAlarm.channelList,
8317
+ delay_time: config2.countDownAlarm.delaySeconds
8215
8318
  },
8216
- count_down_arm: { channel_list: config.countDownArm.channelList, delay_time: config.countDownArm.delaySeconds },
8217
- devices: config.devices.map((d) => ({ action: d.action, device_channel: d.deviceChannel })),
8218
- siren_sensor_action: config.sirenSensorAction.map((d) => ({ action: d.action, device_channel: d.deviceChannel }))
8319
+ count_down_arm: { channel_list: config2.countDownArm.channelList, delay_time: config2.countDownArm.delaySeconds },
8320
+ devices: config2.devices.map((d) => ({ action: d.action, device_channel: d.deviceChannel })),
8321
+ siren_sensor_action: config2.sirenSensorAction.map((d) => ({ action: d.action, device_channel: d.deviceChannel }))
8219
8322
  };
8220
8323
  return setJsonRaw(ARMING_CMD.SET_ALL_ACTION, data, ctx, STATION_CHANNEL2);
8221
8324
  }
@@ -8285,9 +8388,9 @@ var ARMING_MEMBERS = {
8285
8388
  * that integer in `mode_id` with no runtime validation and no readback, so a mode outside this union
8286
8389
  * would be the same unverified guess `setMode` refuses.
8287
8390
  */
8288
- setAlarmDelayConfig: method(({ ctx, sink }) => (mode, config) => {
8391
+ setAlarmDelayConfig: method(({ ctx, sink }) => (mode, config2) => {
8289
8392
  try {
8290
- return sink.dispatch(alarmDelayCommand(mode, config, ctx));
8393
+ return sink.dispatch(alarmDelayCommand(mode, config2, ctx));
8291
8394
  } catch (e) {
8292
8395
  return Promise.reject(e);
8293
8396
  }
@@ -11591,6 +11694,7 @@ var MODULES = [
11591
11694
  LOCATE,
11592
11695
  SMART_DROP,
11593
11696
  DISPLAY,
11697
+ LINK,
11594
11698
  INFO
11595
11699
  ];
11596
11700
  var BY_CAP = new Map(MODULES.map((m) => [m.capability, m]));
@@ -12909,6 +13013,16 @@ var SECURITY_PARAMS = {
12909
13013
  type: "bool",
12910
13014
  provenance: "apk"
12911
13015
  },
13016
+ 1820: {
13017
+ name: "waterSensorWaterState",
13018
+ type: "bool",
13019
+ provenance: "apk"
13020
+ },
13021
+ 1822: {
13022
+ name: "waterSensorTemperatureLow",
13023
+ type: "bool",
13024
+ provenance: "apk"
13025
+ },
12912
13026
  1825: {
12913
13027
  name: "sirenSensorSetAlarmVol",
12914
13028
  type: "number",
@@ -12919,6 +13033,11 @@ var SECURITY_PARAMS = {
12919
13033
  type: "bool",
12920
13034
  provenance: "apk"
12921
13035
  },
13036
+ 1873: {
13037
+ name: "waterSensorTemperatureAlarm",
13038
+ type: "number",
13039
+ provenance: "apk"
13040
+ },
12922
13041
  2001: {
12923
13042
  name: "paramTypeOpenDevice",
12924
13043
  type: "string",
@@ -22158,7 +22277,7 @@ var Fmp4Muxer = class {
22158
22277
  return box("stbl", box("stsd", Buffer.concat([u32(0), u32(1), this.sampleEntry()])), box("stts", Buffer.concat([u32(0), u32(0)])), box("stsc", Buffer.concat([u32(0), u32(0)])), box("stsz", Buffer.concat([u32(0), u32(0), u32(0)])), box("stco", Buffer.concat([u32(0), u32(0)])));
22159
22278
  }
22160
22279
  sampleEntry() {
22161
- const { sample, config } = CODEC_BOXES[this.codec];
22280
+ const { sample, config: config2 } = CODEC_BOXES[this.codec];
22162
22281
  const head = Buffer.alloc(78);
22163
22282
  head.writeUInt16BE(1, 6);
22164
22283
  head.writeUInt16BE(this.width, 24);
@@ -22168,7 +22287,7 @@ var Fmp4Muxer = class {
22168
22287
  head.writeUInt16BE(1, 40);
22169
22288
  head.writeUInt16BE(24, 74);
22170
22289
  head.writeInt16BE(-1, 76);
22171
- const cfg = box(config, this.codec === "h264" ? this.avcC() : this.hvcC());
22290
+ const cfg = box(config2, this.codec === "h264" ? this.avcC() : this.hvcC());
22172
22291
  return box(sample, Buffer.concat([head, cfg]));
22173
22292
  }
22174
22293
  avcC() {
@@ -22315,8 +22434,8 @@ var Fmp4Muxer = class {
22315
22434
  return box("mp4a", head, this.esds());
22316
22435
  }
22317
22436
  esds() {
22318
- const config = this.audioConfig ?? Buffer.from([20, 8]);
22319
- const specific = descriptor(5, config);
22437
+ const config2 = this.audioConfig ?? Buffer.from([20, 8]);
22438
+ const specific = descriptor(5, config2);
22320
22439
  const decoder = descriptor(4, Buffer.concat([Buffer.from([64, 21, 0, 0, 0]), u32(64e3), u32(32e3), specific]));
22321
22440
  const sl = descriptor(6, Buffer.from([2]));
22322
22441
  return box("esds", u32(0), descriptor(3, Buffer.concat([u16(2), Buffer.from([0]), decoder, sl])));
@@ -22549,6 +22668,7 @@ function stationChannels(devices) {
22549
22668
 
22550
22669
  // dist/transport/p2p/command-router.js
22551
22670
  var DIRECT_CMD_SENDS = 5;
22671
+ var STATION_TABLE_QUERY = Buffer.from("ff00000087030000", "hex");
22552
22672
  var LEVEL2_GRACE_MS = 25e3;
22553
22673
  var KEEP_LEVEL1_SESSION_MODELS = /* @__PURE__ */ new Set(["T8423"]);
22554
22674
  var P2P_STATION_WAITS = {
@@ -22933,7 +23053,10 @@ var P2PCommandRouter = class _P2PCommandRouter {
22933
23053
  return;
22934
23054
  this.tearDownStation(stationSn);
22935
23055
  });
22936
- session.on("level2Ready", ({ cipherId }) => this.deps.onLevel2Ready(stationSn, cipherId));
23056
+ session.on("level2Ready", ({ cipherId }) => {
23057
+ session.sendRawLevel2Bytes(STATION_TABLE_QUERY, 0, P2P_ENVELOPE.GET_CAMERA_INFO, 8);
23058
+ this.deps.onLevel2Ready(stationSn, cipherId);
23059
+ });
22937
23060
  session.on("peerAnswer", (atMs) => {
22938
23061
  if (this.manager.get(stationSn) === session)
22939
23062
  this.deps.onPeerAnswer?.(stationSn, atMs);
@@ -23769,11 +23892,11 @@ var P2PCommandRouter = class _P2PCommandRouter {
23769
23892
  * Resolve a scalar `"set-param"` intent to a concrete P2P frame — the ONE place that maps a
23770
23893
  * capability's *what* (param + value + {@link ScalarForm}) to the *how* (encryption level + wire):
23771
23894
  * `"auto"` defers the level to {@link sendBySessionLevel} — the ONE decision point — while
23772
- * `"int-string"` pins L1 and `"direct-binary"` pins L2.
23895
+ * `"int-string"` and `"int-pair"` pin L1 and `"direct-binary"` pins L2.
23773
23896
  */
23774
23897
  async resolveScalarParam(sn, param, value, form) {
23775
- if (form === "int-string") {
23776
- await this.sendIntStringCommand(sn, param, value);
23898
+ if (form === "int-string" || form === "int-pair") {
23899
+ await this.sendIntStringCommand(sn, param, value, form);
23777
23900
  return;
23778
23901
  }
23779
23902
  if (form === "direct-binary") {
@@ -24400,10 +24523,10 @@ var P2PCommandRouter = class _P2PCommandRouter {
24400
24523
  * SoloCam-spotlight / Cam2C-3). No level-2 key needed (standalone level-1 ECB). Fire-and-forget,
24401
24524
  * repeated for RF resilience.
24402
24525
  */
24403
- async sendIntStringCommand(sn, outerCmd, value) {
24526
+ async sendIntStringCommand(sn, outerCmd, value, form) {
24404
24527
  const { session, channel, accountId } = await this.resolveSession(sn);
24405
24528
  for (let i = 0; i < DIRECT_CMD_SENDS; i++) {
24406
- session.sendIntStringCommand(outerCmd, value, channel, accountId, channel);
24529
+ session.sendIntStringCommand(outerCmd, value, channel, form === "int-pair" ? "" : accountId, channel);
24407
24530
  await sleep2(200);
24408
24531
  }
24409
24532
  }
@@ -24423,9 +24546,8 @@ var P2PCommandRouter = class _P2PCommandRouter {
24423
24546
  mValue3: 0,
24424
24547
  payload: { switch: enabled ? 1 : 0 }
24425
24548
  }), "utf-8");
24426
- const camInfoPre = Buffer.from("ff00000087030000", "hex");
24427
24549
  const gap = () => sleep2(150);
24428
- session.sendRawLevel2Bytes(camInfoPre, 255, P2P_ENVELOPE.GET_CAMERA_INFO, 8);
24550
+ session.sendRawLevel2Bytes(STATION_TABLE_QUERY, 255, P2P_ENVELOPE.GET_CAMERA_INFO, 8);
24429
24551
  await gap();
24430
24552
  for (let i = 0; i < 2; i++) {
24431
24553
  session.sendRawLevel2Bytes(setJson2, 0, P2P_ENVELOPE.SET_PAYLOAD, 8);
@@ -24435,7 +24557,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
24435
24557
  session.sendRawLevel2Bytes(setJson2, channel, P2P_ENVELOPE.SET_PAYLOAD, 8);
24436
24558
  await gap();
24437
24559
  }
24438
- session.sendRawLevel2Bytes(camInfoPre, 0, P2P_ENVELOPE.GET_CAMERA_INFO, 8);
24560
+ session.sendRawLevel2Bytes(STATION_TABLE_QUERY, 0, P2P_ENVELOPE.GET_CAMERA_INFO, 8);
24439
24561
  }
24440
24562
  /**
24441
24563
  * Route a control command (`{commandType, data}`) to a device over P2P: HomeBase-attached →
@@ -25188,7 +25310,7 @@ async function loadNativePeerFactory() {
25188
25310
  const message = UNBUILT_ARCHES.has(process.arch) ? `the WebRTC transport for a HomeBase S1 Pro (T9000) needs 'node-datachannel', which has no build for ${process.arch}; a 64-bit OS is needed to drive a T9000` : "the WebRTC transport for a HomeBase S1 Pro (T9000) could not load 'node-datachannel'";
25189
25311
  throw new Error(message, { cause: err });
25190
25312
  }
25191
- return (name, config) => new ndc.PeerConnection(name, config);
25313
+ return (name, config2) => new ndc.PeerConnection(name, config2);
25192
25314
  }
25193
25315
  var ANSWER_TIMEOUT_MS = 15e3;
25194
25316
  var RtcPeer = class extends EventEmitter9 {
@@ -25220,13 +25342,13 @@ var RtcPeer = class extends EventEmitter9 {
25220
25342
  if (this.pc)
25221
25343
  return;
25222
25344
  const createPeer = this.opts.createPeer ?? await loadNativePeerFactory();
25223
- const config = {
25345
+ const config2 = {
25224
25346
  iceServers: turnServers(turn),
25225
25347
  iceTransportPolicy: this.opts.iceTransportPolicy ?? "relay",
25226
25348
  maxMessageSize: ANKER_MAX_MESSAGE_SIZE,
25227
25349
  enableIceTcp: true
25228
25350
  };
25229
- const pc = createPeer("eufy-sdk", config);
25351
+ const pc = createPeer("eufy-sdk", config2);
25230
25352
  this.pc = pc;
25231
25353
  pc.onLocalDescription((sdp, type) => {
25232
25354
  if (String(type).toLowerCase() === "answer" && this.localAnswer) {
@@ -27805,15 +27927,15 @@ var TuyaClient = class {
27805
27927
  endpoint;
27806
27928
  http;
27807
27929
  session;
27808
- constructor(config = {}) {
27809
- this.signer = config.signer ?? new HmacSigner();
27810
- this.env = config.env ?? DEFAULT_TUYA_ENV;
27811
- this.endpoint = config.endpoint;
27812
- this.http = config.http;
27930
+ constructor(config2 = {}) {
27931
+ this.signer = config2.signer ?? new HmacSigner();
27932
+ this.env = config2.env ?? DEFAULT_TUYA_ENV;
27933
+ this.endpoint = config2.endpoint;
27934
+ this.http = config2.http;
27813
27935
  this.session = {
27814
- sid: config.sid ?? "",
27815
- deviceId: config.deviceId ?? genDeviceId(),
27816
- chKey: config.chKey ?? TUYA_CHKEY
27936
+ sid: config2.sid ?? "",
27937
+ deviceId: config2.deviceId ?? genDeviceId(),
27938
+ chKey: config2.chKey ?? TUYA_CHKEY
27817
27939
  };
27818
27940
  }
27819
27941
  /** The current per-install session identity (sid empty until {@link login}). */
@@ -27976,9 +28098,9 @@ var TuyaCommandRouter = class {
27976
28098
  * `virtualId`, or the device's serial when the record carries none of them.
27977
28099
  */
27978
28100
  snMap = /* @__PURE__ */ new Map();
27979
- constructor(config = {}) {
27980
- this.allowUnverified = config.allowUnverified ?? false;
27981
- this.http = config.http;
28101
+ constructor(config2 = {}) {
28102
+ this.allowUnverified = config2.allowUnverified ?? false;
28103
+ this.http = config2.http;
27982
28104
  }
27983
28105
  /**
27984
28106
  * Supply credentials for lazy Tuya login. Called by the facade after a successful mega login.
@@ -33705,7 +33827,6 @@ export {
33705
33827
  EMPTY_DP_CATALOG,
33706
33828
  EUFYLIFE_LOCAL_KEY_HEX,
33707
33829
  EUFY_MEGA_LOCAL_KEY_HEX,
33708
- EntryAlarmTone,
33709
33830
  EufyCloudErrorCode,
33710
33831
  EufyMega,
33711
33832
  FCM,
@@ -33722,6 +33843,7 @@ export {
33722
33843
  LEAK_MEMBERS,
33723
33844
  LIFE_PARAMS,
33724
33845
  LIGHT_MEMBERS,
33846
+ LINK_MEMBERS,
33725
33847
  LIVE_TRACE_MESSAGE,
33726
33848
  LOCATE_MEMBERS,
33727
33849
  LOCK_MEMBERS,