@camstack/addon-provider-dreame 0.2.148 → 0.2.149

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
@@ -44,7 +44,7 @@ let net = require("net");
44
44
  net = __toESM(net, 1);
45
45
  let zlib = require("zlib");
46
46
  zlib = __toESM(zlib, 1);
47
- //#region ../types/dist/event-category-7pZEBmVg.mjs
47
+ //#region ../types/dist/event-category-BfJwUd2S.mjs
48
48
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
49
49
  EventCategory["SystemBoot"] = "system.boot";
50
50
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -361,6 +361,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
361
361
  */
362
362
  EventCategory["PipelineDetectionBlind"] = "pipeline.detection-blind";
363
363
  /**
364
+ * A camera's motion source has fired constantly with no subject for an hour
365
+ * (motion duty or session count over the bar, zero detections) — it is not
366
+ * blind, its motion zones or sensitivity want tuning (D907). Paired with
367
+ * {@link PipelineMotionCalm}.
368
+ */
369
+ EventCategory["PipelineMotionNoisy"] = "pipeline.motion-noisy";
370
+ /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
371
+ EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
372
+ /**
364
373
  * Per-camera pipeline config was mutated by the orchestrator
365
374
  * (3-level settings change via `setAgentAddonDefaults` /
366
375
  * `setCameraStepToggle` / `setCameraPipelineForAgent` or a
@@ -5467,7 +5476,7 @@ var ZodIssueCode$1 = {
5467
5476
  var ZodFirstPartyTypeKind$1;
5468
5477
  ZodFirstPartyTypeKind$1 || (ZodFirstPartyTypeKind$1 = {});
5469
5478
  //#endregion
5470
- //#region ../types/dist/sleep-CueztlR1.mjs
5479
+ //#region ../types/dist/sleep-DJFUfKvA.mjs
5471
5480
  /**
5472
5481
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5473
5482
  * window to float samples (D455).
@@ -7996,7 +8005,7 @@ var EntryDescriptorSchema = object({
7996
8005
  /** JSON Schema of the entry body; `secret: true` marks a password field. */
7997
8006
  fields: record(string(), unknown())
7998
8007
  }).strict();
