@mega-yfue/eufy-sdk 0.5.0-beta.7 → 0.5.0-beta.9

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.
@@ -234,6 +234,11 @@ export declare class EufyMega extends EventEmitter {
234
234
  *
235
235
  * The evidence set is widened, never replaced: the ids come back through the cloud record, and a
236
236
  * record that omits a realtime-only id would otherwise un-know it and re-trigger on the next report.
237
+ *
238
+ * The device is re-resolved against the fresh record before binding. A property gated on a
239
+ * realtime-only param enters the schema only once that param is evidence, and the value that made it
240
+ * evidence was stored before the property existed, so the record's realtime params are applied again
241
+ * to land it under the property's name.
237
242
  */
238
243
  private rebindReads;
239
244
  /** The distinct credential scopes the current MQTT roster needs — no devices on a scope, no connection. */
@@ -835,11 +840,14 @@ export declare class EufyMega extends EventEmitter {
835
840
  */
836
841
  setProperty(sn: string, name: string, value: boolean | number | string): Promise<void>;
837
842
  /**
838
- * Restart a HomeBase.
843
+ * Restart a station: a HomeBase, or a camera-family device that is its own station.
839
844
  *
840
- * **HomeBases only** — restart is a hub operation, so a non-HomeBase serial (a camera, an NVR)
841
- * throws rather than doing nothing. The hub drops its connection and returns after a minute or two,
842
- * so everything behind it is briefly offline. Verified on real hardware.
845
+ * The restart is `RESTART_HUB` on the station's broadcast channel, so it reaches whatever owns the
846
+ * P2P session. A standalone camera or doorbell owns its own session and is the station it restarts.
847
+ * A camera attached to a HomeBase is not — the same frame would restart its HomeBase — so its serial
848
+ * throws, as does any non-camera serial, rather than doing nothing. The device drops its connection
849
+ * and returns after a minute or two; a HomeBase takes everything behind it offline meanwhile.
850
+ * Verified on real hardware against a HomeBase and a standalone indoor camera.
843
851
  */
844
852
  reboot(sn: string): Promise<void>;
845
853
  /**
@@ -857,6 +865,7 @@ export declare class EufyMega extends EventEmitter {
857
865
  * exactly the reads it has.
858
866
  */
859
867
  private fetchDpCatalog;
868
+ /** The {@link CommandContext} for a device, built from `known` when that record is already in hand. */
860
869
  private commandContext;
861
870
  /**
862
871
  * **Generic P2P request/reply query** — sends a `SET_PAYLOAD` sub-command and resolves with the
package/dist/index.js CHANGED
@@ -11179,8 +11179,10 @@ function resolveDevice(rec) {
11179
11179
  }
11180
11180
  function resolveProperties(rec, codec, capabilities) {
11181
11181
  const paramIds = /* @__PURE__ */ new Set();
11182
- if (rec.params && typeof rec.params === "object") {
11183
- for (const k of Object.keys(rec.params)) {
11182
+ for (const reported of [rec.params, rec.dpParams]) {
11183
+ if (!reported || typeof reported !== "object")
11184
+ continue;
11185
+ for (const k of Object.keys(reported)) {
11184
11186
  const n = Number(k);
11185
11187
  if (Number.isFinite(n))
11186
11188
  paramIds.add(n);
@@ -16249,6 +16251,21 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
16249
16251
  this.send(this.connectAddress, RequestMessageType.DATA, data);
16250
16252
  return true;
16251
16253
  }
16254
+ /**
16255
+ * The level-1 twin of {@link sendRawLevel2Bytes}: the plaintext `payload` sealed AES-128-ECB under the
16256
+ * level-1 key, signCode 1 — the seal of the level-1 media start. Returns `false` when not connected.
16257
+ */
16258
+ sendRawLevel1Bytes(payload, channel, outerCmd) {
16259
+ if (!this.connectAddress)
16260
+ return false;
16261
+ const data = Buffer.concat([
16262
+ buildCommandHeader(this.seqNumber, outerCmd),
16263
+ buildRawCommandPayload(encryptP2PData(paddingP2PData(payload), this.level1Key), channel, 1)
16264
+ ]);
16265
+ this.seqNumber = this.seqNumber + 1 & 65535;
16266
+ this.send(this.connectAddress, RequestMessageType.DATA, data);
16267
+ return true;
16268
+ }
16252
16269
  /**
16253
16270
  * Stop the realtime media stream (`CMD_STOP_REALTIME_MEDIA`, 1004) on a camera `channel`, in the shape
16254
16271
  * its runtime topology takes, as {@link startLiveMedia} selects its start:
@@ -21197,14 +21214,20 @@ var P2PCommandRouter = class _P2PCommandRouter {
21197
21214
  }
21198
21215
  }
21199
21216
  /**
21200
- * Restart a HomeBase. `RESTART_HUB` (1034) is a station-scalar on the broadcast channel 255: a
21201
- * level-2 frame whose body is `[u32 value][account_id padded]` — the same shape as the hub
21202
- * alarm-volume control. ✅ Wire-confirmed byte-exact from a capture of the app's own Restart
21203
- * (2026-08-03) and HW-tested: the captured frame carried value `0` and rebooted the hub. Replays
21204
- * like every other level-2 control, so a single dropped datagram doesn't lose it.
21217
+ * Restart the station that owns `sn`'s P2P session. `RESTART_HUB` (1034) is a station-scalar on the
21218
+ * broadcast channel 255 whose body is `[u32 value][account_id padded]`, value `0`. ✅ Wire-confirmed
21219
+ * byte-exact as a level-2 frame from a capture of the app's own HomeBase Restart (2026-08-03).
21220
+ *
21221
+ * The seal follows the session, as {@link sendBySessionLevel} defines it: a keyed session takes the
21222
+ * level-2 frame, a keyless one — a standalone camera, which never negotiates a key — the same body
21223
+ * sealed level-1. Both are replayed {@link DIRECT_CMD_SENDS}× at 200ms, as every other unacknowledged
21224
+ * control here is.
21205
21225
  */
21206
21226
  async rebootStation(sn) {
21207
- await this.replayLevel2Send(sn, `reboot ${sn}`, ({ session, accountId }) => session.sendRawLevel2Bytes(buildDirectBinaryBody(0, accountId), 255, P2P_ENVELOPE.RESTART_HUB, 8));
21227
+ await this.sendBySessionLevel(sn, {
21228
+ l2: (resolved) => this.replayLevel2Send(sn, `reboot ${sn}`, ({ session, accountId }) => session.sendRawLevel2Bytes(buildDirectBinaryBody(0, accountId), 255, P2P_ENVELOPE.RESTART_HUB, 8), resolved),
21229
+ l1: (resolved) => this.replayLevel2Send(sn, `reboot ${sn}`, ({ session, accountId }) => session.sendRawLevel1Bytes(buildDirectBinaryBody(0, accountId), 255, P2P_ENVELOPE.RESTART_HUB), resolved)
21230
+ });
21208
21231
  }
21209
21232
  /**
21210
21233
  * A {@link MediaProvider} bound to one serial — resolves the session then calls `p2p/media`.
@@ -25983,13 +26006,22 @@ var EufyMega = class extends EventEmitter9 {
25983
26006
  *
25984
26007
  * The evidence set is widened, never replaced: the ids come back through the cloud record, and a
25985
26008
  * record that omits a realtime-only id would otherwise un-know it and re-trigger on the next report.
26009
+ *
26010
+ * The device is re-resolved against the fresh record before binding. A property gated on a
26011
+ * realtime-only param enters the schema only once that param is evidence, and the value that made it
26012
+ * evidence was stored before the property existed, so the record's realtime params are applied again
26013
+ * to land it under the property's name.
25986
26014
  */
25987
26015
  async rebindReads(sn) {
25988
26016
  const dev = this.liveDevices.get(sn)?.deref();
25989
26017
  if (!dev)
25990
26018
  return;
25991
26019
  try {
25992
- const ctx = await this.commandContext(sn);
26020
+ const rec = await this.registry.record(sn);
26021
+ dev.reresolve(rec);
26022
+ if (rec.dpParams)
26023
+ this.applyAndAnnounce(dev, rec.dpParams);
26024
+ const ctx = await this.commandContext(sn, rec);
25993
26025
  dev.bindActions(ctx, this.commandSinkFor(sn), this.mediaProviderFor(sn), this.ff09SettingsReaderFor(sn, ctx), rawDpCodec);
25994
26026
  this.boundParamIds.set(sn, /* @__PURE__ */ new Set([...this.boundParamIds.get(sn) ?? [], ...ctx.paramIds]));
25995
26027
  this.emit("deviceState", this.deviceState(sn));
@@ -27310,16 +27342,20 @@ var EufyMega = class extends EventEmitter9 {
27310
27342
  await this.commandSinkFor(sn).dispatch(cmd);
27311
27343
  }
27312
27344
  /**
27313
- * Restart a HomeBase.
27345
+ * Restart a station: a HomeBase, or a camera-family device that is its own station.
27314
27346
  *
27315
- * **HomeBases only** — restart is a hub operation, so a non-HomeBase serial (a camera, an NVR)
27316
- * throws rather than doing nothing. The hub drops its connection and returns after a minute or two,
27317
- * so everything behind it is briefly offline. Verified on real hardware.
27347
+ * The restart is `RESTART_HUB` on the station's broadcast channel, so it reaches whatever owns the
27348
+ * P2P session. A standalone camera or doorbell owns its own session and is the station it restarts.
27349
+ * A camera attached to a HomeBase is not — the same frame would restart its HomeBase — so its serial
27350
+ * throws, as does any non-camera serial, rather than doing nothing. The device drops its connection
27351
+ * and returns after a minute or two; a HomeBase takes everything behind it offline meanwhile.
27352
+ * Verified on real hardware against a HomeBase and a standalone indoor camera.
27318
27353
  */
27319
27354
  async reboot(sn) {
27320
27355
  const ctx = await this.commandContext(sn);
27321
- if (!isHomeBase({ deviceType: ctx.deviceType, model: ctx.model })) {
27322
- throw new Error(`reboot: ${sn} is not a HomeBase (deviceType ${ctx.deviceType ?? "?"}, model ${ctx.model ?? "?"}) \u2014 restart is a hub-only operation`);
27356
+ const standaloneCamera = ctx.codec === "camera" && this.p2p.stationKeyOf(sn) === sn;
27357
+ if (!isHomeBase({ deviceType: ctx.deviceType, model: ctx.model }) && !standaloneCamera) {
27358
+ throw new Error(`reboot: ${sn} is neither a HomeBase nor a standalone camera (deviceType ${ctx.deviceType ?? "?"}, model ${ctx.model ?? "?"}) \u2014 restart addresses the station that owns the P2P session`);
27323
27359
  }
27324
27360
  await this.p2p.rebootStation(sn);
27325
27361
  }
@@ -27351,8 +27387,9 @@ var EufyMega = class extends EventEmitter9 {
27351
27387
  return EMPTY_DP_CATALOG;
27352
27388
  }
27353
27389
  }
27354
- async commandContext(sn) {
27355
- const rec = await this.registry.record(sn);
27390
+ /** The {@link CommandContext} for a device, built from `known` when that record is already in hand. */
27391
+ async commandContext(sn, known) {
27392
+ const rec = known ?? await this.registry.record(sn);
27356
27393
  const dev = this.registry.require(sn);
27357
27394
  const raw = dev.raw ?? {};
27358
27395
  const rawChannel = raw.device_channel;