@mega-yfue/eufy-sdk 0.6.0-beta.46 → 0.6.0-beta.48

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,161 +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
- * Rejected, not clamped, outside the tone enum: a fire-and-forget write of a bogus tone would look like
5156
- * it worked. The wire is a direct-binary scalar `[ch][value][acct]` at signCode 8 — the app's own
5157
- * captured frame.
5158
- */
5159
- alarmSoundType: {
5160
- param: CONTACT_CMD.ALARM_SOUND_TYPE,
5161
- type: "enum",
5162
- kind: "enum",
5163
- enumValues: enumLabels(EntryAlarmTone),
5164
- provenance: "verified",
5165
- requires: [CONTACT_CMD.ALARM_SOUND_TYPE],
5166
- args: [{ name: "tone", kind: "enum", description: "Silent (0) or one of the four chimes." }],
5167
- 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.",
5168
- write: (v, ctx) => {
5169
- const tone = coerceEnumValue(EntryAlarmTone, v);
5170
- return tone === void 0 ? void 0 : setScalar(CONTACT_CMD.ALARM_SOUND_TYPE, tone, ctx, "auto");
5171
- }
5172
- },
5173
- /**
5174
- * `1350` SET_PAYLOAD, `mChannel` = the device channel, `mValue3` 0, payload carrying the channel and a
5175
- * transaction stamp — the byte-shape of the app's own captured frame. Out of range is refused rather
5176
- * than clamped: the app's slider has no values outside it, so one is a caller error, not a nudge.
5177
- *
5178
- * Level-2 only, though the `mValue3` 0 would allow `"auto"`: an entry sensor's session IS its
5179
- * HomeBase's, which always holds a key — the second of `setPayload`'s two conditions, not an oversight
5180
- * of the first.
5181
- */
5182
- alarmVolume: {
5183
- param: CONTACT_CMD.ALARM_VOLUME,
5184
- type: "number",
5185
- kind: "scalar",
5186
- provenance: "verified",
5187
- requires: [CONTACT_CMD.ALARM_VOLUME],
5188
- min: ALARM_VOLUME_MIN,
5189
- max: ALARM_VOLUME_MAX,
5190
- args: [
5191
- {
5192
- name: "level",
5193
- kind: "scalar",
5194
- min: ALARM_VOLUME_MIN,
5195
- max: ALARM_VOLUME_MAX,
5196
- description: "A device level, not a percentage."
5197
- }
5198
- ],
5199
- 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.",
5200
- write: (v, ctx) => {
5201
- const level = Number(v);
5202
- 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;
5203
- }
5204
- }
5205
- };
5206
- var CONTACT = {
5207
- capability: "contact",
5208
- description: "Entry (door/window) sensor: contact state and the built-in alarm tone/volume.",
5209
- members: CONTACT_MEMBERS,
5210
- properties: propertiesOf(CONTACT_MEMBERS),
5211
- /** A reported entry-sensor contact param (1550) is the verified proof of a contact sensor. */
5212
- detection: { evidenceParams: [1550] },
5213
- /**
5214
- * Inbound `contactState`: the contact param (1550) changing on a cloud poll (open/closed), AND the FCM
5215
- * door-sensor push (`CusPushEvent.DOOR_SENSOR` = 3) — verified live on a T8900 (both fire).
5216
- *
5217
- * Both sources carry the state under a different key, so each normalises to `open`: the poll's `to` is
5218
- * the 1550 param value, and the push's is the wire's single-letter `e`. Polarity for the push is the
5219
- * V6 app's own `CusPushMode.isSensorOpen()`, which is exactly `"1".equals(e)` — anything else,
5220
- * including absent, is closed — read under its `a == PUSH_DOOR_SENSOR_EVT` branch to choose the
5221
- * "sensor is opened" vs "sensor is closed" notification string.
5222
- */
5223
- events: [
5224
- {
5225
- source: "poll",
5226
- match: 1550,
5227
- emit: "contactState",
5228
- derive: (s) => contactOpen(s.source === "poll" ? s.to : void 0)
5229
- },
5230
- {
5231
- source: "push",
5232
- match: CusPushEvent.DOOR_SENSOR,
5233
- emit: "contactState",
5234
- derive: (s) => contactOpen(s.source === "push" ? s.payload?.["e"] : void 0)
5235
- }
5236
- ],
5237
- /**
5238
- * The contact is a settled state reported by three transports, so one door movement would otherwise
5239
- * be announced up to three times — the station notify, then the same value as an FCM push ~2 s
5240
- * later, then again on the next cloud poll.
5241
- */
5242
- stateEvents: [{ event: "contactState", field: "open" }],
5243
- /**
5244
- * The station's own notify — the fastest contact signal there is, and the only one that needs no
5245
- * cloud at all (see {@link SENSOR_STATUS_CMD} for the measured lead over the push).
5246
- *
5247
- * A declarative `events` row can't express this: those match a push event id or a poll param id,
5248
- * while the state here is nested inside a frame's `params` array under an inner command id. Hence
5249
- * the decode hook, which is the documented escape hatch for exactly that.
5250
- *
5251
- * Emits nothing when the notify carries no contact param, so a status frame reporting only battery
5252
- * or signal strength doesn't fabricate a contact event.
5253
- */
5254
- decodeEvent(signal) {
5255
- const raw = statusParam(signal, CONTACT_PARAM);
5256
- if (raw === void 0)
5257
- return null;
5258
- return { event: "contactState", payload: contactOpen(raw) };
5259
- }
5260
- };
5261
-
5262
5124
  // dist/model/device-family.js
