@camstack/addon-provider-ecowitt 0.1.9 → 0.1.10
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 +521 -207
- package/dist/addon.mjs +521 -207
- package/package.json +2 -2
package/dist/addon.js
CHANGED
|
@@ -4,6 +4,7 @@ var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).expor
|
|
|
4
4
|
//#endregion
|
|
5
5
|
let http = require("http");
|
|
6
6
|
let events = require("events");
|
|
7
|
+
let dgram = require("dgram");
|
|
7
8
|
//#region ../../node_modules/zod/v4/core/core.js
|
|
8
9
|
var _a$1;
|
|
9
10
|
function $constructor(name, initializer, params) {
|
|
@@ -4647,7 +4648,7 @@ function preprocess(fn, schema) {
|
|
|
4647
4648
|
});
|
|
4648
4649
|
}
|
|
4649
4650
|
//#endregion
|
|
4650
|
-
//#region ../types/dist/sleep-
|
|
4651
|
+
//#region ../types/dist/sleep-C2M2zF7x.mjs
|
|
4651
4652
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
4652
4653
|
EventCategory["SystemBoot"] = "system.boot";
|
|
4653
4654
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -6215,6 +6216,12 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
|
|
|
6215
6216
|
DeviceType["Switch"] = "switch";
|
|
6216
6217
|
DeviceType["Sensor"] = "sensor";
|
|
6217
6218
|
DeviceType["Thermostat"] = "thermostat";
|
|
6219
|
+
/** Air-conditioner / heat-pump climate device (HVAC) — shares the
|
|
6220
|
+
* `climate-control` cap surface with `Thermostat` but renders a
|
|
6221
|
+
* dedicated AC-appropriate control UI (mode chips, fan speed,
|
|
6222
|
+
* independent vertical/horizontal swing). Sources: native Gree, and
|
|
6223
|
+
* reusable by other AC integrations. */
|
|
6224
|
+
DeviceType["Climate"] = "climate";
|
|
6218
6225
|
DeviceType["Button"] = "button";
|
|
6219
6226
|
/** Generic stateless event emitter — carries a device's EXACT declared
|
|
6220
6227
|
* event vocabulary verbatim (no normalization). Installed with the
|
|
@@ -9010,7 +9017,7 @@ var climateControlCapability = {
|
|
|
9010
9017
|
scope: "device",
|
|
9011
9018
|
deviceNative: true,
|
|
9012
9019
|
mode: "singleton",
|
|
9013
|
-
deviceTypes: [DeviceType.Thermostat],
|
|
9020
|
+
deviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
|
|
9014
9021
|
methods: {
|
|
9015
9022
|
setMode: method(object({
|
|
9016
9023
|
deviceId: number().int().nonnegative(),
|
|
@@ -13609,10 +13616,30 @@ var deviceProviderCapability = {
|
|
|
13609
13616
|
type: string()
|
|
13610
13617
|
}))),
|
|
13611
13618
|
supportsDiscovery: method(object({}), boolean()),
|
|
13612
|
-
|
|
13619
|
+
/**
|
|
13620
|
+
* Run a network scan. `params` carries optional provider-specific scan
|
|
13621
|
+
* inputs (e.g. a broadcast address / subnet for cross-subnet discovery),
|
|
13622
|
+
* shaped by `getDiscoveryParamsSchema`. Omitted for the generic scan
|
|
13623
|
+
* (provider uses its local-network default).
|
|
13624
|
+
*/
|
|
13625
|
+
discoverDevices: method(object({ params: record(string(), unknown()).optional() }), array(DiscoveryCandidateSchema), {
|
|
13613
13626
|
kind: "mutation",
|
|
13614
13627
|
auth: "admin"
|
|
13615
13628
|
}),
|
|
13629
|
+
/**
|
|
13630
|
+
* Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
|
|
13631
|
+
* provider accepts (e.g. Gree's broadcast address for a different subnet).
|
|
13632
|
+
* `null` when the provider takes no extra scan params — the generic
|
|
13633
|
+
* aggregated scan never renders this; the per-integration scan does.
|
|
13634
|
+
*/
|
|
13635
|
+
getDiscoveryParamsSchema: method(object({}), CreationSchemaOutputSchema),
|
|
13636
|
+
/**
|
|
13637
|
+
* The DeviceType this provider creates via manual add (Camera for
|
|
13638
|
+
* Reolink/ONVIF, Container for Gree, Hub for Ecowitt). `null` when the
|
|
13639
|
+
* provider does not support manual creation. Lets the Add-Device dialog
|
|
13640
|
+
* pick the right type instead of assuming Camera.
|
|
13641
|
+
*/
|
|
13642
|
+
getManualCreationType: method(object({}), object({ deviceType: _enum(DeviceType).nullable() })),
|
|
13616
13643
|
adoptDiscoveredDevice: method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
|
|
13617
13644
|
kind: "mutation",
|
|
13618
13645
|
auth: "admin"
|
|
@@ -13736,9 +13763,23 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
13736
13763
|
async supportsDiscovery() {
|
|
13737
13764
|
return false;
|
|
13738
13765
|
}
|
|
13739
|
-
async discoverDevices() {
|
|
13766
|
+
async discoverDevices(_input) {
|
|
13740
13767
|
return [];
|
|
13741
13768
|
}
|
|
13769
|
+
/** Extra per-scan input form (e.g. a broadcast address for another subnet).
|
|
13770
|
+
* Null = no extra params. Override in providers that support scoped scans. */
|
|
13771
|
+
async getDiscoveryParamsSchema() {
|
|
13772
|
+
return null;
|
|
13773
|
+
}
|
|
13774
|
+
/**
|
|
13775
|
+
* The DeviceType this provider creates via manual add — derived from the
|
|
13776
|
+
* `deviceClasses` map (first registered type). `null` when manual creation is
|
|
13777
|
+
* unsupported. Lets the Add-Device dialog pick the right type per provider.
|
|
13778
|
+
*/
|
|
13779
|
+
async getManualCreationType() {
|
|
13780
|
+
if (!await this.supportsManualCreation()) return { deviceType: null };
|
|
13781
|
+
return { deviceType: Object.values(DeviceType).find((t) => this.deviceClasses[t] !== void 0) ?? null };
|
|
13782
|
+
}
|
|
13742
13783
|
async adoptDiscoveredDevice(_input) {
|
|
13743
13784
|
throw new Error(`${this.providerName} provider does not support discovery-based adoption`);
|
|
13744
13785
|
}
|
|
@@ -15583,7 +15624,10 @@ method(object({
|
|
|
15583
15624
|
}), FieldProbeResultSchema, {
|
|
15584
15625
|
kind: "mutation",
|
|
15585
15626
|
auth: "admin"
|
|
15586
|
-
}), method(
|
|
15627
|
+
}), method(object({
|
|
15628
|
+
addonId: string(),
|
|
15629
|
+
integrationId: string()
|
|
15630
|
+
}), object({ filters: array(AdoptionFilterSchema) }), { auth: "admin" }), method(ListCandidatesInputSchema.extend({ addonId: string() }), ListCandidatesOutputSchema, { auth: "admin" }), method(object({
|
|
15587
15631
|
addonId: string(),
|
|
15588
15632
|
integrationId: string()
|
|
15589
15633
|
}), AdoptionStatusSchema, {
|
|
@@ -15598,7 +15642,24 @@ method(object({
|
|
|
15598
15642
|
}), method(ResyncInputSchema, ResyncResultSchema, {
|
|
15599
15643
|
kind: "mutation",
|
|
15600
15644
|
auth: "admin"
|
|
15645
|
+
}), method(object({}), object({ providers: array(object({
|
|
15646
|
+
addonId: string(),
|
|
15647
|
+
label: string()
|
|
15648
|
+
})).readonly() }), { auth: "admin" }), method(object({}), object({ groups: array(object({
|
|
15649
|
+
addonId: string(),
|
|
15650
|
+
label: string(),
|
|
15651
|
+
candidates: array(DiscoveryCandidateSchema).readonly(),
|
|
15652
|
+
error: string().nullable()
|
|
15653
|
+
})).readonly() }), {
|
|
15654
|
+
kind: "mutation",
|
|
15655
|
+
auth: "admin"
|
|
15601
15656
|
}), method(object({
|
|
15657
|
+
addonId: string(),
|
|
15658
|
+
params: record(string(), unknown()).optional()
|
|
15659
|
+
}), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
|
|
15660
|
+
kind: "mutation",
|
|
15661
|
+
auth: "admin"
|
|
15662
|
+
}), method(object({ addonId: string() }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
|
|
15602
15663
|
deviceId: number(),
|
|
15603
15664
|
key: string(),
|
|
15604
15665
|
value: unknown()
|
|
@@ -20721,6 +20782,12 @@ Object.freeze({
|
|
|
20721
20782
|
addonId: null,
|
|
20722
20783
|
access: "create"
|
|
20723
20784
|
},
|
|
20785
|
+
"deviceManager.adoptionListCandidateFilters": {
|
|
20786
|
+
capName: "device-manager",
|
|
20787
|
+
capScope: "system",
|
|
20788
|
+
addonId: null,
|
|
20789
|
+
access: "view"
|
|
20790
|
+
},
|
|
20724
20791
|
"deviceManager.adoptionListCandidates": {
|
|
20725
20792
|
capName: "device-manager",
|
|
20726
20793
|
capScope: "system",
|
|
@@ -20769,12 +20836,30 @@ Object.freeze({
|
|
|
20769
20836
|
addonId: null,
|
|
20770
20837
|
access: "create"
|
|
20771
20838
|
},
|
|
20839
|
+
"deviceManager.discoverAllProviders": {
|
|
20840
|
+
capName: "device-manager",
|
|
20841
|
+
capScope: "system",
|
|
20842
|
+
addonId: null,
|
|
20843
|
+
access: "create"
|
|
20844
|
+
},
|
|
20772
20845
|
"deviceManager.discoverDevices": {
|
|
20773
20846
|
capName: "device-manager",
|
|
20774
20847
|
capScope: "system",
|
|
20775
20848
|
addonId: null,
|
|
20776
20849
|
access: "create"
|
|
20777
20850
|
},
|
|
20851
|
+
"deviceManager.discoverProvider": {
|
|
20852
|
+
capName: "device-manager",
|
|
20853
|
+
capScope: "system",
|
|
20854
|
+
addonId: null,
|
|
20855
|
+
access: "create"
|
|
20856
|
+
},
|
|
20857
|
+
"deviceManager.discoveryProviders": {
|
|
20858
|
+
capName: "device-manager",
|
|
20859
|
+
capScope: "system",
|
|
20860
|
+
addonId: null,
|
|
20861
|
+
access: "view"
|
|
20862
|
+
},
|
|
20778
20863
|
"deviceManager.enable": {
|
|
20779
20864
|
capName: "device-manager",
|
|
20780
20865
|
capScope: "system",
|
|
@@ -20925,6 +21010,18 @@ Object.freeze({
|
|
|
20925
21010
|
addonId: null,
|
|
20926
21011
|
access: "create"
|
|
20927
21012
|
},
|
|
21013
|
+
"deviceManager.providerCreationType": {
|
|
21014
|
+
capName: "device-manager",
|
|
21015
|
+
capScope: "system",
|
|
21016
|
+
addonId: null,
|
|
21017
|
+
access: "view"
|
|
21018
|
+
},
|
|
21019
|
+
"deviceManager.providerDiscoveryParamsSchema": {
|
|
21020
|
+
capName: "device-manager",
|
|
21021
|
+
capScope: "system",
|
|
21022
|
+
addonId: null,
|
|
21023
|
+
access: "view"
|
|
21024
|
+
},
|
|
20928
21025
|
"deviceManager.registerDevice": {
|
|
20929
21026
|
capName: "device-manager",
|
|
20930
21027
|
capScope: "system",
|
|
@@ -21141,6 +21238,18 @@ Object.freeze({
|
|
|
21141
21238
|
addonId: null,
|
|
21142
21239
|
access: "view"
|
|
21143
21240
|
},
|
|
21241
|
+
"deviceProvider.getDiscoveryParamsSchema": {
|
|
21242
|
+
capName: "device-provider",
|
|
21243
|
+
capScope: "system",
|
|
21244
|
+
addonId: null,
|
|
21245
|
+
access: "view"
|
|
21246
|
+
},
|
|
21247
|
+
"deviceProvider.getManualCreationType": {
|
|
21248
|
+
capName: "device-provider",
|
|
21249
|
+
capScope: "system",
|
|
21250
|
+
addonId: null,
|
|
21251
|
+
access: "view"
|
|
21252
|
+
},
|
|
21144
21253
|
"deviceProvider.getStatus": {
|
|
21145
21254
|
capName: "device-provider",
|
|
21146
21255
|
capScope: "system",
|
|
@@ -23979,183 +24088,6 @@ object({
|
|
|
23979
24088
|
schemaVersion: literal(1)
|
|
23980
24089
|
});
|
|
23981
24090
|
//#endregion
|
|
23982
|
-
//#region src/config.ts
|
|
23983
|
-
/**
|
|
23984
|
-
* Operator-supplied Ecowitt gateway settings for ONE broker (= one station).
|
|
23985
|
-
* Two transports behind a `transport` discriminator:
|
|
23986
|
-
*
|
|
23987
|
-
* - `local` (local-poll): the addon polls the gateway over HTTP (`host`/`port`/
|
|
23988
|
-
* optional `password`, with poll + mapping intervals).
|
|
23989
|
-
* - `listener` (push): the addon runs an HTTP server the gateway uploads to
|
|
23990
|
-
* (the gateway's "Customized" upload must point at the hub's LAN IP:port).
|
|
23991
|
-
*
|
|
23992
|
-
* Local-poll is the recommended default — it has no inbound-IP constraint.
|
|
23993
|
-
*/
|
|
23994
|
-
var baseFields = {
|
|
23995
|
-
/** Human-overridable station identity used to scope the adopted gateway. */
|
|
23996
|
-
stationId: string().default("").describe("Optional stable station id (auto-derived when blank)") };
|
|
23997
|
-
var ecowittConfigSchema = discriminatedUnion("transport", [object({
|
|
23998
|
-
transport: literal("local"),
|
|
23999
|
-
host: string().min(1).describe("Ecowitt gateway host name or IP address"),
|
|
24000
|
-
port: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(1).max(65535).default(80)).describe("Gateway HTTP port"),
|
|
24001
|
-
password: string().default("").describe("Gateway password (optional — newer firmware)"),
|
|
24002
|
-
pollIntervalMs: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(1e3).max(36e5).default(6e4)).describe("Live-data poll interval (ms)"),
|
|
24003
|
-
mappingIntervalMs: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(1e4).max(864e5).default(6e5)).describe("Sensor-map refresh interval (ms)"),
|
|
24004
|
-
...baseFields
|
|
24005
|
-
}), object({
|
|
24006
|
-
transport: literal("listener"),
|
|
24007
|
-
listenPort: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(1).max(65535).default(4199)).describe("Local TCP port the gateway uploads to"),
|
|
24008
|
-
listenHost: string().default("").describe("Optional bind host (blank = all interfaces)"),
|
|
24009
|
-
...baseFields
|
|
24010
|
-
})]);
|
|
24011
|
-
/**
|
|
24012
|
-
* Narrow an `unknown` value to a shallow-cloned plain object record, or an empty
|
|
24013
|
-
* record when it is not a plain object. Mirrors the Homematic `toRecord` helper —
|
|
24014
|
-
* a cast-free boundary narrowing.
|
|
24015
|
-
*/
|
|
24016
|
-
function toRecord(value) {
|
|
24017
|
-
if (typeof value === "object" && value !== null && !Array.isArray(value)) return Object.fromEntries(Object.entries(value));
|
|
24018
|
-
return {};
|
|
24019
|
-
}
|
|
24020
|
-
/** Coerce a loose settings blob through the connection schema, applying defaults.
|
|
24021
|
-
* A blob with no `transport` defaults to `local` for back-compat with simple forms. */
|
|
24022
|
-
function settingsToEcowittConfig(settings) {
|
|
24023
|
-
const raw = toRecord(settings);
|
|
24024
|
-
if (raw["transport"] !== "local" && raw["transport"] !== "listener") raw["transport"] = "local";
|
|
24025
|
-
return ecowittConfigSchema.parse(raw);
|
|
24026
|
-
}
|
|
24027
|
-
/**
|
|
24028
|
-
* Persisted config for an Ecowitt gateway {@link import('@camstack/types').DeviceType.Hub}
|
|
24029
|
-
* device. The operator-supplied CONNECTION lives directly on the device (Reolink
|
|
24030
|
-
* pattern) — there is no broker registry. The gateway owns its own live nodewitt
|
|
24031
|
-
* client keyed on its own `stableId`; group Container children reference it via
|
|
24032
|
-
* their `gatewayId` config field.
|
|
24033
|
-
*/
|
|
24034
|
-
var ecowittGatewaySchema = object({
|
|
24035
|
-
/** The transport + endpoint the gateway's live client dials / binds. */
|
|
24036
|
-
connection: ecowittConfigSchema,
|
|
24037
|
-
/** Provenance marker used to scope offline / cascade helpers. */
|
|
24038
|
-
system: literal("ecowitt-gateway").optional()
|
|
24039
|
-
});
|
|
24040
|
-
/**
|
|
24041
|
-
* Hand-written connection form for the gateway device-creation UI (standalone
|
|
24042
|
-
* mode — Reolink pattern). The connection lives on the gateway DEVICE config,
|
|
24043
|
-
* not in a broker registry. A `name` field is included so the operator names
|
|
24044
|
-
* the gateway Hub at creation time (mirrors the Reolink creation form).
|
|
24045
|
-
*/
|
|
24046
|
-
function buildConnectionFormSchema() {
|
|
24047
|
-
return { sections: [
|
|
24048
|
-
{
|
|
24049
|
-
id: "identity",
|
|
24050
|
-
title: "Gateway",
|
|
24051
|
-
columns: 1,
|
|
24052
|
-
fields: [{
|
|
24053
|
-
type: "text",
|
|
24054
|
-
key: "name",
|
|
24055
|
-
label: "Name",
|
|
24056
|
-
required: true,
|
|
24057
|
-
placeholder: "Weather station"
|
|
24058
|
-
}]
|
|
24059
|
-
},
|
|
24060
|
-
{
|
|
24061
|
-
id: "transport",
|
|
24062
|
-
title: "Transport",
|
|
24063
|
-
description: "Choose how CamStack reads the gateway. Local poll connects out to the gateway (recommended). Push listener runs a server the gateway uploads to — configure the gateway \"Customized\" upload to point at this hub.",
|
|
24064
|
-
columns: 1,
|
|
24065
|
-
fields: [{
|
|
24066
|
-
type: "select",
|
|
24067
|
-
key: "transport",
|
|
24068
|
-
label: "Transport",
|
|
24069
|
-
default: "local",
|
|
24070
|
-
options: [{
|
|
24071
|
-
value: "local",
|
|
24072
|
-
label: "Local poll (recommended)"
|
|
24073
|
-
}, {
|
|
24074
|
-
value: "listener",
|
|
24075
|
-
label: "Push listener"
|
|
24076
|
-
}]
|
|
24077
|
-
}]
|
|
24078
|
-
},
|
|
24079
|
-
{
|
|
24080
|
-
id: "local",
|
|
24081
|
-
title: "Local poll settings",
|
|
24082
|
-
columns: 2,
|
|
24083
|
-
fields: [
|
|
24084
|
-
{
|
|
24085
|
-
type: "text",
|
|
24086
|
-
key: "host",
|
|
24087
|
-
label: "Gateway host / IP",
|
|
24088
|
-
placeholder: "192.168.1.50",
|
|
24089
|
-
showWhen: {
|
|
24090
|
-
field: "transport",
|
|
24091
|
-
equals: "local"
|
|
24092
|
-
}
|
|
24093
|
-
},
|
|
24094
|
-
{
|
|
24095
|
-
type: "number",
|
|
24096
|
-
key: "port",
|
|
24097
|
-
label: "HTTP port",
|
|
24098
|
-
min: 1,
|
|
24099
|
-
max: 65535,
|
|
24100
|
-
default: 80,
|
|
24101
|
-
showWhen: {
|
|
24102
|
-
field: "transport",
|
|
24103
|
-
equals: "local"
|
|
24104
|
-
}
|
|
24105
|
-
},
|
|
24106
|
-
{
|
|
24107
|
-
type: "password",
|
|
24108
|
-
key: "password",
|
|
24109
|
-
label: "Password (optional)",
|
|
24110
|
-
showToggle: true,
|
|
24111
|
-
showWhen: {
|
|
24112
|
-
field: "transport",
|
|
24113
|
-
equals: "local"
|
|
24114
|
-
}
|
|
24115
|
-
},
|
|
24116
|
-
{
|
|
24117
|
-
type: "number",
|
|
24118
|
-
key: "pollIntervalMs",
|
|
24119
|
-
label: "Poll interval (ms)",
|
|
24120
|
-
min: 1e3,
|
|
24121
|
-
max: 36e5,
|
|
24122
|
-
default: 6e4,
|
|
24123
|
-
showWhen: {
|
|
24124
|
-
field: "transport",
|
|
24125
|
-
equals: "local"
|
|
24126
|
-
}
|
|
24127
|
-
}
|
|
24128
|
-
]
|
|
24129
|
-
},
|
|
24130
|
-
{
|
|
24131
|
-
id: "listener",
|
|
24132
|
-
title: "Push listener settings",
|
|
24133
|
-
columns: 2,
|
|
24134
|
-
fields: [{
|
|
24135
|
-
type: "number",
|
|
24136
|
-
key: "listenPort",
|
|
24137
|
-
label: "Listen port",
|
|
24138
|
-
min: 1,
|
|
24139
|
-
max: 65535,
|
|
24140
|
-
default: 4199,
|
|
24141
|
-
showWhen: {
|
|
24142
|
-
field: "transport",
|
|
24143
|
-
equals: "listener"
|
|
24144
|
-
}
|
|
24145
|
-
}, {
|
|
24146
|
-
type: "text",
|
|
24147
|
-
key: "listenHost",
|
|
24148
|
-
label: "Bind host (optional)",
|
|
24149
|
-
placeholder: "all interfaces",
|
|
24150
|
-
showWhen: {
|
|
24151
|
-
field: "transport",
|
|
24152
|
-
equals: "listener"
|
|
24153
|
-
}
|
|
24154
|
-
}]
|
|
24155
|
-
}
|
|
24156
|
-
] };
|
|
24157
|
-
}
|
|
24158
|
-
//#endregion
|
|
24159
24091
|
//#region node_modules/undici/lib/core/symbols.js
|
|
24160
24092
|
var require_symbols$4 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
24161
24093
|
module.exports = {
|
|
@@ -45073,6 +45005,101 @@ var TypedEmitter = class {
|
|
|
45073
45005
|
return this;
|
|
45074
45006
|
}
|
|
45075
45007
|
};
|
|
45008
|
+
var CMD_BROADCAST = 18;
|
|
45009
|
+
var DISCOVERY_PORT = 46e3;
|
|
45010
|
+
function buildScanPacket() {
|
|
45011
|
+
const size = 3;
|
|
45012
|
+
const checksum = CMD_BROADCAST + size & 255;
|
|
45013
|
+
return Buffer.from([
|
|
45014
|
+
255,
|
|
45015
|
+
255,
|
|
45016
|
+
CMD_BROADCAST,
|
|
45017
|
+
size,
|
|
45018
|
+
checksum
|
|
45019
|
+
]);
|
|
45020
|
+
}
|
|
45021
|
+
function verifyChecksum(buf) {
|
|
45022
|
+
if (buf.length < 4) return false;
|
|
45023
|
+
let sum = 0;
|
|
45024
|
+
for (let i = 2; i < buf.length - 1; i += 1) sum = sum + (buf[i] ?? 0) & 255;
|
|
45025
|
+
return sum === buf[buf.length - 1];
|
|
45026
|
+
}
|
|
45027
|
+
function parseBroadcastResponse(buf) {
|
|
45028
|
+
if (buf.length < 18) return null;
|
|
45029
|
+
if (buf[0] !== 255 || buf[1] !== 255 || buf[2] !== CMD_BROADCAST) return null;
|
|
45030
|
+
if (!verifyChecksum(buf)) return null;
|
|
45031
|
+
const mac = macFromBytes(buf.subarray(5, 11));
|
|
45032
|
+
const tcpPort = buf.readUInt16BE(15);
|
|
45033
|
+
const ssidLen = buf[17] ?? 0;
|
|
45034
|
+
const ssidEnd = Math.min(18 + ssidLen, buf.length - 1);
|
|
45035
|
+
const { module, model, firmware } = splitSsid(buf.subarray(18, ssidEnd).toString("ascii").trim());
|
|
45036
|
+
return {
|
|
45037
|
+
mac,
|
|
45038
|
+
module,
|
|
45039
|
+
model,
|
|
45040
|
+
firmware,
|
|
45041
|
+
tcpPort
|
|
45042
|
+
};
|
|
45043
|
+
}
|
|
45044
|
+
function macFromBytes(bytes) {
|
|
45045
|
+
return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join(":");
|
|
45046
|
+
}
|
|
45047
|
+
function splitSsid(ssid) {
|
|
45048
|
+
const spaceIdx = ssid.indexOf(" ");
|
|
45049
|
+
const module = spaceIdx >= 0 ? ssid.slice(0, spaceIdx) : ssid;
|
|
45050
|
+
const firmwareRaw = spaceIdx >= 0 ? ssid.slice(spaceIdx + 1).trim() : "";
|
|
45051
|
+
const wifiIdx = module.indexOf("-WIFI");
|
|
45052
|
+
return {
|
|
45053
|
+
module,
|
|
45054
|
+
model: wifiIdx > 0 ? module.slice(0, wifiIdx) : void 0,
|
|
45055
|
+
firmware: firmwareRaw.length > 0 ? firmwareRaw : void 0
|
|
45056
|
+
};
|
|
45057
|
+
}
|
|
45058
|
+
function discoverGateways(socket, opts) {
|
|
45059
|
+
const port = opts?.port ?? DISCOVERY_PORT;
|
|
45060
|
+
const broadcastAddr = opts?.broadcastAddr ?? "255.255.255.255";
|
|
45061
|
+
const timeoutMs = opts?.timeoutMs ?? 3e3;
|
|
45062
|
+
const byMac = /* @__PURE__ */ new Map();
|
|
45063
|
+
socket.onMessage((m) => {
|
|
45064
|
+
const frame = parseBroadcastResponse(m.data);
|
|
45065
|
+
if (frame === null) return;
|
|
45066
|
+
byMac.set(frame.mac, {
|
|
45067
|
+
ip: m.address,
|
|
45068
|
+
mac: frame.mac,
|
|
45069
|
+
name: frame.module.length > 0 ? frame.module : frame.mac,
|
|
45070
|
+
...frame.model !== void 0 ? { model: frame.model } : {},
|
|
45071
|
+
...frame.firmware !== void 0 ? { firmware: frame.firmware } : {}
|
|
45072
|
+
});
|
|
45073
|
+
});
|
|
45074
|
+
socket.setBroadcast(true);
|
|
45075
|
+
const scan = buildScanPacket();
|
|
45076
|
+
return new Promise((resolve) => {
|
|
45077
|
+
socket.send(scan, port, broadcastAddr);
|
|
45078
|
+
setTimeout(() => resolve([...byMac.values()]), timeoutMs);
|
|
45079
|
+
});
|
|
45080
|
+
}
|
|
45081
|
+
function createDgramSocket() {
|
|
45082
|
+
return new Promise((resolve, reject) => {
|
|
45083
|
+
const socket = (0, dgram.createSocket)({
|
|
45084
|
+
type: "udp4",
|
|
45085
|
+
reuseAddr: true
|
|
45086
|
+
});
|
|
45087
|
+
socket.once("error", reject);
|
|
45088
|
+
socket.bind(() => {
|
|
45089
|
+
socket.removeListener("error", reject);
|
|
45090
|
+
resolve({
|
|
45091
|
+
send: (data, port, address) => new Promise((res, rej) => socket.send(data, port, address, (err) => err ? rej(err) : res())),
|
|
45092
|
+
onMessage: (handler) => socket.on("message", (data, rinfo) => handler({
|
|
45093
|
+
data,
|
|
45094
|
+
address: rinfo.address,
|
|
45095
|
+
port: rinfo.port
|
|
45096
|
+
})),
|
|
45097
|
+
setBroadcast: (enabled) => socket.setBroadcast(enabled),
|
|
45098
|
+
close: () => new Promise((res) => socket.close(() => res()))
|
|
45099
|
+
});
|
|
45100
|
+
});
|
|
45101
|
+
});
|
|
45102
|
+
}
|
|
45076
45103
|
function toError(error) {
|
|
45077
45104
|
return error instanceof Error ? error : new Error(String(error));
|
|
45078
45105
|
}
|
|
@@ -45097,6 +45124,21 @@ var Ecowitt = class _Ecowitt {
|
|
|
45097
45124
|
return _Ecowitt.#buildListener(defaultListenerBuilder(options));
|
|
45098
45125
|
}
|
|
45099
45126
|
/**
|
|
45127
|
+
* Discover Ecowitt gateways on the network via the `CMD_BROADCAST` UDP probe (port 46000). Opens a
|
|
45128
|
+
* throwaway UDP socket, sweeps the broadcast address (local subnet by default, or a directed
|
|
45129
|
+
* broadcast passed via {@link DiscoverOptions.broadcastAddr} for another subnet), collects
|
|
45130
|
+
* responders deduped by MAC, and closes the socket. No `Ecowitt` instance or connection is
|
|
45131
|
+
* created — the caller maps the results to whatever onboarding flow it uses.
|
|
45132
|
+
*/
|
|
45133
|
+
static async discover(options) {
|
|
45134
|
+
const socket = await createDgramSocket();
|
|
45135
|
+
try {
|
|
45136
|
+
return await discoverGateways(socket, options);
|
|
45137
|
+
} finally {
|
|
45138
|
+
await socket.close().catch(() => void 0);
|
|
45139
|
+
}
|
|
45140
|
+
}
|
|
45141
|
+
/**
|
|
45100
45142
|
* Build a local-transport facade from a transport builder. The wiring closure that hands the
|
|
45101
45143
|
* builder its `onReadings`/`onError` callbacks lives HERE, inside the class, so it can call the
|
|
45102
45144
|
* `#`-private ingest seams directly — no public method, no cast. `#`-private so it is unreachable
|
|
@@ -45302,6 +45344,219 @@ objectType({
|
|
|
45302
45344
|
})
|
|
45303
45345
|
});
|
|
45304
45346
|
//#endregion
|
|
45347
|
+
//#region src/config.ts
|
|
45348
|
+
/**
|
|
45349
|
+
* Operator-supplied Ecowitt gateway settings for ONE broker (= one station).
|
|
45350
|
+
* Two transports behind a `transport` discriminator:
|
|
45351
|
+
*
|
|
45352
|
+
* - `local` (local-poll): the addon polls the gateway over HTTP (`host`/`port`/
|
|
45353
|
+
* optional `password`, with poll + mapping intervals).
|
|
45354
|
+
* - `listener` (push): the addon runs an HTTP server the gateway uploads to
|
|
45355
|
+
* (the gateway's "Customized" upload must point at the hub's LAN IP:port).
|
|
45356
|
+
*
|
|
45357
|
+
* Local-poll is the recommended default — it has no inbound-IP constraint.
|
|
45358
|
+
*/
|
|
45359
|
+
var baseFields = {
|
|
45360
|
+
/** Human-overridable station identity used to scope the adopted gateway. */
|
|
45361
|
+
stationId: string().default("").describe("Optional stable station id (auto-derived when blank)") };
|
|
45362
|
+
var ecowittConfigSchema = discriminatedUnion("transport", [object({
|
|
45363
|
+
transport: literal("local"),
|
|
45364
|
+
host: string().min(1).describe("Ecowitt gateway host name or IP address"),
|
|
45365
|
+
port: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(1).max(65535).default(80)).describe("Gateway HTTP port"),
|
|
45366
|
+
password: string().default("").describe("Gateway password (optional — newer firmware)"),
|
|
45367
|
+
pollIntervalMs: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(1e3).max(36e5).default(6e4)).describe("Live-data poll interval (ms)"),
|
|
45368
|
+
mappingIntervalMs: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(1e4).max(864e5).default(6e5)).describe("Sensor-map refresh interval (ms)"),
|
|
45369
|
+
...baseFields
|
|
45370
|
+
}), object({
|
|
45371
|
+
transport: literal("listener"),
|
|
45372
|
+
listenPort: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(1).max(65535).default(4199)).describe("Local TCP port the gateway uploads to"),
|
|
45373
|
+
listenHost: string().default("").describe("Optional bind host (blank = all interfaces)"),
|
|
45374
|
+
...baseFields
|
|
45375
|
+
})]);
|
|
45376
|
+
/**
|
|
45377
|
+
* Narrow an `unknown` value to a shallow-cloned plain object record, or an empty
|
|
45378
|
+
* record when it is not a plain object. Mirrors the Homematic `toRecord` helper —
|
|
45379
|
+
* a cast-free boundary narrowing.
|
|
45380
|
+
*/
|
|
45381
|
+
function toRecord(value) {
|
|
45382
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) return Object.fromEntries(Object.entries(value));
|
|
45383
|
+
return {};
|
|
45384
|
+
}
|
|
45385
|
+
/** Coerce a loose settings blob through the connection schema, applying defaults.
|
|
45386
|
+
* A blob with no `transport` defaults to `local` for back-compat with simple forms. */
|
|
45387
|
+
function settingsToEcowittConfig(settings) {
|
|
45388
|
+
const raw = toRecord(settings);
|
|
45389
|
+
if (raw["transport"] !== "local" && raw["transport"] !== "listener") raw["transport"] = "local";
|
|
45390
|
+
return ecowittConfigSchema.parse(raw);
|
|
45391
|
+
}
|
|
45392
|
+
/**
|
|
45393
|
+
* Persisted config for an Ecowitt gateway {@link import('@camstack/types').DeviceType.Hub}
|
|
45394
|
+
* device. The operator-supplied CONNECTION lives directly on the device (Reolink
|
|
45395
|
+
* pattern) — there is no broker registry. The gateway owns its own live nodewitt
|
|
45396
|
+
* client keyed on its own `stableId`; group Container children reference it via
|
|
45397
|
+
* their `gatewayId` config field.
|
|
45398
|
+
*/
|
|
45399
|
+
var ecowittGatewaySchema = object({
|
|
45400
|
+
/** The transport + endpoint the gateway's live client dials / binds. */
|
|
45401
|
+
connection: ecowittConfigSchema,
|
|
45402
|
+
/** Provenance marker used to scope offline / cascade helpers. */
|
|
45403
|
+
system: literal("ecowitt-gateway").optional()
|
|
45404
|
+
});
|
|
45405
|
+
/**
|
|
45406
|
+
* Hand-written connection form for the gateway device-creation UI (standalone
|
|
45407
|
+
* mode — Reolink pattern). The connection lives on the gateway DEVICE config,
|
|
45408
|
+
* not in a broker registry. A `name` field is included so the operator names
|
|
45409
|
+
* the gateway Hub at creation time (mirrors the Reolink creation form).
|
|
45410
|
+
*/
|
|
45411
|
+
function buildConnectionFormSchema() {
|
|
45412
|
+
return { sections: [
|
|
45413
|
+
{
|
|
45414
|
+
id: "identity",
|
|
45415
|
+
title: "Gateway",
|
|
45416
|
+
columns: 1,
|
|
45417
|
+
fields: [{
|
|
45418
|
+
type: "text",
|
|
45419
|
+
key: "name",
|
|
45420
|
+
label: "Name",
|
|
45421
|
+
required: true,
|
|
45422
|
+
placeholder: "Weather station"
|
|
45423
|
+
}]
|
|
45424
|
+
},
|
|
45425
|
+
{
|
|
45426
|
+
id: "transport",
|
|
45427
|
+
title: "Transport",
|
|
45428
|
+
description: "Choose how CamStack reads the gateway. Local poll connects out to the gateway (recommended). Push listener runs a server the gateway uploads to — configure the gateway \"Customized\" upload to point at this hub.",
|
|
45429
|
+
columns: 1,
|
|
45430
|
+
fields: [{
|
|
45431
|
+
type: "select",
|
|
45432
|
+
key: "transport",
|
|
45433
|
+
label: "Transport",
|
|
45434
|
+
default: "local",
|
|
45435
|
+
options: [{
|
|
45436
|
+
value: "local",
|
|
45437
|
+
label: "Local poll (recommended)"
|
|
45438
|
+
}, {
|
|
45439
|
+
value: "listener",
|
|
45440
|
+
label: "Push listener"
|
|
45441
|
+
}]
|
|
45442
|
+
}]
|
|
45443
|
+
},
|
|
45444
|
+
{
|
|
45445
|
+
id: "local",
|
|
45446
|
+
title: "Local poll settings",
|
|
45447
|
+
columns: 2,
|
|
45448
|
+
fields: [
|
|
45449
|
+
{
|
|
45450
|
+
type: "text",
|
|
45451
|
+
key: "host",
|
|
45452
|
+
label: "Gateway host / IP",
|
|
45453
|
+
placeholder: "192.168.1.50",
|
|
45454
|
+
showWhen: {
|
|
45455
|
+
field: "transport",
|
|
45456
|
+
equals: "local"
|
|
45457
|
+
}
|
|
45458
|
+
},
|
|
45459
|
+
{
|
|
45460
|
+
type: "number",
|
|
45461
|
+
key: "port",
|
|
45462
|
+
label: "HTTP port",
|
|
45463
|
+
min: 1,
|
|
45464
|
+
max: 65535,
|
|
45465
|
+
default: 80,
|
|
45466
|
+
showWhen: {
|
|
45467
|
+
field: "transport",
|
|
45468
|
+
equals: "local"
|
|
45469
|
+
}
|
|
45470
|
+
},
|
|
45471
|
+
{
|
|
45472
|
+
type: "password",
|
|
45473
|
+
key: "password",
|
|
45474
|
+
label: "Password (optional)",
|
|
45475
|
+
showToggle: true,
|
|
45476
|
+
showWhen: {
|
|
45477
|
+
field: "transport",
|
|
45478
|
+
equals: "local"
|
|
45479
|
+
}
|
|
45480
|
+
},
|
|
45481
|
+
{
|
|
45482
|
+
type: "number",
|
|
45483
|
+
key: "pollIntervalMs",
|
|
45484
|
+
label: "Poll interval (ms)",
|
|
45485
|
+
min: 1e3,
|
|
45486
|
+
max: 36e5,
|
|
45487
|
+
default: 6e4,
|
|
45488
|
+
showWhen: {
|
|
45489
|
+
field: "transport",
|
|
45490
|
+
equals: "local"
|
|
45491
|
+
}
|
|
45492
|
+
}
|
|
45493
|
+
]
|
|
45494
|
+
},
|
|
45495
|
+
{
|
|
45496
|
+
id: "listener",
|
|
45497
|
+
title: "Push listener settings",
|
|
45498
|
+
columns: 2,
|
|
45499
|
+
fields: [{
|
|
45500
|
+
type: "number",
|
|
45501
|
+
key: "listenPort",
|
|
45502
|
+
label: "Listen port",
|
|
45503
|
+
min: 1,
|
|
45504
|
+
max: 65535,
|
|
45505
|
+
default: 4199,
|
|
45506
|
+
showWhen: {
|
|
45507
|
+
field: "transport",
|
|
45508
|
+
equals: "listener"
|
|
45509
|
+
}
|
|
45510
|
+
}, {
|
|
45511
|
+
type: "text",
|
|
45512
|
+
key: "listenHost",
|
|
45513
|
+
label: "Bind host (optional)",
|
|
45514
|
+
placeholder: "all interfaces",
|
|
45515
|
+
showWhen: {
|
|
45516
|
+
field: "transport",
|
|
45517
|
+
equals: "listener"
|
|
45518
|
+
}
|
|
45519
|
+
}]
|
|
45520
|
+
}
|
|
45521
|
+
] };
|
|
45522
|
+
}
|
|
45523
|
+
/**
|
|
45524
|
+
* Slugify a host / station id into a readable, stable row-key fragment. Exported so the discovery
|
|
45525
|
+
* candidate's `stableId` matches exactly what `generateStableId` derives on adopt (else a re-scan of
|
|
45526
|
+
* an already-added gateway would not be detected as onboarded).
|
|
45527
|
+
*/
|
|
45528
|
+
function slugifyHost(value) {
|
|
45529
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
45530
|
+
}
|
|
45531
|
+
/**
|
|
45532
|
+
* Extra per-scan inputs for Ecowitt network discovery. Mirrors Gree's scan form. On a flat network,
|
|
45533
|
+
* leave blank to sweep the addon node's local subnet. Gateways on ANOTHER subnet usually can't be
|
|
45534
|
+
* reached by a directed broadcast (most routers don't forward it) — enter the gateway's IP instead:
|
|
45535
|
+
* the `CMD_BROADCAST` probe answers a unicast just as well and returns the gateway's identity.
|
|
45536
|
+
*/
|
|
45537
|
+
function buildDiscoveryParamsFormSchema() {
|
|
45538
|
+
return { sections: [{
|
|
45539
|
+
id: "scan",
|
|
45540
|
+
title: "Scan options",
|
|
45541
|
+
description: "Leave empty to scan the local subnet. To find a gateway on a different subnet, enter that subnet’s broadcast address (e.g. 192.168.20.255) or the gateway’s IP directly (e.g. 192.168.20.181).",
|
|
45542
|
+
columns: 1,
|
|
45543
|
+
fields: [{
|
|
45544
|
+
type: "text",
|
|
45545
|
+
key: "broadcastAddress",
|
|
45546
|
+
label: "Broadcast address or gateway IP",
|
|
45547
|
+
required: false,
|
|
45548
|
+
placeholder: "192.168.20.255"
|
|
45549
|
+
}, {
|
|
45550
|
+
type: "number",
|
|
45551
|
+
key: "timeoutMs",
|
|
45552
|
+
label: "UDP timeout (ms)",
|
|
45553
|
+
min: 500,
|
|
45554
|
+
max: 3e4,
|
|
45555
|
+
default: 3e3
|
|
45556
|
+
}]
|
|
45557
|
+
}] };
|
|
45558
|
+
}
|
|
45559
|
+
//#endregion
|
|
45305
45560
|
//#region src/ecowitt-integration-manager.ts
|
|
45306
45561
|
/** Build the real nodewitt facade for a transport. Pure. */
|
|
45307
45562
|
function defaultEcowittFacadeFactory(config) {
|
|
@@ -45458,6 +45713,15 @@ var EcowittIntegrationManager = class {
|
|
|
45458
45713
|
*/
|
|
45459
45714
|
var EcowittFacadeResolver = class {
|
|
45460
45715
|
#facades = /* @__PURE__ */ new Map();
|
|
45716
|
+
/**
|
|
45717
|
+
* Per-gateway snapshot fan-out. Container + sensor children subscribe here
|
|
45718
|
+
* instead of attaching directly to the nodewitt facade's EventEmitter — a
|
|
45719
|
+
* station with >10 sensors would otherwise trip Node's default MaxListeners
|
|
45720
|
+
* cap (one listener per child on a single emitter). The gateway device pumps
|
|
45721
|
+
* this bus once per snapshot from its single facade subscription; a plain
|
|
45722
|
+
* `Set` of callbacks has no listener limit.
|
|
45723
|
+
*/
|
|
45724
|
+
#snapshotSubs = /* @__PURE__ */ new Map();
|
|
45461
45725
|
/** Publish or remove the client for a gateway id. `null` removes the entry. */
|
|
45462
45726
|
set(gatewayId, facade) {
|
|
45463
45727
|
if (facade === null) {
|
|
@@ -45466,6 +45730,28 @@ var EcowittFacadeResolver = class {
|
|
|
45466
45730
|
}
|
|
45467
45731
|
this.#facades.set(gatewayId, facade);
|
|
45468
45732
|
}
|
|
45733
|
+
/** Subscribe a child to per-gateway snapshot ticks. Returns an unsubscribe fn. */
|
|
45734
|
+
onSnapshot(gatewayId, cb) {
|
|
45735
|
+
let set = this.#snapshotSubs.get(gatewayId);
|
|
45736
|
+
if (!set) {
|
|
45737
|
+
set = /* @__PURE__ */ new Set();
|
|
45738
|
+
this.#snapshotSubs.set(gatewayId, set);
|
|
45739
|
+
}
|
|
45740
|
+
set.add(cb);
|
|
45741
|
+
return () => {
|
|
45742
|
+
const current = this.#snapshotSubs.get(gatewayId);
|
|
45743
|
+
current?.delete(cb);
|
|
45744
|
+
if (current && current.size === 0) this.#snapshotSubs.delete(gatewayId);
|
|
45745
|
+
};
|
|
45746
|
+
}
|
|
45747
|
+
/** Fan a single facade snapshot out to every subscribed child for a gateway. */
|
|
45748
|
+
emitSnapshot(gatewayId) {
|
|
45749
|
+
const set = this.#snapshotSubs.get(gatewayId);
|
|
45750
|
+
if (!set) return;
|
|
45751
|
+
for (const cb of set) try {
|
|
45752
|
+
cb();
|
|
45753
|
+
} catch {}
|
|
45754
|
+
}
|
|
45469
45755
|
/** The live client for a gateway id, or null when unknown / stopped. */
|
|
45470
45756
|
get(gatewayId) {
|
|
45471
45757
|
return this.#facades.get(gatewayId) ?? null;
|
|
@@ -45481,6 +45767,7 @@ var EcowittFacadeResolver = class {
|
|
|
45481
45767
|
/** Remove all registered clients (called on full shutdown). */
|
|
45482
45768
|
clear() {
|
|
45483
45769
|
this.#facades.clear();
|
|
45770
|
+
this.#snapshotSubs.clear();
|
|
45484
45771
|
}
|
|
45485
45772
|
};
|
|
45486
45773
|
/** The single in-process per-gateway client resolver shared between the gateway
|
|
@@ -45693,13 +45980,14 @@ function deviceGroupForSensor(category, channel) {
|
|
|
45693
45980
|
//#endregion
|
|
45694
45981
|
//#region src/ecowitt-discovery.ts
|
|
45695
45982
|
/**
|
|
45696
|
-
* The native id a group Container candidate is keyed on:
|
|
45697
|
-
* `
|
|
45698
|
-
*
|
|
45699
|
-
* id
|
|
45983
|
+
* The native id a group Container candidate is keyed on: `<gatewayId>:<group>`.
|
|
45984
|
+
* `gatewayId` is the gateway Hub's stableId, which ALREADY carries the `ecowitt:`
|
|
45985
|
+
* prefix (see `addon.ts` generateStableId) — so this must NOT re-add it, else the
|
|
45986
|
+
* id double-prefixes to `ecowitt:ecowitt:<gw>:<group>`. One physical gateway
|
|
45987
|
+
* yields THREE candidates (one per {@link EcowittDeviceGroup}).
|
|
45700
45988
|
*/
|
|
45701
45989
|
function groupNativeId(gatewayId, group) {
|
|
45702
|
-
return
|
|
45990
|
+
return `${gatewayId}:${group}`;
|
|
45703
45991
|
}
|
|
45704
45992
|
/** A preview accessory child for one sensor. */
|
|
45705
45993
|
function sensorChild(sensor) {
|
|
@@ -45836,9 +46124,6 @@ var EcowittSensorDevice = class extends BaseDevice {
|
|
|
45836
46124
|
system: "ecowitt"
|
|
45837
46125
|
});
|
|
45838
46126
|
}
|
|
45839
|
-
get facade() {
|
|
45840
|
-
return ecowittFacades.get(this.gatewayId);
|
|
45841
|
-
}
|
|
45842
46127
|
resolveSensor() {
|
|
45843
46128
|
return ecowittFacades.getSensor(this.gatewayId, this.sensorId);
|
|
45844
46129
|
}
|
|
@@ -45857,13 +46142,12 @@ var EcowittSensorDevice = class extends BaseDevice {
|
|
|
45857
46142
|
this.snapshotUnsub = null;
|
|
45858
46143
|
}
|
|
45859
46144
|
}
|
|
45860
|
-
/**
|
|
46145
|
+
/** Subscribe to per-gateway snapshot ticks (recompute this sensor's slice).
|
|
46146
|
+
* Uses the resolver's shared fan-out bus rather than the facade EventEmitter
|
|
46147
|
+
* directly, so N sensors on one gateway never trip Node's MaxListeners cap.
|
|
46148
|
+
* Works even before the facade exists — `recomputeSlice` no-ops on a missing
|
|
46149
|
+
* sensor and replays once the gateway pumps the first snapshot. */
|
|
45861
46150
|
attachSnapshotListener() {
|
|
45862
|
-
const facade = this.facade;
|
|
45863
|
-
if (!facade) {
|
|
45864
|
-
this.ctx.logger.debug("EcowittSensorDevice: facade not present; no live listener yet", { meta: { sensorId: this.sensorId } });
|
|
45865
|
-
return;
|
|
45866
|
-
}
|
|
45867
46151
|
const onSnapshot = () => {
|
|
45868
46152
|
try {
|
|
45869
46153
|
this.recomputeSlice();
|
|
@@ -45874,10 +46158,7 @@ var EcowittSensorDevice = class extends BaseDevice {
|
|
|
45874
46158
|
} });
|
|
45875
46159
|
}
|
|
45876
46160
|
};
|
|
45877
|
-
|
|
45878
|
-
this.snapshotUnsub = () => {
|
|
45879
|
-
facade.off("snapshot", onSnapshot);
|
|
45880
|
-
};
|
|
46161
|
+
this.snapshotUnsub = ecowittFacades.onSnapshot(this.gatewayId, onSnapshot);
|
|
45881
46162
|
}
|
|
45882
46163
|
/** Recompute + write this sensor's typed cap slice (+ battery) from the live reading. */
|
|
45883
46164
|
recomputeSlice() {
|
|
@@ -46223,6 +46504,7 @@ var EcowittGatewayDevice = class extends BaseDevice {
|
|
|
46223
46504
|
this.reconcileGroupSensors(sensors).catch((err) => {
|
|
46224
46505
|
this.ctx.logger.warn("Ecowitt gateway: sensor reconcile failed", { meta: { error: errMsg(err) } });
|
|
46225
46506
|
});
|
|
46507
|
+
ecowittFacades.emitSnapshot(this.stableId);
|
|
46226
46508
|
}
|
|
46227
46509
|
registerDeviceDiscovery() {
|
|
46228
46510
|
this.ctx.registerNativeCap(deviceDiscoveryCapability, {
|
|
@@ -46429,7 +46711,43 @@ var EcowittProviderAddon = class extends BaseDeviceProvider {
|
|
|
46429
46711
|
super({});
|
|
46430
46712
|
}
|
|
46431
46713
|
async supportsDiscovery() {
|
|
46432
|
-
return
|
|
46714
|
+
return true;
|
|
46715
|
+
}
|
|
46716
|
+
async getDiscoveryParamsSchema() {
|
|
46717
|
+
return buildDiscoveryParamsFormSchema();
|
|
46718
|
+
}
|
|
46719
|
+
async discoverDevices(input) {
|
|
46720
|
+
const broadcastAddr = typeof input?.params?.["broadcastAddress"] === "string" ? input.params["broadcastAddress"].trim() : "";
|
|
46721
|
+
const timeoutMs = typeof input?.params?.["timeoutMs"] === "number" ? input.params["timeoutMs"] : void 0;
|
|
46722
|
+
const gateways = await Ecowitt.discover({
|
|
46723
|
+
...broadcastAddr.length > 0 ? { broadcastAddr } : {},
|
|
46724
|
+
...timeoutMs !== void 0 ? { timeoutMs } : {}
|
|
46725
|
+
});
|
|
46726
|
+
this.ctx.logger.info("Ecowitt discovery complete", { meta: {
|
|
46727
|
+
count: gateways.length,
|
|
46728
|
+
broadcastAddr: broadcastAddr.length > 0 ? broadcastAddr : "local"
|
|
46729
|
+
} });
|
|
46730
|
+
return gateways.map((g) => {
|
|
46731
|
+
const displayName = g.model ?? g.name;
|
|
46732
|
+
return {
|
|
46733
|
+
stableId: `ecowitt:${slugifyHost(g.ip)}`,
|
|
46734
|
+
type: DeviceType.Hub,
|
|
46735
|
+
suggestedName: displayName,
|
|
46736
|
+
prefilledConfig: {
|
|
46737
|
+
name: displayName,
|
|
46738
|
+
connection: {
|
|
46739
|
+
transport: "local",
|
|
46740
|
+
host: g.ip
|
|
46741
|
+
}
|
|
46742
|
+
}
|
|
46743
|
+
};
|
|
46744
|
+
});
|
|
46745
|
+
}
|
|
46746
|
+
async adoptDiscoveredDevice(input) {
|
|
46747
|
+
return this.createDevice({
|
|
46748
|
+
type: DeviceType.Hub,
|
|
46749
|
+
config: input.candidate.prefilledConfig
|
|
46750
|
+
});
|
|
46433
46751
|
}
|
|
46434
46752
|
async supportsManualCreation() {
|
|
46435
46753
|
return true;
|
|
@@ -46486,10 +46804,10 @@ var EcowittProviderAddon = class extends BaseDeviceProvider {
|
|
|
46486
46804
|
generateStableId(_type, config) {
|
|
46487
46805
|
const connection = extractConnection(config);
|
|
46488
46806
|
const station = typeof connection?.stationId === "string" ? connection.stationId.trim() : "";
|
|
46489
|
-
if (station.length > 0) return `ecowitt:${
|
|
46807
|
+
if (station.length > 0) return `ecowitt:${slugifyHost(station)}`;
|
|
46490
46808
|
if (connection?.transport === "local") {
|
|
46491
46809
|
const host = connection.host.trim();
|
|
46492
|
-
if (host.length > 0) return `ecowitt:${
|
|
46810
|
+
if (host.length > 0) return `ecowitt:${slugifyHost(host)}`;
|
|
46493
46811
|
}
|
|
46494
46812
|
if (connection?.transport === "listener") return `ecowitt:listen-${connection.listenPort}`;
|
|
46495
46813
|
throw new Error("Ecowitt: gateway resolved neither a station id nor a host/listen endpoint — cannot persist a stable row key.");
|
|
@@ -46503,10 +46821,6 @@ function extractConnection(config) {
|
|
|
46503
46821
|
if (transport !== "local" && transport !== "listener") return null;
|
|
46504
46822
|
return settingsToEcowittConfig(raw);
|
|
46505
46823
|
}
|
|
46506
|
-
/** Flatten a host / station id into a readable row-key slug. */
|
|
46507
|
-
function slug(value) {
|
|
46508
|
-
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
46509
|
-
}
|
|
46510
46824
|
//#endregion
|
|
46511
46825
|
exports.ECOWITT_DEVICE_GROUPS = ECOWITT_DEVICE_GROUPS;
|
|
46512
46826
|
exports.EcowittProviderAddon = EcowittProviderAddon;
|