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