5263
5125
  var INDOOR_CAMERA_TYPES = /* @__PURE__ */ new Set([
5264
5126
  DeviceType.INDOOR_CAMERA,
@@ -5902,7 +5764,7 @@ var CAMERA_CMD = {
5902
5764
  * (1013). (Some models omit full colour; enum in {@link NightVision}.)
5903
5765
  */
5904
5766
  NIGHT_VISION_TYPE: 1277,
5905
- /** 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]. */
5906
5768
  IC_NIGHT_VISION_TYPE: 1013,
5907
5769
  /**
5908
5770
  * Push-notification STYLE — how much a detection push carries: text alone, a thumbnail, or text
@@ -6013,10 +5875,12 @@ var SoundDetectionType = {
6013
5875
  var NightVision = {
6014
5876
  /** Off — never use infrared. */
6015
5877
  Off: 0,
6016
- /** 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. */
6017
5879
  Infrared: 1,
6018
5880
  /** Full colour night vision (models with a spotlight / starlight sensor). */
6019
- FullColor: 2
5881
+ FullColor: 2,
5882
+ /** Infrared held on — black-and-white night vision, not switched by the light level. */
5883
+ InfraredOn: 3
6020
5884
  };
6021
5885
  var RecordingQuality = {
6022
5886
  HD720: "HD (720P)",
@@ -6094,8 +5958,8 @@ function powerValue(on, ctx) {
6094
5958
  function isT8410HomeBase3(ctx) {
6095
5959
  return ctx.model === "T8410" && ctx.stationSerial?.startsWith("T8030") === true;
6096
5960
  }
6097
- function isT8442(ctx) {
6098
- return ctx.model === "T8442";
5961
+ function readsNightVisionOn2002(ctx) {
5962
+ return ctx.paramIds?.has(2002) === true && !ctx.paramIds.has(CAMERA_CMD.NIGHT_VISION_TYPE);
6099
5963
  }
6100
5964
  function usesHomeBasePowerPayload(ctx) {
6101
5965
  if (!isT8410HomeBase3(ctx) || !ctx.firmwareVersion || !/^\d+(?:\.\d+){2,3}$/.test(ctx.firmwareVersion))
@@ -6310,36 +6174,27 @@ var CAMERA_MEMBERS = {
6310
6174
  return style == null ? void 0 : setJson(CAMERA_CMD.PUSH_NOTIFY_TYPE, { value: style, transaction: String(Date.now()) }, ctx);
6311
6175
  }
6312
6176
  },
6313
- /**
6314
- * Three modes, and `FullColor` is the conditional one: a model without a spotlight or starlight
6315
- * sensor omits it, and the wire accepts the value regardless — the device's own reported set is the
6316
- * only statement of which of the three it has. `coerceEnumValue` rejects a value outside
6317
- * {@link NightVision} rather than coercing it. The payload key on the wire is `night_sion`.
6318
- *
6319
- * 1277 carries more than these three on some models: the V6 app's per-model mappings put modes on
6320
- * 3, 5, 6 and 8 as well, and which model takes which mapping is not in the app bundle. A reported value
6321
- * outside the published options reads as `undefined` rather than as an unnamed number.
6322
- */
6177
+ /** Modes come from the thing description (`thing`); unlisted values read as `undefined`. */
6323
6178
  nightVision: {
6324
6179
  param: CAMERA_CMD.NIGHT_VISION_TYPE,
6325
6180
  type: "enum",
6326
6181
  kind: "enum",
6327
- enumValues: { 0: "Off", 1: "Infrared", 2: "Full Color" },
6328
- 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"],
6329
6184
  provenance: "verified",
6330
6185
  available: isCameraCodec,
6331
- readAvailable: (ctx) => !isT8442(ctx),
6332
- readAliases: [{ paramType: 2002, available: isT8442 }],
6333
- 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.",
6334
6190
  write: (v, ctx) => {
6335
6191
  const nv = coerceEnumValue(NightVision, v);
6336
6192
  if (nv == null)
6337
6193
  return void 0;
6338
- if (isT8442(ctx)) {
6339
- if (nv === NightVision.FullColor)
6340
- throw new Error("nightVision: Full Color is not supported on T8442 (only Off/Infrared)");
6341
- return setScalar(CAMERA_CMD.IC_NIGHT_VISION_TYPE, nv, ctx, "int-string");
6342
- }
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");
6343
6198
  return setPayload(CAMERA_CMD.NIGHT_VISION_TYPE, { channel: ctx.channel, night_sion: nv }, ctx, 0, 0, "auto");
6344
6199
  }
6345
6200
  },
@@ -7573,29 +7428,330 @@ var PTZ = {
7573
7428
  }
7574
7429
  };
7575
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
+
7576
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
+ };
7577
7705
  var LEAK_MEMBERS = {
7578
7706
  /**
7579
- * The alarm flag, modelled as state even though the sensor announces a leak as a PUSH EVENT rather
7580
- * than by holding a param — so 1560 is a `guessed` placeholder and the evidence gate will normally
7581
- * leave this getter uninstalled. The push event is the reliable channel for a leak; this read is
7582
- * 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.
7583
7709
  */
7584
7710
  leakDetected: {
7585
- param: 1560,
7711
+ param: LEAK_PARAM.WATER,
7586
7712
  type: "bool",
7587
7713
  kind: "boolean",
7588
- provenance: "guessed",
7589
- 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)."
7724
+ },
7725
+ /**
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.
7729
+ */
7730
+ lowTemperatureThreshold: {
7731
+ param: LEAK_PARAM.LOW_TEMPERATURE_THRESHOLD,
7732
+ type: "number",
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)."
7590
7739
  }
7591
7740
  };
7592
7741
  var LEAK = {
7593
7742
  capability: "leak",
7594
- description: "Water-leak / freeze sensor alarm state.",
7743
+ description: "Water-leak and freeze sensor: water and low-temperature state, and the freeze threshold.",
7595
7744
  members: LEAK_MEMBERS,
7596
7745
  properties: propertiesOf(LEAK_MEMBERS),
7597
- // Leak sensors expose no stable state param; the model/name (water/leak/freeze) is the signal.
7598
- 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
+ }
7599
7755
  };
