@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.mjs CHANGED
@@ -28,7 +28,7 @@ import { URL as URL$1 } from "url";
28
28
  import { spawn as spawn$1 } from "node:child_process";
29
29
  import os, { tmpdir } from "node:os";
30
30
  import { createReadStream } from "node:fs";
31
- //#region ../types/dist/event-category-DOl51-Ij.mjs
31
+ //#region ../types/dist/event-category-BfJwUd2S.mjs
32
32
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
33
33
  EventCategory["SystemBoot"] = "system.boot";
34
34
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -345,6 +345,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
345
345
  */
346
346
  EventCategory["PipelineDetectionBlind"] = "pipeline.detection-blind";
347
347
  /**
348
+ * A camera's motion source has fired constantly with no subject for an hour
349
+ * (motion duty or session count over the bar, zero detections) — it is not
350
+ * blind, its motion zones or sensitivity want tuning (D907). Paired with
351
+ * {@link PipelineMotionCalm}.
352
+ */
353
+ EventCategory["PipelineMotionNoisy"] = "pipeline.motion-noisy";
354
+ /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
355
+ EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
356
+ /**
348
357
  * Per-camera pipeline config was mutated by the orchestrator
349
358
  * (3-level settings change via `setAgentAddonDefaults` /
350
359
  * `setCameraStepToggle` / `setCameraPipelineForAgent` or a
@@ -716,6 +725,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
716
725
  EventCategory["BackupRestored"] = "backup.restored";
717
726
  EventCategory["NotificationDispatched"] = "notification.dispatched";
718
727
  EventCategory["NotificationFailed"] = "notification.failed";
728
+ /**
729
+ * A notification rule that cannot work (D905): its stored row does not load,
730
+ * or it still names a device / delivery target the prune CONFIRMED deleted.
731
+ * One edge per rule per distinct set of reasons; never on an unanswered read.
732
+ */
733
+ EventCategory["NotificationRuleBroken"] = "notification.rule-broken";
734
+ /** The clear of {@link NotificationRuleBroken}: the rule loads and names nothing deleted. */
735
+ EventCategory["NotificationRuleRepaired"] = "notification.rule-repaired";
719
736
  EventCategory["DeviceUpdated"] = "device.updated";
720
737
  /**
721
738
  * Transport-level connectivity. Emitted by the device driver when
@@ -5443,7 +5460,7 @@ var ZodIssueCode = {
5443
5460
  var ZodFirstPartyTypeKind;
5444
5461
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5445
5462
  //#endregion
5446
- //#region ../types/dist/sleep-_sV1BE2f.mjs
5463
+ //#region ../types/dist/sleep-DJFUfKvA.mjs
5447
5464
  /**
5448
5465
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5449
5466
  * window to float samples (D455).
@@ -7727,7 +7744,7 @@ function stringProp(value, key) {
7727
7744
  return typeof found === "string" ? found : void 0;
7728
7745
  }
7729
7746
  /** The code an error carries: its own, its cause's, or a tRPC `data.code`. */
