@camstack/addon-provider-reolink 1.2.201 → 1.2.203

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
@@ -36,7 +36,7 @@ let node_os = require("node:os");
36
36
  node_os = require_chunk.__toESM(node_os, 1);
37
37
  let node_fs = require("node:fs");
38
38
  node_fs = require_chunk.__toESM(node_fs, 1);
39
- //#region ../types/dist/event-category-DOl51-Ij.mjs
39
+ //#region ../types/dist/event-category-BfJwUd2S.mjs
40
40
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
41
41
  EventCategory["SystemBoot"] = "system.boot";
42
42
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -353,6 +353,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
353
353
  */
354
354
  EventCategory["PipelineDetectionBlind"] = "pipeline.detection-blind";
355
355
  /**
356
+ * A camera's motion source has fired constantly with no subject for an hour
357
+ * (motion duty or session count over the bar, zero detections) — it is not
358
+ * blind, its motion zones or sensitivity want tuning (D907). Paired with
359
+ * {@link PipelineMotionCalm}.
360
+ */
361
+ EventCategory["PipelineMotionNoisy"] = "pipeline.motion-noisy";
362
+ /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
363
+ EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
364
+ /**
356
365
  * Per-camera pipeline config was mutated by the orchestrator
357
366
  * (3-level settings change via `setAgentAddonDefaults` /
358
367
  * `setCameraStepToggle` / `setCameraPipelineForAgent` or a
@@ -724,6 +733,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
724
733
  EventCategory["BackupRestored"] = "backup.restored";
725
734
  EventCategory["NotificationDispatched"] = "notification.dispatched";
726
735
  EventCategory["NotificationFailed"] = "notification.failed";
736
+ /**
737
+ * A notification rule that cannot work (D905): its stored row does not load,
738
+ * or it still names a device / delivery target the prune CONFIRMED deleted.
739
+ * One edge per rule per distinct set of reasons; never on an unanswered read.
740
+ */
741
+ EventCategory["NotificationRuleBroken"] = "notification.rule-broken";
742
+ /** The clear of {@link NotificationRuleBroken}: the rule loads and names nothing deleted. */
743
+ EventCategory["NotificationRuleRepaired"] = "notification.rule-repaired";
727
744
  EventCategory["DeviceUpdated"] = "device.updated";
728
745
  /**
729
746
  * Transport-level connectivity. Emitted by the device driver when
@@ -5451,7 +5468,7 @@ var ZodIssueCode = {
5451
5468
  var ZodFirstPartyTypeKind;
5452
5469
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5453
5470
  //#endregion
5454
- //#region ../types/dist/sleep-_sV1BE2f.mjs
5471
+ //#region ../types/dist/sleep-DJFUfKvA.mjs
5455
5472
  /**
5456
5473
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5457
5474
  * window to float samples (D455).
@@ -7735,7 +7752,7 @@ function stringProp(value, key) {
7735
7752
  return typeof found === "string" ? found : void 0;
7736
7753
  }
7737
7754
  /** The code an error carries: its own, its cause's, or a tRPC `data.code`. */