7600
7756
 
7601
7757
  // dist/model/capabilities/smoke.js
@@ -8153,16 +8309,16 @@ function armingCommand(mode, ctx) {
8153
8309
  }
8154
8310
  return setPayload(ARMING_CMD.SET_ARMING, { mode_type: ARMING_MODE_WIRE[mode], user_name: ctx.accountName }, ctx, 0, void 0, "auto");
8155
8311
  }
8156
- function alarmDelayCommand(mode, config, ctx) {
8312
+ function alarmDelayCommand(mode, config2, ctx) {
8157
8313
  const data = {
8158
8314
  mode_id: ARMING_MODE_WIRE[mode],
8159
8315
  count_down_alarm: {
8160
- channel_list: config.countDownAlarm.channelList,
8161
- delay_time: config.countDownAlarm.delaySeconds
8316
+ channel_list: config2.countDownAlarm.channelList,
8317
+ delay_time: config2.countDownAlarm.delaySeconds
8162
8318
  },
8163
- count_down_arm: { channel_list: config.countDownArm.channelList, delay_time: config.countDownArm.delaySeconds },
8164
- devices: config.devices.map((d) => ({ action: d.action, device_channel: d.deviceChannel })),
8165
- 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 }))
8166
8322
  };
8167
8323
  return setJsonRaw(ARMING_CMD.SET_ALL_ACTION, data, ctx, STATION_CHANNEL2);