7730
- function errorCode(err) {
7747
+ function errorCode$1(err) {
7731
7748
  const out = {};
7732
7749
  const cause = typeof err === "object" && err !== null ? Reflect.get(err, "cause") : void 0;
7733
7750
  const data = typeof err === "object" && err !== null ? Reflect.get(err, "data") : void 0;
@@ -7747,7 +7764,7 @@ function errorCode(err) {
7747
7764
  function classifyStoreError(err) {
7748
7765
  const message = err instanceof Error ? err.message : String(err);
7749
7766
  if (RESTART_FINGERPRINTS.some((f) => message.includes(f))) return "restart";
7750
- const code = errorCode(err);
7767
+ const code = errorCode$1(err);
7751
7768
  if (code.node !== void 0 && TRANSPORT_CODES.has(code.node)) return "transport";
7752
7769
  if (code.trpc !== void 0 && UNAVAILABLE_TRPC_CODES.has(code.trpc)) return "transport";
7753
7770
  if (typeof err === "object" && err !== null) {
@@ -7976,7 +7993,7 @@ var EntryDescriptorSchema = object({
7976
7993
  /** JSON Schema of the entry body; `secret: true` marks a password field. */
7977
7994
  fields: record(string(), unknown())
7978
7995
  }).strict();
7979
- /** What crosses the wire and sits in the engine's catalog: JSON only, no Zod. */
7996
+ /** What crosses the wire from an addon: JSON only, no Zod. An unknown key is refused. */
7980
7997
  var RecordKindDeclarationSchema = object({
7981
7998
  database: _enum(RECORD_DATABASES).default("settings"),
7982
7999
  class: _enum(RECORD_CLASSES),
@@ -7995,14 +8012,6 @@ var RecordKindDeclarationSchema = object({
7995
8012
  secretPaths: array(string().min(1)).default([]),
7996
8013
  /** The id key of each array that carries a secret (`{ headers: 'name' }`). */
7997
8014
  secretArrayKeys: record(string(), string()).default({}),
7998
- /**
7999
- * RETIRED (D900): the legacy table a kind adopted from during the records
8000
- * migration. Nothing declares it any more and nothing reads it; the schema
8001
- * still ACCEPTS it because the catalog rows the migration wrote carry it
8002
- * (an orphaned kind is never redeclared) and an addon built before the
8003
- * removal still sends it. Never set it.
8004
- */
8005
- adoptsFrom: string().min(1).optional(),
8006
8015
  /** Kinds that must agree through `transact` sit in one file under one family name. */
8007
8016
  transactFamily: string().min(1).optional(),
8008
8017
  entry: EntryDescriptorSchema.optional()
@@ -13294,17 +13303,6 @@ object({
13294
13303
  createdAt: number(),
13295
13304
  updatedAt: number()
13296
13305
  });
13297
- object({
13298
- id: string(),
13299
- label: string(),
13300
- isAdmin: boolean().default(false),
13301
- allowedProviders: AllowedProviderSchema,
13302
- allowedDevices: AllowedDevicesSchema,
13303
- tokenHash: string(),
13304
- tokenPrefix: string(),
13305
- createdAt: number(),
13306
- lastUsedAt: number().optional()
13307
- });
13308
13306
  object({
13309
13307
  id: string(),
13310
13308
  userId: string(),
@@ -17286,16 +17284,36 @@ var RecordKindStateSchema = _enum([
17286
17284
  var DeclareKindResultSchema = object({
17287
17285
  kindId: number().int(),
17288
17286
  state: RecordKindStateSchema,
17289
- createdIndexes: array(string()),
17290
- /**
17291
- * RETIRED (D900): always `null`. It carried the records-migration boot guard;
17292
- * it stays on the wire, null, because an addon built before the removal reads
17293
- * an ABSENT field as "adoption pending" and refuses to start.
17294
- */
17295
- adoptionPending: _null()
17287
+ createdIndexes: array(string())
17296
17288
  });
17297
17289
  /** Keychain refs a write released (the old marker is gone from the row): the owner deletes the values. */
17298
17290
  var SecretRefsList = array(string());
17291
+ /**
17292
+ * One op of a `transact`: the single-row writes, each on a kind of the caller.
17293
+ * All ops of one call share ONE database file and ONE `transactFamily`.
17294
+ */
17295
+ var RecordTransactOpSchema = discriminatedUnion("op", [
17296
+ object({
17297
+ op: literal("put"),
17298
+ kind: RecordKindNameSchema,
17299
+ record: RecordInputSchema,
17300
+ ifRev: number().int().min(0).optional()
17301
+ }),
17302
+ object({
17303
+ op: literal("patch"),
17304
+ kind: RecordKindNameSchema,
17305
+ id: RecordIdSchema,
17306
+ data: RecordBodySchema,
17307
+ at: number().int().nullable().optional(),
17308
+ expiresAt: number().int().nullable().optional(),
17309
+ ifRev: number().int().min(1).optional()
17310
+ }),
17311
+ object({
17312
+ op: literal("delete"),
17313
+ kind: RecordKindNameSchema,
17314
+ id: RecordIdSchema
17315
+ })
17316
+ ]);
17299
17317
  var RecordSecretRefsSchema = record(string(), record(string(), string()));
17300
17318
  method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema) }), object({ kinds: record(string(), DeclareKindResultSchema) }), {
17301
17319
  kind: "mutation",
@@ -17402,6 +17420,16 @@ method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema)
17402
17420
  }), {
17403
17421
  kind: "mutation",
17404
17422
  auth: "admin"
17423
+ }), method(object({ ops: array(RecordTransactOpSchema).min(1).max(RECORD_BATCH_MAX) }), object({
17424
+ results: array(object({
17425
+ rev: number().int().nullable(),
17426
+ deleted: boolean()
17427
+ })),
17428
+ releasedRefs: SecretRefsList,
17429
+ secretRefs: SecretRefsList
17430
+ }), {
17431
+ kind: "mutation",
17432
+ auth: "admin"
17405
17433
  }), method(object({
17406
17434
  deviceId: number().int(),
17407
17435
  kinds: array(RecordKindNameSchema).optional()
@@ -43149,9 +43177,13 @@ var NcSystemEventKindSchema = _enum([
43149
43177
  "node-updating",
43150
43178
  "node-inference-unavailable",
43151
43179
  "detection-blind",
43180
+ "motion-noisy",
43181
+ "motion-calm",
43152
43182
  "recording-stalled",
43153
43183
  "recording-healthy",
43154
43184
  "recording-degraded",
43185
+ "device-attention-required",
43186
+ "device-attention-resolved",
43155
43187
  "storage-location-low",
43156
43188
  "storage-location-critical",
43157
43189
  "storage-location-ok",
@@ -43175,6 +43207,8 @@ var NcSystemEventKindSchema = _enum([
43175
43207
  "alert-critical",
43176
43208
  "alert-error",
43177
43209
  "alert-critical-resolved",
43210
+ "rule-broken",
43211
+ "rule-repaired",
43178
43212
  "camera-online",
43179
43213
  "camera-offline",
43180
43214
  "camera-disabled",
@@ -56995,6 +57029,16 @@ var DishwasherStatusSchema = object({
56995
57029
  powerState: string().nullable(),
56996
57030
  saltLevel: DishwasherConsumableLevelSchema.nullable(),
56997
57031
  rinseAidLevel: DishwasherConsumableLevelSchema.nullable(),
57032
+ /**
57033
+ * True while the appliance asks for its filter to be cleaned (smart-filter, filter-system or
57034
+ * combined care-and-filter reminder raised); false when every such reminder reads clear; null
57035
+ * when none is reported. A cleared reminder is not an unknown one.
57036
+ */
57037
+ filterCleaningDue: boolean().nullable(),
57038
+ /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
57039
+ machineCareDue: boolean().nullable(),
57040
+ /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
57041
+ machineCareRemainingRuns: number().int().nonnegative().nullable(),
56998
57042
  /** Option states: null when the appliance does not expose the option or has not said. */
56999
57043
  halfLoad: boolean().nullable(),
57000
57044
  hygienePlus: boolean().nullable(),
@@ -66215,16 +66259,6 @@ var UpdateUserInputSchema = object({
66215
66259
  allowedDevices: record(string(), union([array(string()), literal("*")])).optional(),
66216
66260
  scopes: array(TokenScopeSchema).optional()
66217
66261
  });
66218
- var ApiKeySummarySchema = object({
66219
- id: string(),
66220
- label: string(),
66221
- isAdmin: boolean().default(false),
66222
- allowedProviders: union([array(string()), literal("*")]).optional(),
66223
- allowedDevices: record(string(), union([array(string()), literal("*")])).optional(),
66224
- tokenPrefix: string(),
66225
- createdAt: number(),
66226
- lastUsedAt: number().optional()
66227
- });
66228
66262
  var OauthSessionSummarySchema = object({
66229
66263
  id: string(),
66230
66264
  userId: string(),
@@ -66246,8 +66280,7 @@ var TotpStatusSchema = object({
66246
66280
  confirmedAt: number().nullable()
66247
66281
  });
66248
66282
  /**
66249
- * user-management — singleton capability for user accounts and the
66250
- * legacy API-key rows.
66283
+ * user-management — singleton capability for user accounts.
66251
66284
  *
66252
66285
  * v2 model: every method gated by `auth: 'admin'`. Admin-only delegation
66253
66286
  * — the subset check on `setUserScopes` is moot
@@ -66303,10 +66336,6 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
66303
66336
  }), UserSummarySchema.extend({ passwordHash: string() }).nullable(), {
66304
66337
  kind: "mutation",
66305
66338
  access: "view"
66306
- }), method(_void(), array(ApiKeySummarySchema), { auth: "admin" }), method(object({ id: string() }), object({ success: literal(true) }), {
66307
- kind: "mutation",
66308
- auth: "admin",
66309
- access: "delete"
66310
66339
  }), method(object({ userId: string() }), TotpSetupResultSchema, {
66311
66340
  kind: "mutation",
66312
66341
  auth: "admin",
@@ -75777,6 +75806,12 @@ Object.freeze({
75777
75806
  addonId: null,
75778
75807
  access: "view"
75779
75808
  },
75809
+ "records.transact": {
75810
+ capName: "records",
75811
+ capScope: "system",
75812
+ addonId: null,
75813
+ access: "create"
75814
+ },
75780
75815
  "sceneMonitor.captureReference": {
75781
75816
  capName: "scene-monitor",
75782
75817
  capScope: "device",
@@ -77109,12 +77144,6 @@ Object.freeze({
77109
77144
  addonId: null,
77110
77145
  access: "view"
77111
77146
  },
77112
- "userManagement.listApiKeys": {
77113
- capName: "user-management",
77114
- capScope: "system",
77115
- addonId: null,
77116
- access: "view"
77117
- },
77118
77147
  "userManagement.listExternalIdentities": {
77119
77148
  capName: "user-management",
77120
77149
  capScope: "system",
@@ -77169,12 +77198,6 @@ Object.freeze({
77169
77198
  addonId: null,
77170
77199
  access: "create"
77171
77200
  },
77172
- "userManagement.revokeApiKey": {
77173
- capName: "user-management",
77174
- capScope: "system",
77175
- addonId: null,
77176
- access: "delete"
77177
- },
77178
77201
  "userManagement.revokeOauthSession": {
77179
77202
  capName: "user-management",
77180
77203
  capScope: "system",
@@ -80691,9 +80714,13 @@ new Map(Object.entries({
80691
80714
  "stream-online": "device-events",
80692
80715
  "stream-offline": "device-events",
80693
80716
  "detection-blind": "device-events",
80717
+ "motion-noisy": "device-events",
80718
+ "motion-calm": "device-events",
80694
80719
  "recording-stalled": "device-events",
80695
80720
  "recording-healthy": "device-events",
80696
80721
  "recording-degraded": "device-events",
80722
+ "device-attention-required": "device-events",
80723
+ "device-attention-resolved": "device-events",
80697
80724
  "node-online": "system",
80698
80725
  "node-offline": "system",
80699
80726
  "node-restarting": "system",
@@ -80710,6 +80737,8 @@ new Map(Object.entries({
80710
80737
  "alert-critical": "system",
80711
80738
  "alert-error": "system",
80712
80739
  "alert-critical-resolved": "system",
80740
+ "rule-broken": "system",
80741
+ "rule-repaired": "system",
80713
80742
  "storage-location-low": "system",
80714
80743
  "storage-location-critical": "system",
80715
80744
  "storage-location-ok": "system",
@@ -80774,6 +80803,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
80774
80803
  label: "Detecting nothing",
80775
80804
  kinds: ["detection-blind"]
80776
80805
  },
80806
+ {
80807
+ id: "motion-noise",
80808
+ family: "device-events",
80809
+ label: "Motion without a subject",
80810
+ kinds: ["motion-noisy", "motion-calm"]
80811
+ },
80777
80812
  {
80778
80813
  id: "recording",
80779
80814
  family: "device-events",
@@ -80784,6 +80819,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
80784
80819
  "recording-degraded"
80785
80820
  ]
80786
80821
  },
80822
+ {
80823
+ id: "device-attention",
80824
+ family: "device-events",
80825
+ label: "Needs attention",
80826
+ kinds: ["device-attention-required", "device-attention-resolved"]
80827
+ },
80787
80828
  {
80788
80829
  id: "nodes",
80789
80830
  family: "system",
@@ -80844,6 +80885,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
80844
80885
  "alert-critical-resolved"
80845
80886
  ]
80846
80887
  },
80888
+ {
80889
+ id: "rule-health",
80890
+ family: "system",
80891
+ label: "Notification rules",
80892
+ kinds: ["rule-broken", "rule-repaired"]
80893
+ },
80847
80894
  {
80848
80895
  id: "alarm",
80849
80896
  family: "alarm",
@@ -80917,6 +80964,7 @@ var ALERT_EPISODE = [
80917
80964
  "alert-error",
80918
80965
  "alert-critical-resolved"
80919
80966
  ];
80967
+ var DEVICE_ATTENTION = ["device-attention-required", "device-attention-resolved"];
80920
80968
  var UPDATES = [
80921
80969
  "addon-update-available",
80922
80970
  "server-update-available",
@@ -80946,9 +80994,12 @@ var DEVICE_EVENTS = [
80946
80994
  "stream-online",
80947
80995
  "stream-offline",
80948
80996
  "detection-blind",
80997
+ "motion-noisy",
80998
+ "motion-calm",
80949
80999
  "recording-stalled",
80950
81000
  "recording-healthy",
80951
81001
  "recording-degraded",
81002
+ ...DEVICE_ATTENTION,
80952
81003
  ...ALARM
80953
81004
  ];
80954
81005
  /** Every kind a summary may speak about — the two summarisable families. */
@@ -81116,7 +81167,13 @@ var STATE_SUMMARY_VARS = [
81116
81167
  [...RULE_TEXT],
81117
81168
  [...RULE_TEXT],
81118
81169
  [...RULE_TEXT],
81119
- ([...RULE_TEXT], [...ALERT_EPISODE]),
81170
+ [...RULE_TEXT],
81171
+ [...RULE_TEXT],
81172
+ ([...RULE_TEXT], [...DEVICE_ATTENTION, ...ALERT_EPISODE]),
81173
+ [...RULE_TEXT],
81174
+ [...RULE_TEXT],
81175
+ [...RULE_TEXT],
81176
+ [...RULE_TEXT],
81120
81177
  [...RULE_TEXT],
81121
81178
  [...RULE_TEXT],
81122
81179
  [...RULE_TEXT],
@@ -81134,11 +81191,11 @@ var STATE_SUMMARY_VARS = [
81134
81191
  [...RULE_TEXT],
81135
81192
  [...RULE_TEXT],
81136
81193
  [...RULE_TEXT],
81194
+ ([...RULE_TEXT], [...DEVICE_ATTENTION, ...ALERT_EPISODE]),
81137
81195
  ([...RULE_TEXT], [...ALERT_EPISODE]),
81196
+ ([...RULE_TEXT], [...DEVICE_ATTENTION, ...ALERT_EPISODE]),
81138
81197
  ([...RULE_TEXT], [...ALERT_EPISODE]),
81139
- ([...RULE_TEXT], [...ALERT_EPISODE]),
81140
- ([...RULE_TEXT], [...ALERT_EPISODE]),
81141
- ([...RULE_TEXT], [...ALERT_EPISODE]),
81198
+ ([...RULE_TEXT], [...DEVICE_ATTENTION, ...ALERT_EPISODE]),
81142
81199
  [...RULE_TEXT],
81143
81200
  [...RULE_TEXT],
81144
81201
  [...RULE_TEXT],
@@ -81159,6 +81216,125 @@ var STATE_SUMMARY_VARS = [
81159
81216
  [...RULE_TEXT],
81160
81217
  ...STATE_SUMMARY_VARS
81161
81218
  ];
81219
+ new Map([
81220
+ {
81221
+ id: "device-connectivity",
81222
+ family: "device-events",
81223
+ label: "Device connectivity (offline / back online)",
81224
+ bad: ["device-offline"],
81225
+ recovery: ["device-online"],
81226
+ snapshotLabel: "Devices offline now",
81227
+ snapshotSection: "device-state"
81228
+ },
81229
+ {
81230
+ id: "device-switch",
81231
+ family: "device-events",
81232
+ label: "Device switched off / back on",
81233
+ bad: ["device-disabled"],
81234
+ recovery: ["device-enabled"],
81235
+ snapshotLabel: "Devices switched off now",
81236
+ snapshotSection: "device-state"
81237
+ },
81238
+ {
81239
+ id: "stream-connectivity",
81240
+ family: "device-events",
81241
+ label: "Stream connectivity (offline / back online)",
81242
+ bad: ["stream-offline"],
81243
+ recovery: ["stream-online"],
81244
+ snapshotLabel: "Streams offline now",
81245
+ snapshotSection: "streams"
81246
+ },
81247
+ {
81248
+ id: "battery",
81249
+ family: "device-events",
81250
+ label: "Battery (low / back to normal)",
81251
+ bad: ["device-battery-low"],
81252
+ recovery: ["device-battery-normal"],
81253
+ snapshotLabel: "Batteries low now",
81254
+ snapshotSection: "battery"
81255
+ },
81256
+ {
81257
+ id: "consumables",
81258
+ family: "device-events",
81259
+ label: "Consumables (low / replaced)",
81260
+ bad: ["device-consumable-low"],
81261
+ recovery: ["device-consumable-normal"],
81262
+ snapshotLabel: "Consumables low now",
81263
+ snapshotSection: "consumables"
81264
+ },
81265
+ {
81266
+ id: "tanks",
81267
+ family: "device-events",
81268
+ label: "Tanks (low or full / back to normal)",
81269
+ bad: ["device-tank-low", "device-tank-full"],
81270
+ recovery: ["device-tank-normal"],
81271
+ snapshotLabel: "Tanks low or full now",
81272
+ snapshotSection: "tanks"
81273
+ },
81274
+ {
81275
+ id: "motion-noise",
81276
+ family: "device-events",
81277
+ label: "Motion without a subject (constant / calm again)",
81278
+ bad: ["motion-noisy"],
81279
+ recovery: ["motion-calm"],
81280
+ snapshotLabel: "Cameras with constant motion now",
81281
+ snapshotSection: null
81282
+ },
81283
+ {
81284
+ id: "recording-health",
81285
+ family: "device-events",
81286
+ label: "Recording health (stalled or degraded / recording again)",
81287
+ bad: ["recording-stalled", "recording-degraded"],
81288
+ recovery: ["recording-healthy"],
81289
+ snapshotLabel: "Recordings stalled now",
81290
+ snapshotSection: null
81291
+ },
81292
+ {
81293
+ id: "device-attention",
81294
+ family: "device-events",
81295
+ label: "Device needs attention (raised / resolved)",
81296
+ bad: ["device-attention-required"],
81297
+ recovery: ["device-attention-resolved"],
81298
+ snapshotLabel: "Devices needing attention now",
81299
+ snapshotSection: null
81300
+ },
81301
+ {
81302
+ id: "node-connectivity",
81303
+ family: "system",
81304
+ label: "Node connectivity (offline / back online)",
81305
+ bad: ["node-offline"],
81306
+ recovery: ["node-online"],
81307
+ snapshotLabel: "Nodes offline now",
81308
+ snapshotSection: "nodes"
81309
+ },
81310
+ {
81311
+ id: "storage-space",
81312
+ family: "system",
81313
+ label: "Storage space (low or critical / ok again)",
81314
+ bad: ["storage-location-low", "storage-location-critical"],
81315
+ recovery: ["storage-location-ok"],
81316
+ snapshotLabel: "Storage locations low now",
81317
+ snapshotSection: null
81318
+ },
81319
+ {
81320
+ id: "critical-alerts",
81321
+ family: "system",
81322
+ label: "Critical alerts (raised / resolved)",
81323
+ bad: ["alert-critical"],
81324
+ recovery: ["alert-critical-resolved"],
81325
+ snapshotLabel: "Critical alerts open now",
81326
+ snapshotSection: null
81327
+ },
81328
+ {
81329
+ id: "rule-health",
81330
+ family: "system",
81331
+ label: "Rule health (broken / repaired)",
81332
+ bad: ["rule-broken"],
81333
+ recovery: ["rule-repaired"],
81334
+ snapshotLabel: "Notification rules broken now",
81335
+ snapshotSection: null
81336
+ }
81337
+ ].flatMap((spec) => [...spec.bad, ...spec.recovery].map((kind) => [kind, spec])));
81162
81338
  new Set([
81163
81339
  "disarmed",
81164
81340
  "armed_home",
@@ -188446,8 +188622,8 @@ var CLIP_REFRESH_SECTION_ID = "clip-catalog-refresh";
188446
188622
  * settings nobody could reason about. So the difference is in the operator's
188447
188623
  * own text, at both levels, not only in a record.
188448
188624
  */
188449
- 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.";
188450
- 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.";
188625
+ 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.";
188626
+ 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.";
188451
188627
  /**
188452
188628
  * The addon-level (global) default. Per-camera overrides live on the device,
188453
188629
  * under the SAME key, and a device that sets nothing — or resets — resolves to
@@ -188457,7 +188633,7 @@ function buildClipRefreshSettingsSchema() {
188457
188633
  return { sections: [{
188458
188634
  id: CLIP_REFRESH_SECTION_ID,
188459
188635
  title: "Clip catalog refresh",
188460
- 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.",
188636
+ description: "Default for every camera; a camera can override it in its Recording settings.",
188461
188637
  columns: 1,
188462
188638
  fields: [{
188463
188639
  type: "number",
@@ -188487,7 +188663,7 @@ function buildClipRefreshDeviceSection(input) {
188487
188663
  id: CLIP_REFRESH_SECTION_ID,
188488
188664
  tab: "recording",
188489
188665
  title: "Clip catalog refresh",
188490
- 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.",
188666
+ description: "Leave the interval empty to use the default from the Reolink addon settings.",
188491
188667
  columns: 1,
188492
188668
  fields: [{
188493
188669
  type: "number",
@@ -188766,7 +188942,7 @@ function buildSnapshotFreshnessSection(cache, opts) {
188766
188942
  tab: "advanced",
188767
188943
  title: "Snapshot freshness",
188768
188944
  titleKey: "provider-reolink:snapshotFreshnessSection.s_snapshotFreshness.title",
188769
- 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.",
188945
+ description: "Groups without a stamp are of unknown age, not fresh.",
188770
188946
  descriptionKey: "provider-reolink:snapshotFreshnessSection.s_snapshotFreshness.description",
188771
188947
  columns: 1,
188772
188948
  fields: [{
@@ -189797,7 +189973,7 @@ var BatteryHourlyRefresh = class {
189797
189973
  //#endregion
189798
189974
  //#region src/battery-refresh-settings.ts
189799
189975
  var BATTERY_REFRESH_SECTION_ID = "battery-refresh";
189800
- 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).`;
189976
+ var BATTERY_REFRESH_MINUTES_DESCRIPTION = "A camera that slept the whole window is woken once to refresh. 0 turns the scheduled refresh off.";
189801
189977
  /** Battery cameras only: a mains camera keeps its facts fresh on its own socket. */
189802
189978
  function buildBatteryRefreshDeviceSection(input) {
189803
189979
  if (!input.isBattery) return [];
@@ -189805,7 +189981,6 @@ function buildBatteryRefreshDeviceSection(input) {
189805
189981
  id: BATTERY_REFRESH_SECTION_ID,
189806
189982
  tab: "general",
189807
189983
  title: "Scheduled refresh",
189808
- description: "How often this battery camera is guaranteed to have been read, so a battery level never goes stale unseen.",
189809
189984
  columns: 1,
189810
189985
  fields: [{
189811
189986
  type: "number",
@@ -189899,7 +190074,7 @@ async function refreshSnapshotCacheOnce(input) {
189899
190074
  */
189900
190075
  var CH_BAICHUAN = declareLogChannel({
189901
190076
  name: "provider-reolink.baichuan",
189902
- 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.",
190077
+ description: "Every Baichuan protocol frame this camera sends and receives. Loud: arm it for one camera and a short window.",
189903
190078
  defaultLevel: "info",
189904
190079
  perDevice: true
189905
190080
  });
@@ -189914,7 +190089,7 @@ var CH_BAICHUAN = declareLogChannel({
189914
190089
  */
189915
190090
  var CH_HANDSHAKE = declareLogChannel({
189916
190091
  name: "provider-reolink.handshake",
189917
- 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.",
190092
+ description: "Login and socket lifecycle for this camera. Arm it when a camera looks present but answers nothing.",
189918
190093
  defaultLevel: "info",
189919
190094
  perDevice: true
189920
190095
  });
@@ -190323,7 +190498,7 @@ function buildOsdSection(snapshot, values, opts) {
190323
190498
  tab: "image",
190324
190499
  title: "OSD overlay",
190325
190500
  titleKey: "provider-reolink:osdSection.s_osd.title",
190326
- 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.",
190501
+ description: "An overlay the camera holds somewhere else keeps that placement unless a corner is chosen.",
190327
190502
  descriptionKey: "provider-reolink:osdSection.s_osd.description",
190328
190503
  columns: 2,
190329
190504
  fields: [
@@ -190335,8 +190510,6 @@ function buildOsdSection(snapshot, values, opts) {
190335
190510
  key: "osdChannelName",
190336
190511
  label: "Channel name",
190337
190512
  labelKey: "provider-reolink:fields.osdChannelName.label",
190338
- description: "Text shown in the channel-name overlay.",
190339
- descriptionKey: "provider-reolink:fields.osdChannelName.description",
190340
190513
  default: values.osdChannelName,
190341
190514
  placeholder: "Front door"
190342
190515
  },
@@ -190820,7 +190993,7 @@ var SirenAccessory = class extends BaseDevice {
190820
190993
  id: "siren",
190821
190994
  title: "Siren",
190822
190995
  titleKey: "provider-reolink:schema.s_siren.title",
190823
- description: "Tone duration when triggered. Set to 0 for \"play until turned off\"; positive values pulse for that many seconds.",
190996
+ description: "0 = play until turned off.",
190824
190997
  descriptionKey: "provider-reolink:schema.s_siren.description",
190825
190998
  columns: 1,
190826
190999
  fields: [{
@@ -190828,8 +191001,6 @@ var SirenAccessory = class extends BaseDevice {
190828
191001
  key: "sirenDurationS",
190829
191002
  label: "Default duration (seconds)",
190830
191003
  labelKey: "provider-reolink:schema.s_siren.sirenDurationS.label",
190831
- description: "Forwarded to `setSiren(on=true, duration)` (cmd_id 30) when the operator flips the switch.",
190832
- descriptionKey: "provider-reolink:schema.s_siren.sirenDurationS.description",
190833
191004
  default: 0,
190834
191005
  min: 0,
190835
191006
  max: 300,
@@ -190839,16 +191010,12 @@ var SirenAccessory = class extends BaseDevice {
190839
191010
  id: "motion-siren",
190840
191011
  title: "Motion-triggered siren",
190841
191012
  titleKey: "provider-reolink:schema.s_motion-siren.title",
190842
- 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.",
190843
- descriptionKey: "provider-reolink:schema.s_motion-siren.description",
190844
191013
  columns: 1,
190845
191014
  fields: [{
190846
191015
  type: "select",
190847
191016
  key: "motionSirenDetectType",
190848
191017
  label: "Detection classes",
190849
191018
  labelKey: "provider-reolink:schema.s_motion-siren.motionSirenDetectType.label",
190850
- description: "Object/motion classes that arm the siren. `MD` = generic motion detection.",
190851
- descriptionKey: "provider-reolink:schema.s_motion-siren.motionSirenDetectType.description",
190852
191019
  options: SIREN_DETECT_TYPE_CHOICES.map((v) => ({
190853
191020
  value: v,
190854
191021
  label: v.replace(/,/g, " + ").replace(/_/g, "+")
@@ -191312,16 +191479,12 @@ var FloodlightAccessory = class extends BaseDevice {
191312
191479
  id: "motion-floodlight",
191313
191480
  title: "Motion-triggered floodlight",
191314
191481
  titleKey: "provider-reolink:schema.s_motion-floodlight.title",
191315
- 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.",
191316
- descriptionKey: "provider-reolink:schema.s_motion-floodlight.description",
191317
191482
  columns: 2,
191318
191483
  fields: [{
191319
191484
  type: "number",
191320
191485
  key: "motionLightDurationS",
191321
191486
  label: "Light duration (s)",
191322
191487
  labelKey: "provider-reolink:schema.s_motion-floodlight.motionLightDurationS.label",
191323
- description: "Seconds the floodlight stays lit after a motion event. Reolink range 1..600s.",
191324
- descriptionKey: "provider-reolink:schema.s_motion-floodlight.motionLightDurationS.description",
191325
191488
  min: 1,
191326
191489
  max: 600,
191327
191490
  step: 1,
@@ -191331,8 +191494,6 @@ var FloodlightAccessory = class extends BaseDevice {
191331
191494
  key: "motionLightDetectType",
191332
191495
  label: "Detection classes",
191333
191496
  labelKey: "provider-reolink:schema.s_motion-floodlight.motionLightDetectType.label",
191334
- description: "Object classes that turn the floodlight on. Composed values (e.g. \"people,vehicle\") fire when ANY listed class detects.",
191335
- descriptionKey: "provider-reolink:schema.s_motion-floodlight.motionLightDetectType.description",
191336
191497
  options: FLOODLIGHT_DETECT_TYPE_CHOICES.map((v) => ({
191337
191498
  value: v,
191338
191499
  label: v.replace(/,/g, " + ").replace(/_/g, "+")
@@ -191572,8 +191733,6 @@ var PirAccessory = class extends BaseDevice {
191572
191733
  id: "pir",
191573
191734
  title: "PIR sensor",
191574
191735
  titleKey: "provider-reolink:schema.s_pir.title",
191575
- description: "Hardware passive-infrared trigger configuration. Pushed via cmd_id 153 (SetPirInfo).",
191576
- descriptionKey: "provider-reolink:schema.s_pir.description",
191577
191736
  columns: 2,
191578
191737
  fields: [
191579
191738
  {
@@ -191581,8 +191740,6 @@ var PirAccessory = class extends BaseDevice {
191581
191740
  key: "pirSensitive",
191582
191741
  label: "Sensitivity",
191583
191742
  labelKey: "provider-reolink:schema.s_pir.pirSensitive.label",
191584
- description: "Higher value → triggers on smaller motion. Most cameras max at 100.",
191585
- descriptionKey: "provider-reolink:schema.s_pir.pirSensitive.description",
191586
191743
  min: 1,
191587
191744
  max: 100,
191588
191745
  step: 1,
@@ -191594,7 +191751,6 @@ var PirAccessory = class extends BaseDevice {
191594
191751
  key: "pirIntervalS",
191595
191752
  label: "Cooldown (seconds)",
191596
191753
  labelKey: "provider-reolink:schema.s_pir.pirIntervalS.label",
191597
- description: `Minimum delay between consecutive PIR triggers. Reolink app range 5..${intervalMax}s; firmware clamps on push.`,
191598
191754
  min: 5,
191599
191755
  max: intervalMax,
191600
191756
  step: 1,
@@ -191607,8 +191763,6 @@ var PirAccessory = class extends BaseDevice {
191607
191763
  key: "pirReduceAlarm",
191608
191764
  label: "Reduce false alarms",
191609
191765
  labelKey: "provider-reolink:schema.s_pir.pirReduceAlarm.label",
191610
- description: "Trades detection range for fewer false positives. Maps to firmware `reduceAlarm` flag.",
191611
- descriptionKey: "provider-reolink:schema.s_pir.pirReduceAlarm.description",
191612
191766
  ...typeof reduceAlarm === "boolean" ? { default: reduceAlarm } : {},
191613
191767
  style: "switch",
191614
191768
  span: 2
@@ -193157,8 +193311,6 @@ function buildSessionsTabSections(snap, opts) {
193157
193311
  return [{
193158
193312
  id: "sessions-status",
193159
193313
  title: opts.title ?? "Sessions",
193160
- description: "Diagnostics for the Baichuan control channel. Auto-refreshes when older than 1 minute; click Refresh to force.",
193161
- descriptionKey: "provider-reolink:sessionsTabSections.s_sessions-status.description",
193162
193314
  tab: opts.tabId,
193163
193315
  order: 0,
193164
193316
  columns: 1,
@@ -193176,8 +193328,6 @@ function buildSessionsTabSections(snap, opts) {
193176
193328
  key: "sessionsOnlineUsers",
193177
193329
  label: "Online users (firmware)",
193178
193330
  labelKey: "provider-reolink:sessionsTabSections.s_sessions-status.sessionsOnlineUsers.label",
193179
- description: "Active user sessions reported by the device (cmd_id 145). Includes other admins, mobile app, NVR linkups.",
193180
- descriptionKey: "provider-reolink:sessionsTabSections.s_sessions-status.sessionsOnlineUsers.description",
193181
193331
  columns: [
193182
193332
  {
193183
193333
  key: "userName",
@@ -193220,8 +193370,6 @@ function buildSessionsTabSections(snap, opts) {
193220
193370
  key: "sessionsSocketPool",
193221
193371
  label: "Our socket pool",
193222
193372
  labelKey: "provider-reolink:sessionsTabSections.s_sessions-status.sessionsSocketPool.label",
193223
- description: "Open Baichuan TCP/UDP sockets this process holds for the device. One row per pool tag.",
193224
- descriptionKey: "provider-reolink:sessionsTabSections.s_sessions-status.sessionsSocketPool.description",
193225
193373
  columns: [{
193226
193374
  key: "tag",
193227
193375
  label: "Tag",
@@ -193242,8 +193390,6 @@ function buildSessionsTabSections(snap, opts) {
193242
193390
  key: "sessionsDedicated",
193243
193391
  label: "Dedicated lib sessions",
193244
193392
  labelKey: "provider-reolink:sessionsTabSections.s_sessions-status.sessionsDedicated.label",
193245
- description: "Internal lib-side dedicated sessions (streaming/talk channels). Older lib versions leave this list empty.",
193246
- descriptionKey: "provider-reolink:sessionsTabSections.s_sessions-status.sessionsDedicated.description",
193247
193393
  columns: [{
193248
193394
  key: "key",
193249
193395
  label: "Session key",
@@ -193464,6 +193610,137 @@ function motionSensorOnSimpleEvent(event) {
193464
193610
  if (typeof motion !== "object" || motion === null || !("source" in motion)) return null;
193465
193611
  return isMotionSensor(motion.source) ? motion.source : null;
193466
193612
  }
193613
+ var CONNECTIVITY_CODES = new Set([
193614
+ "EHOSTUNREACH",
193615
+ "EHOSTDOWN",
193616
+ "ENETUNREACH",
193617
+ "ENETDOWN",
193618
+ "ECONNREFUSED",
193619
+ "ECONNRESET",
193620
+ "ETIMEDOUT",
193621
+ "EADDRNOTAVAIL",
193622
+ "EPIPE"
193623
+ ]);
193624
+ var CONNECTIVITY_TEXT = /EHOSTUNREACH|EHOSTDOWN|ENETUNREACH|ENETDOWN|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EADDRNOTAVAIL|EPIPE|timed? ?out|socket hang up|socket closed|not connected/i;
193625
+ function errorCode(err) {
193626
+ if (typeof err !== "object" || err === null || !("code" in err)) return null;
193627
+ const code = err.code;
193628
+ return typeof code === "string" ? code : null;
193629
+ }
193630
+ /**
193631
+ * Did this failure say the camera could not be REACHED? An answer that came
193632
+ * back (wrong password, a rejected command) proves the camera is there, and is
193633
+ * not a reason to call it offline.
193634
+ */
193635
+ function isConnectivityFailure(err) {
193636
+ const code = errorCode(err);
193637
+ if (code !== null && CONNECTIVITY_CODES.has(code)) return true;
193638
+ const text = err instanceof Error ? err.message : String(err);
193639
+ return CONNECTIVITY_TEXT.test(text);
193640
+ }
193641
+ var ReachabilityOfflineTracker = class {
193642
+ deps;
193643
+ stateValue = "reachable";
193644
+ since = 0;
193645
+ failures = 0;
193646
+ lastReason = "";
193647
+ offlineAt = 0;
193648
+ timer = null;
193649
+ probing = false;
193650
+ disposed = false;
193651
+ windowMs;
193652
+ recoveryProbeMs;
193653
+ constructor(deps) {
193654
+ this.deps = deps;
193655
+ this.windowMs = deps.windowMs ?? 12e4;
193656
+ this.recoveryProbeMs = deps.recoveryProbeMs ?? 3e4;
193657
+ }
193658
+ get state() {
193659
+ return this.stateValue;
193660
+ }
193661
+ /** The camera could not be reached (socket lost, a login refused at the network level). */
193662
+ noteFailure(reason) {
193663
+ if (this.disposed) return;
193664
+ this.failures += 1;
193665
+ this.lastReason = reason;
193666
+ if (this.stateValue !== "reachable") return;
193667
+ this.stateValue = "failing";
193668
+ this.since = Date.now();
193669
+ this.arm(this.windowMs, () => this.confirm());
193670
+ }
193671
+ /** The camera answered (a login, a stream). Closes a window; ends an outage once. */
193672
+ noteReachable() {
193673
+ if (this.disposed) return;
193674
+ const wasOffline = this.stateValue === "offline";
193675
+ const offlineForMs = Date.now() - this.offlineAt;
193676
+ this.clear();
193677
+ if (wasOffline) this.deps.onOnline({ offlineForMs });
193678
+ }
193679
+ /** The firmware said what the camera is (a push): forget our own verdict, emit nothing. */
193680
+ reset() {
193681
+ this.clear();
193682
+ }
193683
+ dispose() {
193684
+ this.disposed = true;
193685
+ this.clear();
193686
+ }
193687
+ clear() {
193688
+ if (this.timer !== null) clearTimeout(this.timer);
193689
+ this.timer = null;
193690
+ this.stateValue = "reachable";
193691
+ this.failures = 0;
193692
+ this.lastReason = "";
193693
+ }
193694
+ arm(ms, fire) {
193695
+ if (this.timer !== null) clearTimeout(this.timer);
193696
+ this.timer = setTimeout(() => {
193697
+ this.timer = null;
193698
+ fire();
193699
+ }, ms);
193700
+ if (typeof this.timer.unref === "function") this.timer.unref();
193701
+ }
193702
+ /** End of the window: one fresh probe must agree before the camera is called offline. */
193703
+ confirm() {
193704
+ this.runProbe((result) => {
193705
+ if (this.stateValue !== "failing") return;
193706
+ if (result === "reachable") {
193707
+ this.noteReachable();
193708
+ return;
193709
+ }
193710
+ this.stateValue = "offline";
193711
+ this.offlineAt = Date.now();
193712
+ this.deps.onOffline({
193713
+ since: this.since,
193714
+ failures: this.failures,
193715
+ lastReason: this.lastReason
193716
+ });
193717
+ this.arm(this.recoveryProbeMs, () => this.recover());
193718
+ });
193719
+ }
193720
+ recover() {
193721
+ this.runProbe((result) => {
193722
+ if (this.stateValue !== "offline") return;
193723
+ if (result === "reachable") {
193724
+ this.noteReachable();
193725
+ return;
193726
+ }
193727
+ this.arm(this.recoveryProbeMs, () => this.recover());
193728
+ });
193729
+ }
193730
+ async runProbe(then) {
193731
+ if (this.probing || this.disposed) return;
193732
+ this.probing = true;
193733
+ let result;
193734
+ try {
193735
+ result = await this.deps.probe();
193736
+ } catch {
193737
+ result = "unreachable";
193738
+ } finally {
193739
+ this.probing = false;
193740
+ }
193741
+ if (!this.disposed) then(result);
193742
+ }
193743
+ };
193467
193744
  //#endregion
193468
193745
  //#region src/reolink-camera.ts
193469
193746
  /**
@@ -193854,6 +194131,17 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
193854
194131
  /** Pending reconnect timer; cleared on successful reconnect or removeDevice. */
193855
194132
  reconnectTimer = null;
193856
194133
  /**
194134
+ * D904: offline from SUSTAINED unreachability on a mains camera that owns its
194135
+ * socket. Fed by socket loss and network-level login failures; its verdicts
194136
+ * are the normal DeviceOffline / DeviceOnline. Never fed for a battery camera
194137
+ * (asleep is not gone, D173) nor for a hub child (the Hub owns that socket).
194138
+ */
194139
+ reachability = new ReachabilityOfflineTracker({
194140
+ probe: () => this.probeReachability(),
194141
+ onOffline: (info) => this.onReachabilityOffline(info),
194142
+ onOnline: (info) => this.onReachabilityOnline(info)
194143
+ });
194144
+ /**
193857
194145
  * Read-through to the `battery.sleeping` runtime-state slice with
193858
194146
  * a `false` default for non-battery cams (no slice). Internal
193859
194147
  * shorthand — driver code uses `this.state.battery.sleeping = b` to write
@@ -195486,7 +195774,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
195486
195774
  tab: "motion",
195487
195775
  title: "Battery motion (sleep push)",
195488
195776
  titleKey: "provider-reolink:emailPushSection.s_sleep-push.title",
195489
- 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.",
195777
+ description: "Prefer the HaCfg webhook when the firmware supports it; arm only one path.",
195490
195778
  descriptionKey: "provider-reolink:emailPushSection.s_sleep-push.description",
195491
195779
  columns: 1,
195492
195780
  fields: [
@@ -195552,7 +195840,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
195552
195840
  tab: "email",
195553
195841
  title: "SMTP server",
195554
195842
  titleKey: "provider-reolink:emailTabSections.s_email-smtp.title",
195555
- 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.",
195843
+ description: "Leave the password blank to keep the current one.",
195556
195844
  descriptionKey: "provider-reolink:emailTabSections.s_email-smtp.description",
195557
195845
  columns: 2,
195558
195846
  fields: [
@@ -195607,8 +195895,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
195607
195895
  tab: "email",
195608
195896
  title: "Recipients",
195609
195897
  titleKey: "provider-reolink:emailTabSections.s_email-recipients.title",
195610
- description: "Up to three recipient addresses the camera sends motion mail to.",
195611
- descriptionKey: "provider-reolink:emailTabSections.s_email-recipients.description",
195612
195898
  columns: 1,
195613
195899
  fields: [
195614
195900
  {
@@ -195637,8 +195923,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
195637
195923
  tab: "email",
195638
195924
  title: "Alarm schedule",
195639
195925
  titleKey: "provider-reolink:emailTabSections.s_email-schedule.title",
195640
- description: "Master enable for the camera's email alarm schedule (EmailTask).",
195641
- descriptionKey: "provider-reolink:emailTabSections.s_email-schedule.description",
195642
195926
  columns: 1,
195643
195927
  fields: [{
195644
195928
  type: "boolean",
@@ -198609,6 +198893,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
198609
198893
  clearTimeout(this.reconnectTimer);
198610
198894
  this.reconnectTimer = null;
198611
198895
  }
198896
+ this.reachability.dispose();
198612
198897
  this.ctx.logger.info("Removing Reolink camera", { meta: { stableId: this.stableId } });
198613
198898
  const ids = [...this.publishedStreamIds];
198614
198899
  await Promise.all(ids.map((id) => this.ctx.api.streamBroker.retractCameraStream.mutate({
@@ -199513,7 +199798,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
199513
199798
  tab: "general",
199514
199799
  title: "Camera data",
199515
199800
  titleKey: "provider-reolink:schema.s_driver-refresh.title",
199516
- 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.",
199801
+ description: "Wakes a sleeping camera and re-reads everything; no other read here wakes it.",
199517
199802
  descriptionKey: "provider-reolink:schema.s_driver-refresh.description",
199518
199803
  columns: 1,
199519
199804
  fields: [{
@@ -199532,8 +199817,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
199532
199817
  tab: "general",
199533
199818
  title: "Connection",
199534
199819
  titleKey: "provider-reolink:schema.s_connection.title",
199535
- description: "Baichuan protocol connection settings.",
199536
- descriptionKey: "provider-reolink:schema.s_connection.description",
199537
199820
  columns: 2,
199538
199821
  fields: [
199539
199822
  {
@@ -199576,7 +199859,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
199576
199859
  tab: "network",
199577
199860
  title: "Service ports",
199578
199861
  titleKey: "provider-reolink:schema.s_network-ports.title",
199579
- 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.",
199862
+ description: "Changing a port breaks existing URLs for that protocol.",
199580
199863
  descriptionKey: "provider-reolink:schema.s_network-ports.description",
199581
199864
  columns: 2,
199582
199865
  fields: [
@@ -199677,8 +199960,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
199677
199960
  tab: "network",
199678
199961
  title: "Time / NTP",
199679
199962
  titleKey: "provider-reolink:schema.s_time-ntp.title",
199680
- 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.",
199681
- descriptionKey: "provider-reolink:schema.s_time-ntp.description",
199682
199963
  columns: 2,
199683
199964
  fields: [
199684
199965
  {
@@ -199686,8 +199967,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
199686
199967
  key: "timeZoneId",
199687
199968
  label: "Timezone",
199688
199969
  labelKey: "provider-reolink:schema.s_time-ntp.timeZoneId.label",
199689
- 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.",
199690
- descriptionKey: "provider-reolink:schema.s_time-ntp.timeZoneId.description",
199691
199970
  default: this.config.get("timeZoneId") ?? reolinkTimeZoneToIana(this.config.get("deviceCache")?.systemGeneralSnapshot?.timeZone) ?? "",
199692
199971
  span: 2
199693
199972
  },
@@ -199736,16 +200015,12 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
199736
200015
  tab: "motion",
199737
200016
  title: "On-camera motion (MdAlarm)",
199738
200017
  titleKey: "provider-reolink:schema.s_motion.title",
199739
- 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.",
199740
- descriptionKey: "provider-reolink:schema.s_motion.description",
199741
200018
  columns: 2,
199742
200019
  fields: [{
199743
200020
  type: "boolean",
199744
200021
  key: "motionEnabled",
199745
200022
  label: "Motion detection",
199746
200023
  labelKey: "provider-reolink:schema.s_motion.motionEnabled.label",
199747
- description: "Master switch for the camera's on-board motion detector. Disabling stops the camera from emitting motion events on the alarm channel.",
199748
- descriptionKey: "provider-reolink:schema.s_motion.motionEnabled.description",
199749
200024
  default: motionSnap.enabled ?? true,
199750
200025
  style: "switch",
199751
200026
  span: 2
@@ -199754,8 +200029,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
199754
200029
  key: "motionSensitivity",
199755
200030
  label: "Sensitivity",
199756
200031
  labelKey: "provider-reolink:schema.s_motion.motionSensitivity.label",
199757
- description: "Higher = triggers on smaller motion. Reolink user-facing range 1..50.",
199758
- descriptionKey: "provider-reolink:schema.s_motion.motionSensitivity.description",
199759
200032
  min: 1,
199760
200033
  max: 50,
199761
200034
  step: 1,
@@ -199770,8 +200043,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
199770
200043
  tab: "motion",
199771
200044
  title: "AI detection sensitivity",
199772
200045
  titleKey: "provider-reolink:schema.s_ai-sensitivity.title",
199773
- 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.",
199774
- descriptionKey: "provider-reolink:schema.s_ai-sensitivity.description",
199775
200046
  columns: 2,
199776
200047
  fields: [
199777
200048
  [
@@ -199840,7 +200111,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
199840
200111
  type: "slider",
199841
200112
  key: stayKey,
199842
200113
  label: `${label} stay time (s)`,
199843
- description: "Seconds the camera keeps the AI alarm asserted after the object leaves frame.",
199844
200114
  min: 0,
199845
200115
  max: 600,
199846
200116
  step: 1,
@@ -199903,8 +200173,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
199903
200173
  tab: "image",
199904
200174
  title: "Image",
199905
200175
  titleKey: "provider-reolink:schema.s_image.title",
199906
- 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.",
199907
- descriptionKey: "provider-reolink:schema.s_image.description",
199908
200176
  columns: 2,
199909
200177
  fields: [
199910
200178
  {
@@ -199975,24 +200243,18 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
199975
200243
  tab: "image",
199976
200244
  title: "Advanced (ISP)",
199977
200245
  titleKey: "provider-reolink:schema.s_isp.title",
199978
- description: "Day/Night + exposure + HDR. Pushed via `SetIsp` (cmd_id 25). The day/night threshold uses cmd_id 297 (DayNightThreshold).",
199979
- descriptionKey: "provider-reolink:schema.s_isp.description",
199980
200246
  columns: 2,
199981
200247
  fields: [{
199982
200248
  type: "text",
199983
200249
  key: "ispDayNight",
199984
200250
  label: "Day/Night mode",
199985
200251
  labelKey: "provider-reolink:schema.s_isp.ispDayNight.label",
199986
- description: "Camera-specific values — common: `auto`, `color`, `blackAndWhite`. The driver normalizes case before push.",
199987
- descriptionKey: "provider-reolink:schema.s_isp.ispDayNight.description",
199988
200252
  default: imgSnap.dayNight ?? "auto"
199989
200253
  }, {
199990
200254
  type: "text",
199991
200255
  key: "ispExposure",
199992
200256
  label: "Exposure",
199993
200257
  labelKey: "provider-reolink:schema.s_isp.ispExposure.label",
199994
- description: "Common values: `auto`, `manual`. Lowercased before push.",
199995
- descriptionKey: "provider-reolink:schema.s_isp.ispExposure.description",
199996
200258
  default: "auto"
199997
200259
  }]
199998
200260
  }] : [],
@@ -200001,8 +200263,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200001
200263
  tab: "image",
200002
200264
  title: "IR + status LED",
200003
200265
  titleKey: "provider-reolink:schema.s_ir-lights.title",
200004
- 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.",
200005
- descriptionKey: "provider-reolink:schema.s_ir-lights.description",
200006
200266
  columns: 2,
200007
200267
  fields: [
200008
200268
  {
@@ -200034,7 +200294,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200034
200294
  key: "irLightsBrightness",
200035
200295
  label: "IR brightness",
200036
200296
  labelKey: "provider-reolink:schema.s_ir-lights.irLightsBrightness.label",
200037
- description: "Only applied when the camera advertises programmable IR brightness. 0..255 raw range; many models clamp to 0..100.",
200297
+ description: "Only applied when the camera supports adjustable IR brightness.",
200038
200298
  descriptionKey: "provider-reolink:schema.s_ir-lights.irLightsBrightness.description",
200039
200299
  min: 0,
200040
200300
  max: 255,
@@ -200048,8 +200308,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200048
200308
  key: "statusLedEnabled",
200049
200309
  label: "Status LED",
200050
200310
  labelKey: "provider-reolink:schema.s_ir-lights.statusLedEnabled.label",
200051
- description: "The visible status/power LED on the camera body. Pushed via `SetIrLights` (cmd_id 209) as `lightState`.",
200052
- descriptionKey: "provider-reolink:schema.s_ir-lights.statusLedEnabled.description",
200053
200311
  default: cache?.ledSnapshot?.statusEnabled ?? true,
200054
200312
  style: "switch",
200055
200313
  span: cache?.ledSnapshot?.doorbellSupported === true ? 1 : 2
@@ -200059,8 +200317,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200059
200317
  key: "doorbellLedEnabled",
200060
200318
  label: "Doorbell LED",
200061
200319
  labelKey: "provider-reolink:schema.s_ir-lights.doorbellLedEnabled.label",
200062
- description: "The doorbell button ring LED. Pushed via `SetIrLights` (cmd_id 209) as `doorbellLightState`.",
200063
- descriptionKey: "provider-reolink:schema.s_ir-lights.doorbellLedEnabled.description",
200064
200320
  default: cache?.ledSnapshot?.doorbellEnabled ?? true,
200065
200321
  style: "switch"
200066
200322
  }] : []
@@ -200075,7 +200331,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200075
200331
  tab: "image",
200076
200332
  title: "Auto focus",
200077
200333
  titleKey: "provider-reolink:schema.s_auto-focus.title",
200078
- 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.",
200334
+ description: "Disabling locks the lens at its current focus.",
200079
200335
  descriptionKey: "provider-reolink:schema.s_auto-focus.description",
200080
200336
  columns: 1,
200081
200337
  fields: [{
@@ -200092,8 +200348,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200092
200348
  tab: "audio",
200093
200349
  title: "Audio output",
200094
200350
  titleKey: "provider-reolink:schema.s_audio.title",
200095
- 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.",
200096
- descriptionKey: "provider-reolink:schema.s_audio.description",
200097
200351
  columns: 2,
200098
200352
  fields: [
200099
200353
  {
@@ -200113,7 +200367,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200113
200367
  key: "audioTalkAndReplyVolume",
200114
200368
  label: "Talk + reply volume",
200115
200369
  labelKey: "provider-reolink:schema.s_audio.audioTalkAndReplyVolume.label",
200116
- description: "Two-way audio + canned reply playback. Doorbell-only on most firmwares.",
200370
+ description: "Doorbell models only.",
200117
200371
  descriptionKey: "provider-reolink:schema.s_audio.audioTalkAndReplyVolume.description",
200118
200372
  min: 0,
200119
200373
  max: 100,
@@ -200127,7 +200381,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200127
200381
  key: "audioVisitorVolume",
200128
200382
  label: "Visitor volume",
200129
200383
  labelKey: "provider-reolink:schema.s_audio.audioVisitorVolume.label",
200130
- description: "Visitor / chime playback channel. Doorbell-only on most firmwares.",
200384
+ description: "Doorbell models only.",
200131
200385
  descriptionKey: "provider-reolink:schema.s_audio.audioVisitorVolume.description",
200132
200386
  min: 0,
200133
200387
  max: 100,
@@ -200144,8 +200398,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200144
200398
  tab: "audio",
200145
200399
  title: "Audio noise reduction",
200146
200400
  titleKey: "provider-reolink:schema.s_audio-noise.title",
200147
- 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).",
200148
- descriptionKey: "provider-reolink:schema.s_audio-noise.description",
200149
200401
  columns: 1,
200150
200402
  fields: [{
200151
200403
  type: "slider",
@@ -200167,7 +200419,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200167
200419
  tab: "audio",
200168
200420
  title: "Intercom",
200169
200421
  titleKey: "provider-reolink:schema.s_intercom.title",
200170
- description: "Push-to-talk pipeline tuning. Settings take effect on the next talk session — restart any active session for changes to apply.",
200422
+ description: "Changes apply to the next talk session.",
200171
200423
  descriptionKey: "provider-reolink:schema.s_intercom.description",
200172
200424
  columns: 2,
200173
200425
  fields: [
@@ -200176,7 +200428,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200176
200428
  key: "intercomBlocksPerPayload",
200177
200429
  label: "Blocks per payload",
200178
200430
  labelKey: "provider-reolink:schema.s_intercom.intercomBlocksPerPayload.label",
200179
- description: "Lower = lower latency (more sendAudio calls/sec). Higher = fewer calls, less per-call socket overhead. Range 1..8. Default 1 (lowest latency).",
200431
+ description: "Lower is lower latency, higher is fewer calls. Range 1..8.",
200180
200432
  descriptionKey: "provider-reolink:schema.s_intercom.intercomBlocksPerPayload.description",
200181
200433
  min: 1,
200182
200434
  max: 8,
@@ -200190,7 +200442,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200190
200442
  key: "intercomMaxBacklogMs",
200191
200443
  label: "Max backlog (ms)",
200192
200444
  labelKey: "provider-reolink:schema.s_intercom.intercomMaxBacklogMs.label",
200193
- 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.",
200445
+ description: "Higher is more stable on slow systems but adds latency. Range 20..5000.",
200194
200446
  descriptionKey: "provider-reolink:schema.s_intercom.intercomMaxBacklogMs.description",
200195
200447
  min: 20,
200196
200448
  max: 5e3,
@@ -200204,7 +200456,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200204
200456
  key: "intercomGain",
200205
200457
  label: "Output gain",
200206
200458
  labelKey: "provider-reolink:schema.s_intercom.intercomGain.label",
200207
- description: "Multiplier applied to PCM samples before IMA-ADPCM encode. 1.0 = neutral, 2.0 ≈ +6dB, 0.5 ≈ -6dB. Hard-clipped at int16 bounds.",
200459
+ description: "1.0 = neutral, 2.0 ≈ +6 dB, 0.5 ≈ -6 dB.",
200208
200460
  descriptionKey: "provider-reolink:schema.s_intercom.intercomGain.description",
200209
200461
  min: .1,
200210
200462
  max: 10,
@@ -200221,7 +200473,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200221
200473
  tab: "advanced",
200222
200474
  title: "Keyframe interval (GOP)",
200223
200475
  titleKey: "provider-reolink:schema.s_keyframe-gop.title",
200224
- 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.",
200476
+ description: "Lower = more keyframes (faster recovery, higher bitrate).",
200225
200477
  descriptionKey: "provider-reolink:schema.s_keyframe-gop.description",
200226
200478
  columns: 1,
200227
200479
  fields: [{
@@ -200242,8 +200494,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200242
200494
  tab: "advanced",
200243
200495
  title: "PIR detection",
200244
200496
  titleKey: "provider-reolink:schema.s_pir-reduce-alarm.title",
200245
- 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.",
200246
- descriptionKey: "provider-reolink:schema.s_pir-reduce-alarm.description",
200247
200497
  columns: 1,
200248
200498
  fields: [{
200249
200499
  type: "boolean",
@@ -200259,8 +200509,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200259
200509
  tab: "advanced",
200260
200510
  title: "Auto reboot",
200261
200511
  titleKey: "provider-reolink:schema.s_auto-reboot.title",
200262
- 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.",
200263
- descriptionKey: "provider-reolink:schema.s_auto-reboot.description",
200264
200512
  columns: 2,
200265
200513
  fields: [
200266
200514
  {
@@ -200349,7 +200597,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200349
200597
  tab: "advanced",
200350
200598
  title: "Debug",
200351
200599
  titleKey: "provider-reolink:schema.s_debug.title",
200352
- 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.",
200600
+ description: "Debug logging adds log volume; use it temporarily.",
200353
200601
  descriptionKey: "provider-reolink:schema.s_debug.description",
200354
200602
  columns: 1,
200355
200603
  fields: [{
@@ -200357,8 +200605,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200357
200605
  key: "debugGeneral",
200358
200606
  label: "General debug logs",
200359
200607
  labelKey: "provider-reolink:schema.s_debug.debugGeneral.label",
200360
- description: "Forwards `DebugOptions.general=true` — verbose lib protocol logs.",
200361
- descriptionKey: "provider-reolink:schema.s_debug.debugGeneral.description",
200362
200608
  default: false,
200363
200609
  style: "switch"
200364
200610
  }, {
@@ -200366,7 +200612,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
200366
200612
  key: "debugSocketLogs",
200367
200613
  label: "Socket trace categories",
200368
200614
  labelKey: "provider-reolink:schema.s_debug.debugSocketLogs.label",
200369
- description: "Per-area lib tracing. Pick only what you need; each adds log volume.",
200615
+ description: "Each area adds log volume.",
200370
200616
  descriptionKey: "provider-reolink:schema.s_debug.debugSocketLogs.description",
200371
200617
  default: [],
200372
200618
  options: [
@@ -201028,11 +201274,14 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
201028
201274
  await api.login();
201029
201275
  } catch (err) {
201030
201276
  this.loginPromise = null;
201277
+ if (isConnectivityFailure(err)) this.noteUnreachable(err instanceof Error ? err.message : String(err));
201278
+ else if (this.ownsReachability()) this.reachability.noteReachable();
201031
201279
  throw err;
201032
201280
  }
201033
201281
  this.api = api;
201034
201282
  this.loginPromise = null;
201035
201283
  this.reconnectAttempts = 0;
201284
+ if (this.ownsReachability()) this.reachability.noteReachable();
201036
201285
  const persistedDeviceType = this.config.get("deviceCache")?.deviceType;
201037
201286
  if (persistedDeviceType === "battery-cam") {
201038
201287
  this.isBattery = true;
@@ -201095,8 +201344,10 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
201095
201344
  * - 'awake' / 'sleeping': emit DeviceAwake / DeviceSleeping. Battery
201096
201345
  * power-state only — does NOT touch online state.
201097
201346
  * - 'online' / 'offline': emit DeviceOnline / DeviceOffline + flip
201098
- * `this.online`. The ONLY emit site for these — socket close,
201099
- * reconnect, RFC4571 idle teardown all leave online state alone.
201347
+ * `this.online`, and reset the reachability verdict. The only OTHER
201348
+ * source is sustained unreachability on a mains camera (D904,
201349
+ * `onReachabilityOffline`): a single socket close, a reconnect or an
201350
+ * RFC4571 idle teardown still leaves online state alone.
201100
201351
  * - 'battery': refresh cache, emit cap event.
201101
201352
  * - 'doorbell', 'daynight', 'other': informational debug log only.
201102
201353
  */
@@ -201207,6 +201458,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
201207
201458
  }
201208
201459
  if (event.type === "online" || event.type === "offline") {
201209
201460
  const next = event.type === "online";
201461
+ this.reachability.reset();
201210
201462
  if (next !== this.online) {
201211
201463
  this.markOnline(next);
201212
201464
  this.ctx.eventBus.emit(createEvent(next ? EventCategory.DeviceOnline : EventCategory.DeviceOffline, eventSource, next ? {
@@ -201417,6 +201669,67 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
201417
201669
  await current.close({ reason });
201418
201670
  } catch {}
201419
201671
  }
201672
+ /** D904 applies to a mains camera that dials its own socket — not a battery cam, not a hub child. */
201673
+ ownsReachability() {
201674
+ return !this.isBattery && !this.isHubChild();
201675
+ }
201676
+ /** The camera could not be reached: open (or extend) the reachability window. */
201677
+ noteUnreachable(reason) {
201678
+ if (!this.ownsReachability()) return;
201679
+ this.reachability.noteFailure(reason);
201680
+ }
201681
+ /**
201682
+ * One fresh login, for the tracker's confirmation and recovery probes. A
201683
+ * battery cam or hub child is never probed (D173) and answers `reachable`,
201684
+ * which closes the window instead of calling it offline.
201685
+ */
201686
+ async probeReachability() {
201687
+ if (!this.ownsReachability()) return "reachable";
201688
+ try {
201689
+ await this.ensureApi();
201690
+ return "reachable";
201691
+ } catch (err) {
201692
+ return isConnectivityFailure(err) ? "unreachable" : "reachable";
201693
+ }
201694
+ }
201695
+ /** Sustained unreachability, confirmed: the normal DeviceOffline, once. */
201696
+ onReachabilityOffline(info) {
201697
+ if (!this.online) {
201698
+ this.ctx.logger.info("Reolink camera unreachable, already offline — nothing to emit", {
201699
+ tags: { deviceId: this.id },
201700
+ meta: { ...info }
201701
+ });
201702
+ return;
201703
+ }
201704
+ this.ctx.logger.warn("Reolink camera unreachable for the whole window — marked offline", {
201705
+ tags: { deviceId: this.id },
201706
+ meta: {
201707
+ ...info,
201708
+ unreachableForMs: Date.now() - info.since
201709
+ }
201710
+ });
201711
+ this.markOnline(false);
201712
+ this.ctx.eventBus.emit(createEvent(EventCategory.DeviceOffline, this.eventSource(), {
201713
+ deviceId: this.id,
201714
+ providerId: REOLINK_ADDON_ID,
201715
+ reason: "unreachable"
201716
+ }));
201717
+ }
201718
+ /** The first success after a reachability outage: the normal DeviceOnline, once. */
201719
+ onReachabilityOnline(info) {
201720
+ if (this.online) return;
201721
+ this.ctx.logger.info("Reolink camera reachable again — marked online", {
201722
+ tags: { deviceId: this.id },
201723
+ meta: { ...info }
201724
+ });
201725
+ this.markOnline(true);
201726
+ this.ctx.eventBus.emit(createEvent(EventCategory.DeviceOnline, this.eventSource(), {
201727
+ deviceId: this.id,
201728
+ providerId: REOLINK_ADDON_ID,
201729
+ profileCount: 0,
201730
+ reason: "reachable-again"
201731
+ }));
201732
+ }
201420
201733
  scheduleReconnect(reason) {
201421
201734
  if (this.reconnectTimer) return;
201422
201735
  if (this.isBattery) {
@@ -201430,6 +201743,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
201430
201743
  });
201431
201744
  return;
201432
201745
  }
201746
+ this.noteUnreachable(reason);
201433
201747
  const attempt = ++this.reconnectAttempts;
201434
201748
  const backoffMs = Math.min(3e4, 1e3 * 2 ** Math.min(attempt - 1, 5));
201435
201749
  this.ctx.logger.info("Baichuan reconnect scheduled", {
@@ -201869,7 +202183,7 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
201869
202183
  tab: "general",
201870
202184
  title: "Connection",
201871
202185
  titleKey: "provider-reolink:hub.s_connection.title",
201872
- 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.",
202186
+ description: "Child cameras inherit these credentials.",
201873
202187
  descriptionKey: "provider-reolink:hub.s_connection.description",
201874
202188
  columns: 2,
201875
202189
  fields: [
@@ -201913,8 +202227,6 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
201913
202227
  tab: "general",
201914
202228
  title: "Hub identity",
201915
202229
  titleKey: "provider-reolink:hub.s_identity.title",
201916
- description: "Read-only summary from the autodetect probe. Channel count and model are persisted at first contact so the panel renders without re-probing.",
201917
- descriptionKey: "provider-reolink:hub.s_identity.description",
201918
202230
  columns: 2,
201919
202231
  fields: [{
201920
202232
  type: "text",
@@ -201937,7 +202249,7 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
201937
202249
  tab: "advanced",
201938
202250
  title: "Debug",
201939
202251
  titleKey: "provider-reolink:hub.s_debug.title",
201940
- 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.",
202252
+ description: "Debug logging adds log volume; use it temporarily.",
201941
202253
  descriptionKey: "provider-reolink:hub.s_debug.description",
201942
202254
  columns: 1,
201943
202255
  fields: [{
@@ -201945,8 +202257,6 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
201945
202257
  key: "debugGeneral",
201946
202258
  label: "General debug logs",
201947
202259
  labelKey: "provider-reolink:hub.s_debug.debugGeneral.label",
201948
- description: "Forwards `DebugOptions.general=true` — verbose lib protocol logs.",
201949
- descriptionKey: "provider-reolink:hub.s_debug.debugGeneral.description",
201950
202260
  default: false,
201951
202261
  style: "switch"
201952
202262
  }, {
@@ -201954,7 +202264,7 @@ var ReolinkHub = class ReolinkHub extends BaseDevice {
201954
202264
  key: "debugSocketLogs",
201955
202265
  label: "Socket trace categories",
201956
202266
  labelKey: "provider-reolink:hub.s_debug.debugSocketLogs.label",
201957
- description: "Per-area lib tracing. Pick only what you need; each adds log volume.",
202267
+ description: "Each area adds log volume.",
201958
202268
  descriptionKey: "provider-reolink:hub.s_debug.debugSocketLogs.description",
201959
202269
  default: [],
201960
202270
  options: [
@@ -202717,7 +203027,7 @@ function buildDiscoveryParamsFormSchema() {
202717
203027
  id: "credentials",
202718
203028
  title: "Credentials",
202719
203029
  titleKey: "provider-reolink:discoveryParams.s_credentials.title",
202720
- 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.",
203030
+ description: "Assumes all cameras share these credentials.",
202721
203031
  descriptionKey: "provider-reolink:discoveryParams.s_credentials.description",
202722
203032
  columns: 2,
202723
203033
  fields: [{
@@ -202739,7 +203049,7 @@ function buildDiscoveryParamsFormSchema() {
202739
203049
  id: "scan",
202740
203050
  title: "Scan options",
202741
203051
  titleKey: "provider-reolink:discoveryParams.s_scan.title",
202742
- 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).",
203052
+ description: "Leave blank to scan the local subnet; for another subnet enter its CIDR (e.g. 192.168.20.0/24).",
202743
203053
  descriptionKey: "provider-reolink:discoveryParams.s_scan.description",
202744
203054
  columns: 1,
202745
203055
  fields: [
@@ -202790,8 +203100,6 @@ function buildCreationFormSchema() {
202790
203100
  id: "credentials",
202791
203101
  title: "Credentials",
202792
203102
  titleKey: "provider-reolink:creation.s_credentials.title",
202793
- description: "Required for the autodetect probe — entered before the host so Test can run immediately.",
202794
- descriptionKey: "provider-reolink:creation.s_credentials.description",
202795
203103
  columns: 2,
202796
203104
  fields: [{
202797
203105
  type: "text",
@@ -202813,8 +203121,6 @@ function buildCreationFormSchema() {
202813
203121
  id: "connection",
202814
203122
  title: "Connection",
202815
203123
  titleKey: "provider-reolink:creation.s_connection.title",
202816
- description: "Click Test to auto-detect the device type (regular camera / NVR / battery / UDP-only). The detected type is then used at save time.",
202817
- descriptionKey: "provider-reolink:creation.s_connection.description",
202818
203124
  columns: 2,
202819
203125
  fields: [
202820
203126
  {
@@ -202823,7 +203129,7 @@ function buildCreationFormSchema() {
202823
203129
  label: "Connection mode",
202824
203130
  labelKey: "provider-reolink:creation.s_connection.transport.label",
202825
203131
  default: "auto",
202826
- description: "Auto: try TCP first, fall back to UDP — best for unknown devices. UDP is required for battery cameras behind NAT.",
203132
+ description: "Auto tries TCP, then UDP. Battery cameras behind NAT need UDP.",
202827
203133
  descriptionKey: "provider-reolink:creation.s_connection.transport.description",
202828
203134
  options: [
202829
203135
  {
@@ -202850,7 +203156,7 @@ function buildCreationFormSchema() {
202850
203156
  labelKey: "provider-reolink:creation.s_connection.host.label",
202851
203157
  required: true,
202852
203158
  placeholder: "192.168.10.100",
202853
- 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.",
203159
+ description: "In UDP-only mode leave blank and use the UID below.",
202854
203160
  descriptionKey: "provider-reolink:creation.s_connection.host.description",
202855
203161
  inputType: "text"
202856
203162
  },
@@ -202863,8 +203169,6 @@ function buildCreationFormSchema() {
202863
203169
  min: 1,
202864
203170
  max: 65535,
202865
203171
  step: 1,
202866
- description: "Baichuan port. Default 9000 — applies to TCP. Ignored for UDP transport.",
202867
- descriptionKey: "provider-reolink:creation.s_connection.port.description",
202868
203172
  showWhen: {
202869
203173
  field: "transport",
202870
203174
  notEquals: "udp"
@@ -202876,7 +203180,7 @@ function buildCreationFormSchema() {
202876
203180
  label: "Device UID",
202877
203181
  labelKey: "provider-reolink:creation.s_connection.uid.label",
202878
203182
  placeholder: "9527000XXXXXXXX",
202879
- description: "Required for battery / UDP cameras when not on the same LAN. Optional for local-direct UDP discovery and ignored for pure TCP.",
203183
+ description: "Needed for battery / UDP cameras that are not on the same LAN.",
202880
203184
  descriptionKey: "provider-reolink:creation.s_connection.uid.description",
202881
203185
  showWhen: {
202882
203186
  field: "transport",
@@ -203417,7 +203721,7 @@ function buildEmailPushSettingsSchema(recommendedHost) {
203417
203721
  id: "email-push",
203418
203722
  title: "Battery-cam Motion (Email/SMTP Push)",
203419
203723
  titleKey: "provider-reolink:emailPushSettings.s_email-push.title",
203420
- 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.",
203724
+ description: "Lets battery cameras report motion by email without being woken.",
203421
203725
  descriptionKey: "provider-reolink:emailPushSettings.s_email-push.description",
203422
203726
  columns: 2,
203423
203727
  fields: [
@@ -203457,7 +203761,7 @@ function buildEmailPushSettingsSchema(recommendedHost) {
203457
203761
  id: "email-push-auth",
203458
203762
  title: "Email Push Authentication",
203459
203763
  titleKey: "provider-reolink:emailPushSettings.s_email-push-auth.title",
203460
- 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.",
203764
+ description: "Random values are generated on first boot; re-run Auto-configure on cameras after changing them.",
203461
203765
  descriptionKey: "provider-reolink:emailPushSettings.s_email-push-auth.description",
203462
203766
  columns: 2,
203463
203767
  fields: [
@@ -203666,7 +203970,7 @@ function buildBaichuanWebhookSettingsSchema(recommendedHost) {
203666
203970
  id: "baichuan-webhook",
203667
203971
  title: "Battery-cam Motion (HaCfg Webhook)",
203668
203972
  titleKey: "provider-reolink:baichuanWebhookSettings.s_baichuan-webhook.title",
203669
- 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).",
203973
+ description: "Preferred over email push when the camera firmware supports it.",
203670
203974
  descriptionKey: "provider-reolink:baichuanWebhookSettings.s_baichuan-webhook.description",
203671
203975
  columns: 2,
203672
203976
  fields: [