7999
- /** What crosses the wire and sits in the engine's catalog: JSON only, no Zod. */
8008
+ /** What crosses the wire from an addon: JSON only, no Zod. An unknown key is refused. */
8000
8009
  var RecordKindDeclarationSchema = object({
8001
8010
  database: _enum(RECORD_DATABASES).default("settings"),
8002
8011
  class: _enum(RECORD_CLASSES),
@@ -8015,14 +8024,6 @@ var RecordKindDeclarationSchema = object({
8015
8024
  secretPaths: array(string().min(1)).default([]),
8016
8025
  /** The id key of each array that carries a secret (`{ headers: 'name' }`). */
8017
8026
  secretArrayKeys: record(string(), string()).default({}),
8018
- /**
8019
- * RETIRED (D900): the legacy table a kind adopted from during the records
8020
- * migration. Nothing declares it any more and nothing reads it; the schema
8021
- * still ACCEPTS it because the catalog rows the migration wrote carry it
8022
- * (an orphaned kind is never redeclared) and an addon built before the
8023
- * removal still sends it. Never set it.
8024
- */
8025
- adoptsFrom: string().min(1).optional(),
8026
8027
  /** Kinds that must agree through `transact` sit in one file under one family name. */
8027
8028
  transactFamily: string().min(1).optional(),
8028
8029
  entry: EntryDescriptorSchema.optional()
@@ -12714,17 +12715,6 @@ object({
12714
12715
  createdAt: number(),
12715
12716
  updatedAt: number()
12716
12717
  });
12717
- object({
12718
- id: string(),
12719
- label: string(),
12720
- isAdmin: boolean().default(false),
12721
- allowedProviders: AllowedProviderSchema,
12722
- allowedDevices: AllowedDevicesSchema,
12723
- tokenHash: string(),
12724
- tokenPrefix: string(),
12725
- createdAt: number(),
12726
- lastUsedAt: number().optional()
12727
- });
12728
12718
  object({
12729
12719
  id: string(),
12730
12720
  userId: string(),
@@ -16706,16 +16696,36 @@ var RecordKindStateSchema = _enum([
16706
16696
  var DeclareKindResultSchema = object({
16707
16697
  kindId: number().int(),
16708
16698
  state: RecordKindStateSchema,
16709
- createdIndexes: array(string()),
16710
- /**
16711
- * RETIRED (D900): always `null`. It carried the records-migration boot guard;
16712
- * it stays on the wire, null, because an addon built before the removal reads
16713
- * an ABSENT field as "adoption pending" and refuses to start.
16714
- */
16715
- adoptionPending: _null()
16699
+ createdIndexes: array(string())
16716
16700
  });
16717
16701
  /** Keychain refs a write released (the old marker is gone from the row): the owner deletes the values. */
16718
16702
  var SecretRefsList = array(string());
16703
+ /**
16704
+ * One op of a `transact`: the single-row writes, each on a kind of the caller.
16705
+ * All ops of one call share ONE database file and ONE `transactFamily`.
16706
+ */
16707
+ var RecordTransactOpSchema = discriminatedUnion("op", [
16708
+ object({
16709
+ op: literal("put"),
16710
+ kind: RecordKindNameSchema,
16711
+ record: RecordInputSchema,
16712
+ ifRev: number().int().min(0).optional()
16713
+ }),
16714
+ object({
16715
+ op: literal("patch"),
16716
+ kind: RecordKindNameSchema,
16717
+ id: RecordIdSchema,
16718
+ data: RecordBodySchema,
16719
+ at: number().int().nullable().optional(),
16720
+ expiresAt: number().int().nullable().optional(),
16721
+ ifRev: number().int().min(1).optional()
16722
+ }),
16723
+ object({
16724
+ op: literal("delete"),
16725
+ kind: RecordKindNameSchema,
16726
+ id: RecordIdSchema
16727
+ })
16728
+ ]);
16719
16729
  var RecordSecretRefsSchema = record(string(), record(string(), string()));
16720
16730
  method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema) }), object({ kinds: record(string(), DeclareKindResultSchema) }), {
16721
16731
  kind: "mutation",
@@ -16822,6 +16832,16 @@ method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema)
16822
16832
  }), {
16823
16833
  kind: "mutation",
16824
16834
  auth: "admin"
16835
+ }), method(object({ ops: array(RecordTransactOpSchema).min(1).max(RECORD_BATCH_MAX) }), object({
16836
+ results: array(object({
16837
+ rev: number().int().nullable(),
16838
+ deleted: boolean()
16839
+ })),
16840
+ releasedRefs: SecretRefsList,
16841
+ secretRefs: SecretRefsList
16842
+ }), {
16843
+ kind: "mutation",
16844
+ auth: "admin"
16825
16845
  }), method(object({
16826
16846
  deviceId: number().int(),
16827
16847
  kinds: array(RecordKindNameSchema).optional()
@@ -42445,9 +42465,13 @@ var NcSystemEventKindSchema = _enum([
42445
42465
  "node-updating",
42446
42466
  "node-inference-unavailable",
42447
42467
  "detection-blind",
42468
+ "motion-noisy",
42469
+ "motion-calm",
42448
42470
  "recording-stalled",
42449
42471
  "recording-healthy",
42450
42472
  "recording-degraded",
42473
+ "device-attention-required",
42474
+ "device-attention-resolved",
42451
42475
  "storage-location-low",
42452
42476
  "storage-location-critical",
42453
42477
  "storage-location-ok",
@@ -55796,6 +55820,16 @@ var DishwasherStatusSchema = object({
55796
55820
  powerState: string().nullable(),
55797
55821
  saltLevel: DishwasherConsumableLevelSchema.nullable(),
55798
55822
  rinseAidLevel: DishwasherConsumableLevelSchema.nullable(),
55823
+ /**
55824
+ * True while the appliance asks for its filter to be cleaned (smart-filter, filter-system or
55825
+ * combined care-and-filter reminder raised); false when every such reminder reads clear; null
55826
+ * when none is reported. A cleared reminder is not an unknown one.
55827
+ */
55828
+ filterCleaningDue: boolean().nullable(),
55829
+ /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
55830
+ machineCareDue: boolean().nullable(),
55831
+ /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
55832
+ machineCareRemainingRuns: number().int().nonnegative().nullable(),
55799
55833
  /** Option states: null when the appliance does not expose the option or has not said. */
55800
55834
  halfLoad: boolean().nullable(),
55801
55835
  hygienePlus: boolean().nullable(),
@@ -64594,16 +64628,6 @@ var UpdateUserInputSchema = object({
64594
64628
  allowedDevices: record(string(), union([array(string()), literal("*")])).optional(),
64595
64629
  scopes: array(TokenScopeSchema).optional()
64596
64630
  });
64597
- var ApiKeySummarySchema = object({
64598
- id: string(),
64599
- label: string(),
64600
- isAdmin: boolean().default(false),
64601
- allowedProviders: union([array(string()), literal("*")]).optional(),
64602
- allowedDevices: record(string(), union([array(string()), literal("*")])).optional(),
64603
- tokenPrefix: string(),
64604
- createdAt: number(),
64605
- lastUsedAt: number().optional()
64606
- });
64607
64631
  var OauthSessionSummarySchema = object({
64608
64632
  id: string(),
64609
64633
  userId: string(),
@@ -64625,8 +64649,7 @@ var TotpStatusSchema = object({
64625
64649
  confirmedAt: number().nullable()
64626
64650
  });
64627
64651
  /**
64628
- * user-management — singleton capability for user accounts and the
64629
- * legacy API-key rows.
64652
+ * user-management — singleton capability for user accounts.
64630
64653
  *
64631
64654
  * v2 model: every method gated by `auth: 'admin'`. Admin-only delegation
64632
64655
  * — the subset check on `setUserScopes` is moot
@@ -64682,10 +64705,6 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
64682
64705
  }), UserSummarySchema.extend({ passwordHash: string() }).nullable(), {
64683
64706
  kind: "mutation",
64684
64707
  access: "view"
64685
- }), method(_void(), array(ApiKeySummarySchema), { auth: "admin" }), method(object({ id: string() }), object({ success: literal(true) }), {
64686
- kind: "mutation",
64687
- auth: "admin",
64688
- access: "delete"
64689
64708
  }), method(object({ userId: string() }), TotpSetupResultSchema, {
64690
64709
  kind: "mutation",
64691
64710
  auth: "admin",
@@ -74157,6 +74176,12 @@ Object.freeze({
74157
74176
  addonId: null,
74158
74177
  access: "view"
74159
74178
  },
74179
+ "records.transact": {
74180
+ capName: "records",
74181
+ capScope: "system",
74182
+ addonId: null,
74183
+ access: "create"
74184
+ },
74160
74185
  "sceneMonitor.captureReference": {
74161
74186
  capName: "scene-monitor",
74162
74187
  capScope: "device",
@@ -75489,12 +75514,6 @@ Object.freeze({
75489
75514
  addonId: null,
75490
75515
  access: "view"
75491
75516
  },
75492
- "userManagement.listApiKeys": {
75493
- capName: "user-management",
75494
- capScope: "system",
75495
- addonId: null,
75496
- access: "view"
75497
- },
75498
75517
  "userManagement.listExternalIdentities": {
75499
75518
  capName: "user-management",
75500
75519
  capScope: "system",
@@ -75549,12 +75568,6 @@ Object.freeze({
75549
75568
  addonId: null,
75550
75569
  access: "create"
75551
75570
  },
75552
- "userManagement.revokeApiKey": {
75553
- capName: "user-management",
75554
- capScope: "system",
75555
- addonId: null,
75556
- access: "delete"
75557
- },
75558
75571
  "userManagement.revokeOauthSession": {
75559
75572
  capName: "user-management",
75560
75573
  capScope: "system",
@@ -78919,9 +78932,13 @@ new Map(Object.entries({
78919
78932
  "stream-online": "device-events",
78920
78933
  "stream-offline": "device-events",
78921
78934
  "detection-blind": "device-events",
78935
+ "motion-noisy": "device-events",
78936
+ "motion-calm": "device-events",
78922
78937
  "recording-stalled": "device-events",
78923
78938
  "recording-healthy": "device-events",
78924
78939
  "recording-degraded": "device-events",
78940
+ "device-attention-required": "device-events",
78941
+ "device-attention-resolved": "device-events",
78925
78942
  "node-online": "system",
78926
78943
  "node-offline": "system",
78927
78944
  "node-restarting": "system",
@@ -79004,6 +79021,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
79004
79021
  label: "Detecting nothing",
79005
79022
  kinds: ["detection-blind"]
79006
79023
  },
79024
+ {
79025
+ id: "motion-noise",
79026
+ family: "device-events",
79027
+ label: "Motion without a subject",
79028
+ kinds: ["motion-noisy", "motion-calm"]
79029
+ },
79007
79030
  {
79008
79031
  id: "recording",
79009
79032
  family: "device-events",
@@ -79014,6 +79037,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
79014
79037
  "recording-degraded"
79015
79038
  ]
79016
79039
  },
79040
+ {
79041
+ id: "device-attention",
79042
+ family: "device-events",
79043
+ label: "Needs attention",
79044
+ kinds: ["device-attention-required", "device-attention-resolved"]
79045
+ },
79017
79046
  {
79018
79047
  id: "nodes",
79019
79048
  family: "system",
@@ -79153,6 +79182,7 @@ var ALERT_EPISODE = [
79153
79182
  "alert-error",
79154
79183
  "alert-critical-resolved"
79155
79184
  ];
79185
+ var DEVICE_ATTENTION = ["device-attention-required", "device-attention-resolved"];
79156
79186
  var UPDATES = [
79157
79187
  "addon-update-available",
79158
79188
  "server-update-available",
@@ -79182,9 +79212,12 @@ var DEVICE_EVENTS = [
79182
79212
  "stream-online",
79183
79213
  "stream-offline",
79184
79214
  "detection-blind",
79215
+ "motion-noisy",
79216
+ "motion-calm",
79185
79217
  "recording-stalled",
79186
79218
  "recording-healthy",
79187
79219
  "recording-degraded",
79220
+ ...DEVICE_ATTENTION,
79188
79221
  ...ALARM
79189
79222
  ];
79190
79223
  /** Every kind a summary may speak about — the two summarisable families. */
@@ -79352,7 +79385,9 @@ var STATE_SUMMARY_VARS = [
79352
79385
  [...RULE_TEXT],
79353
79386
  [...RULE_TEXT],
79354
79387
  [...RULE_TEXT],
79355
- ([...RULE_TEXT], [...ALERT_EPISODE]),
79388
+ [...RULE_TEXT],
79389
+ [...RULE_TEXT],
79390
+ ([...RULE_TEXT], [...DEVICE_ATTENTION, ...ALERT_EPISODE]),
79356
79391
  [...RULE_TEXT],
79357
79392
  [...RULE_TEXT],
79358
79393
  [...RULE_TEXT],
@@ -79374,11 +79409,11 @@ var STATE_SUMMARY_VARS = [
79374
79409
  [...RULE_TEXT],
79375
79410
  [...RULE_TEXT],
79376
79411
  [...RULE_TEXT],
79412
+ ([...RULE_TEXT], [...DEVICE_ATTENTION, ...ALERT_EPISODE]),
79377
79413
  ([...RULE_TEXT], [...ALERT_EPISODE]),
79414
+ ([...RULE_TEXT], [...DEVICE_ATTENTION, ...ALERT_EPISODE]),
79378
79415
  ([...RULE_TEXT], [...ALERT_EPISODE]),
79379
- ([...RULE_TEXT], [...ALERT_EPISODE]),
79380
- ([...RULE_TEXT], [...ALERT_EPISODE]),
79381
- ([...RULE_TEXT], [...ALERT_EPISODE]),
79416
+ ([...RULE_TEXT], [...DEVICE_ATTENTION, ...ALERT_EPISODE]),
79382
79417
  [...RULE_TEXT],
79383
79418
  [...RULE_TEXT],
79384
79419
  [...RULE_TEXT],
@@ -79454,6 +79489,15 @@ new Map([
79454
79489
  snapshotLabel: "Tanks low or full now",
79455
79490
  snapshotSection: "tanks"
79456
79491
  },
79492
+ {
79493
+ id: "motion-noise",
79494
+ family: "device-events",
79495
+ label: "Motion without a subject (constant / calm again)",
79496
+ bad: ["motion-noisy"],
79497
+ recovery: ["motion-calm"],
79498
+ snapshotLabel: "Cameras with constant motion now",
79499
+ snapshotSection: null
79500
+ },
79457
79501
  {
79458
79502
  id: "recording-health",
79459
79503
  family: "device-events",
@@ -79463,6 +79507,15 @@ new Map([
79463
79507
  snapshotLabel: "Recordings stalled now",
79464
79508
  snapshotSection: null
79465
79509
  },
79510
+ {
79511
+ id: "device-attention",
79512
+ family: "device-events",
79513
+ label: "Device needs attention (raised / resolved)",
79514
+ bad: ["device-attention-required"],
79515
+ recovery: ["device-attention-resolved"],
79516
+ snapshotLabel: "Devices needing attention now",
79517
+ snapshotSection: null
79518
+ },
79466
79519
  {
79467
79520
  id: "node-connectivity",
79468
79521
  family: "system",
@@ -132949,8 +133002,6 @@ function buildConnectionFormSchema() {
132949
133002
  id: "credentials",
132950
133003
  title: "Dreamehome account",
132951
133004
  titleKey: "provider-dreame:connection.s_credentials.title",
132952
- description: "Sign in with your Dreamehome (Dreame app) account credentials.",
132953
- descriptionKey: "provider-dreame:connection.s_credentials.description",
132954
133005
  columns: 1,
132955
133006
  fields: [{
132956
133007
  type: "text",
@@ -132972,8 +133023,6 @@ function buildConnectionFormSchema() {
132972
133023
  id: "region",
132973
133024
  title: "Region",
132974
133025
  titleKey: "provider-dreame:connection.s_region.title",
132975
- description: "Select the Dreame cloud region your account is registered in.",
132976
- descriptionKey: "provider-dreame:connection.s_region.description",
132977
133026
  columns: 1,
132978
133027
  fields: [{
132979
133028
  type: "select",
@@ -135392,6 +135441,81 @@ function rfcUrl(host, port) {
135392
135441
  return `tcp://${host}:${String(port)}`;
135393
135442
  }
135394
135443
  //#endregion
135444
+ //#region src/devices/dreame-pin-attention.ts
135445
+ var REFUSED_CODE_MINUS_ONE = /startMonitor refused \(code -1\)/;
135446
+ /** The refusal an error carries, or `null` when it is some other failure. */
135447
+ function classifyPinRefusal(err, accessCodeSet) {
135448
+ const message = err instanceof Error ? err.message : String(err);
135449
+ if (!REFUSED_CODE_MINUS_ONE.test(message)) return null;
135450
+ return accessCodeSet ? "rejected" : "missing";
135451
+ }
135452
+ function pinAlertId(deviceId) {
135453
+ return `dreame.pin-required:${String(deviceId)}`;
135454
+ }
135455
+ function buildPinAlert(deviceId, deviceName, reason) {
135456
+ const now = Date.now();
135457
+ return {
135458
+ id: pinAlertId(deviceId),
135459
+ category: "device.credentials-required",
135460
+ severity: "critical",
135461
+ title: reason === "missing" ? "Camera PIN required" : "Camera PIN rejected",
135462
+ message: reason === "missing" ? `${deviceName} needs its camera PIN before its video can open. Enter it in the device settings.` : `${deviceName} refused the camera PIN that is set. Enter the correct PIN in the device settings.`,
135463
+ action: {
135464
+ label: "Enter PIN",
135465
+ href: `/devices/${String(deviceId)}`,
135466
+ navigation: "app"
135467
+ },
135468
+ status: "active",
135469
+ read: false,
135470
+ createdAt: now,
135471
+ updatedAt: now,
135472
+ source: {
135473
+ type: "device",
135474
+ id: String(deviceId)
135475
+ },
135476
+ metadata: { episodeKey: reason }
135477
+ };
135478
+ }
135479
+ /**
135480
+ * One device's PIN episode. It raises the alert ONCE per episode, and while the
135481
+ * episode is open it tells the wake path to stand down: a refusal that needs the
135482
+ * operator is terminal until the PIN setting changes, so the broker's 30 s
135483
+ * re-dial must not re-open the monitor and fail again.
135484
+ */
135485
+ var PinAttention = class {
135486
+ ports;
135487
+ open = null;
135488
+ constructor(ports) {
135489
+ this.ports = ports;
135490
+ }
135491
+ /** True while the PIN is known to be unusable — the wake must not try. */
135492
+ get blocked() {
135493
+ return this.open !== null;
135494
+ }
135495
+ /** A wake failed. Raises the alert on the first refusal of an episode. */
135496
+ async refused(reason) {
135497
+ if (this.open !== null) return "already-open";
135498
+ this.open = reason;
135499
+ await this.ports.emit(buildPinAlert(this.ports.deviceId, this.ports.deviceName(), reason));
135500
+ return "raised";
135501
+ }
135502
+ /** A monitor opened: the PIN works, the episode is over. */
135503
+ async succeeded() {
135504
+ return this.settingChanged();
135505
+ }
135506
+ /**
135507
+ * The PIN setting changed: the episode is no longer known to be terminal, so
135508
+ * the next wake tries again. The alert is resolved here — a still-wrong PIN
135509
+ * raises a fresh episode on that wake.
135510
+ */
135511
+ async settingChanged() {
135512
+ if (this.open === null) return false;
135513
+ this.open = null;
135514
+ await this.ports.resolve(pinAlertId(this.ports.deviceId));
135515
+ return true;
135516
+ }
135517
+ };
135518
+ //#endregion
135395
135519
  //#region src/devices/dreame-navigation-actions.ts
135396
135520
  /** Forward throttle magnitude for a full-forward nudge (device speed units). */
135397
135521
  var FORWARD_SPEED = 200;
@@ -181583,15 +181707,13 @@ var CAMERA_SETTINGS_SCHEMA = { sections: [{
181583
181707
  id: "camera-backend",
181584
181708
  title: "Video backend",
181585
181709
  titleKey: "provider-dreame:camera_settings.s_camera-backend.title",
181586
- description: "Which cloud video service this robot streams through. The two are not interchangeable: only the Aliyun path is implemented here — a robot on Tencent can be seen in the Dreame app but not by CamStack, because Tencent offers it a proprietary P2P plane we do not speak. Changing this asks the ROBOT to switch and waits until its SDK reports ready; the robot is the authority on where it actually is.",
181710
+ description: "Only the Aliyun path is implemented; a robot on Tencent cannot be viewed by CamStack. Changing this asks the robot to switch.",
181587
181711
  descriptionKey: "provider-dreame:camera_settings.s_camera-backend.description",
181588
181712
  fields: [{
181589
181713
  type: "select",
181590
181714
  key: "videoVendor",
181591
181715
  label: "Backend",
181592
181716
  labelKey: "provider-dreame:camera_settings.s_camera-backend.videoVendor.label",
181593
- description: "Aliyun streams through a plain relay and works today. Tencent is offered so the other path can be tested once it exists.",
181594
- descriptionKey: "provider-dreame:camera_settings.s_camera-backend.videoVendor.description",
181595
181717
  options: [{
181596
181718
  value: "ali",
181597
181719
  label: "Aliyun LinkVisual (supported)",
@@ -181777,6 +181899,10 @@ var DreameCameraDevice = class extends DreameChildDevice {
181777
181899
  const data = event.data;
181778
181900
  if ((typeof data === "object" && data !== null && "deviceId" in data ? data.deviceId : null) !== this.id) return;
181779
181901
  if (this.session === null) {
181902
+ if (this.pinAttention.blocked) {
181903
+ this.ctx.logger.debug("robot camera: wake skipped — the camera PIN needs the operator", { tags: { deviceId: this.id } });
181904
+ return;
181905
+ }
181780
181906
  this.openStream().catch(() => void 0);
181781
181907
  return;
181782
181908
  }
@@ -181863,8 +181989,65 @@ var DreameCameraDevice = class extends DreameChildDevice {
181863
181989
  durationMs: Date.now() - startedAt
181864
181990
  };
181865
181991
  }
181992
+ /**
181993
+ * The PIN episode (D911): a monitor refused for want of the camera PIN needs
181994
+ * the operator, so it raises ONE critical alert and the wake stands down
181995
+ * until the PIN setting changes.
181996
+ */
181997
+ pinAttention = new PinAttention({
181998
+ deviceId: this.id,
181999
+ deviceName: () => this.name,
182000
+ emit: async (alert) => {
182001
+ const emit = this.ctx.api.alerts?.emit;
182002
+ if (emit === void 0) {
182003
+ this.ctx.logger.warn("robot camera: PIN alert skipped — alerts capability missing", { tags: { deviceId: this.id } });
182004
+ return;
182005
+ }
182006
+ try {
182007
+ await emit.mutate(alert);
182008
+ this.ctx.logger.warn("robot camera: PIN alert raised — the wake stops until the PIN changes", {
182009
+ tags: { deviceId: this.id },
182010
+ meta: {
182011
+ alertId: alert.id,
182012
+ episodeKey: alert.metadata?.["episodeKey"]
182013
+ }
182014
+ });
182015
+ } catch (err) {
182016
+ this.ctx.logger.warn("robot camera: PIN alert emit failed", {
182017
+ tags: { deviceId: this.id },
182018
+ meta: { error: errMsg(err) }
182019
+ });
182020
+ }
182021
+ },
182022
+ resolve: async (alertId) => {
182023
+ const update = this.ctx.api.alerts?.update;
182024
+ if (update === void 0) return;
182025
+ try {
182026
+ await update.mutate({
182027
+ alertId,
182028
+ patch: {
182029
+ status: "completed",
182030
+ read: true
182031
+ }
182032
+ });
182033
+ this.ctx.logger.info("robot camera: PIN alert resolved", {
182034
+ tags: { deviceId: this.id },
182035
+ meta: { alertId }
182036
+ });
182037
+ } catch (err) {
182038
+ this.ctx.logger.warn("robot camera: PIN alert resolve failed", {
182039
+ tags: { deviceId: this.id },
182040
+ meta: {
182041
+ alertId,
182042
+ error: errMsg(err)
182043
+ }
182044
+ });
182045
+ }
182046
+ }
182047
+ });
181866
182048
  openStream() {
181867
182049
  if (this.opening !== null) return this.opening;
182050
+ if (this.pinAttention.blocked) return Promise.reject(/* @__PURE__ */ new Error("camera PIN unusable — waiting for the operator"));
181868
182051
  const run = (async () => {
181869
182052
  const startedAt = Date.now();
181870
182053
  try {
@@ -181896,6 +182079,7 @@ var DreameCameraDevice = class extends DreameChildDevice {
181896
182079
  this.publishBattery();
181897
182080
  this.publishCatalog();
181898
182081
  await this.publishRelay(this.session);
182082
+ this.pinAttention.succeeded();
181899
182083
  this.ctx.logger.info("robot camera: monitor open, RTP server published", {
181900
182084
  tags: { deviceId: this.id },
181901
182085
  meta: {
@@ -181912,6 +182096,9 @@ var DreameCameraDevice = class extends DreameChildDevice {
181912
182096
  error: errMsg(err)
181913
182097
  }
181914
182098
  });
182099
+ const code = this.config.get("navigationAccessCode");
182100
+ const refusal = classifyPinRefusal(err, code !== void 0 && code !== "");
182101
+ if (refusal !== null) await this.pinAttention.refused(refusal);
181915
182102
  throw err;
181916
182103
  }
181917
182104
  })();
@@ -182129,6 +182316,7 @@ var DreameCameraDevice = class extends DreameChildDevice {
182129
182316
  }
182130
182317
  }
182131
182318
  if (!("navigationAccessCode" in patch)) return;
182319
+ await this.pinAttention.settingChanged();
182132
182320
  await this.closeStream("removed");
182133
182321
  this.controller = null;
182134
182322
  this.controllerHandle = null;
@@ -183948,8 +184136,6 @@ var DreameVacuumMapDevice = class extends DreameChildDevice {
183948
184136
  id: "map-style",
183949
184137
  title: "Map Style",
183950
184138
  titleKey: "provider-dreame:schema.s_map-style.title",
183951
- description: "Colour scheme and overlays for the rendered cleaning map.",
183952
- descriptionKey: "provider-dreame:schema.s_map-style.description",
183953
184139
  columns: 2,
183954
184140
  fields: [
183955
184141
  {
package/dist/addon.mjs CHANGED
@@ -41,7 +41,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
41
41
  var __toCommonJS = (mod) => __hasOwnProp.call(mod, "module.exports") ? mod["module.exports"] : __copyProps(__defProp({}, "__esModule", { value: true }), mod);
42
42
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
43
43
  //#endregion
44
- //#region ../types/dist/event-category-7pZEBmVg.mjs
44
+ //#region ../types/dist/event-category-BfJwUd2S.mjs
45
45
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
46
46
  EventCategory["SystemBoot"] = "system.boot";
47
47
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -358,6 +358,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
358
358
  */
359
359
  EventCategory["PipelineDetectionBlind"] = "pipeline.detection-blind";
360
360
  /**
361
+ * A camera's motion source has fired constantly with no subject for an hour
362
+ * (motion duty or session count over the bar, zero detections) — it is not
363
+ * blind, its motion zones or sensitivity want tuning (D907). Paired with
364
+ * {@link PipelineMotionCalm}.
365
+ */
366
+ EventCategory["PipelineMotionNoisy"] = "pipeline.motion-noisy";
367
+ /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
368
+ EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
369
+ /**
361
370
  * Per-camera pipeline config was mutated by the orchestrator
362
371
  * (3-level settings change via `setAgentAddonDefaults` /
363
372
  * `setCameraStepToggle` / `setCameraPipelineForAgent` or a
@@ -5464,7 +5473,7 @@ var ZodIssueCode$1 = {
5464
5473
  var ZodFirstPartyTypeKind$1;
5465
5474
  ZodFirstPartyTypeKind$1 || (ZodFirstPartyTypeKind$1 = {});
5466
5475
  //#endregion
5467
- //#region ../types/dist/sleep-CueztlR1.mjs
5476
+ //#region ../types/dist/sleep-DJFUfKvA.mjs
5468
5477
  /**
5469
5478
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5470
5479
  * window to float samples (D455).
@@ -7993,7 +8002,7 @@ var EntryDescriptorSchema = object({
7993
8002
  /** JSON Schema of the entry body; `secret: true` marks a password field. */
7994
8003
  fields: record(string(), unknown())
7995
8004
  }).strict();
7996
- /** What crosses the wire and sits in the engine's catalog: JSON only, no Zod. */
8005
+ /** What crosses the wire from an addon: JSON only, no Zod. An unknown key is refused. */
7997
8006
  var RecordKindDeclarationSchema = object({
7998
8007
  database: _enum(RECORD_DATABASES).default("settings"),
7999
8008
  class: _enum(RECORD_CLASSES),
@@ -8012,14 +8021,6 @@ var RecordKindDeclarationSchema = object({
8012
8021
  secretPaths: array(string().min(1)).default([]),
8013
8022
  /** The id key of each array that carries a secret (`{ headers: 'name' }`). */
8014
8023
  secretArrayKeys: record(string(), string()).default({}),
8015
- /**
8016
- * RETIRED (D900): the legacy table a kind adopted from during the records
8017
- * migration. Nothing declares it any more and nothing reads it; the schema
8018
- * still ACCEPTS it because the catalog rows the migration wrote carry it
8019
- * (an orphaned kind is never redeclared) and an addon built before the
8020
- * removal still sends it. Never set it.
8021
- */
8022
- adoptsFrom: string().min(1).optional(),
8023
8024
  /** Kinds that must agree through `transact` sit in one file under one family name. */
8024
8025
  transactFamily: string().min(1).optional(),
8025
8026
  entry: EntryDescriptorSchema.optional()
@@ -12711,17 +12712,6 @@ object({
12711
12712
  createdAt: number(),
12712
12713
  updatedAt: number()
12713
12714
  });
12714
- object({
12715
- id: string(),
12716
- label: string(),
12717
- isAdmin: boolean().default(false),
12718
- allowedProviders: AllowedProviderSchema,
12719
- allowedDevices: AllowedDevicesSchema,
12720
- tokenHash: string(),
12721
- tokenPrefix: string(),
12722
- createdAt: number(),
12723
- lastUsedAt: number().optional()
12724
- });
12725
12715
  object({
12726
12716
  id: string(),
12727
12717
  userId: string(),
@@ -16703,16 +16693,36 @@ var RecordKindStateSchema = _enum([
16703
16693
  var DeclareKindResultSchema = object({
16704
16694
  kindId: number().int(),
16705
16695
  state: RecordKindStateSchema,
16706
- createdIndexes: array(string()),
16707
- /**
16708
- * RETIRED (D900): always `null`. It carried the records-migration boot guard;
16709
- * it stays on the wire, null, because an addon built before the removal reads
16710
- * an ABSENT field as "adoption pending" and refuses to start.
16711
- */
16712
- adoptionPending: _null()
16696
+ createdIndexes: array(string())
16713
16697
  });
16714
16698
  /** Keychain refs a write released (the old marker is gone from the row): the owner deletes the values. */
16715
16699
  var SecretRefsList = array(string());
16700
+ /**
16701
+ * One op of a `transact`: the single-row writes, each on a kind of the caller.
16702
+ * All ops of one call share ONE database file and ONE `transactFamily`.
16703
+ */
16704
+ var RecordTransactOpSchema = discriminatedUnion("op", [
16705
+ object({
16706
+ op: literal("put"),
16707
+ kind: RecordKindNameSchema,
16708
+ record: RecordInputSchema,
16709
+ ifRev: number().int().min(0).optional()
16710
+ }),
16711
+ object({
16712
+ op: literal("patch"),
16713
+ kind: RecordKindNameSchema,
16714
+ id: RecordIdSchema,
16715
+ data: RecordBodySchema,
16716
+ at: number().int().nullable().optional(),
16717
+ expiresAt: number().int().nullable().optional(),
16718
+ ifRev: number().int().min(1).optional()
16719
+ }),
16720
+ object({
16721
+ op: literal("delete"),
16722
+ kind: RecordKindNameSchema,
16723
+ id: RecordIdSchema
16724
+ })
16725
+ ]);
16716
16726
  var RecordSecretRefsSchema = record(string(), record(string(), string()));
16717
16727
  method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema) }), object({ kinds: record(string(), DeclareKindResultSchema) }), {
16718
16728
  kind: "mutation",
@@ -16819,6 +16829,16 @@ method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema)
16819
16829
  }), {
16820
16830
  kind: "mutation",
16821
16831
  auth: "admin"
16832
+ }), method(object({ ops: array(RecordTransactOpSchema).min(1).max(RECORD_BATCH_MAX) }), object({
16833
+ results: array(object({
16834
+ rev: number().int().nullable(),
16835
+ deleted: boolean()
16836
+ })),
16837
+ releasedRefs: SecretRefsList,
16838
+ secretRefs: SecretRefsList
16839
+ }), {
16840
+ kind: "mutation",
16841
+ auth: "admin"
16822
16842
  }), method(object({
16823
16843
  deviceId: number().int(),
16824
16844
  kinds: array(RecordKindNameSchema).optional()
@@ -42442,9 +42462,13 @@ var NcSystemEventKindSchema = _enum([
42442
42462
  "node-updating",
42443
42463
  "node-inference-unavailable",
42444
42464
  "detection-blind",
42465
+ "motion-noisy",
42466
+ "motion-calm",
42445
42467
  "recording-stalled",
42446
42468
  "recording-healthy",
42447
42469
  "recording-degraded",
42470
+ "device-attention-required",
42471
+ "device-attention-resolved",
42448
42472
  "storage-location-low",
42449
42473
  "storage-location-critical",
42450
42474
  "storage-location-ok",
@@ -55793,6 +55817,16 @@ var DishwasherStatusSchema = object({
55793
55817
  powerState: string().nullable(),
55794
55818
  saltLevel: DishwasherConsumableLevelSchema.nullable(),
55795
55819
  rinseAidLevel: DishwasherConsumableLevelSchema.nullable(),
55820
+ /**
55821
+ * True while the appliance asks for its filter to be cleaned (smart-filter, filter-system or
55822
+ * combined care-and-filter reminder raised); false when every such reminder reads clear; null
55823
+ * when none is reported. A cleared reminder is not an unknown one.
55824
+ */
55825
+ filterCleaningDue: boolean().nullable(),
55826
+ /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
55827
+ machineCareDue: boolean().nullable(),
55828
+ /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
55829
+ machineCareRemainingRuns: number().int().nonnegative().nullable(),
55796
55830
  /** Option states: null when the appliance does not expose the option or has not said. */
55797
55831
  halfLoad: boolean().nullable(),
55798
55832
  hygienePlus: boolean().nullable(),
@@ -64591,16 +64625,6 @@ var UpdateUserInputSchema = object({
64591
64625
  allowedDevices: record(string(), union([array(string()), literal("*")])).optional(),
64592
64626
  scopes: array(TokenScopeSchema).optional()
64593
64627
  });
64594
- var ApiKeySummarySchema = object({
64595
- id: string(),
64596
- label: string(),
64597
- isAdmin: boolean().default(false),
64598
- allowedProviders: union([array(string()), literal("*")]).optional(),
64599
- allowedDevices: record(string(), union([array(string()), literal("*")])).optional(),
64600
- tokenPrefix: string(),
64601
- createdAt: number(),
64602
- lastUsedAt: number().optional()
64603
- });
64604
64628
  var OauthSessionSummarySchema = object({
64605
64629
  id: string(),
64606
64630
  userId: string(),
@@ -64622,8 +64646,7 @@ var TotpStatusSchema = object({
64622
64646
  confirmedAt: number().nullable()
64623
64647
  });
64624
64648
  /**
64625
- * user-management — singleton capability for user accounts and the
64626
- * legacy API-key rows.
64649
+ * user-management — singleton capability for user accounts.
64627
64650
  *
64628
64651
  * v2 model: every method gated by `auth: 'admin'`. Admin-only delegation
64629
64652
  * — the subset check on `setUserScopes` is moot
@@ -64679,10 +64702,6 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
64679
64702
  }), UserSummarySchema.extend({ passwordHash: string() }).nullable(), {
64680
64703
  kind: "mutation",
64681
64704
  access: "view"
64682
- }), method(_void(), array(ApiKeySummarySchema), { auth: "admin" }), method(object({ id: string() }), object({ success: literal(true) }), {
64683
- kind: "mutation",
64684
- auth: "admin",
64685
- access: "delete"
64686
64705
  }), method(object({ userId: string() }), TotpSetupResultSchema, {
64687
64706
  kind: "mutation",
64688
64707
  auth: "admin",
@@ -74154,6 +74173,12 @@ Object.freeze({
74154
74173
  addonId: null,
74155
74174
  access: "view"
74156
74175
  },
74176
+ "records.transact": {
74177
+ capName: "records",
74178
+ capScope: "system",
74179
+ addonId: null,
74180
+ access: "create"
74181
+ },
74157
74182
  "sceneMonitor.captureReference": {
74158
74183
  capName: "scene-monitor",
74159
74184
  capScope: "device",
@@ -75486,12 +75511,6 @@ Object.freeze({
75486
75511
  addonId: null,
75487
75512
  access: "view"
75488
75513
  },
75489
- "userManagement.listApiKeys": {
75490
- capName: "user-management",
75491
- capScope: "system",
75492
- addonId: null,
75493
- access: "view"
75494
- },
75495
75514
  "userManagement.listExternalIdentities": {
75496
75515
  capName: "user-management",
75497
75516
  capScope: "system",
@@ -75546,12 +75565,6 @@ Object.freeze({
75546
75565
  addonId: null,
75547
75566
  access: "create"
75548
75567
  },
75549
- "userManagement.revokeApiKey": {
75550
- capName: "user-management",
75551
- capScope: "system",
75552
- addonId: null,
75553
- access: "delete"
75554
- },
75555
75568
  "userManagement.revokeOauthSession": {
75556
75569
  capName: "user-management",
75557
75570
  capScope: "system",
@@ -78916,9 +78929,13 @@ new Map(Object.entries({
78916
78929
  "stream-online": "device-events",
78917
78930
  "stream-offline": "device-events",
78918
78931
  "detection-blind": "device-events",
78932
+ "motion-noisy": "device-events",
78933
+ "motion-calm": "device-events",
78919
78934
  "recording-stalled": "device-events",
78920
78935
  "recording-healthy": "device-events",
78921
78936
  "recording-degraded": "device-events",
78937
+ "device-attention-required": "device-events",
78938
+ "device-attention-resolved": "device-events",
78922
78939
  "node-online": "system",
78923
78940
  "node-offline": "system",
78924
78941
  "node-restarting": "system",
@@ -79001,6 +79018,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
79001
79018
  label: "Detecting nothing",
79002
79019
  kinds: ["detection-blind"]
79003
79020
  },
79021
+ {
79022
+ id: "motion-noise",
79023
+ family: "device-events",
79024
+ label: "Motion without a subject",
79025
+ kinds: ["motion-noisy", "motion-calm"]
79026
+ },
79004
79027
  {
79005
79028
  id: "recording",
79006
79029
  family: "device-events",
@@ -79011,6 +79034,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
79011
79034
  "recording-degraded"
79012
79035
  ]
79013
79036
  },
79037
+ {
79038
+ id: "device-attention",
79039
+ family: "device-events",
79040
+ label: "Needs attention",
79041
+ kinds: ["device-attention-required", "device-attention-resolved"]
79042
+ },
79014
79043
  {
79015
79044
  id: "nodes",
79016
79045
  family: "system",
@@ -79150,6 +79179,7 @@ var ALERT_EPISODE = [
79150
79179
  "alert-error",
79151
79180
  "alert-critical-resolved"
79152
79181
  ];
79182
+ var DEVICE_ATTENTION = ["device-attention-required", "device-attention-resolved"];
79153
79183
  var UPDATES = [
79154
79184
  "addon-update-available",
79155
79185
  "server-update-available",
@@ -79179,9 +79209,12 @@ var DEVICE_EVENTS = [
79179
79209
  "stream-online",
79180
79210
  "stream-offline",
79181
79211
  "detection-blind",
79212
+ "motion-noisy",
79213
+ "motion-calm",
79182
79214
  "recording-stalled",
79183
79215
  "recording-healthy",
79184
79216
  "recording-degraded",
79217
+ ...DEVICE_ATTENTION,
79185
79218
  ...ALARM
79186
79219
  ];
79187
79220
  /** Every kind a summary may speak about — the two summarisable families. */
@@ -79349,7 +79382,9 @@ var STATE_SUMMARY_VARS = [
79349
79382
  [...RULE_TEXT],
79350
79383
  [...RULE_TEXT],
79351
79384
  [...RULE_TEXT],
79352
- ([...RULE_TEXT], [...ALERT_EPISODE]),
79385
+ [...RULE_TEXT],
79386
+ [...RULE_TEXT],
79387
+ ([...RULE_TEXT], [...DEVICE_ATTENTION, ...ALERT_EPISODE]),
79353
79388
  [...RULE_TEXT],
79354
79389
  [...RULE_TEXT],
79355
79390
  [...RULE_TEXT],
@@ -79371,11 +79406,11 @@ var STATE_SUMMARY_VARS = [
79371
79406
  [...RULE_TEXT],
79372
79407
  [...RULE_TEXT],
79373
79408
  [...RULE_TEXT],
79409
+ ([...RULE_TEXT], [...DEVICE_ATTENTION, ...ALERT_EPISODE]),
79374
79410
  ([...RULE_TEXT], [...ALERT_EPISODE]),
79411
+ ([...RULE_TEXT], [...DEVICE_ATTENTION, ...ALERT_EPISODE]),
79375
79412
  ([...RULE_TEXT], [...ALERT_EPISODE]),
79376
- ([...RULE_TEXT], [...ALERT_EPISODE]),
79377
- ([...RULE_TEXT], [...ALERT_EPISODE]),
79378
- ([...RULE_TEXT], [...ALERT_EPISODE]),
79413
+ ([...RULE_TEXT], [...DEVICE_ATTENTION, ...ALERT_EPISODE]),
79379
79414
  [...RULE_TEXT],
79380
79415
  [...RULE_TEXT],
79381
79416
  [...RULE_TEXT],
@@ -79451,6 +79486,15 @@ new Map([
79451
79486
  snapshotLabel: "Tanks low or full now",
79452
79487
  snapshotSection: "tanks"
79453
79488
  },
79489
+ {
79490
+ id: "motion-noise",
79491
+ family: "device-events",
79492
+ label: "Motion without a subject (constant / calm again)",
79493
+ bad: ["motion-noisy"],
79494
+ recovery: ["motion-calm"],
79495
+ snapshotLabel: "Cameras with constant motion now",
79496
+ snapshotSection: null
79497
+ },
79454
79498
  {
79455
79499
  id: "recording-health",
79456
79500
  family: "device-events",
@@ -79460,6 +79504,15 @@ new Map([
79460
79504
  snapshotLabel: "Recordings stalled now",
79461
79505
  snapshotSection: null
79462
79506
  },
79507
+ {
79508
+ id: "device-attention",
79509
+ family: "device-events",
79510
+ label: "Device needs attention (raised / resolved)",
79511
+ bad: ["device-attention-required"],
79512
+ recovery: ["device-attention-resolved"],
79513
+ snapshotLabel: "Devices needing attention now",
79514
+ snapshotSection: null
79515
+ },
79463
79516
  {
79464
79517
  id: "node-connectivity",
79465
79518
  family: "system",
@@ -132946,8 +132999,6 @@ function buildConnectionFormSchema() {
132946
132999
  id: "credentials",
132947
133000
  title: "Dreamehome account",
132948
133001
  titleKey: "provider-dreame:connection.s_credentials.title",
132949
- description: "Sign in with your Dreamehome (Dreame app) account credentials.",
132950
- descriptionKey: "provider-dreame:connection.s_credentials.description",
132951
133002
  columns: 1,
132952
133003
  fields: [{
132953
133004
  type: "text",
@@ -132969,8 +133020,6 @@ function buildConnectionFormSchema() {
132969
133020
  id: "region",
132970
133021
  title: "Region",
132971
133022
  titleKey: "provider-dreame:connection.s_region.title",
132972
- description: "Select the Dreame cloud region your account is registered in.",
132973
- descriptionKey: "provider-dreame:connection.s_region.description",
132974
133023
  columns: 1,
132975
133024
  fields: [{
132976
133025
  type: "select",
@@ -135389,6 +135438,81 @@ function rfcUrl(host, port) {
135389
135438
  return `tcp://${host}:${String(port)}`;
135390
135439
  }
135391
135440
  //#endregion
135441
+ //#region src/devices/dreame-pin-attention.ts
135442
+ var REFUSED_CODE_MINUS_ONE = /startMonitor refused \(code -1\)/;
135443
+ /** The refusal an error carries, or `null` when it is some other failure. */
135444
+ function classifyPinRefusal(err, accessCodeSet) {
135445
+ const message = err instanceof Error ? err.message : String(err);
135446
+ if (!REFUSED_CODE_MINUS_ONE.test(message)) return null;
135447
+ return accessCodeSet ? "rejected" : "missing";
135448
+ }
135449
+ function pinAlertId(deviceId) {
135450
+ return `dreame.pin-required:${String(deviceId)}`;
135451
+ }
135452
+ function buildPinAlert(deviceId, deviceName, reason) {
135453
+ const now = Date.now();
135454
+ return {
135455
+ id: pinAlertId(deviceId),
135456
+ category: "device.credentials-required",
135457
+ severity: "critical",
135458
+ title: reason === "missing" ? "Camera PIN required" : "Camera PIN rejected",
135459
+ message: reason === "missing" ? `${deviceName} needs its camera PIN before its video can open. Enter it in the device settings.` : `${deviceName} refused the camera PIN that is set. Enter the correct PIN in the device settings.`,
135460
+ action: {
135461
+ label: "Enter PIN",
135462
+ href: `/devices/${String(deviceId)}`,
135463
+ navigation: "app"
135464
+ },
135465
+ status: "active",
135466
+ read: false,
135467
+ createdAt: now,
135468
+ updatedAt: now,
135469
+ source: {
135470
+ type: "device",
135471
+ id: String(deviceId)
135472
+ },
135473
+ metadata: { episodeKey: reason }
135474
+ };
135475
+ }
135476
+ /**
135477
+ * One device's PIN episode. It raises the alert ONCE per episode, and while the
135478
+ * episode is open it tells the wake path to stand down: a refusal that needs the
135479
+ * operator is terminal until the PIN setting changes, so the broker's 30 s
135480
+ * re-dial must not re-open the monitor and fail again.
135481
+ */
135482
+ var PinAttention = class {
135483
+ ports;
135484
+ open = null;
135485
+ constructor(ports) {
135486
+ this.ports = ports;
135487
+ }
135488
+ /** True while the PIN is known to be unusable — the wake must not try. */
135489
+ get blocked() {
135490
+ return this.open !== null;
135491
+ }
135492
+ /** A wake failed. Raises the alert on the first refusal of an episode. */
135493
+ async refused(reason) {
135494
+ if (this.open !== null) return "already-open";
135495
+ this.open = reason;
135496
+ await this.ports.emit(buildPinAlert(this.ports.deviceId, this.ports.deviceName(), reason));
135497
+ return "raised";
135498
+ }
135499
+ /** A monitor opened: the PIN works, the episode is over. */
135500
+ async succeeded() {
135501
+ return this.settingChanged();
135502
+ }
135503
+ /**
135504
+ * The PIN setting changed: the episode is no longer known to be terminal, so
135505
+ * the next wake tries again. The alert is resolved here — a still-wrong PIN
135506
+ * raises a fresh episode on that wake.
135507
+ */
135508
+ async settingChanged() {
135509
+ if (this.open === null) return false;
135510
+ this.open = null;
135511
+ await this.ports.resolve(pinAlertId(this.ports.deviceId));
135512
+ return true;
135513
+ }
135514
+ };
135515
+ //#endregion
135392
135516
  //#region src/devices/dreame-navigation-actions.ts
135393
135517
  /** Forward throttle magnitude for a full-forward nudge (device speed units). */
135394
135518
  var FORWARD_SPEED = 200;
@@ -181580,15 +181704,13 @@ var CAMERA_SETTINGS_SCHEMA = { sections: [{
181580
181704
  id: "camera-backend",
181581
181705
  title: "Video backend",
181582
181706
  titleKey: "provider-dreame:camera_settings.s_camera-backend.title",
181583
- description: "Which cloud video service this robot streams through. The two are not interchangeable: only the Aliyun path is implemented here — a robot on Tencent can be seen in the Dreame app but not by CamStack, because Tencent offers it a proprietary P2P plane we do not speak. Changing this asks the ROBOT to switch and waits until its SDK reports ready; the robot is the authority on where it actually is.",
181707
+ description: "Only the Aliyun path is implemented; a robot on Tencent cannot be viewed by CamStack. Changing this asks the robot to switch.",
181584
181708
  descriptionKey: "provider-dreame:camera_settings.s_camera-backend.description",
181585
181709
  fields: [{
181586
181710
  type: "select",
181587
181711
  key: "videoVendor",
181588
181712
  label: "Backend",
181589
181713
  labelKey: "provider-dreame:camera_settings.s_camera-backend.videoVendor.label",
181590
- description: "Aliyun streams through a plain relay and works today. Tencent is offered so the other path can be tested once it exists.",
181591
- descriptionKey: "provider-dreame:camera_settings.s_camera-backend.videoVendor.description",
181592
181714
  options: [{
181593
181715
  value: "ali",
181594
181716
  label: "Aliyun LinkVisual (supported)",
@@ -181774,6 +181896,10 @@ var DreameCameraDevice = class extends DreameChildDevice {
181774
181896
  const data = event.data;
181775
181897
  if ((typeof data === "object" && data !== null && "deviceId" in data ? data.deviceId : null) !== this.id) return;
181776
181898
  if (this.session === null) {
181899
+ if (this.pinAttention.blocked) {
181900
+ this.ctx.logger.debug("robot camera: wake skipped — the camera PIN needs the operator", { tags: { deviceId: this.id } });
181901
+ return;
181902
+ }
181777
181903
  this.openStream().catch(() => void 0);
181778
181904
  return;
181779
181905
  }
@@ -181860,8 +181986,65 @@ var DreameCameraDevice = class extends DreameChildDevice {
181860
181986
  durationMs: Date.now() - startedAt
181861
181987
  };
181862
181988
  }
181989
+ /**
181990
+ * The PIN episode (D911): a monitor refused for want of the camera PIN needs
181991
+ * the operator, so it raises ONE critical alert and the wake stands down
181992
+ * until the PIN setting changes.
181993
+ */
181994
+ pinAttention = new PinAttention({
181995
+ deviceId: this.id,
181996
+ deviceName: () => this.name,
181997
+ emit: async (alert) => {
181998
+ const emit = this.ctx.api.alerts?.emit;
181999
+ if (emit === void 0) {
182000
+ this.ctx.logger.warn("robot camera: PIN alert skipped — alerts capability missing", { tags: { deviceId: this.id } });
182001
+ return;
182002
+ }
182003
+ try {
182004
+ await emit.mutate(alert);
182005
+ this.ctx.logger.warn("robot camera: PIN alert raised — the wake stops until the PIN changes", {
182006
+ tags: { deviceId: this.id },
182007
+ meta: {
182008
+ alertId: alert.id,
182009
+ episodeKey: alert.metadata?.["episodeKey"]
182010
+ }
182011
+ });
182012
+ } catch (err) {
182013
+ this.ctx.logger.warn("robot camera: PIN alert emit failed", {
182014
+ tags: { deviceId: this.id },
182015
+ meta: { error: errMsg(err) }
182016
+ });
182017
+ }
182018
+ },
182019
+ resolve: async (alertId) => {
182020
+ const update = this.ctx.api.alerts?.update;
182021
+ if (update === void 0) return;
182022
+ try {
182023
+ await update.mutate({
182024
+ alertId,
182025
+ patch: {
182026
+ status: "completed",
182027
+ read: true
182028
+ }
182029
+ });
182030
+ this.ctx.logger.info("robot camera: PIN alert resolved", {
182031
+ tags: { deviceId: this.id },
182032
+ meta: { alertId }
182033
+ });
182034
+ } catch (err) {
182035
+ this.ctx.logger.warn("robot camera: PIN alert resolve failed", {
182036
+ tags: { deviceId: this.id },
182037
+ meta: {
182038
+ alertId,
182039
+ error: errMsg(err)
182040
+ }
182041
+ });
182042
+ }
182043
+ }
182044
+ });
181863
182045
  openStream() {
181864
182046
  if (this.opening !== null) return this.opening;
182047
+ if (this.pinAttention.blocked) return Promise.reject(/* @__PURE__ */ new Error("camera PIN unusable — waiting for the operator"));
181865
182048
  const run = (async () => {
181866
182049
  const startedAt = Date.now();
181867
182050
  try {
@@ -181893,6 +182076,7 @@ var DreameCameraDevice = class extends DreameChildDevice {
181893
182076
  this.publishBattery();
181894
182077
  this.publishCatalog();
181895
182078
  await this.publishRelay(this.session);
182079
+ this.pinAttention.succeeded();
181896
182080
  this.ctx.logger.info("robot camera: monitor open, RTP server published", {
181897
182081
  tags: { deviceId: this.id },
181898
182082
  meta: {
@@ -181909,6 +182093,9 @@ var DreameCameraDevice = class extends DreameChildDevice {
181909
182093
  error: errMsg(err)
181910
182094
  }
181911
182095
  });
182096
+ const code = this.config.get("navigationAccessCode");
182097
+ const refusal = classifyPinRefusal(err, code !== void 0 && code !== "");
182098
+ if (refusal !== null) await this.pinAttention.refused(refusal);
181912
182099
  throw err;
181913
182100
  }
181914
182101
  })();
@@ -182126,6 +182313,7 @@ var DreameCameraDevice = class extends DreameChildDevice {
182126
182313
  }
182127
182314
  }
182128
182315
  if (!("navigationAccessCode" in patch)) return;
182316
+ await this.pinAttention.settingChanged();
182129
182317
  await this.closeStream("removed");
182130
182318
  this.controller = null;
182131
182319
  this.controllerHandle = null;
@@ -183945,8 +184133,6 @@ var DreameVacuumMapDevice = class extends DreameChildDevice {
183945
184133
  id: "map-style",
183946
184134
  title: "Map Style",
183947
184135
  titleKey: "provider-dreame:schema.s_map-style.title",
183948
- description: "Colour scheme and overlays for the rendered cleaning map.",
183949
- descriptionKey: "provider-dreame:schema.s_map-style.description",
183950
184136
  columns: 2,
183951
184137
  fields: [
183952
184138
  {
package/i18n/en.json CHANGED
@@ -1,24 +1,20 @@
1
1
  {
2
2
  "connection.s_credentials.title": "Dreamehome account",
3
- "connection.s_credentials.description": "Sign in with your Dreamehome (Dreame app) account credentials.",
4
3
  "connection.s_credentials.username.label": "Email / username",
5
4
  "connection.s_credentials.password.label": "Password",
6
5
  "connection.s_region.title": "Region",
7
- "connection.s_region.description": "Select the Dreame cloud region your account is registered in.",
8
6
  "connection.s_region.region.label": "Region",
9
7
  "connection.s_advanced.title": "Advanced",
10
8
  "connection.s_advanced.pollIntervalMs.label": "Poll fallback interval (ms, 0 = disabled)",
11
9
  "camera_settings.s_camera-backend.title": "Video backend",
12
- "camera_settings.s_camera-backend.description": "Which cloud video service this robot streams through. The two are not interchangeable: only the Aliyun path is implemented here — a robot on Tencent can be seen in the Dreame app but not by CamStack, because Tencent offers it a proprietary P2P plane we do not speak. Changing this asks the ROBOT to switch and waits until its SDK reports ready; the robot is the authority on where it actually is.",
10
+ "camera_settings.s_camera-backend.description": "Only the Aliyun path is implemented; a robot on Tencent cannot be viewed by CamStack. Changing this asks the robot to switch.",
13
11
  "camera_settings.s_camera-backend.videoVendor.label": "Backend",
14
- "camera_settings.s_camera-backend.videoVendor.description": "Aliyun streams through a plain relay and works today. Tencent is offered so the other path can be tested once it exists.",
15
12
  "camera_settings.s_camera-backend.videoVendor.o_ali.label": "Aliyun LinkVisual (supported)",
16
13
  "camera_settings.s_camera-backend.videoVendor.o_tx.label": "Tencent IoT-Video (not implemented yet)",
17
14
  "camera_settings.s_camera-access.title": "Camera access",
18
15
  "camera_settings.s_camera-access.description": "The privacy code set when the robot camera was paired. The robot refuses to open its camera without it.",
19
16
  "camera_settings.s_camera-access.navigationAccessCode.label": "Camera access code",
20
17
  "schema.s_map-style.title": "Map Style",
21
- "schema.s_map-style.description": "Colour scheme and overlays for the rendered cleaning map.",
22
18
  "schema.s_map-style.mapColorScheme.label": "Colour scheme",
23
19
  "schema.s_map-style.mapShowPath.label": "Cleaning path",
24
20
  "schema.s_map-style.mapShowRobot.label": "Robot",
package/i18n/it.json CHANGED
@@ -1,24 +1,20 @@
1
1
  {
2
2
  "connection.s_credentials.title": "Account Dreamehome",
3
- "connection.s_credentials.description": "Accedi con le credenziali del tuo account Dreamehome (app Dreame).",
4
3
  "connection.s_credentials.username.label": "Email / nome utente",
5
4
  "connection.s_credentials.password.label": "Password",
6
5
  "connection.s_region.title": "Regione",
7
- "connection.s_region.description": "Seleziona la regione del cloud Dreame in cui è registrato il tuo account.",
8
6
  "connection.s_region.region.label": "Regione",
9
7
  "connection.s_advanced.title": "Avanzate",
10
8
  "connection.s_advanced.pollIntervalMs.label": "Intervallo di polling di ripiego (ms, 0 = disattivato)",
11
9
  "camera_settings.s_camera-backend.title": "Servizio video",
12
- "camera_settings.s_camera-backend.description": "Il servizio video cloud attraverso cui questo robot trasmette. I due non sono intercambiabili: qui è implementato solo il percorso Aliyun. Un robot su Tencent si vede nell'app Dreame ma non da CamStack, perché Tencent gli offre un piano P2P proprietario che non parliamo. Cambiare questa impostazione chiede al ROBOT di passare e attende che il suo SDK segnali pronto: è il robot a decidere dove si trova davvero.",
10
+ "camera_settings.s_camera-backend.description": "È implementato solo il percorso Aliyun; un robot su Tencent non può essere visto da CamStack. Il cambio chiede al robot di passare.",
13
11
  "camera_settings.s_camera-backend.videoVendor.label": "Servizio",
14
- "camera_settings.s_camera-backend.videoVendor.description": "Aliyun trasmette tramite un relay semplice e funziona già. Tencent è previsto per poter testare l'altro percorso quando esisterà.",
15
12
  "camera_settings.s_camera-backend.videoVendor.o_ali.label": "Aliyun LinkVisual (supportato)",
16
13
  "camera_settings.s_camera-backend.videoVendor.o_tx.label": "Tencent IoT-Video (non ancora implementato)",
17
14
  "camera_settings.s_camera-access.title": "Accesso alla telecamera",
18
15
  "camera_settings.s_camera-access.description": "Il codice privacy impostato quando la telecamera del robot è stata associata. Il robot non apre la telecamera senza di esso.",
19
16
  "camera_settings.s_camera-access.navigationAccessCode.label": "Codice di accesso alla telecamera",
20
17
  "schema.s_map-style.title": "Stile della mappa",
21
- "schema.s_map-style.description": "Schema di colori e sovrapposizioni per la mappa di pulizia.",
22
18
  "schema.s_map-style.mapColorScheme.label": "Schema di colori",
23
19
  "schema.s_map-style.mapShowPath.label": "Percorso di pulizia",
24
20
  "schema.s_map-style.mapShowRobot.label": "Robot",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-dreame",
3
- "version": "0.2.148",
3
+ "version": "0.2.149",
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",