@camstack/addon-provider-ecowitt 0.1.12 → 0.1.13

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 +353 -27
  2. package/dist/addon.mjs +353 -27
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -4645,7 +4645,7 @@ function preprocess(fn, schema) {
4645
4645
  });
4646
4646
  }
4647
4647
  //#endregion
4648
- //#region ../types/dist/sleep-C2M2zF7x.mjs
4648
+ //#region ../types/dist/sleep-MHm--th-.mjs
4649
4649
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4650
4650
  EventCategory["SystemBoot"] = "system.boot";
4651
4651
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -5907,6 +5907,7 @@ var CamStreamKindSchema = _enum([
5907
5907
  "pull-rtsp",
5908
5908
  "pull-rtmp",
5909
5909
  "pull-http",
5910
+ "pull-flv",
5910
5911
  "pull-rfc4571",
5911
5912
  "push-annexb",
5912
5913
  "derived"
@@ -16043,7 +16044,7 @@ var AddBrokerInputSchema = object({
16043
16044
  });
16044
16045
  var AddBrokerResultSchema = object({ id: string() });
16045
16046
  var IdInputSchema = object({ id: string() });
16046
- var TestResultSchema = discriminatedUnion("ok", [object({
16047
+ var TestResultSchema$1 = discriminatedUnion("ok", [object({
16047
16048
  ok: literal(true),
16048
16049
  latencyMs: number()
16049
16050
  }), object({
@@ -16066,7 +16067,7 @@ var StatusSchema = object({
16066
16067
  brokerCount: number(),
16067
16068
  embeddedRunning: boolean()
16068
16069
  });
16069
- 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);
16070
+ 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);
16070
16071
  var NetworkEndpointSchema = object({
16071
16072
  url: string(),
16072
16073
  hostname: string(),
@@ -16100,23 +16101,198 @@ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
16100
16101
  sourcePort: number().optional()
16101
16102
  });
16102
16103
  method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
16103
- method(object({
16104
- title: string(),
16104
+ /**
16105
+ * notification-output — canonical, capability-gated notification delivery.
16106
+ *
16107
+ * Apprise-derived model (see
16108
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
16109
+ * callers emit ONE canonical `Notification`; each provider declares a
16110
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
16111
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
16112
+ * message to what the kind supports — callers never special-case a service.
16113
+ *
16114
+ * DESIGN DECISIONS (locked):
16115
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
16116
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
16117
+ * cap. Rationale: the admin UI needs one uniform surface across the
16118
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
16119
+ * alternative would fork the UI per addon and cannot host the
16120
+ * discovery→adopt flow.
16121
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
16122
+ * the generated cap-mount auto-`concatCollection`-fans them across every
16123
+ * registered provider (notifiers addon + HA addon) so one catalog is
16124
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
16125
+ * `addonId` the generated collection router extracts from the call input.
16126
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
16127
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
16128
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
16129
+ * base64 fallback needed.
16130
+ *
16131
+ * TODO (deferred, closed-set change — separate decision): add
16132
+ * `providerKind: 'notify'` so notification providers surface on the unified
16133
+ * admin "Integrations" page.
16134
+ */
16135
+ /**
16136
+ * Zentik-derived typed-media enum — the superset across every kind. Each
16137
+ * adapter picks what it supports and the degrade engine filters the rest.
16138
+ */
16139
+ var AttachmentMediaTypeSchema = _enum([
16140
+ "image",
16141
+ "video",
16142
+ "gif",
16143
+ "audio",
16144
+ "icon"
16145
+ ]);
16146
+ /**
16147
+ * A single attachment. Exactly one of `url` (remote source, most adapters
16148
+ * prefer this) or `bytes` (inline source; required for Pushover-style
16149
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
16150
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
16151
+ */
16152
+ var AttachmentSchema = object({
16153
+ mediaType: AttachmentMediaTypeSchema,
16154
+ url: string().optional(),
16155
+ bytes: _instanceof(Uint8Array).optional(),
16156
+ mime: string().optional(),
16157
+ name: string().optional()
16158
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
16159
+ var NotificationFormatSchema = _enum([
16160
+ "text",
16161
+ "markdown",
16162
+ "html"
16163
+ ]);
16164
+ /** A single tap-through action button. */
16165
+ var NotificationActionSchema = object({
16166
+ id: string(),
16167
+ label: string(),
16168
+ url: string().optional()
16169
+ });
16170
+ /**
16171
+ * The canonical notification. `body` is the only hard field (Apprise model).
16172
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
16173
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
16174
+ * the adapter maps this ordinal onto its native level. `level?` is an
16175
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
16176
+ * `priority` for that one target.
16177
+ */
16178
+ var NotificationSchema = object({
16105
16179
  body: string(),
16106
- imageUrl: string().optional(),
16180
+ title: string().optional(),
16181
+ format: NotificationFormatSchema.default("text"),
16182
+ priority: number().int().min(1).max(5).default(3),
16183
+ level: string().optional(),
16184
+ attachments: array(AttachmentSchema).optional(),
16185
+ clickUrl: string().optional(),
16186
+ actions: array(NotificationActionSchema).optional(),
16187
+ sound: string().optional(),
16188
+ ttl: number().optional(),
16189
+ tag: string().optional(),
16107
16190
  deviceId: number().optional(),
16108
16191
  eventId: string().optional(),
16109
- priority: _enum([
16110
- "low",
16111
- "normal",
16112
- "high",
16113
- "critical"
16114
- ]).default("normal"),
16115
16192
  metadata: record(string(), unknown()).optional()
16116
- }), _void(), { kind: "mutation" }), method(_void(), object({
16193
+ });
16194
+ /** One declared native severity/priority level for a kind. */
16195
+ var TargetKindLevelSchema = object({
16196
+ id: string(),
16197
+ label: string(),
16198
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
16199
+ ordinal: number().int().min(1).max(5).nullable(),
16200
+ flags: object({
16201
+ critical: boolean().optional(),
16202
+ silent: boolean().optional(),
16203
+ noPush: boolean().optional()
16204
+ }).optional(),
16205
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
16206
+ requires: array(string()).optional(),
16207
+ description: string().optional()
16208
+ });
16209
+ /** The full capability block consulted before dispatch. */
16210
+ var TargetKindCapsSchema = object({
16211
+ attachments: object({
16212
+ mediaTypes: array(AttachmentMediaTypeSchema),
16213
+ mode: _enum([
16214
+ "url",
16215
+ "bytes",
16216
+ "both"
16217
+ ]),
16218
+ max: number().int().nonnegative(),
16219
+ maxBytes: number().int().positive().optional()
16220
+ }),
16221
+ /** Max action buttons (0 = none). */
16222
+ actions: number().int().nonnegative(),
16223
+ levels: array(TargetKindLevelSchema),
16224
+ format: array(NotificationFormatSchema),
16225
+ clickUrl: boolean(),
16226
+ sound: boolean(),
16227
+ ttl: boolean(),
16228
+ bodyMaxLen: number().int().positive()
16229
+ });
16230
+ /**
16231
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
16232
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
16233
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
16234
+ * the union is large and not meant for runtime validation here; the exported
16235
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
16236
+ */
16237
+ var ConfigSchemaPassthrough = unknown();
16238
+ var TargetKindSchema = object({
16239
+ kind: string(),
16240
+ label: string(),
16241
+ icon: string(),
16242
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
16243
+ addonId: string(),
16244
+ configSchema: ConfigSchemaPassthrough,
16245
+ supportsDiscovery: boolean(),
16246
+ caps: TargetKindCapsSchema
16247
+ });
16248
+ /**
16249
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
16250
+ * (return a presence marker only) when serving `listTargets` — never
16251
+ * round-trip a stored secret to the UI.
16252
+ */
16253
+ var TargetSchema = object({
16254
+ id: string(),
16255
+ name: string(),
16256
+ kind: string(),
16257
+ addonId: string(),
16258
+ enabled: boolean(),
16259
+ config: record(string(), unknown())
16260
+ });
16261
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
16262
+ var DiscoveredTargetSchema = object({
16263
+ kind: string(),
16264
+ suggestedName: string(),
16265
+ config: record(string(), unknown())
16266
+ });
16267
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
16268
+ var RenderedAsSchema = object({
16269
+ level: string(),
16270
+ format: NotificationFormatSchema,
16271
+ attachmentsSent: number().int().nonnegative(),
16272
+ actionsSent: number().int().nonnegative(),
16273
+ truncated: boolean(),
16274
+ dropped: array(string())
16275
+ });
16276
+ var SendResultSchema = object({
16117
16277
  success: boolean(),
16118
- error: string().optional()
16119
- }), { kind: "mutation" });
16278
+ error: string().optional(),
16279
+ renderedAs: RenderedAsSchema.optional()
16280
+ });
16281
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
16282
+ var TestResultSchema = SendResultSchema;
16283
+ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema)), method(object({
16284
+ kind: string(),
16285
+ config: record(string(), unknown()).optional()
16286
+ }), array(DiscoveredTargetSchema)), method(object({
16287
+ targetId: string(),
16288
+ notification: NotificationSchema
16289
+ }), SendResultSchema, { kind: "mutation" }), method(object({
16290
+ targetId: string(),
16291
+ sample: NotificationSchema.optional()
16292
+ }), TestResultSchema, { kind: "mutation" }), method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }), method(object({ targetId: string() }), _void(), { kind: "mutation" }), method(object({
16293
+ targetId: string(),
16294
+ enabled: boolean()
16295
+ }), _void(), { kind: "mutation" });
16120
16296
  /**
16121
16297
  * Zod schemas for persisted record types.
16122
16298
  *
@@ -22111,13 +22287,49 @@ Object.freeze({
22111
22287
  addonId: null,
22112
22288
  access: "create"
22113
22289
  },
22290
+ "notificationOutput.deleteTarget": {
22291
+ capName: "notification-output",
22292
+ capScope: "system",
22293
+ addonId: null,
22294
+ access: "delete"
22295
+ },
22296
+ "notificationOutput.discoverTargets": {
22297
+ capName: "notification-output",
22298
+ capScope: "system",
22299
+ addonId: null,
22300
+ access: "view"
22301
+ },
22302
+ "notificationOutput.listTargetKinds": {
22303
+ capName: "notification-output",
22304
+ capScope: "system",
22305
+ addonId: null,
22306
+ access: "view"
22307
+ },
22308
+ "notificationOutput.listTargets": {
22309
+ capName: "notification-output",
22310
+ capScope: "system",
22311
+ addonId: null,
22312
+ access: "view"
22313
+ },
22114
22314
  "notificationOutput.send": {
22115
22315
  capName: "notification-output",
22116
22316
  capScope: "system",
22117
22317
  addonId: null,
22118
22318
  access: "create"
22119
22319
  },
22120
- "notificationOutput.sendTest": {
22320
+ "notificationOutput.setTargetEnabled": {
22321
+ capName: "notification-output",
22322
+ capScope: "system",
22323
+ addonId: null,
22324
+ access: "create"
22325
+ },
22326
+ "notificationOutput.testTarget": {
22327
+ capName: "notification-output",
22328
+ capScope: "system",
22329
+ addonId: null,
22330
+ access: "create"
22331
+ },
22332
+ "notificationOutput.upsertTarget": {
22121
22333
  capName: "notification-output",
22122
22334
  capScope: "system",
22123
22335
  addonId: null,
@@ -30090,22 +30302,23 @@ function slugifyHost(value) {
30090
30302
  }
30091
30303
  /**
30092
30304
  * Extra per-scan inputs for Ecowitt network discovery. Mirrors Gree's scan form. On a flat network,
30093
- * leave blank to sweep the addon node's local subnet. Gateways on ANOTHER subnet usually can't be
30094
- * reached by a directed broadcast (most routers don't forward it) enter the gateway's IP instead:
30095
- * the `CMD_BROADCAST` probe answers a unicast just as well and returns the gateway's identity.
30305
+ * leave blank to sweep the addon node's local subnet. To reach gateways on OTHER subnets, list one or
30306
+ * more directed broadcast addresses, gateway IPs, or CIDRs (comma / newline separated) each is
30307
+ * swept independently and the responders are merged. A directed broadcast (e.g. 192.168.20.255) or a
30308
+ * unicast gateway IP both work because the `CMD_BROADCAST` probe answers a unicast just as well.
30096
30309
  */
30097
30310
  function buildDiscoveryParamsFormSchema() {
30098
30311
  return { sections: [{
30099
30312
  id: "scan",
30100
30313
  title: "Scan options",
30101
- description: "Leave empty to scan the local subnet. To find a gateway on a different subnet, enter that subnet’s broadcast address (e.g. 192.168.20.255) or the gateway’s IP directly (e.g. 192.168.20.181).",
30314
+ description: "Leave empty to scan the local subnet. To scan other subnets, list their broadcast addresses (e.g. 192.168.20.255), gateway IPs (e.g. 192.168.20.181), or CIDRs (e.g. 192.168.20.0/24) — comma or newline separated. Each subnet is swept independently and unreachable ones are skipped.",
30102
30315
  columns: 1,
30103
30316
  fields: [{
30104
30317
  type: "text",
30105
30318
  key: "broadcastAddress",
30106
- label: "Broadcast address or gateway IP",
30319
+ label: "Broadcast addresses / gateway IPs / CIDRs",
30107
30320
  required: false,
30108
- placeholder: "192.168.20.255"
30321
+ placeholder: "192.168.20.255, 192.168.30.0/24"
30109
30322
  }, {
30110
30323
  type: "number",
30111
30324
  key: "timeoutMs",
@@ -30258,6 +30471,111 @@ var EcowittIntegrationManager = class {
30258
30471
  }
30259
30472
  };
30260
30473
  //#endregion
30474
+ //#region src/ecowitt-network-scan.ts
30475
+ /** Strict dotted-quad IPv4 octet (0–255), no leading-zero ambiguity beyond what routers accept. */
30476
+ function parseOctet(token) {
30477
+ if (!/^\d{1,3}$/.test(token)) return null;
30478
+ const n = Number(token);
30479
+ return n >= 0 && n <= 255 ? n : null;
30480
+ }
30481
+ /** Parse a dotted-quad into its four octets, or null when malformed. */
30482
+ function parseIpv4(value) {
30483
+ const parts = value.split(".");
30484
+ if (parts.length !== 4) return null;
30485
+ const octets = parts.map(parseOctet);
30486
+ if (octets.some((o) => o === null)) return null;
30487
+ return [
30488
+ octets[0],
30489
+ octets[1],
30490
+ octets[2],
30491
+ octets[3]
30492
+ ];
30493
+ }
30494
+ /**
30495
+ * Convert an IPv4 CIDR (e.g. `192.168.20.0/24`) into its directed broadcast address
30496
+ * (`192.168.20.255`) — the address `Ecowitt.discover` sweeps to reach a foreign subnet. Returns
30497
+ * `null` for anything that is not a well-formed IPv4 CIDR (a bare IP, garbage, or an out-of-range
30498
+ * prefix), so the caller can treat the input as a literal broadcast/gateway address instead.
30499
+ */
30500
+ function cidrToBroadcast(value) {
30501
+ const slash = value.indexOf("/");
30502
+ if (slash < 0) return null;
30503
+ const ip = parseIpv4(value.slice(0, slash));
30504
+ const prefixToken = value.slice(slash + 1);
30505
+ if (ip === null || !/^\d{1,2}$/.test(prefixToken)) return null;
30506
+ const prefix = Number(prefixToken);
30507
+ if (prefix < 0 || prefix > 32) return null;
30508
+ const broadcast = ((ip[0] << 24 | ip[1] << 16 | ip[2] << 8 | ip[3]) >>> 0 | (prefix === 0 ? 4294967295 : 4294967295 >>> prefix >>> 0)) >>> 0;
30509
+ return [
30510
+ broadcast >>> 24 & 255,
30511
+ broadcast >>> 16 & 255,
30512
+ broadcast >>> 8 & 255,
30513
+ broadcast & 255
30514
+ ].join(".");
30515
+ }
30516
+ /**
30517
+ * Parse the operator scan input into a de-duplicated, ordered list of directed broadcast targets.
30518
+ *
30519
+ * Accepts a free-form string of comma / newline / whitespace separated tokens where each token is
30520
+ * either a broadcast address (`192.168.20.255`), a bare gateway IP (`192.168.20.181` — the
30521
+ * `CMD_BROADCAST` probe answers a unicast too), or a CIDR (`192.168.20.0/24`, expanded to its
30522
+ * directed broadcast). Blank input yields an empty list, meaning "sweep the local subnet only".
30523
+ * Invalid tokens are dropped rather than throwing — discovery input is best-effort.
30524
+ */
30525
+ function parseBroadcastTargets(raw) {
30526
+ const seen = /* @__PURE__ */ new Set();
30527
+ const out = [];
30528
+ for (const rawToken of raw.split(/[\s,]+/)) {
30529
+ const token = rawToken.trim();
30530
+ if (token.length === 0) continue;
30531
+ const target = token.includes("/") ? cidrToBroadcast(token) : parseIpv4(token) ? token : null;
30532
+ if (target === null || seen.has(target)) continue;
30533
+ seen.add(target);
30534
+ out.push(target);
30535
+ }
30536
+ return out;
30537
+ }
30538
+ /** Deterministic dedup key for a discovered gateway — MAC is the physical identity. */
30539
+ function gatewayKey(gateway) {
30540
+ return gateway.mac.trim().toLowerCase() || gateway.ip;
30541
+ }
30542
+ /**
30543
+ * Cross-subnet Ecowitt discovery: run one {@link EcowittDiscoverFn} sweep per target subnet (or a
30544
+ * single local sweep when there are no targets), then merge the responders de-duplicated by MAC.
30545
+ *
30546
+ * Every sweep runs independently and its failure (an unreachable subnet, a router that drops the
30547
+ * directed broadcast, a socket error) is isolated via `onError` — a single bad subnet never fails the
30548
+ * whole scan. Sweeps run concurrently; results are flattened in target order so the merged output is
30549
+ * stable regardless of which subnet answers first.
30550
+ */
30551
+ async function scanEcowittGateways(input) {
30552
+ const { discover, targets, timeoutMs, onError } = input;
30553
+ const timeoutOpt = timeoutMs !== void 0 ? { timeoutMs } : {};
30554
+ if (targets.length === 0) try {
30555
+ return [...await discover({ ...timeoutOpt })];
30556
+ } catch (error) {
30557
+ onError?.("local", error);
30558
+ return [];
30559
+ }
30560
+ const settled = await Promise.all(targets.map(async (broadcastAddr) => {
30561
+ try {
30562
+ return await discover({
30563
+ broadcastAddr,
30564
+ ...timeoutOpt
30565
+ });
30566
+ } catch (error) {
30567
+ onError?.(broadcastAddr, error);
30568
+ return [];
30569
+ }
30570
+ }));
30571
+ const merged = /* @__PURE__ */ new Map();
30572
+ for (const batch of settled) for (const gateway of batch) {
30573
+ const key = gatewayKey(gateway);
30574
+ if (!merged.has(key)) merged.set(key, gateway);
30575
+ }
30576
+ return [...merged.values()];
30577
+ }
30578
+ //#endregion
30261
30579
  //#region src/ecowitt-gateway.ts
30262
30580
  /**
30263
30581
  * Per-gateway registry that device classes use to reach the live {@link Ecowitt}
@@ -31277,15 +31595,23 @@ var EcowittProviderAddon = class extends BaseDeviceProvider {
31277
31595
  return buildDiscoveryParamsFormSchema();
31278
31596
  }
31279
31597
  async discoverDevices(input) {
31280
- const broadcastAddr = typeof input?.params?.["broadcastAddress"] === "string" ? input.params["broadcastAddress"].trim() : "";
31598
+ const rawTargets = typeof input?.params?.["broadcastAddress"] === "string" ? input.params["broadcastAddress"] : "";
31281
31599
  const timeoutMs = typeof input?.params?.["timeoutMs"] === "number" ? input.params["timeoutMs"] : void 0;
31282
- const gateways = await Ecowitt.discover({
31283
- ...broadcastAddr.length > 0 ? { broadcastAddr } : {},
31284
- ...timeoutMs !== void 0 ? { timeoutMs } : {}
31600
+ const targets = parseBroadcastTargets(rawTargets);
31601
+ const gateways = await scanEcowittGateways({
31602
+ discover: (opts) => Ecowitt.discover(opts),
31603
+ targets,
31604
+ ...timeoutMs !== void 0 ? { timeoutMs } : {},
31605
+ onError: (target, error) => {
31606
+ this.ctx.logger.warn("Ecowitt subnet sweep failed — skipping", { meta: {
31607
+ target,
31608
+ error: errMsg(error)
31609
+ } });
31610
+ }
31285
31611
  });
31286
31612
  this.ctx.logger.info("Ecowitt discovery complete", { meta: {
31287
31613
  count: gateways.length,
31288
- broadcastAddr: broadcastAddr.length > 0 ? broadcastAddr : "local"
31614
+ subnets: targets.length > 0 ? targets : "local"
31289
31615
  } });
31290
31616
  return gateways.map((g) => {
31291
31617
  const displayName = g.model ?? g.name;
package/dist/addon.mjs CHANGED
@@ -4644,7 +4644,7 @@ function preprocess(fn, schema) {
4644
4644
  });
4645
4645
  }
4646
4646
  //#endregion
4647
- //#region ../types/dist/sleep-C2M2zF7x.mjs
4647
+ //#region ../types/dist/sleep-MHm--th-.mjs
4648
4648
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4649
4649
  EventCategory["SystemBoot"] = "system.boot";
4650
4650
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -5906,6 +5906,7 @@ var CamStreamKindSchema = _enum([
5906
5906
  "pull-rtsp",
5907
5907
  "pull-rtmp",
5908
5908
  "pull-http",
5909
+ "pull-flv",
5909
5910
  "pull-rfc4571",
5910
5911
  "push-annexb",
5911
5912
  "derived"
@@ -16042,7 +16043,7 @@ var AddBrokerInputSchema = object({
16042
16043
  });
16043
16044
  var AddBrokerResultSchema = object({ id: string() });
16044
16045
  var IdInputSchema = object({ id: string() });
16045
- var TestResultSchema = discriminatedUnion("ok", [object({
16046
+ var TestResultSchema$1 = discriminatedUnion("ok", [object({
16046
16047
  ok: literal(true),
16047
16048
  latencyMs: number()
16048
16049
  }), object({
@@ -16065,7 +16066,7 @@ var StatusSchema = object({
16065
16066
  brokerCount: number(),
16066
16067
  embeddedRunning: boolean()
16067
16068
  });
16068
- 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);
16069
+ 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);
16069
16070
  var NetworkEndpointSchema = object({
16070
16071
  url: string(),
16071
16072
  hostname: string(),
@@ -16099,23 +16100,198 @@ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
16099
16100
  sourcePort: number().optional()
16100
16101
  });
16101
16102
  method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
16102
- method(object({
16103
- title: string(),
16103
+ /**
16104
+ * notification-output — canonical, capability-gated notification delivery.
16105
+ *
16106
+ * Apprise-derived model (see
16107
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
16108
+ * callers emit ONE canonical `Notification`; each provider declares a
16109
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
16110
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
16111
+ * message to what the kind supports — callers never special-case a service.
16112
+ *
16113
+ * DESIGN DECISIONS (locked):
16114
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
16115
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
16116
+ * cap. Rationale: the admin UI needs one uniform surface across the
16117
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
16118
+ * alternative would fork the UI per addon and cannot host the
16119
+ * discovery→adopt flow.
16120
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
16121
+ * the generated cap-mount auto-`concatCollection`-fans them across every
16122
+ * registered provider (notifiers addon + HA addon) so one catalog is
16123
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
16124
+ * `addonId` the generated collection router extracts from the call input.
16125
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
16126
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
16127
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
16128
+ * base64 fallback needed.
16129
+ *
16130
+ * TODO (deferred, closed-set change — separate decision): add
16131
+ * `providerKind: 'notify'` so notification providers surface on the unified
16132
+ * admin "Integrations" page.
16133
+ */
16134
+ /**
16135
+ * Zentik-derived typed-media enum — the superset across every kind. Each
16136
+ * adapter picks what it supports and the degrade engine filters the rest.
16137
+ */
16138
+ var AttachmentMediaTypeSchema = _enum([
16139
+ "image",
16140
+ "video",
16141
+ "gif",
16142
+ "audio",
16143
+ "icon"
16144
+ ]);
16145
+ /**
16146
+ * A single attachment. Exactly one of `url` (remote source, most adapters
16147
+ * prefer this) or `bytes` (inline source; required for Pushover-style
16148
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
16149
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
16150
+ */
16151
+ var AttachmentSchema = object({
16152
+ mediaType: AttachmentMediaTypeSchema,
16153
+ url: string().optional(),
16154
+ bytes: _instanceof(Uint8Array).optional(),
16155
+ mime: string().optional(),
16156
+ name: string().optional()
16157
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
16158
+ var NotificationFormatSchema = _enum([
16159
+ "text",
16160
+ "markdown",
16161
+ "html"
16162
+ ]);
16163
+ /** A single tap-through action button. */
16164
+ var NotificationActionSchema = object({
16165
+ id: string(),
16166
+ label: string(),
16167
+ url: string().optional()
16168
+ });
16169
+ /**
16170
+ * The canonical notification. `body` is the only hard field (Apprise model).
16171
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
16172
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
16173
+ * the adapter maps this ordinal onto its native level. `level?` is an
16174
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
16175
+ * `priority` for that one target.
16176
+ */
16177
+ var NotificationSchema = object({
16104
16178
  body: string(),
16105
- imageUrl: string().optional(),
16179
+ title: string().optional(),
16180
+ format: NotificationFormatSchema.default("text"),
16181
+ priority: number().int().min(1).max(5).default(3),
16182
+ level: string().optional(),
16183
+ attachments: array(AttachmentSchema).optional(),
16184
+ clickUrl: string().optional(),
16185
+ actions: array(NotificationActionSchema).optional(),
16186
+ sound: string().optional(),
16187
+ ttl: number().optional(),
16188
+ tag: string().optional(),
16106
16189
  deviceId: number().optional(),
16107
16190
  eventId: string().optional(),
16108
- priority: _enum([
16109
- "low",
16110
- "normal",
16111
- "high",
16112
- "critical"
16113
- ]).default("normal"),
16114
16191
  metadata: record(string(), unknown()).optional()
16115
- }), _void(), { kind: "mutation" }), method(_void(), object({
16192
+ });
16193
+ /** One declared native severity/priority level for a kind. */
16194
+ var TargetKindLevelSchema = object({
16195
+ id: string(),
16196
+ label: string(),
16197
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
16198
+ ordinal: number().int().min(1).max(5).nullable(),
16199
+ flags: object({
16200
+ critical: boolean().optional(),
16201
+ silent: boolean().optional(),
16202
+ noPush: boolean().optional()
16203
+ }).optional(),
16204
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
16205
+ requires: array(string()).optional(),
16206
+ description: string().optional()
16207
+ });
16208
+ /** The full capability block consulted before dispatch. */
16209
+ var TargetKindCapsSchema = object({
16210
+ attachments: object({
16211
+ mediaTypes: array(AttachmentMediaTypeSchema),
16212
+ mode: _enum([
16213
+ "url",
16214
+ "bytes",
16215
+ "both"
16216
+ ]),
16217
+ max: number().int().nonnegative(),
16218
+ maxBytes: number().int().positive().optional()
16219
+ }),
16220
+ /** Max action buttons (0 = none). */
16221
+ actions: number().int().nonnegative(),
16222
+ levels: array(TargetKindLevelSchema),
16223
+ format: array(NotificationFormatSchema),
16224
+ clickUrl: boolean(),
16225
+ sound: boolean(),
16226
+ ttl: boolean(),
16227
+ bodyMaxLen: number().int().positive()
16228
+ });
16229
+ /**
16230
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
16231
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
16232
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
16233
+ * the union is large and not meant for runtime validation here; the exported
16234
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
16235
+ */
16236
+ var ConfigSchemaPassthrough = unknown();
16237
+ var TargetKindSchema = object({
16238
+ kind: string(),
16239
+ label: string(),
16240
+ icon: string(),
16241
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
16242
+ addonId: string(),
16243
+ configSchema: ConfigSchemaPassthrough,
16244
+ supportsDiscovery: boolean(),
16245
+ caps: TargetKindCapsSchema
16246
+ });
16247
+ /**
16248
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
16249
+ * (return a presence marker only) when serving `listTargets` — never
16250
+ * round-trip a stored secret to the UI.
16251
+ */
16252
+ var TargetSchema = object({
16253
+ id: string(),
16254
+ name: string(),
16255
+ kind: string(),
16256
+ addonId: string(),
16257
+ enabled: boolean(),
16258
+ config: record(string(), unknown())
16259
+ });
16260
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
16261
+ var DiscoveredTargetSchema = object({
16262
+ kind: string(),
16263
+ suggestedName: string(),
16264
+ config: record(string(), unknown())
16265
+ });
16266
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
16267
+ var RenderedAsSchema = object({
16268
+ level: string(),
16269
+ format: NotificationFormatSchema,
16270
+ attachmentsSent: number().int().nonnegative(),
16271
+ actionsSent: number().int().nonnegative(),
16272
+ truncated: boolean(),
16273
+ dropped: array(string())
16274
+ });
16275
+ var SendResultSchema = object({
16116
16276
  success: boolean(),
16117
- error: string().optional()
16118
- }), { kind: "mutation" });
16277
+ error: string().optional(),
16278
+ renderedAs: RenderedAsSchema.optional()
16279
+ });
16280
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
16281
+ var TestResultSchema = SendResultSchema;
16282
+ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema)), method(object({
16283
+ kind: string(),
16284
+ config: record(string(), unknown()).optional()
16285
+ }), array(DiscoveredTargetSchema)), method(object({
16286
+ targetId: string(),
16287
+ notification: NotificationSchema
16288
+ }), SendResultSchema, { kind: "mutation" }), method(object({
16289
+ targetId: string(),
16290
+ sample: NotificationSchema.optional()
16291
+ }), TestResultSchema, { kind: "mutation" }), method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }), method(object({ targetId: string() }), _void(), { kind: "mutation" }), method(object({
16292
+ targetId: string(),
16293
+ enabled: boolean()
16294
+ }), _void(), { kind: "mutation" });
16119
16295
  /**
16120
16296
  * Zod schemas for persisted record types.
16121
16297
  *
@@ -22110,13 +22286,49 @@ Object.freeze({
22110
22286
  addonId: null,
22111
22287
  access: "create"
22112
22288
  },
22289
+ "notificationOutput.deleteTarget": {
22290
+ capName: "notification-output",
22291
+ capScope: "system",
22292
+ addonId: null,
22293
+ access: "delete"
22294
+ },
22295
+ "notificationOutput.discoverTargets": {
22296
+ capName: "notification-output",
22297
+ capScope: "system",
22298
+ addonId: null,
22299
+ access: "view"
22300
+ },
22301
+ "notificationOutput.listTargetKinds": {
22302
+ capName: "notification-output",
22303
+ capScope: "system",
22304
+ addonId: null,
22305
+ access: "view"
22306
+ },
22307
+ "notificationOutput.listTargets": {
22308
+ capName: "notification-output",
22309
+ capScope: "system",
22310
+ addonId: null,
22311
+ access: "view"
22312
+ },
22113
22313
  "notificationOutput.send": {
22114
22314
  capName: "notification-output",
22115
22315
  capScope: "system",
22116
22316
  addonId: null,
22117
22317
  access: "create"
22118
22318
  },
22119
- "notificationOutput.sendTest": {
22319
+ "notificationOutput.setTargetEnabled": {
22320
+ capName: "notification-output",
22321
+ capScope: "system",
22322
+ addonId: null,
22323
+ access: "create"
22324
+ },
22325
+ "notificationOutput.testTarget": {
22326
+ capName: "notification-output",
22327
+ capScope: "system",
22328
+ addonId: null,
22329
+ access: "create"
22330
+ },
22331
+ "notificationOutput.upsertTarget": {
22120
22332
  capName: "notification-output",
22121
22333
  capScope: "system",
22122
22334
  addonId: null,
@@ -30089,22 +30301,23 @@ function slugifyHost(value) {
30089
30301
  }
30090
30302
  /**
30091
30303
  * Extra per-scan inputs for Ecowitt network discovery. Mirrors Gree's scan form. On a flat network,
30092
- * leave blank to sweep the addon node's local subnet. Gateways on ANOTHER subnet usually can't be
30093
- * reached by a directed broadcast (most routers don't forward it) enter the gateway's IP instead:
30094
- * the `CMD_BROADCAST` probe answers a unicast just as well and returns the gateway's identity.
30304
+ * leave blank to sweep the addon node's local subnet. To reach gateways on OTHER subnets, list one or
30305
+ * more directed broadcast addresses, gateway IPs, or CIDRs (comma / newline separated) each is
30306
+ * swept independently and the responders are merged. A directed broadcast (e.g. 192.168.20.255) or a
30307
+ * unicast gateway IP both work because the `CMD_BROADCAST` probe answers a unicast just as well.
30095
30308
  */
30096
30309
  function buildDiscoveryParamsFormSchema() {
30097
30310
  return { sections: [{
30098
30311
  id: "scan",
30099
30312
  title: "Scan options",
30100
- description: "Leave empty to scan the local subnet. To find a gateway on a different subnet, enter that subnet’s broadcast address (e.g. 192.168.20.255) or the gateway’s IP directly (e.g. 192.168.20.181).",
30313
+ description: "Leave empty to scan the local subnet. To scan other subnets, list their broadcast addresses (e.g. 192.168.20.255), gateway IPs (e.g. 192.168.20.181), or CIDRs (e.g. 192.168.20.0/24) — comma or newline separated. Each subnet is swept independently and unreachable ones are skipped.",
30101
30314
  columns: 1,
30102
30315
  fields: [{
30103
30316
  type: "text",
30104
30317
  key: "broadcastAddress",
30105
- label: "Broadcast address or gateway IP",
30318
+ label: "Broadcast addresses / gateway IPs / CIDRs",
30106
30319
  required: false,
30107
- placeholder: "192.168.20.255"
30320
+ placeholder: "192.168.20.255, 192.168.30.0/24"
30108
30321
  }, {
30109
30322
  type: "number",
30110
30323
  key: "timeoutMs",
@@ -30257,6 +30470,111 @@ var EcowittIntegrationManager = class {
30257
30470
  }
30258
30471
  };
30259
30472
  //#endregion
30473
+ //#region src/ecowitt-network-scan.ts
30474
+ /** Strict dotted-quad IPv4 octet (0–255), no leading-zero ambiguity beyond what routers accept. */
30475
+ function parseOctet(token) {
30476
+ if (!/^\d{1,3}$/.test(token)) return null;
30477
+ const n = Number(token);
30478
+ return n >= 0 && n <= 255 ? n : null;
30479
+ }
30480
+ /** Parse a dotted-quad into its four octets, or null when malformed. */
30481
+ function parseIpv4(value) {
30482
+ const parts = value.split(".");
30483
+ if (parts.length !== 4) return null;
30484
+ const octets = parts.map(parseOctet);
30485
+ if (octets.some((o) => o === null)) return null;
30486
+ return [
30487
+ octets[0],
30488
+ octets[1],
30489
+ octets[2],
30490
+ octets[3]
30491
+ ];
30492
+ }
30493
+ /**
30494
+ * Convert an IPv4 CIDR (e.g. `192.168.20.0/24`) into its directed broadcast address
30495
+ * (`192.168.20.255`) — the address `Ecowitt.discover` sweeps to reach a foreign subnet. Returns
30496
+ * `null` for anything that is not a well-formed IPv4 CIDR (a bare IP, garbage, or an out-of-range
30497
+ * prefix), so the caller can treat the input as a literal broadcast/gateway address instead.
30498
+ */
30499
+ function cidrToBroadcast(value) {
30500
+ const slash = value.indexOf("/");
30501
+ if (slash < 0) return null;
30502
+ const ip = parseIpv4(value.slice(0, slash));
30503
+ const prefixToken = value.slice(slash + 1);
30504
+ if (ip === null || !/^\d{1,2}$/.test(prefixToken)) return null;
30505
+ const prefix = Number(prefixToken);
30506
+ if (prefix < 0 || prefix > 32) return null;
30507
+ const broadcast = ((ip[0] << 24 | ip[1] << 16 | ip[2] << 8 | ip[3]) >>> 0 | (prefix === 0 ? 4294967295 : 4294967295 >>> prefix >>> 0)) >>> 0;
30508
+ return [
30509
+ broadcast >>> 24 & 255,
30510
+ broadcast >>> 16 & 255,
30511
+ broadcast >>> 8 & 255,
30512
+ broadcast & 255
30513
+ ].join(".");
30514
+ }
30515
+ /**
30516
+ * Parse the operator scan input into a de-duplicated, ordered list of directed broadcast targets.
30517
+ *
30518
+ * Accepts a free-form string of comma / newline / whitespace separated tokens where each token is
30519
+ * either a broadcast address (`192.168.20.255`), a bare gateway IP (`192.168.20.181` — the
30520
+ * `CMD_BROADCAST` probe answers a unicast too), or a CIDR (`192.168.20.0/24`, expanded to its
30521
+ * directed broadcast). Blank input yields an empty list, meaning "sweep the local subnet only".
30522
+ * Invalid tokens are dropped rather than throwing — discovery input is best-effort.
30523
+ */
30524
+ function parseBroadcastTargets(raw) {
30525
+ const seen = /* @__PURE__ */ new Set();
30526
+ const out = [];
30527
+ for (const rawToken of raw.split(/[\s,]+/)) {
30528
+ const token = rawToken.trim();
30529
+ if (token.length === 0) continue;
30530
+ const target = token.includes("/") ? cidrToBroadcast(token) : parseIpv4(token) ? token : null;
30531
+ if (target === null || seen.has(target)) continue;
30532
+ seen.add(target);
30533
+ out.push(target);
30534
+ }
30535
+ return out;
30536
+ }
30537
+ /** Deterministic dedup key for a discovered gateway — MAC is the physical identity. */
30538
+ function gatewayKey(gateway) {
30539
+ return gateway.mac.trim().toLowerCase() || gateway.ip;
30540
+ }
30541
+ /**
30542
+ * Cross-subnet Ecowitt discovery: run one {@link EcowittDiscoverFn} sweep per target subnet (or a
30543
+ * single local sweep when there are no targets), then merge the responders de-duplicated by MAC.
30544
+ *
30545
+ * Every sweep runs independently and its failure (an unreachable subnet, a router that drops the
30546
+ * directed broadcast, a socket error) is isolated via `onError` — a single bad subnet never fails the
30547
+ * whole scan. Sweeps run concurrently; results are flattened in target order so the merged output is
30548
+ * stable regardless of which subnet answers first.
30549
+ */
30550
+ async function scanEcowittGateways(input) {
30551
+ const { discover, targets, timeoutMs, onError } = input;
30552
+ const timeoutOpt = timeoutMs !== void 0 ? { timeoutMs } : {};
30553
+ if (targets.length === 0) try {
30554
+ return [...await discover({ ...timeoutOpt })];
30555
+ } catch (error) {
30556
+ onError?.("local", error);
30557
+ return [];
30558
+ }
30559
+ const settled = await Promise.all(targets.map(async (broadcastAddr) => {
30560
+ try {
30561
+ return await discover({
30562
+ broadcastAddr,
30563
+ ...timeoutOpt
30564
+ });
30565
+ } catch (error) {
30566
+ onError?.(broadcastAddr, error);
30567
+ return [];
30568
+ }
30569
+ }));
30570
+ const merged = /* @__PURE__ */ new Map();
30571
+ for (const batch of settled) for (const gateway of batch) {
30572
+ const key = gatewayKey(gateway);
30573
+ if (!merged.has(key)) merged.set(key, gateway);
30574
+ }
30575
+ return [...merged.values()];
30576
+ }
30577
+ //#endregion
30260
30578
  //#region src/ecowitt-gateway.ts
30261
30579
  /**
30262
30580
  * Per-gateway registry that device classes use to reach the live {@link Ecowitt}
@@ -31276,15 +31594,23 @@ var EcowittProviderAddon = class extends BaseDeviceProvider {
31276
31594
  return buildDiscoveryParamsFormSchema();
31277
31595
  }
31278
31596
  async discoverDevices(input) {
31279
- const broadcastAddr = typeof input?.params?.["broadcastAddress"] === "string" ? input.params["broadcastAddress"].trim() : "";
31597
+ const rawTargets = typeof input?.params?.["broadcastAddress"] === "string" ? input.params["broadcastAddress"] : "";
31280
31598
  const timeoutMs = typeof input?.params?.["timeoutMs"] === "number" ? input.params["timeoutMs"] : void 0;
31281
- const gateways = await Ecowitt.discover({
31282
- ...broadcastAddr.length > 0 ? { broadcastAddr } : {},
31283
- ...timeoutMs !== void 0 ? { timeoutMs } : {}
31599
+ const targets = parseBroadcastTargets(rawTargets);
31600
+ const gateways = await scanEcowittGateways({
31601
+ discover: (opts) => Ecowitt.discover(opts),
31602
+ targets,
31603
+ ...timeoutMs !== void 0 ? { timeoutMs } : {},
31604
+ onError: (target, error) => {
31605
+ this.ctx.logger.warn("Ecowitt subnet sweep failed — skipping", { meta: {
31606
+ target,
31607
+ error: errMsg(error)
31608
+ } });
31609
+ }
31284
31610
  });
31285
31611
  this.ctx.logger.info("Ecowitt discovery complete", { meta: {
31286
31612
  count: gateways.length,
31287
- broadcastAddr: broadcastAddr.length > 0 ? broadcastAddr : "local"
31613
+ subnets: targets.length > 0 ? targets : "local"
31288
31614
  } });
31289
31615
  return gateways.map((g) => {
31290
31616
  const displayName = g.model ?? g.name;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-ecowitt",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Ecowitt weather-station device-provider addon for CamStack — wraps the @apocaliss92/nodewitt local-poll / push client",
5
5
  "keywords": [
6
6
  "camstack",