@camstack/addon-provider-ecowitt 0.1.11 → 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.
- package/dist/addon.js +372 -28
- package/dist/addon.mjs +372 -28
- 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-
|
|
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"
|
|
@@ -7066,7 +7067,21 @@ var StorageLocationDeclarationSchema = object({
|
|
|
7066
7067
|
* slots (e.g. `recordingsLow` → `recordings`) so operators only need to
|
|
7067
7068
|
* configure the primary location.
|
|
7068
7069
|
*/
|
|
7069
|
-
defaultsTo: string().optional()
|
|
7070
|
+
defaultsTo: string().optional(),
|
|
7071
|
+
/**
|
|
7072
|
+
* Which node root the seeded `<id>:default` instance is placed under on a
|
|
7073
|
+
* FRESH install:
|
|
7074
|
+
* - `'data'` (default) — the node's data dir (`CAMSTACK_DATA` / boot dir),
|
|
7075
|
+
* the appData volume. Right for small/durable data (backups, logs, models).
|
|
7076
|
+
* - `'media'` — the dedicated media volume (`CAMSTACK_MEDIA_ROOT`) when that
|
|
7077
|
+
* env is set, else falls back to the data root. Right for bulky, hot media
|
|
7078
|
+
* (recordings, event media) that should stay off the appData disk.
|
|
7079
|
+
*
|
|
7080
|
+
* Only affects the seeded default's `basePath`; operators can repoint any
|
|
7081
|
+
* location afterwards, and a `defaultsTo` slot inherits its parent's root
|
|
7082
|
+
* regardless of this field. Absent (the common case) is treated as `'data'`.
|
|
7083
|
+
*/
|
|
7084
|
+
defaultRoot: _enum(["data", "media"]).optional()
|
|
7070
7085
|
});
|
|
7071
7086
|
var DecoderStatsSchema = object({
|
|
7072
7087
|
inputFps: number(),
|
|
@@ -8422,6 +8437,10 @@ var RtspRestreamEntrySchema = object({
|
|
|
8422
8437
|
var BrokerRtspClientSchema = object({
|
|
8423
8438
|
sessionId: string(),
|
|
8424
8439
|
remoteAddr: string(),
|
|
8440
|
+
/** Client `User-Agent` (e.g. `recorder` for the recorder's RTSP pull), or
|
|
8441
|
+
* null/absent when the client sent none. Lets the UI label a consumer by
|
|
8442
|
+
* purpose. Optional so a client built against an older schema stays valid. */
|
|
8443
|
+
userAgent: string().nullish(),
|
|
8425
8444
|
playing: boolean(),
|
|
8426
8445
|
muted: boolean(),
|
|
8427
8446
|
connectedAt: number(),
|
|
@@ -16025,7 +16044,7 @@ var AddBrokerInputSchema = object({
|
|
|
16025
16044
|
});
|
|
16026
16045
|
var AddBrokerResultSchema = object({ id: string() });
|
|
16027
16046
|
var IdInputSchema = object({ id: string() });
|
|
16028
|
-
var TestResultSchema = discriminatedUnion("ok", [object({
|
|
16047
|
+
var TestResultSchema$1 = discriminatedUnion("ok", [object({
|
|
16029
16048
|
ok: literal(true),
|
|
16030
16049
|
latencyMs: number()
|
|
16031
16050
|
}), object({
|
|
@@ -16048,7 +16067,7 @@ var StatusSchema = object({
|
|
|
16048
16067
|
brokerCount: number(),
|
|
16049
16068
|
embeddedRunning: boolean()
|
|
16050
16069
|
});
|
|
16051
|
-
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);
|
|
16052
16071
|
var NetworkEndpointSchema = object({
|
|
16053
16072
|
url: string(),
|
|
16054
16073
|
hostname: string(),
|
|
@@ -16082,23 +16101,198 @@ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
|
|
|
16082
16101
|
sourcePort: number().optional()
|
|
16083
16102
|
});
|
|
16084
16103
|
method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
|
|
16085
|
-
|
|
16086
|
-
|
|
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({
|
|
16087
16179
|
body: string(),
|
|
16088
|
-
|
|
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(),
|
|
16089
16190
|
deviceId: number().optional(),
|
|
16090
16191
|
eventId: string().optional(),
|
|
16091
|
-
priority: _enum([
|
|
16092
|
-
"low",
|
|
16093
|
-
"normal",
|
|
16094
|
-
"high",
|
|
16095
|
-
"critical"
|
|
16096
|
-
]).default("normal"),
|
|
16097
16192
|
metadata: record(string(), unknown()).optional()
|
|
16098
|
-
})
|
|
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({
|
|
16099
16277
|
success: boolean(),
|
|
16100
|
-
error: string().optional()
|
|
16101
|
-
|
|
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" });
|
|
16102
16296
|
/**
|
|
16103
16297
|
* Zod schemas for persisted record types.
|
|
16104
16298
|
*
|
|
@@ -22093,13 +22287,49 @@ Object.freeze({
|
|
|
22093
22287
|
addonId: null,
|
|
22094
22288
|
access: "create"
|
|
22095
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
|
+
},
|
|
22096
22314
|
"notificationOutput.send": {
|
|
22097
22315
|
capName: "notification-output",
|
|
22098
22316
|
capScope: "system",
|
|
22099
22317
|
addonId: null,
|
|
22100
22318
|
access: "create"
|
|
22101
22319
|
},
|
|
22102
|
-
"notificationOutput.
|
|
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": {
|
|
22103
22333
|
capName: "notification-output",
|
|
22104
22334
|
capScope: "system",
|
|
22105
22335
|
addonId: null,
|
|
@@ -30072,22 +30302,23 @@ function slugifyHost(value) {
|
|
|
30072
30302
|
}
|
|
30073
30303
|
/**
|
|
30074
30304
|
* Extra per-scan inputs for Ecowitt network discovery. Mirrors Gree's scan form. On a flat network,
|
|
30075
|
-
* leave blank to sweep the addon node's local subnet.
|
|
30076
|
-
*
|
|
30077
|
-
*
|
|
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.
|
|
30078
30309
|
*/
|
|
30079
30310
|
function buildDiscoveryParamsFormSchema() {
|
|
30080
30311
|
return { sections: [{
|
|
30081
30312
|
id: "scan",
|
|
30082
30313
|
title: "Scan options",
|
|
30083
|
-
description: "Leave empty to scan the local subnet. To
|
|
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.",
|
|
30084
30315
|
columns: 1,
|
|
30085
30316
|
fields: [{
|
|
30086
30317
|
type: "text",
|
|
30087
30318
|
key: "broadcastAddress",
|
|
30088
|
-
label: "Broadcast
|
|
30319
|
+
label: "Broadcast addresses / gateway IPs / CIDRs",
|
|
30089
30320
|
required: false,
|
|
30090
|
-
placeholder: "192.168.20.255"
|
|
30321
|
+
placeholder: "192.168.20.255, 192.168.30.0/24"
|
|
30091
30322
|
}, {
|
|
30092
30323
|
type: "number",
|
|
30093
30324
|
key: "timeoutMs",
|
|
@@ -30240,6 +30471,111 @@ var EcowittIntegrationManager = class {
|
|
|
30240
30471
|
}
|
|
30241
30472
|
};
|
|
30242
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
|
|
30243
30579
|
//#region src/ecowitt-gateway.ts
|
|
30244
30580
|
/**
|
|
30245
30581
|
* Per-gateway registry that device classes use to reach the live {@link Ecowitt}
|
|
@@ -31259,15 +31595,23 @@ var EcowittProviderAddon = class extends BaseDeviceProvider {
|
|
|
31259
31595
|
return buildDiscoveryParamsFormSchema();
|
|
31260
31596
|
}
|
|
31261
31597
|
async discoverDevices(input) {
|
|
31262
|
-
const
|
|
31598
|
+
const rawTargets = typeof input?.params?.["broadcastAddress"] === "string" ? input.params["broadcastAddress"] : "";
|
|
31263
31599
|
const timeoutMs = typeof input?.params?.["timeoutMs"] === "number" ? input.params["timeoutMs"] : void 0;
|
|
31264
|
-
const
|
|
31265
|
-
|
|
31266
|
-
|
|
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
|
+
}
|
|
31267
31611
|
});
|
|
31268
31612
|
this.ctx.logger.info("Ecowitt discovery complete", { meta: {
|
|
31269
31613
|
count: gateways.length,
|
|
31270
|
-
|
|
31614
|
+
subnets: targets.length > 0 ? targets : "local"
|
|
31271
31615
|
} });
|
|
31272
31616
|
return gateways.map((g) => {
|
|
31273
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-
|
|
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"
|
|
@@ -7065,7 +7066,21 @@ var StorageLocationDeclarationSchema = object({
|
|
|
7065
7066
|
* slots (e.g. `recordingsLow` → `recordings`) so operators only need to
|
|
7066
7067
|
* configure the primary location.
|
|
7067
7068
|
*/
|
|
7068
|
-
defaultsTo: string().optional()
|
|
7069
|
+
defaultsTo: string().optional(),
|
|
7070
|
+
/**
|
|
7071
|
+
* Which node root the seeded `<id>:default` instance is placed under on a
|
|
7072
|
+
* FRESH install:
|
|
7073
|
+
* - `'data'` (default) — the node's data dir (`CAMSTACK_DATA` / boot dir),
|
|
7074
|
+
* the appData volume. Right for small/durable data (backups, logs, models).
|
|
7075
|
+
* - `'media'` — the dedicated media volume (`CAMSTACK_MEDIA_ROOT`) when that
|
|
7076
|
+
* env is set, else falls back to the data root. Right for bulky, hot media
|
|
7077
|
+
* (recordings, event media) that should stay off the appData disk.
|
|
7078
|
+
*
|
|
7079
|
+
* Only affects the seeded default's `basePath`; operators can repoint any
|
|
7080
|
+
* location afterwards, and a `defaultsTo` slot inherits its parent's root
|
|
7081
|
+
* regardless of this field. Absent (the common case) is treated as `'data'`.
|
|
7082
|
+
*/
|
|
7083
|
+
defaultRoot: _enum(["data", "media"]).optional()
|
|
7069
7084
|
});
|
|
7070
7085
|
var DecoderStatsSchema = object({
|
|
7071
7086
|
inputFps: number(),
|
|
@@ -8421,6 +8436,10 @@ var RtspRestreamEntrySchema = object({
|
|
|
8421
8436
|
var BrokerRtspClientSchema = object({
|
|
8422
8437
|
sessionId: string(),
|
|
8423
8438
|
remoteAddr: string(),
|
|
8439
|
+
/** Client `User-Agent` (e.g. `recorder` for the recorder's RTSP pull), or
|
|
8440
|
+
* null/absent when the client sent none. Lets the UI label a consumer by
|
|
8441
|
+
* purpose. Optional so a client built against an older schema stays valid. */
|
|
8442
|
+
userAgent: string().nullish(),
|
|
8424
8443
|
playing: boolean(),
|
|
8425
8444
|
muted: boolean(),
|
|
8426
8445
|
connectedAt: number(),
|
|
@@ -16024,7 +16043,7 @@ var AddBrokerInputSchema = object({
|
|
|
16024
16043
|
});
|
|
16025
16044
|
var AddBrokerResultSchema = object({ id: string() });
|
|
16026
16045
|
var IdInputSchema = object({ id: string() });
|
|
16027
|
-
var TestResultSchema = discriminatedUnion("ok", [object({
|
|
16046
|
+
var TestResultSchema$1 = discriminatedUnion("ok", [object({
|
|
16028
16047
|
ok: literal(true),
|
|
16029
16048
|
latencyMs: number()
|
|
16030
16049
|
}), object({
|
|
@@ -16047,7 +16066,7 @@ var StatusSchema = object({
|
|
|
16047
16066
|
brokerCount: number(),
|
|
16048
16067
|
embeddedRunning: boolean()
|
|
16049
16068
|
});
|
|
16050
|
-
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);
|
|
16051
16070
|
var NetworkEndpointSchema = object({
|
|
16052
16071
|
url: string(),
|
|
16053
16072
|
hostname: string(),
|
|
@@ -16081,23 +16100,198 @@ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
|
|
|
16081
16100
|
sourcePort: number().optional()
|
|
16082
16101
|
});
|
|
16083
16102
|
method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
|
|
16084
|
-
|
|
16085
|
-
|
|
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({
|
|
16086
16178
|
body: string(),
|
|
16087
|
-
|
|
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(),
|
|
16088
16189
|
deviceId: number().optional(),
|
|
16089
16190
|
eventId: string().optional(),
|
|
16090
|
-
priority: _enum([
|
|
16091
|
-
"low",
|
|
16092
|
-
"normal",
|
|
16093
|
-
"high",
|
|
16094
|
-
"critical"
|
|
16095
|
-
]).default("normal"),
|
|
16096
16191
|
metadata: record(string(), unknown()).optional()
|
|
16097
|
-
})
|
|
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({
|
|
16098
16276
|
success: boolean(),
|
|
16099
|
-
error: string().optional()
|
|
16100
|
-
|
|
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" });
|
|
16101
16295
|
/**
|
|
16102
16296
|
* Zod schemas for persisted record types.
|
|
16103
16297
|
*
|
|
@@ -22092,13 +22286,49 @@ Object.freeze({
|
|
|
22092
22286
|
addonId: null,
|
|
22093
22287
|
access: "create"
|
|
22094
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
|
+
},
|
|
22095
22313
|
"notificationOutput.send": {
|
|
22096
22314
|
capName: "notification-output",
|
|
22097
22315
|
capScope: "system",
|
|
22098
22316
|
addonId: null,
|
|
22099
22317
|
access: "create"
|
|
22100
22318
|
},
|
|
22101
|
-
"notificationOutput.
|
|
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": {
|
|
22102
22332
|
capName: "notification-output",
|
|
22103
22333
|
capScope: "system",
|
|
22104
22334
|
addonId: null,
|
|
@@ -30071,22 +30301,23 @@ function slugifyHost(value) {
|
|
|
30071
30301
|
}
|
|
30072
30302
|
/**
|
|
30073
30303
|
* Extra per-scan inputs for Ecowitt network discovery. Mirrors Gree's scan form. On a flat network,
|
|
30074
|
-
* leave blank to sweep the addon node's local subnet.
|
|
30075
|
-
*
|
|
30076
|
-
*
|
|
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.
|
|
30077
30308
|
*/
|
|
30078
30309
|
function buildDiscoveryParamsFormSchema() {
|
|
30079
30310
|
return { sections: [{
|
|
30080
30311
|
id: "scan",
|
|
30081
30312
|
title: "Scan options",
|
|
30082
|
-
description: "Leave empty to scan the local subnet. To
|
|
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.",
|
|
30083
30314
|
columns: 1,
|
|
30084
30315
|
fields: [{
|
|
30085
30316
|
type: "text",
|
|
30086
30317
|
key: "broadcastAddress",
|
|
30087
|
-
label: "Broadcast
|
|
30318
|
+
label: "Broadcast addresses / gateway IPs / CIDRs",
|
|
30088
30319
|
required: false,
|
|
30089
|
-
placeholder: "192.168.20.255"
|
|
30320
|
+
placeholder: "192.168.20.255, 192.168.30.0/24"
|
|
30090
30321
|
}, {
|
|
30091
30322
|
type: "number",
|
|
30092
30323
|
key: "timeoutMs",
|
|
@@ -30239,6 +30470,111 @@ var EcowittIntegrationManager = class {
|
|
|
30239
30470
|
}
|
|
30240
30471
|
};
|
|
30241
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
|
|
30242
30578
|
//#region src/ecowitt-gateway.ts
|
|
30243
30579
|
/**
|
|
30244
30580
|
* Per-gateway registry that device classes use to reach the live {@link Ecowitt}
|
|
@@ -31258,15 +31594,23 @@ var EcowittProviderAddon = class extends BaseDeviceProvider {
|
|
|
31258
31594
|
return buildDiscoveryParamsFormSchema();
|
|
31259
31595
|
}
|
|
31260
31596
|
async discoverDevices(input) {
|
|
31261
|
-
const
|
|
31597
|
+
const rawTargets = typeof input?.params?.["broadcastAddress"] === "string" ? input.params["broadcastAddress"] : "";
|
|
31262
31598
|
const timeoutMs = typeof input?.params?.["timeoutMs"] === "number" ? input.params["timeoutMs"] : void 0;
|
|
31263
|
-
const
|
|
31264
|
-
|
|
31265
|
-
|
|
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
|
+
}
|
|
31266
31610
|
});
|
|
31267
31611
|
this.ctx.logger.info("Ecowitt discovery complete", { meta: {
|
|
31268
31612
|
count: gateways.length,
|
|
31269
|
-
|
|
31613
|
+
subnets: targets.length > 0 ? targets : "local"
|
|
31270
31614
|
} });
|
|
31271
31615
|
return gateways.map((g) => {
|
|
31272
31616
|
const displayName = g.model ?? g.name;
|
package/package.json
CHANGED