8168
8324
  }
@@ -8232,9 +8388,9 @@ var ARMING_MEMBERS = {
8232
8388
  * that integer in `mode_id` with no runtime validation and no readback, so a mode outside this union
8233
8389
  * would be the same unverified guess `setMode` refuses.
8234
8390
  */
8235
- setAlarmDelayConfig: method(({ ctx, sink }) => (mode, config) => {
8391
+ setAlarmDelayConfig: method(({ ctx, sink }) => (mode, config2) => {
8236
8392
  try {
8237
- return sink.dispatch(alarmDelayCommand(mode, config, ctx));
8393
+ return sink.dispatch(alarmDelayCommand(mode, config2, ctx));
8238
8394
  } catch (e) {
8239
8395
  return Promise.reject(e);
8240
8396
  }
@@ -12857,6 +13013,16 @@ var SECURITY_PARAMS = {
12857
13013
  type: "bool",
12858
13014
  provenance: "apk"
12859
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
+ },
12860
13026
  1825: {
12861
13027
  name: "sirenSensorSetAlarmVol",
12862
13028
  type: "number",
@@ -12867,6 +13033,11 @@ var SECURITY_PARAMS = {
12867
13033
  type: "bool",
12868
13034
  provenance: "apk"
12869
13035
  },
13036
+ 1873: {
13037
+ name: "waterSensorTemperatureAlarm",
13038
+ type: "number",
13039
+ provenance: "apk"
13040
+ },
12870
13041
  2001: {
12871
13042
  name: "paramTypeOpenDevice",
12872
13043
  type: "string",
@@ -22106,7 +22277,7 @@ var Fmp4Muxer = class {
22106
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)])));
22107
22278
  }
22108
22279
  sampleEntry() {
22109
- const { sample, config } = CODEC_BOXES[this.codec];
22280
+ const { sample, config: config2 } = CODEC_BOXES[this.codec];
22110
22281
  const head = Buffer.alloc(78);
22111
22282
  head.writeUInt16BE(1, 6);
22112
22283
  head.writeUInt16BE(this.width, 24);
@@ -22116,7 +22287,7 @@ var Fmp4Muxer = class {
22116
22287
  head.writeUInt16BE(1, 40);
22117
22288
  head.writeUInt16BE(24, 74);
22118
22289
  head.writeInt16BE(-1, 76);
22119
- const cfg = box(config, this.codec === "h264" ? this.avcC() : this.hvcC());
22290
+ const cfg = box(config2, this.codec === "h264" ? this.avcC() : this.hvcC());
22120
22291
  return box(sample, Buffer.concat([head, cfg]));
22121
22292
  }
22122
22293
  avcC() {
@@ -22263,8 +22434,8 @@ var Fmp4Muxer = class {
22263
22434
  return box("mp4a", head, this.esds());
22264
22435
  }
22265
22436
  esds() {
22266
- const config = this.audioConfig ?? Buffer.from([20, 8]);
22267
- const specific = descriptor(5, config);
22437
+ const config2 = this.audioConfig ?? Buffer.from([20, 8]);
22438
+ const specific = descriptor(5, config2);
22268
22439
  const decoder = descriptor(4, Buffer.concat([Buffer.from([64, 21, 0, 0, 0]), u32(64e3), u32(32e3), specific]));
22269
22440
  const sl = descriptor(6, Buffer.from([2]));
22270
22441
  return box("esds", u32(0), descriptor(3, Buffer.concat([u16(2), Buffer.from([0]), decoder, sl])));
@@ -22497,6 +22668,7 @@ function stationChannels(devices) {
22497
22668
 
22498
22669
  // dist/transport/p2p/command-router.js
22499
22670
  var DIRECT_CMD_SENDS = 5;
22671
+ var STATION_TABLE_QUERY = Buffer.from("ff00000087030000", "hex");
22500
22672
  var LEVEL2_GRACE_MS = 25e3;
22501
22673
  var KEEP_LEVEL1_SESSION_MODELS = /* @__PURE__ */ new Set(["T8423"]);
22502
22674
  var P2P_STATION_WAITS = {
@@ -22881,7 +23053,10 @@ var P2PCommandRouter = class _P2PCommandRouter {
22881
23053
  return;
22882
23054
  this.tearDownStation(stationSn);
22883
23055
  });
22884
- 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
+ });
22885
23060
  session.on("peerAnswer", (atMs) => {
22886
23061
  if (this.manager.get(stationSn) === session)
22887
23062
  this.deps.onPeerAnswer?.(stationSn, atMs);
@@ -23717,11 +23892,11 @@ var P2PCommandRouter = class _P2PCommandRouter {
23717
23892
  * Resolve a scalar `"set-param"` intent to a concrete P2P frame — the ONE place that maps a
23718
23893
  * capability's *what* (param + value + {@link ScalarForm}) to the *how* (encryption level + wire):
23719
23894
  * `"auto"` defers the level to {@link sendBySessionLevel} — the ONE decision point — while
23720
- * `"int-string"` pins L1 and `"direct-binary"` pins L2.
23895
+ * `"int-string"` and `"int-pair"` pin L1 and `"direct-binary"` pins L2.
23721
23896
  */
23722
23897
  async resolveScalarParam(sn, param, value, form) {
23723
- if (form === "int-string") {
23724
- await this.sendIntStringCommand(sn, param, value);
23898
+ if (form === "int-string" || form === "int-pair") {
23899
+ await this.sendIntStringCommand(sn, param, value, form);
23725
23900
  return;
23726
23901
  }
23727
23902
  if (form === "direct-binary") {
@@ -24348,10 +24523,10 @@ var P2PCommandRouter = class _P2PCommandRouter {
24348
24523
  * SoloCam-spotlight / Cam2C-3). No level-2 key needed (standalone level-1 ECB). Fire-and-forget,
24349
24524
  * repeated for RF resilience.
24350
24525
  */
24351
- async sendIntStringCommand(sn, outerCmd, value) {
24526
+ async sendIntStringCommand(sn, outerCmd, value, form) {
24352
24527
  const { session, channel, accountId } = await this.resolveSession(sn);
24353
24528
  for (let i = 0; i < DIRECT_CMD_SENDS; i++) {
24354
- session.sendIntStringCommand(outerCmd, value, channel, accountId, channel);
24529
+ session.sendIntStringCommand(outerCmd, value, channel, form === "int-pair" ? "" : accountId, channel);
24355
24530
  await sleep2(200);
24356
24531
  }
24357
24532
  }
@@ -24371,9 +24546,8 @@ var P2PCommandRouter = class _P2PCommandRouter {
24371
24546
  mValue3: 0,
24372
24547
  payload: { switch: enabled ? 1 : 0 }
24373
24548
  }), "utf-8");
24374
- const camInfoPre = Buffer.from("ff00000087030000", "hex");
24375
24549
  const gap = () => sleep2(150);
24376
- session.sendRawLevel2Bytes(camInfoPre, 255, P2P_ENVELOPE.GET_CAMERA_INFO, 8);
24550
+ session.sendRawLevel2Bytes(STATION_TABLE_QUERY, 255, P2P_ENVELOPE.GET_CAMERA_INFO, 8);
24377
24551
  await gap();
24378
24552
  for (let i = 0; i < 2; i++) {
24379
24553
  session.sendRawLevel2Bytes(setJson2, 0, P2P_ENVELOPE.SET_PAYLOAD, 8);
@@ -24383,7 +24557,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
24383
24557
  session.sendRawLevel2Bytes(setJson2, channel, P2P_ENVELOPE.SET_PAYLOAD, 8);
24384
24558
  await gap();
24385
24559
  }
24386
- session.sendRawLevel2Bytes(camInfoPre, 0, P2P_ENVELOPE.GET_CAMERA_INFO, 8);
24560
+ session.sendRawLevel2Bytes(STATION_TABLE_QUERY, 0, P2P_ENVELOPE.GET_CAMERA_INFO, 8);
24387
24561
  }
24388
24562
  /**
24389
24563
  * Route a control command (`{commandType, data}`) to a device over P2P: HomeBase-attached →
@@ -25136,7 +25310,7 @@ async function loadNativePeerFactory() {
25136
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'";
25137
25311
  throw new Error(message, { cause: err });
25138
25312
  }
25139
- return (name, config) => new ndc.PeerConnection(name, config);
25313
+ return (name, config2) => new ndc.PeerConnection(name, config2);
25140
25314
  }
25141
25315
  var ANSWER_TIMEOUT_MS = 15e3;
25142
25316
  var RtcPeer = class extends EventEmitter9 {
@@ -25168,13 +25342,13 @@ var RtcPeer = class extends EventEmitter9 {
25168
25342
  if (this.pc)
25169
25343
  return;
25170
25344
  const createPeer = this.opts.createPeer ?? await loadNativePeerFactory();
25171
- const config = {
25345
+ const config2 = {
25172
25346
  iceServers: turnServers(turn),
25173
25347
  iceTransportPolicy: this.opts.iceTransportPolicy ?? "relay",
25174
25348
  maxMessageSize: ANKER_MAX_MESSAGE_SIZE,
25175
25349
  enableIceTcp: true
25176
25350
  };
25177
- const pc = createPeer("eufy-sdk", config);
25351
+ const pc = createPeer("eufy-sdk", config2);
25178
25352
  this.pc = pc;
25179
25353
  pc.onLocalDescription((sdp, type) => {
25180
25354
  if (String(type).toLowerCase() === "answer" && this.localAnswer) {
@@ -27753,15 +27927,15 @@ var TuyaClient = class {
27753
27927
  endpoint;
27754
27928
  http;
27755
27929
  session;
27756
- constructor(config = {}) {
27757
- this.signer = config.signer ?? new HmacSigner();
27758
- this.env = config.env ?? DEFAULT_TUYA_ENV;
27759
- this.endpoint = config.endpoint;
27760
- 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;
27761
27935
  this.session = {
27762
- sid: config.sid ?? "",
27763
- deviceId: config.deviceId ?? genDeviceId(),
27764
- chKey: config.chKey ?? TUYA_CHKEY
27936
+ sid: config2.sid ?? "",
27937
+ deviceId: config2.deviceId ?? genDeviceId(),
27938
+ chKey: config2.chKey ?? TUYA_CHKEY
27765
27939
  };
27766
27940
  }
27767
27941
  /** The current per-install session identity (sid empty until {@link login}). */
@@ -27924,9 +28098,9 @@ var TuyaCommandRouter = class {
27924
28098
  * `virtualId`, or the device's serial when the record carries none of them.
27925
28099
  */
27926
28100
  snMap = /* @__PURE__ */ new Map();
27927
- constructor(config = {}) {
27928
- this.allowUnverified = config.allowUnverified ?? false;
27929
- this.http = config.http;
28101
+ constructor(config2 = {}) {
28102
+ this.allowUnverified = config2.allowUnverified ?? false;
28103
+ this.http = config2.http;
27930
28104
  }
27931
28105
  /**
27932
28106
  * Supply credentials for lazy Tuya login. Called by the facade after a successful mega login.
@@ -30804,7 +30978,7 @@ var EufyMega = class _EufyMega extends EventEmitter14 {
30804
30978
  return false;
30805
30979
  if (!device)
30806
30980
  return true;
30807
- device.applyParams(record.params ?? {});
30981
+ this.applyAndAnnounce(device, rawValue === void 0 ? {} : { [refresh.param]: String(rawValue) });
30808
30982
  return this.matchesObservation(device.getProperty(refresh.property)?.value, refresh, before);
30809
30983
  };
30810
30984
  if (refresh.expected !== void 0 && settled())
@@ -33653,7 +33827,6 @@ export {
33653
33827
  EMPTY_DP_CATALOG,
33654
33828
  EUFYLIFE_LOCAL_KEY_HEX,
33655
33829
  EUFY_MEGA_LOCAL_KEY_HEX,
33656
- EntryAlarmTone,
33657
33830
  EufyCloudErrorCode,
33658
33831
  EufyMega,
33659
33832
  FCM,