@camstack/addon-provider-reolink 1.1.13 → 1.1.15

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.
Files changed (3) hide show
  1. package/dist/addon.js +312 -40
  2. package/dist/addon.mjs +312 -40
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -4655,7 +4655,7 @@ function _instanceof(cls, params = {}) {
4655
4655
  return inst;
4656
4656
  }
4657
4657
  //#endregion
4658
- //#region ../types/dist/sleep-C2M2zF7x.mjs
4658
+ //#region ../types/dist/sleep-MHm--th-.mjs
4659
4659
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4660
4660
  EventCategory["SystemBoot"] = "system.boot";
4661
4661
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -5917,6 +5917,7 @@ var CamStreamKindSchema = _enum([
5917
5917
  "pull-rtsp",
5918
5918
  "pull-rtmp",
5919
5919
  "pull-http",
5920
+ "pull-flv",
5920
5921
  "pull-rfc4571",
5921
5922
  "push-annexb",
5922
5923
  "derived"
@@ -16291,7 +16292,7 @@ var AddBrokerInputSchema = object({
16291
16292
  });
16292
16293
  var AddBrokerResultSchema = object({ id: string() });
16293
16294
  var IdInputSchema = object({ id: string() });
16294
- var TestResultSchema = discriminatedUnion("ok", [object({
16295
+ var TestResultSchema$1 = discriminatedUnion("ok", [object({
16295
16296
  ok: literal(true),
16296
16297
  latencyMs: number()
16297
16298
  }), object({
@@ -16314,7 +16315,7 @@ var StatusSchema = object({
16314
16315
  brokerCount: number(),
16315
16316
  embeddedRunning: boolean()
16316
16317
  });
16317
- method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnectionDetailsSchema), method(AddBrokerInputSchema, AddBrokerResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(IdInputSchema, TestResultSchema, { kind: "mutation" }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(_void(), StatusSchema);
16318
+ method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnectionDetailsSchema), method(AddBrokerInputSchema, AddBrokerResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(IdInputSchema, TestResultSchema$1, { kind: "mutation" }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(_void(), StatusSchema);
16318
16319
  var NetworkEndpointSchema = object({
16319
16320
  url: string(),
16320
16321
  hostname: string(),
@@ -16348,23 +16349,198 @@ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
16348
16349
  sourcePort: number().optional()
16349
16350
  });
16350
16351
  method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
16351
- method(object({
16352
- title: string(),
16352
+ /**
16353
+ * notification-output — canonical, capability-gated notification delivery.
16354
+ *
16355
+ * Apprise-derived model (see
16356
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
16357
+ * callers emit ONE canonical `Notification`; each provider declares a
16358
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
16359
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
16360
+ * message to what the kind supports — callers never special-case a service.
16361
+ *
16362
+ * DESIGN DECISIONS (locked):
16363
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
16364
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
16365
+ * cap. Rationale: the admin UI needs one uniform surface across the
16366
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
16367
+ * alternative would fork the UI per addon and cannot host the
16368
+ * discovery→adopt flow.
16369
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
16370
+ * the generated cap-mount auto-`concatCollection`-fans them across every
16371
+ * registered provider (notifiers addon + HA addon) so one catalog is
16372
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
16373
+ * `addonId` the generated collection router extracts from the call input.
16374
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
16375
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
16376
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
16377
+ * base64 fallback needed.
16378
+ *
16379
+ * TODO (deferred, closed-set change — separate decision): add
16380
+ * `providerKind: 'notify'` so notification providers surface on the unified
16381
+ * admin "Integrations" page.
16382
+ */
16383
+ /**
16384
+ * Zentik-derived typed-media enum — the superset across every kind. Each
16385
+ * adapter picks what it supports and the degrade engine filters the rest.
16386
+ */
16387
+ var AttachmentMediaTypeSchema = _enum([
16388
+ "image",
16389
+ "video",
16390
+ "gif",
16391
+ "audio",
16392
+ "icon"
16393
+ ]);
16394
+ /**
16395
+ * A single attachment. Exactly one of `url` (remote source, most adapters
16396
+ * prefer this) or `bytes` (inline source; required for Pushover-style
16397
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
16398
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
16399
+ */
16400
+ var AttachmentSchema = object({
16401
+ mediaType: AttachmentMediaTypeSchema,
16402
+ url: string().optional(),
16403
+ bytes: _instanceof(Uint8Array).optional(),
16404
+ mime: string().optional(),
16405
+ name: string().optional()
16406
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
16407
+ var NotificationFormatSchema = _enum([
16408
+ "text",
16409
+ "markdown",
16410
+ "html"
16411
+ ]);
16412
+ /** A single tap-through action button. */
16413
+ var NotificationActionSchema = object({
16414
+ id: string(),
16415
+ label: string(),
16416
+ url: string().optional()
16417
+ });
16418
+ /**
16419
+ * The canonical notification. `body` is the only hard field (Apprise model).
16420
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
16421
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
16422
+ * the adapter maps this ordinal onto its native level. `level?` is an
16423
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
16424
+ * `priority` for that one target.
16425
+ */
16426
+ var NotificationSchema = object({
16353
16427
  body: string(),
16354
- imageUrl: string().optional(),
16428
+ title: string().optional(),
16429
+ format: NotificationFormatSchema.default("text"),
16430
+ priority: number().int().min(1).max(5).default(3),
16431
+ level: string().optional(),
16432
+ attachments: array(AttachmentSchema).optional(),
16433
+ clickUrl: string().optional(),
16434
+ actions: array(NotificationActionSchema).optional(),
16435
+ sound: string().optional(),
16436
+ ttl: number().optional(),
16437
+ tag: string().optional(),
16355
16438
  deviceId: number().optional(),
16356
16439
  eventId: string().optional(),
16357
- priority: _enum([
16358
- "low",
16359
- "normal",
16360
- "high",
16361
- "critical"
16362
- ]).default("normal"),
16363
16440
  metadata: record(string(), unknown()).optional()
16364
- }), _void(), { kind: "mutation" }), method(_void(), object({
16441
+ });
16442
+ /** One declared native severity/priority level for a kind. */
16443
+ var TargetKindLevelSchema = object({
16444
+ id: string(),
16445
+ label: string(),
16446
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
16447
+ ordinal: number().int().min(1).max(5).nullable(),
16448
+ flags: object({
16449
+ critical: boolean().optional(),
16450
+ silent: boolean().optional(),
16451
+ noPush: boolean().optional()
16452
+ }).optional(),
16453
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
16454
+ requires: array(string()).optional(),
16455
+ description: string().optional()
16456
+ });
16457
+ /** The full capability block consulted before dispatch. */
16458
+ var TargetKindCapsSchema = object({
16459
+ attachments: object({
16460
+ mediaTypes: array(AttachmentMediaTypeSchema),
16461
+ mode: _enum([
16462
+ "url",
16463
+ "bytes",
16464
+ "both"
16465
+ ]),
16466
+ max: number().int().nonnegative(),
16467
+ maxBytes: number().int().positive().optional()
16468
+ }),
16469
+ /** Max action buttons (0 = none). */
16470
+ actions: number().int().nonnegative(),
16471
+ levels: array(TargetKindLevelSchema),
16472
+ format: array(NotificationFormatSchema),
16473
+ clickUrl: boolean(),
16474
+ sound: boolean(),
16475
+ ttl: boolean(),
16476
+ bodyMaxLen: number().int().positive()
16477
+ });
16478
+ /**
16479
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
16480
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
16481
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
16482
+ * the union is large and not meant for runtime validation here; the exported
16483
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
16484
+ */
16485
+ var ConfigSchemaPassthrough = unknown();
16486
+ var TargetKindSchema = object({
16487
+ kind: string(),
16488
+ label: string(),
16489
+ icon: string(),
16490
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
16491
+ addonId: string(),
16492
+ configSchema: ConfigSchemaPassthrough,
16493
+ supportsDiscovery: boolean(),
16494
+ caps: TargetKindCapsSchema
16495
+ });
16496
+ /**
16497
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
16498
+ * (return a presence marker only) when serving `listTargets` — never
16499
+ * round-trip a stored secret to the UI.
16500
+ */
16501
+ var TargetSchema = object({
16502
+ id: string(),
16503
+ name: string(),
16504
+ kind: string(),
16505
+ addonId: string(),
16506
+ enabled: boolean(),
16507
+ config: record(string(), unknown())
16508
+ });
16509
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
16510
+ var DiscoveredTargetSchema = object({
16511
+ kind: string(),
16512
+ suggestedName: string(),
16513
+ config: record(string(), unknown())
16514
+ });
16515
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
16516
+ var RenderedAsSchema = object({
16517
+ level: string(),
16518
+ format: NotificationFormatSchema,
16519
+ attachmentsSent: number().int().nonnegative(),
16520
+ actionsSent: number().int().nonnegative(),
16521
+ truncated: boolean(),
16522
+ dropped: array(string())
16523
+ });
16524
+ var SendResultSchema = object({
16365
16525
  success: boolean(),
16366
- error: string().optional()
16367
- }), { kind: "mutation" });
16526
+ error: string().optional(),
16527
+ renderedAs: RenderedAsSchema.optional()
16528
+ });
16529
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
16530
+ var TestResultSchema = SendResultSchema;
16531
+ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema)), method(object({
16532
+ kind: string(),
16533
+ config: record(string(), unknown()).optional()
16534
+ }), array(DiscoveredTargetSchema)), method(object({
16535
+ targetId: string(),
16536
+ notification: NotificationSchema
16537
+ }), SendResultSchema, { kind: "mutation" }), method(object({
16538
+ targetId: string(),
16539
+ sample: NotificationSchema.optional()
16540
+ }), TestResultSchema, { kind: "mutation" }), method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }), method(object({ targetId: string() }), _void(), { kind: "mutation" }), method(object({
16541
+ targetId: string(),
16542
+ enabled: boolean()
16543
+ }), _void(), { kind: "mutation" });
16368
16544
  /**
16369
16545
  * Zod schemas for persisted record types.
16370
16546
  *
@@ -22731,13 +22907,49 @@ Object.freeze({
22731
22907
  addonId: null,
22732
22908
  access: "create"
22733
22909
  },
22910
+ "notificationOutput.deleteTarget": {
22911
+ capName: "notification-output",
22912
+ capScope: "system",
22913
+ addonId: null,
22914
+ access: "delete"
22915
+ },
22916
+ "notificationOutput.discoverTargets": {
22917
+ capName: "notification-output",
22918
+ capScope: "system",
22919
+ addonId: null,
22920
+ access: "view"
22921
+ },
22922
+ "notificationOutput.listTargetKinds": {
22923
+ capName: "notification-output",
22924
+ capScope: "system",
22925
+ addonId: null,
22926
+ access: "view"
22927
+ },
22928
+ "notificationOutput.listTargets": {
22929
+ capName: "notification-output",
22930
+ capScope: "system",
22931
+ addonId: null,
22932
+ access: "view"
22933
+ },
22734
22934
  "notificationOutput.send": {
22735
22935
  capName: "notification-output",
22736
22936
  capScope: "system",
22737
22937
  addonId: null,
22738
22938
  access: "create"
22739
22939
  },
22740
- "notificationOutput.sendTest": {
22940
+ "notificationOutput.setTargetEnabled": {
22941
+ capName: "notification-output",
22942
+ capScope: "system",
22943
+ addonId: null,
22944
+ access: "create"
22945
+ },
22946
+ "notificationOutput.testTarget": {
22947
+ capName: "notification-output",
22948
+ capScope: "system",
22949
+ addonId: null,
22950
+ access: "create"
22951
+ },
22952
+ "notificationOutput.upsertTarget": {
22741
22953
  capName: "notification-output",
22742
22954
  capScope: "system",
22743
22955
  addonId: null,
@@ -215236,7 +215448,7 @@ function isNativeObjectForwardingEnabled(capState) {
215236
215448
  }
215237
215449
  //#endregion
215238
215450
  //#region src/stream-routing.ts
215239
- var KIND_RE = /^(native|rtsp|rtmp):(.+)$/;
215451
+ var KIND_RE = /^(native|rtsp|rtmp|flv):(.+)$/;
215240
215452
  var CH_PROFILE_RE = /^ch(\d+)-(main|sub|ext)$/;
215241
215453
  var PROFILE_ONLY_RE = /^(main|sub|ext)$/;
215242
215454
  /**
@@ -215286,6 +215498,7 @@ function kindLabel(kind) {
215286
215498
  case "native": return "Native";
215287
215499
  case "rtsp": return "RTSP";
215288
215500
  case "rtmp": return "RTMP";
215501
+ case "flv": return "FLV";
215289
215502
  }
215290
215503
  }
215291
215504
  /**
@@ -219172,6 +219385,23 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
219172
219385
  }
219173
219386
  }
219174
219387
  /**
219388
+ * Synthesize the HTTP-FLV pull URL for a (channel, profile) pair. FLV is
219389
+ * served off the Reolink Baichuan media port (default 1935, app `bcs`) as
219390
+ * `channel<channel>_<profile>.bcs` — the same shape Frigate/go2rtc use. The
219391
+ * lib does NOT surface this URL, so we build it from the SAME connection
219392
+ * creds the RTSP/native paths use (`host`/`username`/`password` on the
219393
+ * device config), URL-encoding the credentials.
219394
+ *
219395
+ * Returns `null` when the host isn't locally resolvable (hub-child cameras
219396
+ * borrow the parent's socket and hold no own `host`), so the caller skips
219397
+ * FLV publication for those devices rather than emitting a broken URL.
219398
+ */
219399
+ buildFlvUrl(channel, profile) {
219400
+ const host = this.config.get("host");
219401
+ if (typeof host !== "string" || host.length === 0) return null;
219402
+ return `http://${host}/flv?port=1935&app=bcs&stream=channel${channel}_${profile}.bcs&user=${encodeURIComponent(String(this.config.get("username") ?? ""))}&password=${encodeURIComponent(String(this.config.get("password") ?? ""))}`;
219403
+ }
219404
+ /**
219175
219405
  * Heavy path: talk to the camera and assemble the descriptors. Only ever
219176
219406
  * entered when there is no cached catalog (see `buildStreamCatalog`).
219177
219407
  *
@@ -219257,6 +219487,27 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
219257
219487
  };
219258
219488
  enqueueAlt("rtsp", streamOptions.rtspStreams);
219259
219489
  enqueueAlt("rtmp", streamOptions.rtmpStreams);
219490
+ for (const s of streamOptions.nativeStreams) {
219491
+ const channel = s.channel ?? (isChild ? ownChannel : 0);
219492
+ const id = buildCamStreamId("flv", channel, s.profile, channelCount);
219493
+ if (desired.has(id)) continue;
219494
+ const flvUrl = this.buildFlvUrl(channel, s.profile);
219495
+ if (!flvUrl) break;
219496
+ const m = s.metadata;
219497
+ desired.set(id, {
219498
+ camStreamId: id,
219499
+ kind: "pull-flv",
219500
+ label: streamLabel(s, "flv"),
219501
+ url: flvUrl,
219502
+ autoEligible: false,
219503
+ ...m && m.width > 0 && m.height > 0 ? { resolution: {
219504
+ width: m.width,
219505
+ height: m.height
219506
+ } } : {},
219507
+ ...m && m.frameRate > 0 ? { fps: m.frameRate } : {},
219508
+ ...normalizeCodecName(m?.videoEncType) ? { codec: normalizeCodecName(m?.videoEncType) } : {}
219509
+ });
219510
+ }
219260
219511
  if (desired.size === 0) {
219261
219512
  this.ctx.logger.warn("buildStreamCatalog: camera reports no streams — empty catalog", { tags: { deviceId: this.id } });
219262
219513
  return [];
@@ -222977,6 +223228,47 @@ function buildCreationFormSchema() {
222977
223228
  ] };
222978
223229
  }
222979
223230
  //#endregion
223231
+ //#region src/reolink-discovery-map.ts
223232
+ /**
223233
+ * Flatten a host (IP / hostname) into a flat row-key slug — shared by `generateStableId` and the
223234
+ * discovery candidate mapping so a host-keyed camera is detected as already onboarded on re-scan.
223235
+ */
223236
+ function slugifyReolinkHost(host) {
223237
+ return host.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
223238
+ }
223239
+ /**
223240
+ * Map discovered Reolink hosts to adoption {@link DiscoveryCandidate}s, de-duplicated by host (the
223241
+ * same camera can answer on more than one discovery method — UDP broadcast, ONVIF, HTTP scan). The
223242
+ * first responder for a host wins. Pure: no I/O.
223243
+ *
223244
+ * The authoritative stableId is `mac-<mac>` (learned during autodetect at adopt time), which discovery
223245
+ * can't produce — so a re-scan of a MAC-keyed camera may still show as addable. The `host-` key still
223246
+ * lets host-added cameras be detected as onboarded on re-scan.
223247
+ */
223248
+ function mapReolinkDiscoveryToCandidates(devices, credentials) {
223249
+ const username = credentials.username?.trim() ?? "";
223250
+ const password = credentials.password ?? "";
223251
+ const byHost = /* @__PURE__ */ new Map();
223252
+ for (const d of devices) if (!byHost.has(d.host)) byHost.set(d.host, d);
223253
+ return [...byHost.values()].map((d) => {
223254
+ const displayName = d.name ?? d.model ?? d.host;
223255
+ return {
223256
+ stableId: `host-${slugifyReolinkHost(d.host)}`,
223257
+ type: DeviceType.Camera,
223258
+ suggestedName: displayName,
223259
+ prefilledConfig: {
223260
+ name: displayName,
223261
+ host: d.host,
223262
+ transport: "auto",
223263
+ ...d.httpPort !== void 0 ? { port: d.httpPort } : {},
223264
+ ...d.uid ? { uid: d.uid } : {},
223265
+ ...username ? { username } : {},
223266
+ ...password ? { password } : {}
223267
+ }
223268
+ };
223269
+ });
223270
+ }
223271
+ //#endregion
222980
223272
  //#region src/autodetect-cache.ts
222981
223273
  var DEFAULT_TTL_MS = 6e4;
222982
223274
  var AutodetectCache = class {
@@ -223489,11 +223781,6 @@ function isMeaningfulIdentifier(s) {
223489
223781
  if (!s || s.length < 6) return false;
223490
223782
  return new Set(s.toLowerCase().split("").filter((c) => c !== "0" && c !== "f")).size >= 1;
223491
223783
  }
223492
- /** Flatten a host (IP / hostname) into a flat row-key slug — shared by `generateStableId` and the
223493
- * discovery candidate mapping so a host-keyed camera is detected as already onboarded on re-scan. */
223494
- function slugifyReolinkHost(host) {
223495
- return host.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
223496
- }
223497
223784
  /**
223498
223785
  * Patch `detection.deviceInfo` + `hostNetworkInfo` in-place with the
223499
223786
  * post-login HOST identifiers. Two distinct gaps to fill:
@@ -223788,24 +224075,9 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
223788
224075
  networkCidr: networkCidr || "local",
223789
224076
  enableOnvif
223790
224077
  } });
223791
- const byHost = /* @__PURE__ */ new Map();
223792
- for (const d of devices) if (!byHost.has(d.host)) byHost.set(d.host, d);
223793
- return [...byHost.values()].map((d) => {
223794
- const displayName = d.name ?? d.model ?? d.host;
223795
- return {
223796
- stableId: `host-${slugifyReolinkHost(d.host)}`,
223797
- type: DeviceType.Camera,
223798
- suggestedName: displayName,
223799
- prefilledConfig: {
223800
- name: displayName,
223801
- host: d.host,
223802
- transport: "auto",
223803
- ...d.httpPort !== void 0 ? { port: d.httpPort } : {},
223804
- ...d.uid ? { uid: d.uid } : {},
223805
- ...username ? { username } : {},
223806
- ...password ? { password } : {}
223807
- }
223808
- };
224078
+ return mapReolinkDiscoveryToCandidates(devices, {
224079
+ username,
224080
+ password
223809
224081
  });
223810
224082
  }
223811
224083
  async adoptDiscoveredDevice(input) {
package/dist/addon.mjs CHANGED
@@ -4650,7 +4650,7 @@ function _instanceof(cls, params = {}) {
4650
4650
  return inst;
4651
4651
  }
4652
4652
  //#endregion
4653
- //#region ../types/dist/sleep-C2M2zF7x.mjs
4653
+ //#region ../types/dist/sleep-MHm--th-.mjs
4654
4654
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4655
4655
  EventCategory["SystemBoot"] = "system.boot";
4656
4656
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -5912,6 +5912,7 @@ var CamStreamKindSchema = _enum([
5912
5912
  "pull-rtsp",
5913
5913
  "pull-rtmp",
5914
5914
  "pull-http",
5915
+ "pull-flv",
5915
5916
  "pull-rfc4571",
5916
5917
  "push-annexb",
5917
5918
  "derived"
@@ -16286,7 +16287,7 @@ var AddBrokerInputSchema = object({
16286
16287
  });
16287
16288
  var AddBrokerResultSchema = object({ id: string() });
16288
16289
  var IdInputSchema = object({ id: string() });
16289
- var TestResultSchema = discriminatedUnion("ok", [object({
16290
+ var TestResultSchema$1 = discriminatedUnion("ok", [object({
16290
16291
  ok: literal(true),
16291
16292
  latencyMs: number()
16292
16293
  }), object({
@@ -16309,7 +16310,7 @@ var StatusSchema = object({
16309
16310
  brokerCount: number(),
16310
16311
  embeddedRunning: boolean()
16311
16312
  });
16312
- method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnectionDetailsSchema), method(AddBrokerInputSchema, AddBrokerResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(IdInputSchema, TestResultSchema, { kind: "mutation" }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(_void(), StatusSchema);
16313
+ method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnectionDetailsSchema), method(AddBrokerInputSchema, AddBrokerResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(IdInputSchema, TestResultSchema$1, { kind: "mutation" }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(_void(), StatusSchema);
16313
16314
  var NetworkEndpointSchema = object({
16314
16315
  url: string(),
16315
16316
  hostname: string(),
@@ -16343,23 +16344,198 @@ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
16343
16344
  sourcePort: number().optional()
16344
16345
  });
16345
16346
  method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
16346
- method(object({
16347
- title: string(),
16347
+ /**
16348
+ * notification-output — canonical, capability-gated notification delivery.
16349
+ *
16350
+ * Apprise-derived model (see
16351
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
16352
+ * callers emit ONE canonical `Notification`; each provider declares a
16353
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
16354
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
16355
+ * message to what the kind supports — callers never special-case a service.
16356
+ *
16357
+ * DESIGN DECISIONS (locked):
16358
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
16359
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
16360
+ * cap. Rationale: the admin UI needs one uniform surface across the
16361
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
16362
+ * alternative would fork the UI per addon and cannot host the
16363
+ * discovery→adopt flow.
16364
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
16365
+ * the generated cap-mount auto-`concatCollection`-fans them across every
16366
+ * registered provider (notifiers addon + HA addon) so one catalog is
16367
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
16368
+ * `addonId` the generated collection router extracts from the call input.
16369
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
16370
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
16371
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
16372
+ * base64 fallback needed.
16373
+ *
16374
+ * TODO (deferred, closed-set change — separate decision): add
16375
+ * `providerKind: 'notify'` so notification providers surface on the unified
16376
+ * admin "Integrations" page.
16377
+ */
16378
+ /**
16379
+ * Zentik-derived typed-media enum — the superset across every kind. Each
16380
+ * adapter picks what it supports and the degrade engine filters the rest.
16381
+ */
16382
+ var AttachmentMediaTypeSchema = _enum([
16383
+ "image",
16384
+ "video",
16385
+ "gif",
16386
+ "audio",
16387
+ "icon"
16388
+ ]);
16389
+ /**
16390
+ * A single attachment. Exactly one of `url` (remote source, most adapters
16391
+ * prefer this) or `bytes` (inline source; required for Pushover-style
16392
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
16393
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
16394
+ */
16395
+ var AttachmentSchema = object({
16396
+ mediaType: AttachmentMediaTypeSchema,
16397
+ url: string().optional(),
16398
+ bytes: _instanceof(Uint8Array).optional(),
16399
+ mime: string().optional(),
16400
+ name: string().optional()
16401
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
16402
+ var NotificationFormatSchema = _enum([
16403
+ "text",
16404
+ "markdown",
16405
+ "html"
16406
+ ]);
16407
+ /** A single tap-through action button. */
16408
+ var NotificationActionSchema = object({
16409
+ id: string(),
16410
+ label: string(),
16411
+ url: string().optional()
16412
+ });
16413
+ /**
16414
+ * The canonical notification. `body` is the only hard field (Apprise model).
16415
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
16416
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
16417
+ * the adapter maps this ordinal onto its native level. `level?` is an
16418
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
16419
+ * `priority` for that one target.
16420
+ */
16421
+ var NotificationSchema = object({
16348
16422
  body: string(),
16349
- imageUrl: string().optional(),
16423
+ title: string().optional(),
16424
+ format: NotificationFormatSchema.default("text"),
16425
+ priority: number().int().min(1).max(5).default(3),
16426
+ level: string().optional(),
16427
+ attachments: array(AttachmentSchema).optional(),
16428
+ clickUrl: string().optional(),
16429
+ actions: array(NotificationActionSchema).optional(),
16430
+ sound: string().optional(),
16431
+ ttl: number().optional(),
16432
+ tag: string().optional(),
16350
16433
  deviceId: number().optional(),
16351
16434
  eventId: string().optional(),
16352
- priority: _enum([
16353
- "low",
16354
- "normal",
16355
- "high",
16356
- "critical"
16357
- ]).default("normal"),
16358
16435
  metadata: record(string(), unknown()).optional()
16359
- }), _void(), { kind: "mutation" }), method(_void(), object({
16436
+ });
16437
+ /** One declared native severity/priority level for a kind. */
16438
+ var TargetKindLevelSchema = object({
16439
+ id: string(),
16440
+ label: string(),
16441
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
16442
+ ordinal: number().int().min(1).max(5).nullable(),
16443
+ flags: object({
16444
+ critical: boolean().optional(),
16445
+ silent: boolean().optional(),
16446
+ noPush: boolean().optional()
16447
+ }).optional(),
16448
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
16449
+ requires: array(string()).optional(),
16450
+ description: string().optional()
16451
+ });
16452
+ /** The full capability block consulted before dispatch. */
16453
+ var TargetKindCapsSchema = object({
16454
+ attachments: object({
16455
+ mediaTypes: array(AttachmentMediaTypeSchema),
16456
+ mode: _enum([
16457
+ "url",
16458
+ "bytes",
16459
+ "both"
16460
+ ]),
16461
+ max: number().int().nonnegative(),
16462
+ maxBytes: number().int().positive().optional()
16463
+ }),
16464
+ /** Max action buttons (0 = none). */
16465
+ actions: number().int().nonnegative(),
16466
+ levels: array(TargetKindLevelSchema),
16467
+ format: array(NotificationFormatSchema),
16468
+ clickUrl: boolean(),
16469
+ sound: boolean(),
16470
+ ttl: boolean(),
16471
+ bodyMaxLen: number().int().positive()
16472
+ });
16473
+ /**
16474
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
16475
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
16476
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
16477
+ * the union is large and not meant for runtime validation here; the exported
16478
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
16479
+ */
16480
+ var ConfigSchemaPassthrough = unknown();
16481
+ var TargetKindSchema = object({
16482
+ kind: string(),
16483
+ label: string(),
16484
+ icon: string(),
16485
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
16486
+ addonId: string(),
16487
+ configSchema: ConfigSchemaPassthrough,
16488
+ supportsDiscovery: boolean(),
16489
+ caps: TargetKindCapsSchema
16490
+ });
16491
+ /**
16492
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
16493
+ * (return a presence marker only) when serving `listTargets` — never
16494
+ * round-trip a stored secret to the UI.
16495
+ */
16496
+ var TargetSchema = object({
16497
+ id: string(),
16498
+ name: string(),
16499
+ kind: string(),
16500
+ addonId: string(),
16501
+ enabled: boolean(),
16502
+ config: record(string(), unknown())
16503
+ });
16504
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
16505
+ var DiscoveredTargetSchema = object({
16506
+ kind: string(),
16507
+ suggestedName: string(),
16508
+ config: record(string(), unknown())
16509
+ });
16510
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
16511
+ var RenderedAsSchema = object({
16512
+ level: string(),
16513
+ format: NotificationFormatSchema,
16514
+ attachmentsSent: number().int().nonnegative(),
16515
+ actionsSent: number().int().nonnegative(),
16516
+ truncated: boolean(),
16517
+ dropped: array(string())
16518
+ });
16519
+ var SendResultSchema = object({
16360
16520
  success: boolean(),
16361
- error: string().optional()
16362
- }), { kind: "mutation" });
16521
+ error: string().optional(),
16522
+ renderedAs: RenderedAsSchema.optional()
16523
+ });
16524
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
16525
+ var TestResultSchema = SendResultSchema;
16526
+ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema)), method(object({
16527
+ kind: string(),
16528
+ config: record(string(), unknown()).optional()
16529
+ }), array(DiscoveredTargetSchema)), method(object({
16530
+ targetId: string(),
16531
+ notification: NotificationSchema
16532
+ }), SendResultSchema, { kind: "mutation" }), method(object({
16533
+ targetId: string(),
16534
+ sample: NotificationSchema.optional()
16535
+ }), TestResultSchema, { kind: "mutation" }), method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }), method(object({ targetId: string() }), _void(), { kind: "mutation" }), method(object({
16536
+ targetId: string(),
16537
+ enabled: boolean()
16538
+ }), _void(), { kind: "mutation" });
16363
16539
  /**
16364
16540
  * Zod schemas for persisted record types.
16365
16541
  *
@@ -22726,13 +22902,49 @@ Object.freeze({
22726
22902
  addonId: null,
22727
22903
  access: "create"
22728
22904
  },
22905
+ "notificationOutput.deleteTarget": {
22906
+ capName: "notification-output",
22907
+ capScope: "system",
22908
+ addonId: null,
22909
+ access: "delete"
22910
+ },
22911
+ "notificationOutput.discoverTargets": {
22912
+ capName: "notification-output",
22913
+ capScope: "system",
22914
+ addonId: null,
22915
+ access: "view"
22916
+ },
22917
+ "notificationOutput.listTargetKinds": {
22918
+ capName: "notification-output",
22919
+ capScope: "system",
22920
+ addonId: null,
22921
+ access: "view"
22922
+ },
22923
+ "notificationOutput.listTargets": {
22924
+ capName: "notification-output",
22925
+ capScope: "system",
22926
+ addonId: null,
22927
+ access: "view"
22928
+ },
22729
22929
  "notificationOutput.send": {
22730
22930
  capName: "notification-output",
22731
22931
  capScope: "system",
22732
22932
  addonId: null,
22733
22933
  access: "create"
22734
22934
  },
22735
- "notificationOutput.sendTest": {
22935
+ "notificationOutput.setTargetEnabled": {
22936
+ capName: "notification-output",
22937
+ capScope: "system",
22938
+ addonId: null,
22939
+ access: "create"
22940
+ },
22941
+ "notificationOutput.testTarget": {
22942
+ capName: "notification-output",
22943
+ capScope: "system",
22944
+ addonId: null,
22945
+ access: "create"
22946
+ },
22947
+ "notificationOutput.upsertTarget": {
22736
22948
  capName: "notification-output",
22737
22949
  capScope: "system",
22738
22950
  addonId: null,
@@ -215216,7 +215428,7 @@ function isNativeObjectForwardingEnabled(capState) {
215216
215428
  }
215217
215429
  //#endregion
215218
215430
  //#region src/stream-routing.ts
215219
- var KIND_RE = /^(native|rtsp|rtmp):(.+)$/;
215431
+ var KIND_RE = /^(native|rtsp|rtmp|flv):(.+)$/;
215220
215432
  var CH_PROFILE_RE = /^ch(\d+)-(main|sub|ext)$/;
215221
215433
  var PROFILE_ONLY_RE = /^(main|sub|ext)$/;
215222
215434
  /**
@@ -215266,6 +215478,7 @@ function kindLabel(kind) {
215266
215478
  case "native": return "Native";
215267
215479
  case "rtsp": return "RTSP";
215268
215480
  case "rtmp": return "RTMP";
215481
+ case "flv": return "FLV";
215269
215482
  }
215270
215483
  }
215271
215484
  /**
@@ -219152,6 +219365,23 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
219152
219365
  }
219153
219366
  }
219154
219367
  /**
219368
+ * Synthesize the HTTP-FLV pull URL for a (channel, profile) pair. FLV is
219369
+ * served off the Reolink Baichuan media port (default 1935, app `bcs`) as
219370
+ * `channel<channel>_<profile>.bcs` — the same shape Frigate/go2rtc use. The
219371
+ * lib does NOT surface this URL, so we build it from the SAME connection
219372
+ * creds the RTSP/native paths use (`host`/`username`/`password` on the
219373
+ * device config), URL-encoding the credentials.
219374
+ *
219375
+ * Returns `null` when the host isn't locally resolvable (hub-child cameras
219376
+ * borrow the parent's socket and hold no own `host`), so the caller skips
219377
+ * FLV publication for those devices rather than emitting a broken URL.
219378
+ */
219379
+ buildFlvUrl(channel, profile) {
219380
+ const host = this.config.get("host");
219381
+ if (typeof host !== "string" || host.length === 0) return null;
219382
+ return `http://${host}/flv?port=1935&app=bcs&stream=channel${channel}_${profile}.bcs&user=${encodeURIComponent(String(this.config.get("username") ?? ""))}&password=${encodeURIComponent(String(this.config.get("password") ?? ""))}`;
219383
+ }
219384
+ /**
219155
219385
  * Heavy path: talk to the camera and assemble the descriptors. Only ever
219156
219386
  * entered when there is no cached catalog (see `buildStreamCatalog`).
219157
219387
  *
@@ -219237,6 +219467,27 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
219237
219467
  };
219238
219468
  enqueueAlt("rtsp", streamOptions.rtspStreams);
219239
219469
  enqueueAlt("rtmp", streamOptions.rtmpStreams);
219470
+ for (const s of streamOptions.nativeStreams) {
219471
+ const channel = s.channel ?? (isChild ? ownChannel : 0);
219472
+ const id = buildCamStreamId("flv", channel, s.profile, channelCount);
219473
+ if (desired.has(id)) continue;
219474
+ const flvUrl = this.buildFlvUrl(channel, s.profile);
219475
+ if (!flvUrl) break;
219476
+ const m = s.metadata;
219477
+ desired.set(id, {
219478
+ camStreamId: id,
219479
+ kind: "pull-flv",
219480
+ label: streamLabel(s, "flv"),
219481
+ url: flvUrl,
219482
+ autoEligible: false,
219483
+ ...m && m.width > 0 && m.height > 0 ? { resolution: {
219484
+ width: m.width,
219485
+ height: m.height
219486
+ } } : {},
219487
+ ...m && m.frameRate > 0 ? { fps: m.frameRate } : {},
219488
+ ...normalizeCodecName(m?.videoEncType) ? { codec: normalizeCodecName(m?.videoEncType) } : {}
219489
+ });
219490
+ }
219240
219491
  if (desired.size === 0) {
219241
219492
  this.ctx.logger.warn("buildStreamCatalog: camera reports no streams — empty catalog", { tags: { deviceId: this.id } });
219242
219493
  return [];
@@ -222957,6 +223208,47 @@ function buildCreationFormSchema() {
222957
223208
  ] };
222958
223209
  }
222959
223210
  //#endregion
223211
+ //#region src/reolink-discovery-map.ts
223212
+ /**
223213
+ * Flatten a host (IP / hostname) into a flat row-key slug — shared by `generateStableId` and the
223214
+ * discovery candidate mapping so a host-keyed camera is detected as already onboarded on re-scan.
223215
+ */
223216
+ function slugifyReolinkHost(host) {
223217
+ return host.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
223218
+ }
223219
+ /**
223220
+ * Map discovered Reolink hosts to adoption {@link DiscoveryCandidate}s, de-duplicated by host (the
223221
+ * same camera can answer on more than one discovery method — UDP broadcast, ONVIF, HTTP scan). The
223222
+ * first responder for a host wins. Pure: no I/O.
223223
+ *
223224
+ * The authoritative stableId is `mac-<mac>` (learned during autodetect at adopt time), which discovery
223225
+ * can't produce — so a re-scan of a MAC-keyed camera may still show as addable. The `host-` key still
223226
+ * lets host-added cameras be detected as onboarded on re-scan.
223227
+ */
223228
+ function mapReolinkDiscoveryToCandidates(devices, credentials) {
223229
+ const username = credentials.username?.trim() ?? "";
223230
+ const password = credentials.password ?? "";
223231
+ const byHost = /* @__PURE__ */ new Map();
223232
+ for (const d of devices) if (!byHost.has(d.host)) byHost.set(d.host, d);
223233
+ return [...byHost.values()].map((d) => {
223234
+ const displayName = d.name ?? d.model ?? d.host;
223235
+ return {
223236
+ stableId: `host-${slugifyReolinkHost(d.host)}`,
223237
+ type: DeviceType.Camera,
223238
+ suggestedName: displayName,
223239
+ prefilledConfig: {
223240
+ name: displayName,
223241
+ host: d.host,
223242
+ transport: "auto",
223243
+ ...d.httpPort !== void 0 ? { port: d.httpPort } : {},
223244
+ ...d.uid ? { uid: d.uid } : {},
223245
+ ...username ? { username } : {},
223246
+ ...password ? { password } : {}
223247
+ }
223248
+ };
223249
+ });
223250
+ }
223251
+ //#endregion
222960
223252
  //#region src/autodetect-cache.ts
222961
223253
  var DEFAULT_TTL_MS = 6e4;
222962
223254
  var AutodetectCache = class {
@@ -223469,11 +223761,6 @@ function isMeaningfulIdentifier(s) {
223469
223761
  if (!s || s.length < 6) return false;
223470
223762
  return new Set(s.toLowerCase().split("").filter((c) => c !== "0" && c !== "f")).size >= 1;
223471
223763
  }
223472
- /** Flatten a host (IP / hostname) into a flat row-key slug — shared by `generateStableId` and the
223473
- * discovery candidate mapping so a host-keyed camera is detected as already onboarded on re-scan. */
223474
- function slugifyReolinkHost(host) {
223475
- return host.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
223476
- }
223477
223764
  /**
223478
223765
  * Patch `detection.deviceInfo` + `hostNetworkInfo` in-place with the
223479
223766
  * post-login HOST identifiers. Two distinct gaps to fill:
@@ -223768,24 +224055,9 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
223768
224055
  networkCidr: networkCidr || "local",
223769
224056
  enableOnvif
223770
224057
  } });
223771
- const byHost = /* @__PURE__ */ new Map();
223772
- for (const d of devices) if (!byHost.has(d.host)) byHost.set(d.host, d);
223773
- return [...byHost.values()].map((d) => {
223774
- const displayName = d.name ?? d.model ?? d.host;
223775
- return {
223776
- stableId: `host-${slugifyReolinkHost(d.host)}`,
223777
- type: DeviceType.Camera,
223778
- suggestedName: displayName,
223779
- prefilledConfig: {
223780
- name: displayName,
223781
- host: d.host,
223782
- transport: "auto",
223783
- ...d.httpPort !== void 0 ? { port: d.httpPort } : {},
223784
- ...d.uid ? { uid: d.uid } : {},
223785
- ...username ? { username } : {},
223786
- ...password ? { password } : {}
223787
- }
223788
- };
224058
+ return mapReolinkDiscoveryToCandidates(devices, {
224059
+ username,
224060
+ password
223789
224061
  });
223790
224062
  }
223791
224063
  async adoptDiscoveredDevice(input) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-reolink",
3
- "version": "1.1.13",
3
+ "version": "1.1.15",
4
4
  "description": "Reolink camera device provider addon for CamStack — native Baichuan protocol",
5
5
  "keywords": [
6
6
  "camstack",