@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.
- package/dist/addon.js +353 -27
- package/dist/addon.mjs +353 -27
- 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"
|
|
@@ -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
|
-
|
|
16104
|
-
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
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.
|
|
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.
|
|
30094
|
-
*
|
|
30095
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
31283
|
-
|
|
31284
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
16103
|
-
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
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.
|
|
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.
|
|
30093
|
-
*
|
|
30094
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
31282
|
-
|
|
31283
|
-
|
|
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
|
-
|
|
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