@camstack/addon-provider-dreame 0.2.93 → 0.2.94

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/addon.js CHANGED
@@ -72605,7 +72605,17 @@ var EventDensityBucketSchema = object({
72605
72605
  bucketStart: number(),
72606
72606
  motion: number().int(),
72607
72607
  object: number().int(),
72608
- audio: number().int()
72608
+ audio: number().int(),
72609
+ /**
72610
+ * Energy-weighted mean dBFS of the audio events in this bucket — the LEVEL,
72611
+ * as opposed to `audio`, which is their COUNT (D431).
72612
+ *
72613
+ * ABSENT when the bucket holds no audio event. Absence means "nothing was
72614
+ * measured", which is not the same claim as silence, and a lane that cannot
72615
+ * tell them apart paints a quiet hour at full scale — which is exactly what
72616
+ * the count-on-a-level-meter did.
72617
+ */
72618
+ audioDbfs: number().optional()
72609
72619
  });
72610
72620
  /**
72611
72621
  * One camera's row in a `getEventDensityBatch` answer.
@@ -82793,7 +82803,23 @@ var RecordingRangeSchema = object({
82793
82803
  });
82794
82804
  var RecordingAvailabilitySchema = object({
82795
82805
  deviceId: number(),
82796
- ranges: array(RecordingRangeSchema)
82806
+ ranges: array(RecordingRangeSchema),
82807
+ /**
82808
+ * Every profile this camera has footage in — not only the one `ranges`
82809
+ * describes (D433).
82810
+ *
82811
+ * `ranges` answers for ONE profile by design: the timeline is a single bar,
82812
+ * and enumerating all of them triples the directory reads for a bar that
82813
+ * would look identical. But the answer used to say nothing about that, so a
82814
+ * caller asking "what can I export?" read the single preferred profile as
82815
+ * the only one that exists — on this deployment every camera records `high`
82816
+ * AND `low`, the low tree holding 14 GB for camera 615 alone, and the export
82817
+ * UI could not see it.
82818
+ *
82819
+ * Cheap: it is the same shallow directory read the profile CHOICE already
82820
+ * makes. Ask for a specific profile's ranges with the `profile` input.
82821
+ */
82822
+ profilesWithFootage: array(string())
82797
82823
  });