7738
- function errorCode(err) {
7755
+ function errorCode$1(err) {
7739
7756
  const out = {};
7740
7757
  const cause = typeof err === "object" && err !== null ? Reflect.get(err, "cause") : void 0;
7741
7758
  const data = typeof err === "object" && err !== null ? Reflect.get(err, "data") : void 0;
@@ -7755,7 +7772,7 @@ function errorCode(err) {
7755
7772
  function classifyStoreError(err) {
7756
7773
  const message = err instanceof Error ? err.message : String(err);
7757
7774
  if (RESTART_FINGERPRINTS.some((f) => message.includes(f))) return "restart";
7758
- const code = errorCode(err);
7775
+ const code = errorCode$1(err);
7759
7776
  if (code.node !== void 0 && TRANSPORT_CODES.has(code.node)) return "transport";
7760
7777
  if (code.trpc !== void 0 && UNAVAILABLE_TRPC_CODES.has(code.trpc)) return "transport";
7761
7778
  if (typeof err === "object" && err !== null) {
@@ -7984,7 +8001,7 @@ var EntryDescriptorSchema = object({
7984
8001
  /** JSON Schema of the entry body; `secret: true` marks a password field. */
7985
8002
  fields: record(string(), unknown())
7986
8003
  }).strict();
7987
- /** What crosses the wire and sits in the engine's catalog: JSON only, no Zod. */
8004
+ /** What crosses the wire from an addon: JSON only, no Zod. An unknown key is refused. */
7988
8005
  var RecordKindDeclarationSchema = object({
7989
8006
  database: _enum(RECORD_DATABASES).default("settings"),
7990
8007
  class: _enum(RECORD_CLASSES),
@@ -8003,14 +8020,6 @@ var RecordKindDeclarationSchema = object({
8003
8020
  secretPaths: array(string().min(1)).default([]),
8004
8021
  /** The id key of each array that carries a secret (`{ headers: 'name' }`). */
8005
8022
  secretArrayKeys: record(string(), string()).default({}),
8006
- /**
8007
- * RETIRED (D900): the legacy table a kind adopted from during the records
8008
- * migration. Nothing declares it any more and nothing reads it; the schema
8009
- * still ACCEPTS it because the catalog rows the migration wrote carry it
8010
- * (an orphaned kind is never redeclared) and an addon built before the
8011
- * removal still sends it. Never set it.
8012
- */
8013
- adoptsFrom: string().min(1).optional(),
8014
8023
  /** Kinds that must agree through `transact` sit in one file under one family name. */
8015
8024
  transactFamily: string().min(1).optional(),
8016
8025
  entry: EntryDescriptorSchema.optional()
@@ -13302,17 +13311,6 @@ object({
13302
13311
  createdAt: number(),
13303
13312
  updatedAt: number()
13304
13313
  });
13305
- object({
13306
- id: string(),
13307
- label: string(),
13308
- isAdmin: boolean().default(false),
13309
- allowedProviders: AllowedProviderSchema,
13310
- allowedDevices: AllowedDevicesSchema,
13311
- tokenHash: string(),
13312
- tokenPrefix: string(),
13313
- createdAt: number(),
13314
- lastUsedAt: number().optional()
13315
- });
13316
13314
  object({
13317
13315
  id: string(),
13318
13316
  userId: string(),
@@ -17294,16 +17292,36 @@ var RecordKindStateSchema = _enum([
17294
17292
  var DeclareKindResultSchema = object({
17295
17293
  kindId: number().int(),
17296
17294
  state: RecordKindStateSchema,
17297
- createdIndexes: array(string()),
17298
- /**
17299
- * RETIRED (D900): always `null`. It carried the records-migration boot guard;
17300
- * it stays on the wire, null, because an addon built before the removal reads
17301
- * an ABSENT field as "adoption pending" and refuses to start.
17302
- */
17303
- adoptionPending: _null()
17295
+ createdIndexes: array(string())
17304
17296
  });
17305
17297
  /** Keychain refs a write released (the old marker is gone from the row): the owner deletes the values. */
17306
17298
  var SecretRefsList = array(string());
17299
+ /**
17300
+ * One op of a `transact`: the single-row writes, each on a kind of the caller.
17301
+ * All ops of one call share ONE database file and ONE `transactFamily`.
17302
+ */
17303
+ var RecordTransactOpSchema = discriminatedUnion("op", [
17304
+ object({
17305
+ op: literal("put"),
17306
+ kind: RecordKindNameSchema,
17307
+ record: RecordInputSchema,
17308
+ ifRev: number().int().min(0).optional()
17309
+ }),
17310
+ object({
17311
+ op: literal("patch"),
17312
+ kind: RecordKindNameSchema,
17313
+ id: RecordIdSchema,
17314
+ data: RecordBodySchema,
17315
+ at: number().int().nullable().optional(),
17316
+ expiresAt: number().int().nullable().optional(),
17317
+ ifRev: number().int().min(1).optional()
17318
+ }),
17319
+ object({
17320
+ op: literal("delete"),
17321
+ kind: RecordKindNameSchema,
17322
+ id: RecordIdSchema
17323
+ })
17324
+ ]);
17307
17325
  var RecordSecretRefsSchema = record(string(), record(string(), string()));
17308
17326
  method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema) }), object({ kinds: record(string(), DeclareKindResultSchema) }), {
17309
17327
  kind: "mutation",
@@ -17410,6 +17428,16 @@ method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema)
17410
17428
  }), {
17411
17429
  kind: "mutation",
17412
17430
  auth: "admin"
17431
+ }), method(object({ ops: array(RecordTransactOpSchema).min(1).max(RECORD_BATCH_MAX) }), object({
17432
+ results: array(object({
17433
+ rev: number().int().nullable(),
17434
+ deleted: boolean()
17435
+ })),
17436
+ releasedRefs: SecretRefsList,
17437
+ secretRefs: SecretRefsList
17438
+ }), {
17439
+ kind: "mutation",
17440
+ auth: "admin"
17413
17441
  }), method(object({
17414
17442
  deviceId: number().int(),
17415
17443
  kinds: array(RecordKindNameSchema).optional()
@@ -43157,9 +43185,13 @@ var NcSystemEventKindSchema = _enum([
43157
43185
  "node-updating",
43158
43186
  "node-inference-unavailable",
43159
43187
  "detection-blind",
43188
+ "motion-noisy",
43189
+ "motion-calm",
43160
43190
  "recording-stalled",
43161
43191
  "recording-healthy",
43162
43192
  "recording-degraded",
43193
+ "device-attention-required",
43194
+ "device-attention-resolved",
43163
43195
  "storage-location-low",
43164
43196
  "storage-location-critical",
43165
43197
  "storage-location-ok",
@@ -43183,6 +43215,8 @@ var NcSystemEventKindSchema = _enum([
43183
43215
  "alert-critical",
43184
43216
  "alert-error",
43185
43217
  "alert-critical-resolved",
43218
+ "rule-broken",
43219
+ "rule-repaired",
43186
43220
  "camera-online",
43187
43221
  "camera-offline",
43188
43222
  "camera-disabled",
@@ -57003,6 +57037,16 @@ var DishwasherStatusSchema = object({
57003
57037
  powerState: string().nullable(),
57004
57038
  saltLevel: DishwasherConsumableLevelSchema.nullable(),
57005
57039
  rinseAidLevel: DishwasherConsumableLevelSchema.nullable(),
57040
+ /**
57041
+ * True while the appliance asks for its filter to be cleaned (smart-filter, filter-system or
57042
+ * combined care-and-filter reminder raised); false when every such reminder reads clear; null
57043
+ * when none is reported. A cleared reminder is not an unknown one.
57044
+ */
57045
+ filterCleaningDue: boolean().nullable(),
57046
+ /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
57047
+ machineCareDue: boolean().nullable(),
57048
+ /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
57049
+ machineCareRemainingRuns: number().int().nonnegative().nullable(),
57006
57050
  /** Option states: null when the appliance does not expose the option or has not said. */
57007
57051
  halfLoad: boolean().nullable(),
57008
57052
  hygienePlus: boolean().nullable(),
@@ -66223,16 +66267,6 @@ var UpdateUserInputSchema = object({
66223
66267
  allowedDevices: record(string(), union([array(string()), literal("*")])).optional(),
66224
66268
  scopes: array(TokenScopeSchema).optional()
66225
66269
  });
66226
- var ApiKeySummarySchema = object({
66227
- id: string(),
66228
- label: string(),
66229
- isAdmin: boolean().default(false),
66230
- allowedProviders: union([array(string()), literal("*")]).optional(),
66231
- allowedDevices: record(string(), union([array(string()), literal("*")])).optional(),
66232
- tokenPrefix: string(),
66233
- createdAt: number(),
66234
- lastUsedAt: number().optional()
66235
- });
66236
66270
  var OauthSessionSummarySchema = object({
66237
66271
  id: string(),
66238
66272
  userId: string(),
@@ -66254,8 +66288,7 @@ var TotpStatusSchema = object({
66254
66288
  confirmedAt: number().nullable()
66255
66289
  });
66256
66290
  /**
66257
- * user-management — singleton capability for user accounts and the
66258
- * legacy API-key rows.
66291
+ * user-management — singleton capability for user accounts.
66259
66292
  *
66260
66293
  * v2 model: every method gated by `auth: 'admin'`. Admin-only delegation
66261
66294
  * — the subset check on `setUserScopes` is moot
@@ -66311,10 +66344,6 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
66311
66344
  }), UserSummarySchema.extend({ passwordHash: string() }).nullable(), {
66312
66345
  kind: "mutation",
66313
66346
  access: "view"
66314
- }), method(_void(), array(ApiKeySummarySchema), { auth: "admin" }), method(object({ id: string() }), object({ success: literal(true) }), {
66315
- kind: "mutation",
66316
- auth: "admin",
66317
- access: "delete"
66318
66347
  }), method(object({ userId: string() }), TotpSetupResultSchema, {
66319
66348
  kind: "mutation",
66320
66349
  auth: "admin",
@@ -75785,6 +75814,12 @@ Object.freeze({
75785
75814
  addonId: null,
75786
75815
  access: "view"
75787
75816
  },
75817
+ "records.transact": {
75818
+ capName: "records",
75819
+ capScope: "system",
75820
+ addonId: null,
75821
+ access: "create"
75822
+ },
75788
75823
  "sceneMonitor.captureReference": {
75789
75824
  capName: "scene-monitor",
75790
75825
  capScope: "device",
@@ -77117,12 +77152,6 @@ Object.freeze({
77117
77152
  addonId: null,
77118
77153
  access: "view"
77119
77154
  },
77120
- "userManagement.listApiKeys": {
77121
- capName: "user-management",
77122
- capScope: "system",
77123
- addonId: null,
77124
- access: "view"
77125
- },
77126
77155
  "userManagement.listExternalIdentities": {
77127
77156
  capName: "user-management",
77128
77157
  capScope: "system",
@@ -77177,12 +77206,6 @@ Object.freeze({
77177
77206
  addonId: null,
77178
77207
  access: "create"
77179
77208
  },
77180
- "userManagement.revokeApiKey": {
77181
- capName: "user-management",
77182
- capScope: "system",
77183
- addonId: null,
77184
- access: "delete"
77185
- },
77186
77209
  "userManagement.revokeOauthSession": {
77187
77210
  capName: "user-management",
77188
77211
  capScope: "system",
@@ -80699,9 +80722,13 @@ new Map(Object.entries({
80699
80722
  "stream-online": "device-events",
80700
80723
  "stream-offline": "device-events",
80701
80724
  "detection-blind": "device-events",
80725
+ "motion-noisy": "device-events",
80726
+ "motion-calm": "device-events",
80702
80727
  "recording-stalled": "device-events",
80703
80728
  "recording-healthy": "device-events",
80704
80729
  "recording-degraded": "device-events",
80730
+ "device-attention-required": "device-events",
80731
+ "device-attention-resolved": "device-events",
80705
80732
  "node-online": "system",
80706
80733
  "node-offline": "system",
80707
80734
  "node-restarting": "system",
@@ -80718,6 +80745,8 @@ new Map(Object.entries({
80718
80745
  "alert-critical": "system",
80719
80746
  "alert-error": "system",
80720
80747
  "alert-critical-resolved": "system",
80748
+ "rule-broken": "system",
80749
+ "rule-repaired": "system",
80721
80750
  "storage-location-low": "system",
80722
80751
  "storage-location-critical": "system",
80723
80752
  "storage-location-ok": "system",
@@ -80782,6 +80811,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
80782
80811
  label: "Detecting nothing",
80783
80812
  kinds: ["detection-blind"]
80784
80813
  },
80814
+ {
80815
+ id: "motion-noise",
80816
+ family: "device-events",
80817
+ label: "Motion without a subject",
80818
+ kinds: ["motion-noisy", "motion-calm"]
80819
+ },
80785
80820
  {
80786
80821
  id: "recording",
80787
80822
  family: "device-events",
@@ -80792,6 +80827,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
80792
80827
  "recording-degraded"
80793
80828
  ]
80794
80829
  },
80830
+ {
80831
+ id: "device-attention",
80832
+ family: "device-events",
80833
+ label: "Needs attention",
80834
+ kinds: ["device-attention-required", "device-attention-resolved"]
80835
+ },
80795
80836
  {
80796
80837
  id: "nodes",
80797
80838
  family: "system",
@@ -80852,6 +80893,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
80852
80893
  "alert-critical-resolved"
80853
80894
  ]
80854
80895
  },
80896
+ {
80897
+ id: "rule-health",
80898
+ family: "system",
80899
+ label: "Notification rules",
80900
+ kinds: ["rule-broken", "rule-repaired"]
80901
+ },
80855
80902
  {
80856
80903
  id: "alarm",
80857
80904
  family: "alarm",
@@ -80925,6 +80972,7 @@ var ALERT_EPISODE = [
80925
80972
  "alert-error",
80926
80973
  "alert-critical-resolved"
80927
80974
  ];
80975
+ var DEVICE_ATTENTION = ["device-attention-required", "device-attention-resolved"];
80928
80976
  var UPDATES = [
80929
80977
  "addon-update-available",
80930
80978
  "server-update-available",
@@ -80954,9 +81002,12 @@ var DEVICE_EVENTS = [
80954
81002
  "stream-online",
80955
81003
  "stream-offline",
80956
81004
  "detection-blind",
81005
+ "motion-noisy",
81006
+ "motion-calm",
80957
81007
  "recording-stalled",
80958
81008
  "recording-healthy",
80959
81009
  "recording-degraded",
81010
+ ...DEVICE_ATTENTION,
80960
81011
  ...ALARM
80961
81012
  ];
80962
81013
  /** Every kind a summary may speak about — the two summarisable families. */
@@ -81124,7 +81175,13 @@ var STATE_SUMMARY_VARS = [
81124
81175
  [...RULE_TEXT],
81125
81176
  [...RULE_TEXT],
81126
81177
  [...RULE_TEXT],
81127
- ([...RULE_TEXT], [...ALERT_EPISODE]),
81178
+ [...RULE_TEXT],
81179
+ [...RULE_TEXT],
81180
+ ([...RULE_TEXT], [...DEVICE_ATTENTION, ...ALERT_EPISODE]),
81181
+ [...RULE_TEXT],
81182
+ [...RULE_TEXT],
81183
+ [...RULE_TEXT],
81184
+ [...RULE_TEXT],
81128
81185
  [...RULE_TEXT],
81129
81186
  [...RULE_TEXT],
81130
81187
  [...RULE_TEXT],
@@ -81142,11 +81199,11 @@ var STATE_SUMMARY_VARS = [
81142
81199
  [...RULE_TEXT],
81143
81200
  [...RULE_TEXT],
81144
81201
  [...RULE_TEXT],
81202
+ ([...RULE_TEXT], [...DEVICE_ATTENTION, ...ALERT_EPISODE]),
81145
81203
  ([...RULE_TEXT], [...ALERT_EPISODE]),
81204
+ ([...RULE_TEXT], [...DEVICE_ATTENTION, ...ALERT_EPISODE]),
81146
81205
  ([...RULE_TEXT], [...ALERT_EPISODE]),
81147
- ([...RULE_TEXT], [...ALERT_EPISODE]),
81148
- ([...RULE_TEXT], [...ALERT_EPISODE]),
81149
- ([...RULE_TEXT], [...ALERT_EPISODE]),
81206
+ ([...RULE_TEXT], [...DEVICE_ATTENTION, ...ALERT_EPISODE]),
81150
81207
  [...RULE_TEXT],
81151
81208
  [...RULE_TEXT],
81152
81209
  [...RULE_TEXT],
@@ -81167,6 +81224,125 @@ var STATE_SUMMARY_VARS = [
81167
81224
  [...RULE_TEXT],
81168
81225
  ...STATE_SUMMARY_VARS
81169
81226
  ];
81227
+ new Map([
81228
+ {
81229
+ id: "device-connectivity",
81230
+ family: "device-events",
81231
+ label: "Device connectivity (offline / back online)",
81232
+ bad: ["device-offline"],
81233
+ recovery: ["device-online"],
81234
+ snapshotLabel: "Devices offline now",
81235
+ snapshotSection: "device-state"
81236
+ },
81237
+ {
81238
+ id: "device-switch",
81239
+ family: "device-events",
81240
+ label: "Device switched off / back on",
81241
+ bad: ["device-disabled"],
81242
+ recovery: ["device-enabled"],
81243
+ snapshotLabel: "Devices switched off now",
81244
+ snapshotSection: "device-state"
81245
+ },
81246
+ {
81247
+ id: "stream-connectivity",
81248
+ family: "device-events",
81249
+ label: "Stream connectivity (offline / back online)",
81250
+ bad: ["stream-offline"],
81251
+ recovery: ["stream-online"],
81252
+ snapshotLabel: "Streams offline now",
81253
+ snapshotSection: "streams"
81254
+ },
81255
+ {
81256
+ id: "battery",
81257
+ family: "device-events",
81258
+ label: "Battery (low / back to normal)",
81259
+ bad: ["device-battery-low"],
81260
+ recovery: ["device-battery-normal"],
81261
+ snapshotLabel: "Batteries low now",
81262
+ snapshotSection: "battery"
81263
+ },
81264
+ {
81265
+ id: "consumables",
81266
+ family: "device-events",
81267
+ label: "Consumables (low / replaced)",
81268
+ bad: ["device-consumable-low"],
81269
+ recovery: ["device-consumable-normal"],
81270
+ snapshotLabel: "Consumables low now",
81271
+ snapshotSection: "consumables"
81272
+ },
81273
+ {
81274
+ id: "tanks",
81275
+ family: "device-events",
81276
+ label: "Tanks (low or full / back to normal)",
81277
+ bad: ["device-tank-low", "device-tank-full"],
81278
+ recovery: ["device-tank-normal"],
81279
+ snapshotLabel: "Tanks low or full now",
81280
+ snapshotSection: "tanks"
81281
+ },
81282
+ {
81283
+ id: "motion-noise",
81284
+ family: "device-events",
81285
+ label: "Motion without a subject (constant / calm again)",
81286
+ bad: ["motion-noisy"],
81287
+ recovery: ["motion-calm"],
81288
+ snapshotLabel: "Cameras with constant motion now",
81289
+ snapshotSection: null
81290
+ },
81291
+ {
81292
+ id: "recording-health",
81293
+ family: "device-events",
81294
+ label: "Recording health (stalled or degraded / recording again)",
81295
+ bad: ["recording-stalled", "recording-degraded"],
81296
+ recovery: ["recording-healthy"],
81297
+ snapshotLabel: "Recordings stalled now",
81298
+ snapshotSection: null
81299
+ },
81300
+ {
81301
+ id: "device-attention",
81302
+ family: "device-events",
81303
+ label: "Device needs attention (raised / resolved)",
81304
+ bad: ["device-attention-required"],
81305
+ recovery: ["device-attention-resolved"],
81306
+ snapshotLabel: "Devices needing attention now",
81307
+ snapshotSection: null
81308
+ },
81309
+ {
81310
+ id: "node-connectivity",
81311
+ family: "system",
81312
+ label: "Node connectivity (offline / back online)",
81313
+ bad: ["node-offline"],
81314
+ recovery: ["node-online"],
81315
+ snapshotLabel: "Nodes offline now",
81316
+ snapshotSection: "nodes"
81317
+ },
81318
+ {
81319
+ id: "storage-space",
81320
+ family: "system",
81321
+ label: "Storage space (low or critical / ok again)",
81322
+ bad: ["storage-location-low", "storage-location-critical"],
81323
+ recovery: ["storage-location-ok"],
81324
+ snapshotLabel: "Storage locations low now",
81325
+ snapshotSection: null
81326
+ },
81327
+ {
81328
+ id: "critical-alerts",
81329
+ family: "system",
81330
+ label: "Critical alerts (raised / resolved)",
81331
+ bad: ["alert-critical"],
81332
+ recovery: ["alert-critical-resolved"],
81333
+ snapshotLabel: "Critical alerts open now",
81334
+ snapshotSection: null
81335
+ },
81336
+ {
81337
+ id: "rule-health",
81338
+ family: "system",
81339
+ label: "Rule health (broken / repaired)",
81340
+ bad: ["rule-broken"],
81341
+ recovery: ["rule-repaired"],
81342
+ snapshotLabel: "Notification rules broken now",
81343
+ snapshotSection: null
81344
+ }
81345
+ ].flatMap((spec) => [...spec.bad, ...spec.recovery].map((kind) => [kind, spec])));
81170
81346
  new Set([
81171
81347
  "disarmed",
81172
81348
  "armed_home",
@@ -188454,8 +188630,8 @@ var CLIP_REFRESH_SECTION_ID = "clip-catalog-refresh";
188454
188630
  * settings nobody could reason about. So the difference is in the operator's
188455
188631
  * own text, at both levels, not only in a record.
188456
188632
  */
188457
- var CLIP_CATALOG_REFRESH_MINUTES_DESCRIPTION = "How often CamStack re-reads what a Reolink camera holds on its own storage. It costs almost nothing on a camera it can reach: a hub child is read through the hub on the session that is already open (measured 65 ms, and the camera itself never hears it), and a mains camera is read on its own socket. On a STANDALONE BATTERY camera the timer cannot read anything at all — logging in is what wakes such a camera — so there it only marks the catalog as ageing, and the real re-read happens the next time the camera wakes up for its own reasons (motion, a stream, something you did). Set 0 to switch the timer off.";
188458
- var CLIP_CATALOG_FORCED_WAKE_DESCRIPTION = "Let the refresh timer WAKE this camera. Off unless you mean it: on a standalone battery camera every read is a login and every login is a wake, so a 15-minute timer is 96 wakes a day (24 at 60 minutes) against the 1–20 clips a day these cameras actually record — five to a hundred times their normal wake count, and the battery pays for all of it. One interval buys exactly one wake, successful or not. It has no effect on a hub child or a mains camera, which are never woken by this timer.";
188633
+ var CLIP_CATALOG_REFRESH_MINUTES_DESCRIPTION = "On a standalone battery camera the timer only marks the catalog as ageing; it never wakes the camera. 0 turns it off.";
188634
+ var CLIP_CATALOG_FORCED_WAKE_DESCRIPTION = "Let the timer wake a standalone battery camera: one wake per interval, at the battery’s cost. No effect on hub children or mains cameras.";
188459
188635
  /**
188460
188636
  * The addon-level (global) default. Per-camera overrides live on the device,
188461
188637
  * under the SAME key, and a device that sets nothing — or resets — resolves to
@@ -188465,7 +188641,7 @@ function buildClipRefreshSettingsSchema() {
188465
188641
  return { sections: [{
188466
188642
  id: CLIP_REFRESH_SECTION_ID,
188467
188643
  title: "Clip catalog refresh",
188468
- description: "The default for every Reolink camera. Each camera can override it in its own Recording settings; a camera that overrides nothing uses this value.",
188644
+ description: "Default for every camera; a camera can override it in its Recording settings.",
188469
188645
  columns: 1,
188470
188646
  fields: [{
188471
188647
  type: "number",
@@ -188495,7 +188671,7 @@ function buildClipRefreshDeviceSection(input) {
188495
188671
  id: CLIP_REFRESH_SECTION_ID,
188496
188672
  tab: "recording",
188497
188673
  title: "Clip catalog refresh",
188498
- description: "How often this camera’s own clip list is re-read. Leave the interval empty to use the default from the Reolink addon settings.",
188674
+ description: "Leave the interval empty to use the default from the Reolink addon settings.",
188499
188675
  columns: 1,
188500
188676
  fields: [{
188501
188677
  type: "number",
@@ -188774,7 +188950,7 @@ function buildSnapshotFreshnessSection(cache, opts) {
188774
188950
  tab: "advanced",
188775
188951
  title: "Snapshot freshness",
188776
188952
  titleKey: "provider-reolink:snapshotFreshnessSection.s_snapshotFreshness.title",
188777
- description: "When each cached camera reading was last fetched. Groups without a stamp were persisted before per-snapshot tracking — their age is unknown, not fresh. Use \"Refresh from camera\" (General) to re-read an awake camera.",
188953
+ description: "Groups without a stamp are of unknown age, not fresh.",
188778
188954
  descriptionKey: "provider-reolink:snapshotFreshnessSection.s_snapshotFreshness.description",
188779
188955
  columns: 1,
188780
188956
  fields: [{
@@ -189805,7 +189981,7 @@ var BatteryHourlyRefresh = class {
189805
189981
  //#endregion
189806
189982
  //#region src/battery-refresh-settings.ts
189807
189983
  var BATTERY_REFRESH_SECTION_ID = "battery-refresh";
189808
- var BATTERY_REFRESH_MINUTES_DESCRIPTION = `Once per window this camera refreshes everything CamStack knows about it — battery level and charging, signal, device info, every state panel and the cached snapshot. If the camera is awake at some point in the window (a motion event, a live view, anything that woke it) it is refreshed then and not woken again; if the whole window passes with the camera asleep, it is woken ONCE near the end of the window, refreshed, and left to go back to sleep. A camera that does not answer is retried at most twice per window. 15–240 minutes, default 60; 0 turns the scheduled refresh off (the battery level then ages until the camera next wakes by itself).`;
189984
+ var BATTERY_REFRESH_MINUTES_DESCRIPTION = "A camera that slept the whole window is woken once to refresh. 0 turns the scheduled refresh off.";
189809
189985
  /** Battery cameras only: a mains camera keeps its facts fresh on its own socket. */
189810
189986
  function buildBatteryRefreshDeviceSection(input) {
189811
189987
  if (!input.isBattery) return [];
@@ -189813,7 +189989,6 @@ function buildBatteryRefreshDeviceSection(input) {
189813
189989
  id: BATTERY_REFRESH_SECTION_ID,
189814
189990
  tab: "general",
189815
189991
  title: "Scheduled refresh",
189816
- description: "How often this battery camera is guaranteed to have been read, so a battery level never goes stale unseen.",
189817
189992
  columns: 1,
189818
189993
  fields: [{
189819
189994
  type: "number",
@@ -189907,7 +190082,7 @@ async function refreshSnapshotCacheOnce(input) {
189907
190082
  */
189908
190083
  var CH_BAICHUAN = declareLogChannel({
189909
190084
  name: "provider-reolink.baichuan",
189910
- description: "Every Baichuan protocol frame this camera sends and receives, plus UDP keepalives and battery-camera sleep inference. Loud on an active camera — arm it for one camera and a short window.",
190085
+ description: "Every Baichuan protocol frame this camera sends and receives. Loud: arm it for one camera and a short window.",
189911
190086
  defaultLevel: "info",
189912
190087
  perDevice: true
189913
190088
  });
@@ -189922,7 +190097,7 @@ var CH_BAICHUAN = declareLogChannel({
189922
190097
  */
189923
190098
  var CH_HANDSHAKE = declareLogChannel({
189924
190099
  name: "provider-reolink.handshake",
189925
- description: "Login and socket lifecycle for this camera: the pre-login dial, the battery-camera wake a login causes, login failures and the reconnects that follow. The channel to arm when a camera looks present but answers nothing.",
190100
+ description: "Login and socket lifecycle for this camera. Arm it when a camera looks present but answers nothing.",
189926
190101
  defaultLevel: "info",
189927
190102
  perDevice: true
189928
190103
  });
@@ -190331,7 +190506,7 @@ function buildOsdSection(snapshot, values, opts) {
190331
190506
  tab: "image",
190332
190507
  title: "OSD overlay",
190333
190508
  titleKey: "provider-reolink:osdSection.s_osd.title",
190334
- description: "On-screen overlays burned into the video by the camera. Read with `GetOsdDatetime` (cmd_id 44) and pushed with `SetOsdDatetime` (cmd_id 45). Positions are the four frame corners; an overlay the camera holds somewhere else keeps that placement unless a corner is chosen.",
190509
+ description: "An overlay the camera holds somewhere else keeps that placement unless a corner is chosen.",
190335
190510
  descriptionKey: "provider-reolink:osdSection.s_osd.description",
190336
190511
  columns: 2,
190337
190512
  fields: [
@@ -190343,8 +190518,6 @@ function buildOsdSection(snapshot, values, opts) {
190343
190518
  key: "osdChannelName",
190344
190519
  label: "Channel name",
190345
190520
  labelKey: "provider-reolink:fields.osdChannelName.label",
190346
- description: "Text shown in the channel-name overlay.",
190347
- descriptionKey: "provider-reolink:fields.osdChannelName.description",
190348
190521
  default: values.osdChannelName,
190349
190522
  placeholder: "Front door"
190350
190523
  },
@@ -190828,7 +191001,7 @@ var SirenAccessory = class extends BaseDevice {
190828
191001
  id: "siren",
190829
191002
  title: "Siren",
190830
191003
  titleKey: "provider-reolink:schema.s_siren.title",
190831
- description: "Tone duration when triggered. Set to 0 for \"play until turned off\"; positive values pulse for that many seconds.",
191004
+ description: "0 = play until turned off.",
190832
191005
  descriptionKey: "provider-reolink:schema.s_siren.description",
190833
191006
  columns: 1,
190834
191007
  fields: [{
@@ -190836,8 +191009,6 @@ var SirenAccessory = class extends BaseDevice {
190836
191009
  key: "sirenDurationS",
190837
191010
  label: "Default duration (seconds)",
190838
191011
  labelKey: "provider-reolink:schema.s_siren.sirenDurationS.label",
190839
- description: "Forwarded to `setSiren(on=true, duration)` (cmd_id 30) when the operator flips the switch.",
190840
- descriptionKey: "provider-reolink:schema.s_siren.sirenDurationS.description",
190841
191012
  default: 0,
190842
191013
  min: 0,
190843
191014
  max: 300,
@@ -190847,16 +191018,12 @@ var SirenAccessory = class extends BaseDevice {
190847
191018
  id: "motion-siren",
190848
191019
  title: "Motion-triggered siren",
190849
191020
  titleKey: "provider-reolink:schema.s_motion-siren.title",
190850
- description: "Detection classes that fire the siren when \"Trigger on motion\" is armed. Forwarded as a `typeScheduleList` to `setSirenOnMotion` (cmd_id 231). Saved values re-apply on every motion-trigger flip.",
190851
- descriptionKey: "provider-reolink:schema.s_motion-siren.description",
190852
191021
  columns: 1,
190853
191022
  fields: [{
190854
191023
  type: "select",
190855
191024
  key: "motionSirenDetectType",
190856
191025
  label: "Detection classes",
190857
191026
  labelKey: "provider-reolink:schema.s_motion-siren.motionSirenDetectType.label",
190858
- description: "Object/motion classes that arm the siren. `MD` = generic motion detection.",
190859
- descriptionKey: "provider-reolink:schema.s_motion-siren.motionSirenDetectType.description",
190860
191027
  options: SIREN_DETECT_TYPE_CHOICES.map((v) => ({
190861
191028
  value: v,
190862
191029
  label: v.replace(/,/g, " + ").replace(/_/g, "+")
@@ -191320,16 +191487,12 @@ var FloodlightAccessory = class extends BaseDevice {
191320
191487
  id: "motion-floodlight",
191321
191488
  title: "Motion-triggered floodlight",
191322
191489
  titleKey: "provider-reolink:schema.s_motion-floodlight.title",
191323
- description: "Pushed to the camera (`FloodlightTask`) when the \"Trigger on motion\" toggle is armed. Saved values also re-apply on every motion-trigger flip so the firmware always honours your preference.",
191324
- descriptionKey: "provider-reolink:schema.s_motion-floodlight.description",
191325
191490
  columns: 2,
191326
191491
  fields: [{
191327
191492
  type: "number",
191328
191493
  key: "motionLightDurationS",
191329
191494
  label: "Light duration (s)",
191330
191495
  labelKey: "provider-reolink:schema.s_motion-floodlight.motionLightDurationS.label",
191331
- description: "Seconds the floodlight stays lit after a motion event. Reolink range 1..600s.",
191332
- descriptionKey: "provider-reolink:schema.s_motion-floodlight.motionLightDurationS.description",
191333
191496
  min: 1,
191334
191497
  max: 600,
191335
191498
  step: 1,
@@ -191339,8 +191502,6 @@ var FloodlightAccessory = class extends BaseDevice {
191339
191502
  key: "motionLightDetectType",
191340
191503
  label: "Detection classes",
191341
191504
  labelKey: "provider-reolink:schema.s_motion-floodlight.motionLightDetectType.label",
191342
- description: "Object classes that turn the floodlight on. Composed values (e.g. \"people,vehicle\") fire when ANY listed class detects.",
191343
- descriptionKey: "provider-reolink:schema.s_motion-floodlight.motionLightDetectType.description",
191344
191505
  options: FLOODLIGHT_DETECT_TYPE_CHOICES.map((v) => ({
191345
191506
  value: v,
191346
191507
  label: v.replace(/,/g, " + ").replace(/_/g, "+")
@@ -191580,8 +191741,6 @@ var PirAccessory = class extends BaseDevice {
191580
191741
  id: "pir",
191581
191742
  title: "PIR sensor",
191582
191743
  titleKey: "provider-reolink:schema.s_pir.title",
191583
- description: "Hardware passive-infrared trigger configuration. Pushed via cmd_id 153 (SetPirInfo).",
191584
- descriptionKey: "provider-reolink:schema.s_pir.description",
191585
191744
  columns: 2,
191586
191745
  fields: [
191587
191746
  {
@@ -191589,8 +191748,6 @@ var PirAccessory = class extends BaseDevice {
191589
191748
  key: "pirSensitive",
191590
191749
  label: "Sensitivity",
191591
191750
  labelKey: "provider-reolink:schema.s_pir.pirSensitive.label",
191592
- description: "Higher value → triggers on smaller motion. Most cameras max at 100.",
191593
- descriptionKey: "provider-reolink:schema.s_pir.pirSensitive.description",
191594
191751
  min: 1,
191595
191752
  max: 100,
191596
191753
  step: 1,
@@ -191602,7 +191759,6 @@ var PirAccessory = class extends BaseDevice {
191602
191759
  key: "pirIntervalS",
191603
191760
  label: "Cooldown (seconds)",
191604
191761
  labelKey: "provider-reolink:schema.s_pir.pirIntervalS.label",
191605
- description: `Minimum delay between consecutive PIR triggers. Reolink app range 5..${intervalMax}s; firmware clamps on push.`,
191606
191762
  min: 5,
191607
191763
  max: intervalMax,
191608
191764
  step: 1,
@@ -191615,8 +191771,6 @@ var PirAccessory = class extends BaseDevice {
191615
191771
  key: "pirReduceAlarm",
191616
191772
  label: "Reduce false alarms",
191617
191773
  labelKey: "provider-reolink:schema.s_pir.pirReduceAlarm.label",
191618
- description: "Trades detection range for fewer false positives. Maps to firmware `reduceAlarm` flag.",
191619
- descriptionKey: "provider-reolink:schema.s_pir.pirReduceAlarm.description",
191620
191774
  ...typeof reduceAlarm === "boolean" ? { default: reduceAlarm } : {},
191621
191775
  style: "switch",
191622
191776
  span: 2
@@ -193165,8 +193319,6 @@ function buildSessionsTabSections(snap, opts) {
193165
193319
  return [{
193166
193320
  id: "sessions-status",
193167
193321
  title: opts.title ?? "Sessions",
193168
- description: "Diagnostics for the Baichuan control channel. Auto-refreshes when older than 1 minute; click Refresh to force.",
193169
- descriptionKey: "provider-reolink:sessionsTabSections.s_sessions-status.description",
193170
193322
  tab: opts.tabId,
193171
193323
  order: 0,
193172
193324
  columns: 1,
@@ -193184,8 +193336,6 @@ function buildSessionsTabSections(snap, opts) {
193184
193336
  key: "sessionsOnlineUsers",
193185
193337
  label: "Online users (firmware)",
193186
193338
  labelKey: "provider-reolink:sessionsTabSections.s_sessions-status.sessionsOnlineUsers.label",
193187
- description: "Active user sessions reported by the device (cmd_id 145). Includes other admins, mobile app, NVR linkups.",
193188
- descriptionKey: "provider-reolink:sessionsTabSections.s_sessions-status.sessionsOnlineUsers.description",
193189
193339
  columns: [
193190
193340
  {
193191
193341
  key: "userName",
@@ -193228,8 +193378,6 @@ function buildSessionsTabSections(snap, opts) {
193228
193378
  key: "sessionsSocketPool",
193229
193379
  label: "Our socket pool",
193230
193380
  labelKey: "provider-reolink:sessionsTabSections.s_sessions-status.sessionsSocketPool.label",
193231
- description: "Open Baichuan TCP/UDP sockets this process holds for the device. One row per pool tag.",
193232
- descriptionKey: "provider-reolink:sessionsTabSections.s_sessions-status.sessionsSocketPool.description",
193233
193381
  columns: [{
193234
193382
  key: "tag",
193235
193383
  label: "Tag",
@@ -193250,8 +193398,6 @@ function buildSessionsTabSections(snap, opts) {
193250
193398
  key: "sessionsDedicated",
193251
193399
  label: "Dedicated lib sessions",
193252
193400
  labelKey: "provider-reolink:sessionsTabSections.s_sessions-status.sessionsDedicated.label",
193253
- description: "Internal lib-side dedicated sessions (streaming/talk channels). Older lib versions leave this list empty.",
193254
- descriptionKey: "provider-reolink:sessionsTabSections.s_sessions-status.sessionsDedicated.description",
193255
193401
  columns: [{
193256
193402
  key: "key",
193257
193403
  label: "Session key",
@@ -193472,6 +193618,137 @@ function motionSensorOnSimpleEvent(event) {
193472
193618
  if (typeof motion !== "object" || motion === null || !("source" in motion)) return null;
193473
193619
  return isMotionSensor(motion.source) ? motion.source : null;
193474
193620
  }
193621
+ var CONNECTIVITY_CODES = new Set([
193622
+ "EHOSTUNREACH",
193623
+ "EHOSTDOWN",
193624
+ "ENETUNREACH",
193625
+ "ENETDOWN",
193626
+ "ECONNREFUSED",
193627
+ "ECONNRESET",
193628
+ "ETIMEDOUT",
193629
+ "EADDRNOTAVAIL",
193630
+ "EPIPE"
193631
+ ]);
193632
+ var CONNECTIVITY_TEXT = /EHOSTUNREACH|EHOSTDOWN|ENETUNREACH|ENETDOWN|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EADDRNOTAVAIL|EPIPE|timed? ?out|socket hang up|socket closed|not connected/i;
193633
+ function errorCode(err) {
193634
+ if (typeof err !== "object" || err === null || !("code" in err)) return null;
193635
+ const code = err.code;
193636
+ return typeof code === "string" ? code : null;
193637
+ }
193638
+ /**
193639
+ * Did this failure say the camera could not be REACHED? An answer that came
193640
+ * back (wrong password, a rejected command) proves the camera is there, and is
193641
+ * not a reason to call it offline.
193642
+ */
193643
+ function isConnectivityFailure(err) {
193644
+ const code = errorCode(err);
193645
+ if (code !== null && CONNECTIVITY_CODES.has(code)) return true;
193646
+ const text = err instanceof Error ? err.message : String(err);
193647
+ return CONNECTIVITY_TEXT.test(text);
193648
+ }
193649
+ var ReachabilityOfflineTracker = class {
193650
+ deps;
193651
+ stateValue = "reachable";
193652
+ since = 0;
193653
+ failures = 0;
193654
+ lastReason = "";
193655
+ offlineAt = 0;
193656
+ timer = null;
193657
+ probing = false;
193658
+ disposed = false;
193659
+ windowMs;
193660
+ recoveryProbeMs;
193661
+ constructor(deps) {
193662
+ this.deps = deps;
193663
+ this.windowMs = deps.windowMs ?? 12e4;
193664
+ this.recoveryProbeMs = deps.recoveryProbeMs ?? 3e4;
193665
+ }
193666
+ get state() {
193667
+ return this.stateValue;
193668
+ }
193669
+ /** The camera could not be reached (socket lost, a login refused at the network level). */
193670
+ noteFailure(reason) {
193671
+ if (this.disposed) return;
193672
+ this.failures += 1;
193673
+ this.lastReason = reason;
193674
+ if (this.stateValue !== "reachable") return;
193675
+ this.stateValue = "failing";
193676
+ this.since = Date.now();
193677
+ this.arm(this.windowMs, () => this.confirm());
193678
+ }
193679
+ /** The camera answered (a login, a stream). Closes a window; ends an outage once. */
193680
+ noteReachable() {
193681
+ if (this.disposed) return;
193682
+ const wasOffline = this.stateValue === "offline";
193683
+ const offlineForMs = Date.now() - this.offlineAt;
193684
+ this.clear();
193685
+ if (wasOffline) this.deps.onOnline({ offlineForMs });
193686
+ }
193687
+ /** The firmware said what the camera is (a push): forget our own verdict, emit nothing. */
193688
+ reset() {
193689
+ this.clear();
193690
+ }
193691
+ dispose() {
193692
+ this.disposed = true;
193693
+ this.clear();
193694
+ }
193695
+ clear() {
193696
+ if (this.timer !== null) clearTimeout(this.timer);
193697
+ this.timer = null;
193698
+ this.stateValue = "reachable";
193699
+ this.failures = 0;
193700
+ this.lastReason = "";
193701
+ }
193702
+ arm(ms, fire) {
193703
+ if (this.timer !== null) clearTimeout(this.timer);
193704
+ this.timer = setTimeout(() => {
193705
+ this.timer = null;
193706
+ fire();
193707
+ }, ms);
193708
+ if (typeof this.timer.unref === "function") this.timer.unref();
193709
+ }
193710
+ /** End of the window: one fresh probe must agree before the camera is called offline. */
193711
+ confirm() {
193712
+ this.runProbe((result) => {
193713
+ if (this.stateValue !== "failing") return;
193714
+ if (result === "reachable") {
193715
+ this.noteReachable();
193716
+ return;
193717
+ }
193718
+ this.stateValue = "offline";
193719
+ this.offlineAt = Date.now();
193720
+ this.deps.onOffline({
193721
+ since: this.since,
193722
+ failures: this.failures,
193723
+ lastReason: this.lastReason
193724
+ });
193725
+ this.arm(this.recoveryProbeMs, () => this.recover());
193726
+ });
193727
+ }
193728
+ recover() {
193729
+ this.runProbe((result) => {
193730
+ if (this.stateValue !== "offline") return;
193731
+ if (result === "reachable") {
193732
+ this.noteReachable();
193733
+ return;
193734
+ }
193735
+ this.arm(this.recoveryProbeMs, () => this.recover());
193736
+ });
193737
+ }
193738
+ async runProbe(then) {
193739
+ if (this.probing || this.disposed) return;
193740
+ this.probing = true;
193741
+ let result;
193742
+ try {
193743
+ result = await this.deps.probe();
193744
+ } catch {
193745
+ result = "unreachable";
193746
+ } finally {
193747
+ this.probing = false;
193748
+ }
193749
+ if (!this.disposed) then(result);
193750
+ }
193751
+ };
193475
193752
  //#endregion
193476
193753
  //#region src/reolink-camera.ts
193477
193754
  /**
@@ -193862,6 +194139,17 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
193862
194139
  /** Pending reconnect timer; cleared on successful reconnect or removeDevice. */
193863
194140
  reconnectTimer = null;
193864
194141
  /**
194142
+ * D904: offline from SUSTAINED unreachability on a mains camera that owns its
194143
+ * socket. Fed by socket loss and network-level login failures; its verdicts
194144
+ * are the normal DeviceOffline / DeviceOnline. Never fed for a battery camera
194145
+ * (asleep is not gone, D173) nor for a hub child (the Hub owns that socket).
194146
+ */
194147
+ reachability = new ReachabilityOfflineTracker({
194148
+ probe: () => this.probeReachability(),
194149
+ onOffline: (info) => this.onReachabilityOffline(info),
194150
+ onOnline: (info) => this.onReachabilityOnline(info)
194151
+ });
194152
+ /**
193865
194153
  * Read-through to the `battery.sleeping` runtime-state slice with
193866
194154
  * a `false` default for non-battery cams (no slice). Internal
193867
194155
  * shorthand — driver code uses `this.state.battery.sleeping = b` to write
@@ -195494,7 +195782,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
195494
195782
  tab: "motion",
195495
195783
  title: "Battery motion (sleep push)",
195496
195784
  titleKey: "provider-reolink:emailPushSection.s_sleep-push.title",
195497
- description: "Battery cameras sleep and can't be polled for motion. Prefer HaCfg webhook when the firmware supports it (cmd 806); otherwise use Email/SMTP. Enable the matching server under Integrations → Reolink, then Auto-configure. Only one path should be armed — the preference below gates which inbound source is accepted.",
195785
+ description: "Prefer the HaCfg webhook when the firmware supports it; arm only one path.",
195498
195786
  descriptionKey: "provider-reolink:emailPushSection.s_sleep-push.description",
195499
195787
  columns: 1,
195500
195788
  fields: [
@@ -195560,7 +195848,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
195560
195848
  tab: "email",
195561
195849
  title: "SMTP server",
195562
195850
  titleKey: "provider-reolink:emailTabSections.s_email-smtp.title",
195563
- description: "The camera's own outgoing-mail (SMTP) settings. Edit and save to write them to the camera, or use \"Auto-configure email push\" under Motion to point it at the built-in server. Leave the password blank to keep the current one.",
195851
+ description: "Leave the password blank to keep the current one.",
195564
195852
  descriptionKey: "provider-reolink:emailTabSections.s_email-smtp.description",
195565
195853
  columns: 2,
195566
195854
  fields: [
@@ -195615,8 +195903,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
195615
195903
  tab: "email",
195616
195904
  title: "Recipients",
195617
195905
  titleKey: "provider-reolink:emailTabSections.s_email-recipients.title",
195618
- description: "Up to three recipient addresses the camera sends motion mail to.",
195619
- descriptionKey: "provider-reolink:emailTabSections.s_email-recipients.description",
195620
195906
  columns: 1,
195621
195907
  fields: [
195622
195908
  {
@@ -195645,8 +195931,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
195645
195931
  tab: "email",
195646
195932
  title: "Alarm schedule",
195647
195933
  titleKey: "provider-reolink:emailTabSections.s_email-schedule.title",
195648
- description: "Master enable for the camera's email alarm schedule (EmailTask).",
195649
- descriptionKey: "provider-reolink:emailTabSections.s_email-schedule.description",
195650
195934
  columns: 1,
195651
195935
  fields: [{
195652
195936
  type: "boolean",
@@ -198617,6 +198901,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
198617
198901
  clearTimeout(this.reconnectTimer);
198618
198902
  this.reconnectTimer = null;
198619
198903
  }
198904
+ this.reachability.dispose();
198620
198905
  this.ctx.logger.info("Removing Reolink camera", { meta: { stableId: this.stableId } });
198621
198906
  const ids = [...this.publishedStreamIds];
198622
198907
  await Promise.all(ids.map((id) => this.ctx.api.streamBroker.retractCameraStream.mutate({
@@ -199521,7 +199806,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
199521
199806
  tab: "general",
199522
199807
  title: "Camera data",
199523
199808
  titleKey: "provider-reolink:schema.s_driver-refresh.title",
199524
- description: "Go and ask the camera now. Every other read here is deliberately cheap about a battery camera — it serves what it already knows and refreshes quietly behind you, and never wakes a sleeping camera just because you opened this tab. This button is the exception: it wakes the camera, re-reads everything, and tells you what it got. Use it after changing settings from the Reolink app, or whenever you want to be sure the form is showing the camera and not a memory of it.",
199809
+ description: "Wakes a sleeping camera and re-reads everything; no other read here wakes it.",
199525
199810
  descriptionKey: "provider-reolink:schema.s_driver-refresh.description",
199526
199811
  columns: 1,
199527
199812
  fields: [{
@@ -199540,8 +199825,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
199540
199825
  tab: "general",
199541
199826
  title: "Connection",
199542
199827
  titleKey: "provider-reolink:schema.s_connection.title",
199543
- description: "Baichuan protocol connection settings.",
199544
- descriptionKey: "provider-reolink:schema.s_connection.description",
199545
199828
  columns: 2,
199546
199829
  fields: [
199547
199830
  {
@@ -199584,7 +199867,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
199584
199867
  tab: "network",
199585
199868
  title: "Service ports",
199586
199869
  titleKey: "provider-reolink:schema.s_network-ports.title",
199587
- description: "Per-protocol enable toggle + port number, pushed via Baichuan `SetNetPort` (cmd_id 36). The Baichuan control port is the Connection → Port field. Changing a port number breaks existing URLs for that protocol until consumers are reconfigured.",
199870
+ description: "Changing a port breaks existing URLs for that protocol.",
199588
199871
  descriptionKey: "provider-reolink:schema.s_network-ports.description",
199589
199872
  columns: 2,
199590
199873
  fields: [
@@ -199685,8 +199968,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
199685
199968
  tab: "network",
199686
199969
  title: "Time / NTP",
199687
199970
  titleKey: "provider-reolink:schema.s_time-ntp.title",
199688
- description: "Network time synchronisation. The camera-side push goes through the native Baichuan NTP commands; until those land in the driver lib the values persist to the driver config and apply on the next lib update.",
199689
- descriptionKey: "provider-reolink:schema.s_time-ntp.description",
199690
199971
  columns: 2,
199691
199972
  fields: [
199692
199973
  {
@@ -199694,8 +199975,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
199694
199975
  key: "timeZoneId",
199695
199976
  label: "Timezone",
199696
199977
  labelKey: "provider-reolink:schema.s_time-ntp.timeZoneId.label",
199697
- description: "Camera timezone. Selecting a zone pushes its standard UTC offset (Baichuan `SetSystemGeneral`, cmd 105) and DST rule (`SetDst`, cmd 107) to the camera — DST is handled automatically by the chosen zone, so summer/winter clocks track correctly without a separate toggle.",
199698
- descriptionKey: "provider-reolink:schema.s_time-ntp.timeZoneId.description",
199699
199978
  default: this.config.get("timeZoneId") ?? reolinkTimeZoneToIana(this.config.get("deviceCache")?.systemGeneralSnapshot?.timeZone) ?? "",
199700
199979
  span: 2
199701
199980
  },
@@ -199744,16 +200023,12 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
199744
200023
  tab: "motion",
199745
200024
  title: "On-camera motion (MdAlarm)",
199746
200025
  titleKey: "provider-reolink:schema.s_motion.title",
199747
- description: "Camera-side motion detection master switch + sensitivity. Pushed via Baichuan `SetMdAlarm` (cmd_id 47). Range 1..50 matches the Reolink mobile app + web UI; the driver inverts to the camera-side raw value (`51 - value`) before push.",
199748
- descriptionKey: "provider-reolink:schema.s_motion.description",
199749
200026
  columns: 2,
199750
200027
  fields: [{
199751
200028
  type: "boolean",
199752
200029
  key: "motionEnabled",
199753
200030
  label: "Motion detection",
199754
200031
  labelKey: "provider-reolink:schema.s_motion.motionEnabled.label",
199755
- description: "Master switch for the camera's on-board motion detector. Disabling stops the camera from emitting motion events on the alarm channel.",
199756
- descriptionKey: "provider-reolink:schema.s_motion.motionEnabled.description",
199757
200032
  default: motionSnap.enabled ?? true,
199758
200033
  style: "switch",
199759
200034
  span: 2
@@ -199762,8 +200037,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
199762
200037
  key: "motionSensitivity",
199763
200038
  label: "Sensitivity",
199764
200039
  labelKey: "provider-reolink:schema.s_motion.motionSensitivity.label",
199765
- description: "Higher = triggers on smaller motion. Reolink user-facing range 1..50.",
199766
- descriptionKey: "provider-reolink:schema.s_motion.motionSensitivity.description",
199767
200040
  min: 1,
199768
200041
  max: 50,
199769
200042
  step: 1,
@@ -199778,8 +200051,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
199778
200051
  tab: "motion",
199779
200052
  title: "AI detection sensitivity",
199780
200053
  titleKey: "provider-reolink:schema.s_ai-sensitivity.title",
199781
- description: "Per-class on-camera AI sensitivity, stay time (dwell) + min/max target-size gates. Pushed via `SetAiAlarm` (cmd_id 343) in one consolidated write. Sensitivity range 0..100; stay time is the seconds the camera keeps the alarm asserted after the object leaves; target-size gates are the fraction of the frame (0..1) an object must occupy to be considered. Each group appears only when the camera advertises the corresponding detector.",
199782
- descriptionKey: "provider-reolink:schema.s_ai-sensitivity.description",
199783
200054
  columns: 2,
199784
200055
  fields: [
199785
200056
  [
@@ -199848,7 +200119,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
199848
200119
  type: "slider",
199849
200120
  key: stayKey,
199850
200121
  label: `${label} stay time (s)`,
199851
- description: "Seconds the camera keeps the AI alarm asserted after the object leaves frame.",
199852
200122
  min: 0,
199853
200123
  max: 600,
199854
200124
  step: 1,
@@ -199911,8 +200181,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
199911
200181
  tab: "image",
199912
200182
  title: "Image",
199913
200183
  titleKey: "provider-reolink:schema.s_image.title",
199914
- description: "Core image settings pushed via Baichuan `SetImage` (cmd_id 25). Each slider rearms the camera on save; defaults seed from a `GetVideoInput` probe at boot (refreshed on every `applySettingsPatch`). Range 0..255 — most Reolink models clamp to 0..100 internally.",
199915
- descriptionKey: "provider-reolink:schema.s_image.description",
199916
200184
  columns: 2,
199917
200185
  fields: [
199918
200186
  {
@@ -199983,24 +200251,18 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
199983
200251
  tab: "image",
199984
200252
  title: "Advanced (ISP)",
199985
200253
  titleKey: "provider-reolink:schema.s_isp.title",
199986
- description: "Day/Night + exposure + HDR. Pushed via `SetIsp` (cmd_id 25). The day/night threshold uses cmd_id 297 (DayNightThreshold).",
199987
- descriptionKey: "provider-reolink:schema.s_isp.description",
199988
200254
  columns: 2,
199989
200255
  fields: [{
199990
200256
  type: "text",
199991
200257
  key: "ispDayNight",
199992
200258
  label: "Day/Night mode",
199993
200259
  labelKey: "provider-reolink:schema.s_isp.ispDayNight.label",
199994
- description: "Camera-specific values — common: `auto`, `color`, `blackAndWhite`. The driver normalizes case before push.",
199995
- descriptionKey: "provider-reolink:schema.s_isp.ispDayNight.description",
199996
200260
  default: imgSnap.dayNight ?? "auto"
199997
200261
  }, {
199998
200262
  type: "text",
199999
200263
  key: "ispExposure",
200000
200264
  label: "Exposure",
200001
200265
  labelKey: "provider-reolink:schema.s_isp.ispExposure.label",
200002
- description: "Common values: `auto`, `manual`. Lowercased before push.",
200003
- descriptionKey: "provider-reolink:schema.s_isp.ispExposure.description",
200004
200266
  default: "auto"
200005
200267
  }]
200006
200268
  }] : [],
@@ -200009,8 +200271,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200009
200271
  tab: "image",
200010
200272
  title: "IR + status LED",
200011
200273
  titleKey: "provider-reolink:schema.s_ir-lights.title",
200012
- description: "IR LED control + per-camera IR brightness. Pushed via `SetIrLights` (cmd_id 209). State `Auto` lets the camera switch IR on automatically at low light; `Off` forces colour-only mode.",
200013
- descriptionKey: "provider-reolink:schema.s_ir-lights.description",
200014
200274
  columns: 2,
200015
200275
  fields: [
200016
200276
  {
@@ -200042,7 +200302,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200042
200302
  key: "irLightsBrightness",
200043
200303
  label: "IR brightness",
200044
200304
  labelKey: "provider-reolink:schema.s_ir-lights.irLightsBrightness.label",
200045
- description: "Only applied when the camera advertises programmable IR brightness. 0..255 raw range; many models clamp to 0..100.",
200305
+ description: "Only applied when the camera supports adjustable IR brightness.",
200046
200306
  descriptionKey: "provider-reolink:schema.s_ir-lights.irLightsBrightness.description",
200047
200307
  min: 0,
200048
200308
  max: 255,
@@ -200056,8 +200316,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200056
200316
  key: "statusLedEnabled",
200057
200317
  label: "Status LED",
200058
200318
  labelKey: "provider-reolink:schema.s_ir-lights.statusLedEnabled.label",
200059
- description: "The visible status/power LED on the camera body. Pushed via `SetIrLights` (cmd_id 209) as `lightState`.",
200060
- descriptionKey: "provider-reolink:schema.s_ir-lights.statusLedEnabled.description",
200061
200319
  default: cache?.ledSnapshot?.statusEnabled ?? true,
200062
200320
  style: "switch",
200063
200321
  span: cache?.ledSnapshot?.doorbellSupported === true ? 1 : 2
@@ -200067,8 +200325,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200067
200325
  key: "doorbellLedEnabled",
200068
200326
  label: "Doorbell LED",
200069
200327
  labelKey: "provider-reolink:schema.s_ir-lights.doorbellLedEnabled.label",
200070
- description: "The doorbell button ring LED. Pushed via `SetIrLights` (cmd_id 209) as `doorbellLightState`.",
200071
- descriptionKey: "provider-reolink:schema.s_ir-lights.doorbellLedEnabled.description",
200072
200328
  default: cache?.ledSnapshot?.doorbellEnabled ?? true,
200073
200329
  style: "switch"
200074
200330
  }] : []
@@ -200083,7 +200339,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200083
200339
  tab: "image",
200084
200340
  title: "Auto focus",
200085
200341
  titleKey: "provider-reolink:schema.s_auto-focus.title",
200086
- description: "Toggles the lens auto-focus subsystem. Pushed via `SetAutoFocus` (cmd_id 225). Disabling locks the lens at its current focal position — useful for fixed-distance scenes where AF would hunt.",
200342
+ description: "Disabling locks the lens at its current focus.",
200087
200343
  descriptionKey: "provider-reolink:schema.s_auto-focus.description",
200088
200344
  columns: 1,
200089
200345
  fields: [{
@@ -200100,8 +200356,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200100
200356
  tab: "audio",
200101
200357
  title: "Audio output",
200102
200358
  titleKey: "provider-reolink:schema.s_audio.title",
200103
- description: "Speaker volumes pushed via `SetAudioCfg` (cmd_id 265). All ranges 0..100. Doorbell models expose the visitor / talkAndReply channels — non-doorbell cameras ignore those without erroring.",
200104
- descriptionKey: "provider-reolink:schema.s_audio.description",
200105
200359
  columns: 2,
200106
200360
  fields: [
200107
200361
  {
@@ -200121,7 +200375,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200121
200375
  key: "audioTalkAndReplyVolume",
200122
200376
  label: "Talk + reply volume",
200123
200377
  labelKey: "provider-reolink:schema.s_audio.audioTalkAndReplyVolume.label",
200124
- description: "Two-way audio + canned reply playback. Doorbell-only on most firmwares.",
200378
+ description: "Doorbell models only.",
200125
200379
  descriptionKey: "provider-reolink:schema.s_audio.audioTalkAndReplyVolume.description",
200126
200380
  min: 0,
200127
200381
  max: 100,
@@ -200135,7 +200389,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200135
200389
  key: "audioVisitorVolume",
200136
200390
  label: "Visitor volume",
200137
200391
  labelKey: "provider-reolink:schema.s_audio.audioVisitorVolume.label",
200138
- description: "Visitor / chime playback channel. Doorbell-only on most firmwares.",
200392
+ description: "Doorbell models only.",
200139
200393
  descriptionKey: "provider-reolink:schema.s_audio.audioVisitorVolume.description",
200140
200394
  min: 0,
200141
200395
  max: 100,
@@ -200152,8 +200406,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200152
200406
  tab: "audio",
200153
200407
  title: "Audio noise reduction",
200154
200408
  titleKey: "provider-reolink:schema.s_audio-noise.title",
200155
- description: "On-camera AI denoise applied to the encoded audio track. Pushed via `SetAudioNoise` (cmd_id 440). 0 disables; 1..3 maps to the Reolink mobile app levels (low / mid / high).",
200156
- descriptionKey: "provider-reolink:schema.s_audio-noise.description",
200157
200409
  columns: 1,
200158
200410
  fields: [{
200159
200411
  type: "slider",
@@ -200175,7 +200427,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200175
200427
  tab: "audio",
200176
200428
  title: "Intercom",
200177
200429
  titleKey: "provider-reolink:schema.s_intercom.title",
200178
- description: "Push-to-talk pipeline tuning. Settings take effect on the next talk session — restart any active session for changes to apply.",
200430
+ description: "Changes apply to the next talk session.",
200179
200431
  descriptionKey: "provider-reolink:schema.s_intercom.description",
200180
200432
  columns: 2,
200181
200433
  fields: [
@@ -200184,7 +200436,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200184
200436
  key: "intercomBlocksPerPayload",
200185
200437
  label: "Blocks per payload",
200186
200438
  labelKey: "provider-reolink:schema.s_intercom.intercomBlocksPerPayload.label",
200187
- description: "Lower = lower latency (more sendAudio calls/sec). Higher = fewer calls, less per-call socket overhead. Range 1..8. Default 1 (lowest latency).",
200439
+ description: "Lower is lower latency, higher is fewer calls. Range 1..8.",
200188
200440
  descriptionKey: "provider-reolink:schema.s_intercom.intercomBlocksPerPayload.description",
200189
200441
  min: 1,
200190
200442
  max: 8,
@@ -200198,7 +200450,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200198
200450
  key: "intercomMaxBacklogMs",
200199
200451
  label: "Max backlog (ms)",
200200
200452
  labelKey: "provider-reolink:schema.s_intercom.intercomMaxBacklogMs.label",
200201
- description: "Maximum PCM backlog before dropping oldest samples to cap latency. Higher = more stable on slow systems but more latency. Range 20..5000. Default 120.",
200453
+ description: "Higher is more stable on slow systems but adds latency. Range 20..5000.",
200202
200454
  descriptionKey: "provider-reolink:schema.s_intercom.intercomMaxBacklogMs.description",
200203
200455
  min: 20,
200204
200456
  max: 5e3,
@@ -200212,7 +200464,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200212
200464
  key: "intercomGain",
200213
200465
  label: "Output gain",
200214
200466
  labelKey: "provider-reolink:schema.s_intercom.intercomGain.label",
200215
- description: "Multiplier applied to PCM samples before IMA-ADPCM encode. 1.0 = neutral, 2.0 ≈ +6dB, 0.5 ≈ -6dB. Hard-clipped at int16 bounds.",
200467
+ description: "1.0 = neutral, 2.0 ≈ +6 dB, 0.5 ≈ -6 dB.",
200216
200468
  descriptionKey: "provider-reolink:schema.s_intercom.intercomGain.description",
200217
200469
  min: .1,
200218
200470
  max: 10,
@@ -200229,7 +200481,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200229
200481
  tab: "advanced",
200230
200482
  title: "Keyframe interval (GOP)",
200231
200483
  titleKey: "provider-reolink:schema.s_keyframe-gop.title",
200232
- description: "Main-stream keyframe interval in seconds. Pushed via `SetEnc` (cmd_id 57) as `<gop><cur>`. Lower = more keyframes (faster seek/recovery, higher bitrate); higher = fewer keyframes (lower bitrate). The camera clamps to its supported range.",
200484
+ description: "Lower = more keyframes (faster recovery, higher bitrate).",
200233
200485
  descriptionKey: "provider-reolink:schema.s_keyframe-gop.description",
200234
200486
  columns: 1,
200235
200487
  fields: [{
@@ -200250,8 +200502,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200250
200502
  tab: "advanced",
200251
200503
  title: "PIR detection",
200252
200504
  titleKey: "provider-reolink:schema.s_pir-reduce-alarm.title",
200253
- description: "Passive-infrared false-alarm filter. Pushed via `SetPirInfo` (cmd_id 213) as `reduceAlarm`. Enabling makes the camera require a stronger/longer PIR signal before triggering, cutting false motion alerts from heat/light flicker.",
200254
- descriptionKey: "provider-reolink:schema.s_pir-reduce-alarm.description",
200255
200505
  columns: 1,
200256
200506
  fields: [{
200257
200507
  type: "boolean",
@@ -200267,8 +200517,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200267
200517
  tab: "advanced",
200268
200518
  title: "Auto reboot",
200269
200519
  titleKey: "provider-reolink:schema.s_auto-reboot.title",
200270
- description: "Scheduled automatic reboot. Pushed via `SetAutoReboot` (cmd_id 100). Useful for long-uptime stability. Choose a day (or every day) + time; the camera reboots once per matching window.",
200271
- descriptionKey: "provider-reolink:schema.s_auto-reboot.description",
200272
200520
  columns: 2,
200273
200521
  fields: [
200274
200522
  {
@@ -200357,7 +200605,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200357
200605
  tab: "advanced",
200358
200606
  title: "Debug",
200359
200607
  titleKey: "provider-reolink:schema.s_debug.title",
200360
- description: "Lib-side Baichuan tracing. Changing these resets the control-channel client (active streams keep running) so new debug options take effect on the next dial.",
200608
+ description: "Debug logging adds log volume; use it temporarily.",
200361
200609
  descriptionKey: "provider-reolink:schema.s_debug.description",
200362
200610
  columns: 1,
200363
200611
  fields: [{
@@ -200365,8 +200613,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200365
200613
  key: "debugGeneral",
200366
200614
  label: "General debug logs",
200367
200615
  labelKey: "provider-reolink:schema.s_debug.debugGeneral.label",
200368
- description: "Forwards `DebugOptions.general=true` — verbose lib protocol logs.",
200369
- descriptionKey: "provider-reolink:schema.s_debug.debugGeneral.description",
200370
200616
  default: false,
200371
200617
  style: "switch"
200372
200618
  }, {
@@ -200374,7 +200620,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200374
200620
  key: "debugSocketLogs",
200375
200621
  label: "Socket trace categories",
200376
200622
  labelKey: "provider-reolink:schema.s_debug.debugSocketLogs.label",
200377
- description: "Per-area lib tracing. Pick only what you need; each adds log volume.",
200623
+ description: "Each area adds log volume.",
200378
200624
  descriptionKey: "provider-reolink:schema.s_debug.debugSocketLogs.description",
200379
200625
  default: [],
200380
200626
  options: [
@@ -201036,11 +201282,14 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
201036
201282
  await api.login();
201037
201283
  } catch (err) {
201038
201284
  this.loginPromise = null;
201285
+ if (isConnectivityFailure(err)) this.noteUnreachable(err instanceof Error ? err.message : String(err));
201286
+ else if (this.ownsReachability()) this.reachability.noteReachable();
201039
201287
  throw err;
201040
201288
  }
201041
201289
  this.api = api;
201042
201290
  this.loginPromise = null;
201043
201291
  this.reconnectAttempts = 0;
201292
+ if (this.ownsReachability()) this.reachability.noteReachable();
201044
201293
  const persistedDeviceType = this.config.get("deviceCache")?.deviceType;
201045
201294
  if (persistedDeviceType === "battery-cam") {
201046
201295
  this.isBattery = true;
@@ -201103,8 +201352,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
201103
201352
  * - 'awake' / 'sleeping': emit DeviceAwake / DeviceSleeping. Battery
201104
201353
  * power-state only — does NOT touch online state.
201105
201354
  * - 'online' / 'offline': emit DeviceOnline / DeviceOffline + flip
201106
- * `this.online`. The ONLY emit site for these — socket close,
201107
- * reconnect, RFC4571 idle teardown all leave online state alone.
201355
+ * `this.online`, and reset the reachability verdict. The only OTHER
201356
+ * source is sustained unreachability on a mains camera (D904,
201357
+ * `onReachabilityOffline`): a single socket close, a reconnect or an
201358
+ * RFC4571 idle teardown still leaves online state alone.
201108
201359
  * - 'battery': refresh cache, emit cap event.
201109
201360
  * - 'doorbell', 'daynight', 'other': informational debug log only.
201110
201361
  */
@@ -201215,6 +201466,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
201215
201466
  }
201216
201467
  if (event.type === "online" || event.type === "offline") {
201217
201468
  const next = event.type === "online";
201469
+ this.reachability.reset();
201218
201470
  if (next !== this.online) {
201219
201471
  this.markOnline(next);
201220
201472
  this.ctx.eventBus.emit(createEvent(next ? EventCategory.DeviceOnline : EventCategory.DeviceOffline, eventSource, next ? {
@@ -201425,6 +201677,67 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
201425
201677
  await current.close({ reason });
201426
201678
  } catch {}
201427
201679
  }
201680
+ /** D904 applies to a mains camera that dials its own socket — not a battery cam, not a hub child. */
201681
+ ownsReachability() {
201682
+ return !this.isBattery && !this.isHubChild();
201683
+ }
201684
+ /** The camera could not be reached: open (or extend) the reachability window. */
201685
+ noteUnreachable(reason) {
201686
+ if (!this.ownsReachability()) return;
201687
+ this.reachability.noteFailure(reason);
201688
+ }
201689
+ /**
201690
+ * One fresh login, for the tracker's confirmation and recovery probes. A
201691
+ * battery cam or hub child is never probed (D173) and answers `reachable`,
201692
+ * which closes the window instead of calling it offline.
201693
+ */
201694
+ async probeReachability() {
201695
+ if (!this.ownsReachability()) return "reachable";
201696
+ try {
201697
+ await this.ensureApi();
201698
+ return "reachable";
201699
+ } catch (err) {
201700
+ return isConnectivityFailure(err) ? "unreachable" : "reachable";
201701
+ }
201702
+ }
201703
+ /** Sustained unreachability, confirmed: the normal DeviceOffline, once. */
201704
+ onReachabilityOffline(info) {
201705
+ if (!this.online) {
201706
+ this.ctx.logger.info("Reolink camera unreachable, already offline — nothing to emit", {
201707
+ tags: { deviceId: this.id },
201708
+ meta: { ...info }
201709
+ });
201710
+ return;
201711
+ }
201712
+ this.ctx.logger.warn("Reolink camera unreachable for the whole window — marked offline", {
201713
+ tags: { deviceId: this.id },
201714
+ meta: {
201715
+ ...info,
201716
+ unreachableForMs: Date.now() - info.since
201717
+ }
201718
+ });
201719
+ this.markOnline(false);
201720
+ this.ctx.eventBus.emit(createEvent(EventCategory.DeviceOffline, this.eventSource(), {
201721
+ deviceId: this.id,
201722
+ providerId: REOLINK_ADDON_ID,
201723
+ reason: "unreachable"
201724
+ }));
201725
+ }
201726
+ /** The first success after a reachability outage: the normal DeviceOnline, once. */
201727
+ onReachabilityOnline(info) {
201728
+ if (this.online) return;
201729
+ this.ctx.logger.info("Reolink camera reachable again — marked online", {
201730
+ tags: { deviceId: this.id },
201731
+ meta: { ...info }
201732
+ });
201733
+ this.markOnline(true);
201734
+ this.ctx.eventBus.emit(createEvent(EventCategory.DeviceOnline, this.eventSource(), {
201735
+ deviceId: this.id,
201736
+ providerId: REOLINK_ADDON_ID,
201737
+ profileCount: 0,
201738
+ reason: "reachable-again"
201739
+ }));
201740
+ }
201428
201741
  scheduleReconnect(reason) {
201429
201742
  if (this.reconnectTimer) return;
201430
201743
  if (this.isBattery) {
@@ -201438,6 +201751,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
201438
201751
  });
201439
201752
  return;
201440
201753
  }
201754
+ this.noteUnreachable(reason);
201441
201755
  const attempt = ++this.reconnectAttempts;
201442
201756
  const backoffMs = Math.min(3e4, 1e3 * 2 ** Math.min(attempt - 1, 5));
201443
201757
  this.ctx.logger.info("Baichuan reconnect scheduled", {
@@ -201877,7 +202191,7 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
201877
202191
  tab: "general",
201878
202192
  title: "Connection",
201879
202193
  titleKey: "provider-reolink:hub.s_connection.title",
201880
- description: "Baichuan protocol connection settings for the hub. The same socket fronts every adopted channel — child cameras inherit these credentials, no per-channel login needed.",
202194
+ description: "Child cameras inherit these credentials.",
201881
202195
  descriptionKey: "provider-reolink:hub.s_connection.description",
201882
202196
  columns: 2,
201883
202197
  fields: [
@@ -201921,8 +202235,6 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
201921
202235
  tab: "general",
201922
202236
  title: "Hub identity",
201923
202237
  titleKey: "provider-reolink:hub.s_identity.title",
201924
- description: "Read-only summary from the autodetect probe. Channel count and model are persisted at first contact so the panel renders without re-probing.",
201925
- descriptionKey: "provider-reolink:hub.s_identity.description",
201926
202238
  columns: 2,
201927
202239
  fields: [{
201928
202240
  type: "text",
@@ -201945,7 +202257,7 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
201945
202257
  tab: "advanced",
201946
202258
  title: "Debug",
201947
202259
  titleKey: "provider-reolink:hub.s_debug.title",
201948
- description: "Lib-side Baichuan tracing. Changing these resets the control-channel client (active streams keep running) so new debug options take effect on the next dial.",
202260
+ description: "Debug logging adds log volume; use it temporarily.",
201949
202261
  descriptionKey: "provider-reolink:hub.s_debug.description",
201950
202262
  columns: 1,
201951
202263
  fields: [{
@@ -201953,8 +202265,6 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
201953
202265
  key: "debugGeneral",
201954
202266
  label: "General debug logs",
201955
202267
  labelKey: "provider-reolink:hub.s_debug.debugGeneral.label",
201956
- description: "Forwards `DebugOptions.general=true` — verbose lib protocol logs.",
201957
- descriptionKey: "provider-reolink:hub.s_debug.debugGeneral.description",
201958
202268
  default: false,
201959
202269
  style: "switch"
201960
202270
  }, {
@@ -201962,7 +202272,7 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
201962
202272
  key: "debugSocketLogs",
201963
202273
  label: "Socket trace categories",
201964
202274
  labelKey: "provider-reolink:hub.s_debug.debugSocketLogs.label",
201965
- description: "Per-area lib tracing. Pick only what you need; each adds log volume.",
202275
+ description: "Each area adds log volume.",
201966
202276
  descriptionKey: "provider-reolink:hub.s_debug.debugSocketLogs.description",
201967
202277
  default: [],
201968
202278
  options: [
@@ -202725,7 +203035,7 @@ function buildDiscoveryParamsFormSchema() {
202725
203035
  id: "credentials",
202726
203036
  title: "Credentials",
202727
203037
  titleKey: "provider-reolink:discoveryParams.s_credentials.title",
202728
- description: "Used to probe the cameras and pre-filled into each result so you can add it in one click. Assumes all cameras share these credentials.",
203038
+ description: "Assumes all cameras share these credentials.",
202729
203039
  descriptionKey: "provider-reolink:discoveryParams.s_credentials.description",
202730
203040
  columns: 2,
202731
203041
  fields: [{
@@ -202747,7 +203057,7 @@ function buildDiscoveryParamsFormSchema() {
202747
203057
  id: "scan",
202748
203058
  title: "Scan options",
202749
203059
  titleKey: "provider-reolink:discoveryParams.s_scan.title",
202750
- description: "Leave the network blank to scan the local subnet. To find cameras on a different subnet, enter its CIDR (e.g. 192.168.20.0/24).",
203060
+ description: "Leave blank to scan the local subnet; for another subnet enter its CIDR (e.g. 192.168.20.0/24).",
202751
203061
  descriptionKey: "provider-reolink:discoveryParams.s_scan.description",
202752
203062
  columns: 1,
202753
203063
  fields: [
@@ -202798,8 +203108,6 @@ function buildCreationFormSchema() {
202798
203108
  id: "credentials",
202799
203109
  title: "Credentials",
202800
203110
  titleKey: "provider-reolink:creation.s_credentials.title",
202801
- description: "Required for the autodetect probe — entered before the host so Test can run immediately.",
202802
- descriptionKey: "provider-reolink:creation.s_credentials.description",
202803
203111
  columns: 2,
202804
203112
  fields: [{
202805
203113
  type: "text",
@@ -202821,8 +203129,6 @@ function buildCreationFormSchema() {
202821
203129
  id: "connection",
202822
203130
  title: "Connection",
202823
203131
  titleKey: "provider-reolink:creation.s_connection.title",
202824
- description: "Click Test to auto-detect the device type (regular camera / NVR / battery / UDP-only). The detected type is then used at save time.",
202825
- descriptionKey: "provider-reolink:creation.s_connection.description",
202826
203132
  columns: 2,
202827
203133
  fields: [
202828
203134
  {
@@ -202831,7 +203137,7 @@ function buildCreationFormSchema() {
202831
203137
  label: "Connection mode",
202832
203138
  labelKey: "provider-reolink:creation.s_connection.transport.label",
202833
203139
  default: "auto",
202834
- description: "Auto: try TCP first, fall back to UDP — best for unknown devices. UDP is required for battery cameras behind NAT.",
203140
+ description: "Auto tries TCP, then UDP. Battery cameras behind NAT need UDP.",
202835
203141
  descriptionKey: "provider-reolink:creation.s_connection.transport.description",
202836
203142
  options: [
202837
203143
  {
@@ -202858,7 +203164,7 @@ function buildCreationFormSchema() {
202858
203164
  labelKey: "provider-reolink:creation.s_connection.host.label",
202859
203165
  required: true,
202860
203166
  placeholder: "192.168.10.100",
202861
- description: "For UDP-only mode you can leave the host blank and supply the camera UID below — the library will discover it on the network.",
203167
+ description: "In UDP-only mode leave blank and use the UID below.",
202862
203168
  descriptionKey: "provider-reolink:creation.s_connection.host.description",
202863
203169
  inputType: "text"
202864
203170
  },
@@ -202871,8 +203177,6 @@ function buildCreationFormSchema() {
202871
203177
  min: 1,
202872
203178
  max: 65535,
202873
203179
  step: 1,
202874
- description: "Baichuan port. Default 9000 — applies to TCP. Ignored for UDP transport.",
202875
- descriptionKey: "provider-reolink:creation.s_connection.port.description",
202876
203180
  showWhen: {
202877
203181
  field: "transport",
202878
203182
  notEquals: "udp"
@@ -202884,7 +203188,7 @@ function buildCreationFormSchema() {
202884
203188
  label: "Device UID",
202885
203189
  labelKey: "provider-reolink:creation.s_connection.uid.label",
202886
203190
  placeholder: "9527000XXXXXXXX",
202887
- description: "Required for battery / UDP cameras when not on the same LAN. Optional for local-direct UDP discovery and ignored for pure TCP.",
203191
+ description: "Needed for battery / UDP cameras that are not on the same LAN.",
202888
203192
  descriptionKey: "provider-reolink:creation.s_connection.uid.description",
202889
203193
  showWhen: {
202890
203194
  field: "transport",
@@ -203425,7 +203729,7 @@ function buildEmailPushSettingsSchema(recommendedHost) {
203425
203729
  id: "email-push",
203426
203730
  title: "Battery-cam Motion (Email/SMTP Push)",
203427
203731
  titleKey: "provider-reolink:emailPushSettings.s_email-push.title",
203428
- description: "Emulate an SMTP server that battery Reolink cameras push to on motion. Lets you capture motion from sleeping battery cams without polling them awake. Enable, then use \"Auto-configure email push\" on each battery camera.",
203732
+ description: "Lets battery cameras report motion by email without being woken.",
203429
203733
  descriptionKey: "provider-reolink:emailPushSettings.s_email-push.description",
203430
203734
  columns: 2,
203431
203735
  fields: [
@@ -203465,7 +203769,7 @@ function buildEmailPushSettingsSchema(recommendedHost) {
203465
203769
  id: "email-push-auth",
203466
203770
  title: "Email Push Authentication",
203467
203771
  titleKey: "provider-reolink:emailPushSettings.s_email-push-auth.title",
203468
- description: "AUTH credentials the camera must present. Reolink firmwares are more reliable with AUTH on. Random values are generated on first boot — re-run Auto-configure on cameras after changing them.",
203772
+ description: "Random values are generated on first boot; re-run Auto-configure on cameras after changing them.",
203469
203773
  descriptionKey: "provider-reolink:emailPushSettings.s_email-push-auth.description",
203470
203774
  columns: 2,
203471
203775
  fields: [
@@ -203674,7 +203978,7 @@ function buildBaichuanWebhookSettingsSchema(recommendedHost) {
203674
203978
  id: "baichuan-webhook",
203675
203979
  title: "Battery-cam Motion (HaCfg Webhook)",
203676
203980
  titleKey: "provider-reolink:baichuanWebhookSettings.s_baichuan-webhook.title",
203677
- description: "HTTP intake for Baichuan HaCfg wake/sleep push (cmd 806/807). Prefer this over SMTP when the camera firmware supports it — lower latency, no AUTH dance. Enable, then use \"Auto-configure HaCfg webhook\" on each battery camera (or set preference to Auto).",
203981
+ description: "Preferred over email push when the camera firmware supports it.",
203678
203982
  descriptionKey: "provider-reolink:baichuanWebhookSettings.s_baichuan-webhook.description",
203679
203983
  columns: 2,
203680
203984
  fields: [