82798
82824
  var RecordingDaysSchema = object({
82799
82825
  deviceId: number(),
@@ -82819,7 +82845,11 @@ var RecordingDaysSchema = object({
82819
82845
  var RecordingAvailabilityForDeviceSchema = object({
82820
82846
  deviceId: number(),
82821
82847
  read: _enum(["read", "unreadable"]),
82822
- ranges: array(RecordingRangeSchema).readonly()
82848
+ ranges: array(RecordingRangeSchema).readonly(),
82849
+ /** Same field, same meaning, as the singular answer (D433). A row that
82850
+ * dropped it would tell a grid caller the archive holds one profile.
82851
+ * Empty on an `'unreadable'` row: nobody looked. */
82852
+ profilesWithFootage: array(string()).readonly()
82823
82853
  });
82824
82854
  /**
82825
82855
  * One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
@@ -83109,7 +83139,14 @@ var ReadWindowBytesResultSchema = discriminatedUnion("kind", [object({
83109
83139
  method(object({
83110
83140
  deviceId: number(),
83111
83141
  fromMs: number(),
83112
- toMs: number()
83142
+ toMs: number(),
83143
+ /**
83144
+ * Answer for THIS profile instead of the preferred one (D433). Absent
83145
+ * keeps the timeline's behaviour — one bar, one profile, one set of
83146
+ * reads. `profilesWithFootage` on the answer says what may be asked
83147
+ * for.
83148
+ */
83149
+ profile: string().optional()
83113
83150
  }), RecordingAvailabilitySchema, {
83114
83151
  kind: "query",
83115
83152
  auth: "protected"
@@ -98475,7 +98512,7 @@ function dreameNavigationCatalog(disabledEntryIds = DREAME_NAVIGATION_DISABLED_E
98475
98512
  function resolveNavigationFeatures(overrides) {
98476
98513
  return {
98477
98514
  ...DREAME_NAVIGATION_DEFAULT_FEATURES,
98478
- ...overrides?.features ?? {}
98515
+ ...overrides?.features
98479
98516
  };
98480
98517
  }
98481
98518
  /** The disabled entry ids in force for ONE device: the addon set plus its own. */
@@ -98585,7 +98622,7 @@ var DreameNetworkLinkMonitor = class {
98585
98622
  meta: {
98586
98623
  reason,
98587
98624
  rawBars: bars,
98588
- ...next ?? {}
98625
+ ...next
98589
98626
  }
98590
98627
  });
98591
98628
  this.reported = true;
@@ -144203,6 +144240,10 @@ async function startRfc4571Server(params) {
144203
144240
  const firstLoss = deferred();
144204
144241
  let pendingLoss = null;
144205
144242
  let toreDown = false;
144243
+ let audioFrames = 0;
144244
+ let videoAccessUnits = 0;
144245
+ let lastAudioAt = 0;
144246
+ let audioStallReported = false;
144206
144247
  let serving = false;
144207
144248
  let lastVideoAt = 0;
144208
144249
  const reportLoss = (loss) => {
@@ -144256,9 +144297,11 @@ async function startRfc4571Server(params) {
144256
144297
  await stream.stop().catch(() => void 0);
144257
144298
  throw err;
144258
144299
  }
144300
+ const graceStartedAt = Date.now();
144259
144301
  await Promise.race([firstAudio.promise, new Promise((resolve) => {
144260
144302
  setTimeout(resolve, audioGraceMs).unref?.();
144261
144303
  })]);
144304
+ const waitedMs = Date.now() - graceStartedAt;
144262
144305
  if (pendingLoss !== null) {
144263
144306
  await stream.stop().catch(() => void 0);
144264
144307
  throw new Error(`relay lost before the server was up (${describeLoss(pendingLoss)})`);
@@ -144278,8 +144321,38 @@ async function startRfc4571Server(params) {
144278
144321
  configHex: audioFacts.configHex
144279
144322
  } : void 0);
144280
144323
  const muxer = new Rfc4571Muxer(consoleInto(logger), VIDEO_PT, audioFacts !== null ? AUDIO_PT : void 0);
144281
- stream.on("videoAccessUnit", (e) => muxer.sendVideoAccessUnit("H264", e.data, e.isKeyframe, e.microseconds));
144282
- if (audioFacts !== null) stream.on("audioFrame", (adts) => muxer.sendAudioAdtsFrame(adts));
144324
+ stream.on("videoAccessUnit", (e) => {
144325
+ videoAccessUnits += 1;
144326
+ muxer.sendVideoAccessUnit("H264", e.data, e.isKeyframe, e.microseconds);
144327
+ });
144328
+ if (audioFacts !== null) {
144329
+ logger.info("rfc4571: serving audio", { meta: {
144330
+ codec: "aac",
144331
+ sampleRate: audioFacts.sampleRate,
144332
+ channels: audioFacts.channels,
144333
+ waitedMs
144334
+ } });
144335
+ lastAudioAt = Date.now();
144336
+ stream.on("audioFrame", (adts) => {
144337
+ audioFrames += 1;
144338
+ lastAudioAt = Date.now();
144339
+ muxer.sendAudioAdtsFrame(adts);
144340
+ });
144341
+ } else {
144342
+ logger.info("rfc4571: no audio within the grace — serving video-only", { meta: {
144343
+ waitedMs,
144344
+ audioGraceMs
144345
+ } });
144346
+ let reportedLate = false;
144347
+ stream.on("audioFrame", () => {
144348
+ if (reportedLate) return;
144349
+ reportedLate = true;
144350
+ logger.warn("rfc4571: audio arrived after the grace — this session stays video-only", { meta: {
144351
+ lateByMs: Date.now() - graceStartedAt - waitedMs,
144352
+ audioGraceMs
144353
+ } });
144354
+ });
144355
+ }
144283
144356
  let liveClients = 0;
144284
144357
  let idleTimer = null;
144285
144358
  const server = node_net.default.createServer((socket) => {
@@ -144309,7 +144382,16 @@ async function startRfc4571Server(params) {
144309
144382
  const addr = server.address();
144310
144383
  if (addr === null || typeof addr === "string") throw new Error("failed to bind the RFC4571 server");
144311
144384
  const stallSweep = setInterval(() => {
144312
- if (toreDown || Date.now() - lastVideoAt < stallTimeoutMs) return;
144385
+ if (toreDown) return;
144386
+ if (audioFacts !== null && !audioStallReported && Date.now() - lastAudioAt >= stallTimeoutMs) {
144387
+ audioStallReported = true;
144388
+ logger.warn("rfc4571: audio stopped while the video kept flowing", { meta: {
144389
+ audioFrames,
144390
+ videoAccessUnits,
144391
+ silentForMs: Date.now() - lastAudioAt
144392
+ } });
144393
+ }
144394
+ if (Date.now() - lastVideoAt < stallTimeoutMs) return;
144313
144395
  reportLoss({
144314
144396
  reason: "stalled",
144315
144397
  error: null
@@ -144328,6 +144410,11 @@ async function startRfc4571Server(params) {
144328
144410
  } : null,
144329
144411
  close: async () => {
144330
144412
  toreDown = true;
144413
+ logger.info("rfc4571: session ended", { meta: {
144414
+ videoAccessUnits,
144415
+ audioFrames,
144416
+ declaredAudio: audioFacts !== null
144417
+ } });
144331
144418
  clearInterval(stallSweep);
144332
144419
  if (idleTimer !== null) clearTimeout(idleTimer);
144333
144420
  try {
package/dist/addon.mjs CHANGED
@@ -72602,7 +72602,17 @@ var EventDensityBucketSchema = object({
72602
72602
  bucketStart: number(),
72603
72603
  motion: number().int(),
72604
72604
  object: number().int(),
72605
- audio: number().int()
72605
+ audio: number().int(),
72606
+ /**
72607
+ * Energy-weighted mean dBFS of the audio events in this bucket — the LEVEL,
72608
+ * as opposed to `audio`, which is their COUNT (D431).
72609
+ *
72610
+ * ABSENT when the bucket holds no audio event. Absence means "nothing was
72611
+ * measured", which is not the same claim as silence, and a lane that cannot
72612
+ * tell them apart paints a quiet hour at full scale — which is exactly what
72613
+ * the count-on-a-level-meter did.
72614
+ */
72615
+ audioDbfs: number().optional()
72606
72616
  });
72607
72617
  /**
72608
72618
  * One camera's row in a `getEventDensityBatch` answer.
@@ -82790,7 +82800,23 @@ var RecordingRangeSchema = object({
82790
82800
  });
82791
82801
  var RecordingAvailabilitySchema = object({
82792
82802
  deviceId: number(),
82793
- ranges: array(RecordingRangeSchema)
82803
+ ranges: array(RecordingRangeSchema),
82804
+ /**
82805
+ * Every profile this camera has footage in — not only the one `ranges`
82806
+ * describes (D433).
82807
+ *
82808
+ * `ranges` answers for ONE profile by design: the timeline is a single bar,
82809
+ * and enumerating all of them triples the directory reads for a bar that
82810
+ * would look identical. But the answer used to say nothing about that, so a
82811
+ * caller asking "what can I export?" read the single preferred profile as
82812
+ * the only one that exists — on this deployment every camera records `high`
82813
+ * AND `low`, the low tree holding 14 GB for camera 615 alone, and the export
82814
+ * UI could not see it.
82815
+ *
82816
+ * Cheap: it is the same shallow directory read the profile CHOICE already
82817
+ * makes. Ask for a specific profile's ranges with the `profile` input.
82818
+ */
82819
+ profilesWithFootage: array(string())
82794
82820
  });
82795
82821
  var RecordingDaysSchema = object({
82796
82822
  deviceId: number(),
@@ -82816,7 +82842,11 @@ var RecordingDaysSchema = object({
82816
82842
  var RecordingAvailabilityForDeviceSchema = object({
82817
82843
  deviceId: number(),
82818
82844
  read: _enum(["read", "unreadable"]),
82819
- ranges: array(RecordingRangeSchema).readonly()
82845
+ ranges: array(RecordingRangeSchema).readonly(),
82846
+ /** Same field, same meaning, as the singular answer (D433). A row that
82847
+ * dropped it would tell a grid caller the archive holds one profile.
82848
+ * Empty on an `'unreadable'` row: nobody looked. */
82849
+ profilesWithFootage: array(string()).readonly()
82820
82850
  });
82821
82851
  /**
82822
82852
  * One camera's row in a `getDaysWithRecordingsBatch` answer. Same rule as
@@ -83106,7 +83136,14 @@ var ReadWindowBytesResultSchema = discriminatedUnion("kind", [object({
83106
83136
  method(object({
83107
83137
  deviceId: number(),
83108
83138
  fromMs: number(),
83109
- toMs: number()
83139
+ toMs: number(),
83140
+ /**
83141
+ * Answer for THIS profile instead of the preferred one (D433). Absent
83142
+ * keeps the timeline's behaviour — one bar, one profile, one set of
83143
+ * reads. `profilesWithFootage` on the answer says what may be asked
83144
+ * for.
83145
+ */
83146
+ profile: string().optional()
83110
83147
  }), RecordingAvailabilitySchema, {
83111
83148
  kind: "query",
83112
83149
  auth: "protected"
@@ -98472,7 +98509,7 @@ function dreameNavigationCatalog(disabledEntryIds = DREAME_NAVIGATION_DISABLED_E
98472
98509
  function resolveNavigationFeatures(overrides) {
98473
98510
  return {
98474
98511
  ...DREAME_NAVIGATION_DEFAULT_FEATURES,
98475
- ...overrides?.features ?? {}
98512
+ ...overrides?.features
98476
98513
  };
98477
98514
  }
98478
98515
  /** The disabled entry ids in force for ONE device: the addon set plus its own. */
@@ -98582,7 +98619,7 @@ var DreameNetworkLinkMonitor = class {
98582
98619
  meta: {
98583
98620
  reason,
98584
98621
  rawBars: bars,
98585
- ...next ?? {}
98622
+ ...next
98586
98623
  }
98587
98624
  });
98588
98625
  this.reported = true;
@@ -144200,6 +144237,10 @@ async function startRfc4571Server(params) {
144200
144237
  const firstLoss = deferred();
144201
144238
  let pendingLoss = null;
144202
144239
  let toreDown = false;
144240
+ let audioFrames = 0;
144241
+ let videoAccessUnits = 0;
144242
+ let lastAudioAt = 0;
144243
+ let audioStallReported = false;
144203
144244
  let serving = false;
144204
144245
  let lastVideoAt = 0;
144205
144246
  const reportLoss = (loss) => {
@@ -144253,9 +144294,11 @@ async function startRfc4571Server(params) {
144253
144294
  await stream.stop().catch(() => void 0);
144254
144295
  throw err;
144255
144296
  }
144297
+ const graceStartedAt = Date.now();
144256
144298
  await Promise.race([firstAudio.promise, new Promise((resolve) => {
144257
144299
  setTimeout(resolve, audioGraceMs).unref?.();
144258
144300
  })]);
144301
+ const waitedMs = Date.now() - graceStartedAt;
144259
144302
  if (pendingLoss !== null) {
144260
144303
  await stream.stop().catch(() => void 0);
144261
144304
  throw new Error(`relay lost before the server was up (${describeLoss(pendingLoss)})`);
@@ -144275,8 +144318,38 @@ async function startRfc4571Server(params) {
144275
144318
  configHex: audioFacts.configHex
144276
144319
  } : void 0);
144277
144320
  const muxer = new Rfc4571Muxer(consoleInto(logger), VIDEO_PT, audioFacts !== null ? AUDIO_PT : void 0);
144278
- stream.on("videoAccessUnit", (e) => muxer.sendVideoAccessUnit("H264", e.data, e.isKeyframe, e.microseconds));
144279
- if (audioFacts !== null) stream.on("audioFrame", (adts) => muxer.sendAudioAdtsFrame(adts));
144321
+ stream.on("videoAccessUnit", (e) => {
144322
+ videoAccessUnits += 1;
144323
+ muxer.sendVideoAccessUnit("H264", e.data, e.isKeyframe, e.microseconds);
144324
+ });
144325
+ if (audioFacts !== null) {
144326
+ logger.info("rfc4571: serving audio", { meta: {
144327
+ codec: "aac",
144328
+ sampleRate: audioFacts.sampleRate,
144329
+ channels: audioFacts.channels,
144330
+ waitedMs
144331
+ } });
144332
+ lastAudioAt = Date.now();
144333
+ stream.on("audioFrame", (adts) => {
144334
+ audioFrames += 1;
144335
+ lastAudioAt = Date.now();
144336
+ muxer.sendAudioAdtsFrame(adts);
144337
+ });
144338
+ } else {
144339
+ logger.info("rfc4571: no audio within the grace — serving video-only", { meta: {
144340
+ waitedMs,
144341
+ audioGraceMs
144342
+ } });
144343
+ let reportedLate = false;
144344
+ stream.on("audioFrame", () => {
144345
+ if (reportedLate) return;
144346
+ reportedLate = true;
144347
+ logger.warn("rfc4571: audio arrived after the grace — this session stays video-only", { meta: {
144348
+ lateByMs: Date.now() - graceStartedAt - waitedMs,
144349
+ audioGraceMs
144350
+ } });
144351
+ });
144352
+ }
144280
144353
  let liveClients = 0;
144281
144354
  let idleTimer = null;
144282
144355
  const server = net.createServer((socket) => {
@@ -144306,7 +144379,16 @@ async function startRfc4571Server(params) {
144306
144379
  const addr = server.address();
144307
144380
  if (addr === null || typeof addr === "string") throw new Error("failed to bind the RFC4571 server");
144308
144381
  const stallSweep = setInterval(() => {
144309
- if (toreDown || Date.now() - lastVideoAt < stallTimeoutMs) return;
144382
+ if (toreDown) return;
144383
+ if (audioFacts !== null && !audioStallReported && Date.now() - lastAudioAt >= stallTimeoutMs) {
144384
+ audioStallReported = true;
144385
+ logger.warn("rfc4571: audio stopped while the video kept flowing", { meta: {
144386
+ audioFrames,
144387
+ videoAccessUnits,
144388
+ silentForMs: Date.now() - lastAudioAt
144389
+ } });
144390
+ }
144391
+ if (Date.now() - lastVideoAt < stallTimeoutMs) return;
144310
144392
  reportLoss({
144311
144393
  reason: "stalled",
144312
144394
  error: null
@@ -144325,6 +144407,11 @@ async function startRfc4571Server(params) {
144325
144407
  } : null,
144326
144408
  close: async () => {
144327
144409
  toreDown = true;
144410
+ logger.info("rfc4571: session ended", { meta: {
144411
+ videoAccessUnits,
144412
+ audioFrames,
144413
+ declaredAudio: audioFacts !== null
144414
+ } });
144328
144415
  clearInterval(stallSweep);
144329
144416
  if (idleTimer !== null) clearTimeout(idleTimer);
144330
144417
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-dreame",
3
- "version": "0.2.93",
3
+ "version": "0.2.94",
4
4
  "description": "Dreame robot-vacuum / lawn-mower device-provider addon for CamStack — wraps the @apocaliss92/nodedreame Dreamehome cloud client",
5
5
  "keywords": [
6
6
  "camstack",