@camstack/addon-provider-ecowitt 0.2.152 → 0.2.154

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.mjs CHANGED
@@ -5833,7 +5833,7 @@ var ZodIssueCode$1 = {
5833
5833
  var ZodFirstPartyTypeKind$1;
5834
5834
  ZodFirstPartyTypeKind$1 || (ZodFirstPartyTypeKind$1 = {});
5835
5835
  //#endregion
5836
- //#region ../types/dist/sleep-BHS4ImRX.mjs
5836
+ //#region ../types/dist/sleep-CSYuxssh.mjs
5837
5837
  /**
5838
5838
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5839
5839
  * window to float samples (D455).
@@ -9163,7 +9163,7 @@ var ProfileRtspEntrySchema = object({
9163
9163
  /**
9164
9164
  * The model weight formats a catalog entry can ship. Schema-free on purpose:
9165
9165
  * `types/models.ts` builds Zod schemas from it, but a runner-closure consumer
9166
- * (the model downloader in `@camstack/system`) needs only the list and must
9166
+ * (the model downloader in `@camstack/server/system`) needs only the list and must
9167
9167
  * reach it through `@camstack/types/addon` without building the cap schema
9168
9168
  * graph (D28).
9169
9169
  */
@@ -12552,6 +12552,71 @@ var EventOwnerTypeSchema = _enum(EVENT_OWNER_TYPES);
12552
12552
  var MediaPresenceOwnerKindSchema = _enum([...EVENT_OWNER_TYPES, "track"]);
12553
12553
  new Set(EVENT_OWNER_TYPES);
12554
12554
  /**
12555
+ * What a published package needs from the rest of the install (D989).
12556
+ *
12557
+ * One published framework package (`@camstack/server`) carries the kernel, the
12558
+ * builtins and the capability contracts, so "which addon runs on which
12559
+ * framework" is a question of version, and the answer is DECLARED by the
12560
+ * package that knows it:
12561
+ *
12562
+ * - an addon says the oldest server it runs on (`requires.server: ">=X"`);
12563
+ * - the server says the oldest addon it works with
12564
+ * (`requires.addons: { "@camstack/addon-x": ">=Y" }`) and the oldest shell
12565
+ * around it (`requires.wrapper: { desktop, image }`);
12566
+ * - a wrapper (the desktop app, the container starter) says which server
12567
+ * range it can host, and reports its own kind and version.
12568
+ *
12569
+ * The publish workflow STAMPS these after the version bump
12570
+ * (`scripts/ci/stamp-requires.mjs`); nobody types a number by hand.
12571
+ *
12572
+ * A requirement that cannot be checked is NOT met: a shell that reports no
12573
+ * version counts as below the minimum, and an unreadable running version is
12574
+ * named in the result (a failed read is never a pass).
12575
+ */
12576
+ /** `>=MAJOR.MINOR.PATCH[-prerelease]` — the only range shape a requirement uses. */
12577
+ var MinVersionSchema = string().regex(/^>=\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/, "a minimum is written \">=X.Y.Z\"");
12578
+ object({
12579
+ /** The oldest `@camstack/server` this package runs on. */
12580
+ server: MinVersionSchema.optional(),
12581
+ /** The oldest installed version of each named addon package. */
12582
+ addons: record(string(), MinVersionSchema).optional(),
12583
+ /** The oldest shell around the server, per shell kind. */
12584
+ wrapper: object({
12585
+ desktop: MinVersionSchema.optional(),
12586
+ image: MinVersionSchema.optional()
12587
+ }).optional()
12588
+ });
12589
+ /** One unmet requirement; each names the versions on both sides. */
12590
+ var UnmetRequirementSchema = discriminatedUnion("kind", [
12591
+ object({
12592
+ kind: literal("server-too-old"),
12593
+ required: string(),
12594
+ /** `null` — the running server version could not be read. */
12595
+ running: string().nullable()
12596
+ }),
12597
+ object({
12598
+ kind: literal("addon-too-old"),
12599
+ packageName: string(),
12600
+ required: string(),
12601
+ /** `null` — the addon is not installed (or its version is unreadable). */
12602
+ running: string().nullable()
12603
+ }),
12604
+ object({
12605
+ kind: literal("wrapper-too-old"),
12606
+ wrapper: _enum(["desktop", "image"]),
12607
+ required: string(),
12608
+ /** `null` — the wrapper reported no version. */
12609
+ running: string().nullable()
12610
+ })
12611
+ ]);
12612
+ var RequiredAddonUpdateSchema = object({
12613
+ packageName: string(),
12614
+ /** The minimum the target server declares. */
12615
+ required: string(),
12616
+ /** What this node has; `null` when the addon is not installed. */
12617
+ installed: string().nullable()
12618
+ });
12619
+ /**
12555
12620
  * Which integration instance a settings form is for. An addon that serves several instances
12556
12621
  * (`instanceMode: multiple`) can show a different form per instance — an NVR integration
12557
12622
  * needs its connection, the default integration needs the addon's own sections.
@@ -40826,7 +40891,8 @@ function normalizeUnit(raw) {
40826
40891
  if (lower === "dbm") return "dBm";
40827
40892
  if (lower === "dbfs") return "dBFS";
40828
40893
  if (lower === "inhg") return "inHg";
40829
- if (lower === "mm/h") return "mm/h";
40894
+ if (lower === "mm/h" || lower === "mm/hr") return "mm/h";
40895
+ if (lower === "in/h" || lower === "in/hr") return "in/h";
40830
40896
  if (lower === "w/m²" || lower === "w/m2") return "W/m²";
40831
40897
  if (lower === "°" || lower === "deg" || lower === "degree" || lower === "degrees") return "°";
40832
40898
  if (lower === "kbps") return "kbps";
@@ -43045,7 +43111,7 @@ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSche
43045
43111
  * rebuilds without manual reload.
43046
43112
  *
43047
43113
  * The hub-local builtin `addon-pages-aggregator` (see
43048
- * `@camstack/system/builtins/addon-pages-aggregator`) registers the
43114
+ * `@camstack/server/system/builtins/addon-pages-aggregator`) registers the
43049
43115
  * provider. Splitting the public aggregator from the raw collection
43050
43116
  * keeps both ends in codegen — there's no hand-written
43051
43117
  * `addon-pages.router.ts` wrapper anymore.
@@ -43345,7 +43411,7 @@ method(_void(), array(WidgetMetadataSchema).readonly(), { auth: "admin" });
43345
43411
  * manual reload — same scheme used by `addon-pages`.
43346
43412
  *
43347
43413
  * The hub-local builtin `addon-widgets-aggregator` (see
43348
- * `@camstack/system/builtins/addon-widgets-aggregator`) registers the
43414
+ * `@camstack/server/system/builtins/addon-widgets-aggregator`) registers the
43349
43415
  * provider. Splitting the public aggregator from the raw collection
43350
43416
  * keeps both ends in codegen — there's no hand-written wrapper.
43351
43417
  */
@@ -61545,7 +61611,18 @@ var ServerUpdateActionResultSchema = object({
61545
61611
  * store (D877), present when the pre-flight refused the update
61546
61612
  * (`reason: 'addon-would-refuse'`). Optional for version skew.
61547
61613
  */
61548
- blockers: array(UpdateBlockerSchema).optional()
61614
+ blockers: array(UpdateBlockerSchema).optional(),
61615
+ /**
61616
+ * The minimums the target server declares and this node does not meet
61617
+ * (D989), present when the update was refused with
61618
+ * `reason: 'requirements-unmet'`. Each names the versions on both sides.
61619
+ */
61620
+ unmet: array(UnmetRequirementSchema).optional(),
61621
+ /**
61622
+ * The addons this node has below the minimum the staged server declares —
61623
+ * the ones to update WITH the server. Present on an accepted update.
61624
+ */
61625
+ requiredAddonUpdates: array(RequiredAddonUpdateSchema).optional()
61549
61626
  });
61550
61627
  var LaunchOverridesSchema = object({
61551
61628
  /** The persisted heap ceiling in MB, or `null` when none is set. */
@@ -64089,7 +64166,9 @@ var PackageUpdateSchema = object({
64089
64166
  latestVersion: string(),
64090
64167
  category: _enum(["addon", "core"]),
64091
64168
  requiresRestart: boolean(),
64092
- isSystem: boolean()
64169
+ isSystem: boolean(),
64170
+ /** Held: what the newer version needs and this node does not meet (D989). */
64171
+ blockedBy: array(UnmetRequirementSchema).readonly().optional()
64093
64172
  });
64094
64173
  var PackageVersionInfoSchema = object({
64095
64174
  version: string(),
@@ -69372,7 +69451,7 @@ var lawnMowerControlCapability = {
69372
69451
  * clients — they reverse-connect to the hub. Exposing their interfaces
69373
69452
  * via the same surface would leak internal topology with no upside.
69374
69453
  *
69375
- * Implementation in `@camstack/system/builtins/local-network/`.
69454
+ * Implementation in `@camstack/server/system/builtins/local-network/`.
69376
69455
  */
69377
69456
  /** Coarse classification derived from the interface name + IP range. */
69378
69457
  var InterfaceKindEnum = _enum([
@@ -71260,7 +71339,7 @@ onStatusChanged: { data: object({
71260
71339
  * jitter, and observed/peak bandwidth per device + per client.
71261
71340
  *
71262
71341
  * Implementation lives in the server's `NetworkQualityService` (thin
71263
- * wrapper over the shared `NetworkQualityTracker` from `@camstack/system`).
71342
+ * wrapper over the shared `NetworkQualityTracker` from `@camstack/server/system`).
71264
71343
  * The provider is registered from `trpc.router.ts` against the existing
71265
71344
  * service instance — no addon owns this state.
71266
71345
  *
@@ -73411,7 +73490,7 @@ var ptzAutotrackCapability = {
73411
73490
  * - **Native**: the camera's own firmware autotrack, registered per device by
73412
73491
  * the driver when the probe says it exists (Reolink: yes; Hikvision:
73413
73492
  * model-dependent).
73414
- * - **Wrapper**: the emulated implementation (`@camstack/system`'s `autotrack` builtin),
73493
+ * - **Wrapper**: the emulated implementation (`@camstack/server/system`'s `autotrack` builtin),
73415
73494
  * which follows a subject with `ptz.move` pulses driven by the detection
73416
73495
  * pipeline. `defaultActive: false` — the operator binds it per camera in
73417
73496
  * the bindings UI.
@@ -95149,6 +95228,73 @@ function deviceGroupForSensor(category, channel) {
95149
95228
  return "gateway";
95150
95229
  }
95151
95230
  //#endregion
95231
+ //#region src/ecowitt-sensor-groups.ts
95232
+ /** Resolve `deviceGroup` from a persisted container config (defaults to 'gateway'). */
95233
+ function configDeviceGroup(config) {
95234
+ const g = config["deviceGroup"];
95235
+ return ECOWITT_DEVICE_GROUPS.find((group) => group === g) ?? "gateway";
95236
+ }
95237
+ /** Build the persisted sensor entry (incl. self-describing category) from a live Sensor. */
95238
+ function sensorToEntry(s) {
95239
+ return {
95240
+ sensorId: s.id,
95241
+ quantity: s.quantity,
95242
+ ...s.channel !== void 0 ? { channel: s.channel } : {},
95243
+ ...s.name !== void 0 ? { name: s.name } : {},
95244
+ ...s.category !== void 0 ? { category: s.category } : {}
95245
+ };
95246
+ }
95247
+ /** Bucket live sensors into their device group. */
95248
+ function bucketSensorsByGroup(sensors) {
95249
+ const byGroup = /* @__PURE__ */ new Map();
95250
+ for (const group of ECOWITT_DEVICE_GROUPS) byGroup.set(group, []);
95251
+ for (const s of sensors) {
95252
+ const group = deviceGroupForSensor(s.category, s.channel);
95253
+ byGroup.get(group)?.push(sensorToEntry(s));
95254
+ }
95255
+ return byGroup;
95256
+ }
95257
+ /** Structural equality of two persisted sensor lists (order-independent by id + name). */
95258
+ function sensorsEqual(a, b) {
95259
+ if (a.length !== b.length) return false;
95260
+ const aById = /* @__PURE__ */ new Map();
95261
+ for (const entry of a) if (typeof entry === "object" && entry !== null && "sensorId" in entry) {
95262
+ const id = entry.sensorId;
95263
+ if (typeof id === "string") {
95264
+ const name = "name" in entry && typeof entry.name === "string" ? entry.name : void 0;
95265
+ aById.set(id, name);
95266
+ }
95267
+ }
95268
+ for (const entry of b) {
95269
+ if (!aById.has(entry.sensorId)) return false;
95270
+ if (aById.get(entry.sensorId) !== entry.name) return false;
95271
+ }
95272
+ return true;
95273
+ }
95274
+ /** The measurement key of a sensor id: the part after the first ':' (`8E:0x02` gives `0x02`). */
95275
+ function sensorKeyOf(sensorId) {
95276
+ const i = sensorId.indexOf(":");
95277
+ return i < 0 ? sensorId : sensorId.slice(i + 1);
95278
+ }
95279
+ /** The owner prefix of a sensor id (`8E` of `8E:0x02`, `gateway` for a gateway-owned reading). */
95280
+ function sensorOwnerOf(sensorId) {
95281
+ const i = sensorId.indexOf(":");
95282
+ return i < 0 ? "" : sensorId.slice(0, i);
95283
+ }
95284
+ /**
95285
+ * The persisted entries a Container of `group` may spawn as children. An early snapshot had no
95286
+ * model, so nodewitt called a weather-station reading `gateway:<key>`; once the sensor map is
95287
+ * known the same reading is `<hardwareId>:<key>`. An entry is dropped when its live sensor (the
95288
+ * exact id, or for a `gateway:` id the hardware-owned sensor of the same key) belongs to another
95289
+ * group. An entry the live set does not know is kept: unknown is not gone (D49).
95290
+ */
95291
+ function entriesForGroup(entries, group, live) {
95292
+ return entries.filter((entry) => {
95293
+ const sensor = live.find((s) => s.id === entry.sensorId) ?? (sensorOwnerOf(entry.sensorId) === "gateway" ? live.find((s) => sensorOwnerOf(s.id) !== "gateway" && sensorKeyOf(s.id) === sensorKeyOf(entry.sensorId)) : void 0);
95294
+ return sensor === void 0 || deviceGroupForSensor(sensor.category, sensor.channel) === group;
95295
+ });
95296
+ }
95297
+ //#endregion
95152
95298
  //#region src/devices/ecowitt-sensor-device.ts
95153
95299
  /**
95154
95300
  * Persisted config every Ecowitt sensor child carries. `sensorId` resolves the
@@ -95486,7 +95632,7 @@ var EcowittContainerDevice = class extends BaseDevice {
95486
95632
  });
95487
95633
  }
95488
95634
  getAccessoryChildren() {
95489
- return this.config.values.sensors.map((sensor) => {
95635
+ return entriesForGroup(this.config.values.sensors, this.config.values.deviceGroup, ecowittFacades.getSensors(this.gatewayId)).map((sensor) => {
95490
95636
  const meta = {
95491
95637
  type: DeviceType.Sensor,
95492
95638
  role: roleForQuantity(sensor.quantity),
@@ -99333,7 +99479,7 @@ function parseValue(input) {
99333
99479
  raw
99334
99480
  };
99335
99481
  }
99336
- function str(item, field) {
99482
+ function str$1(item, field) {
99337
99483
  const v = item[field];
99338
99484
  if (typeof v === "string") return v;
99339
99485
  if (typeof v === "number") return String(v);
@@ -99373,7 +99519,7 @@ function makeReading(key, val, unit, forceId) {
99373
99519
  }
99374
99520
  }
99375
99521
  function channelOf(item) {
99376
- const ch = str(item, "channel");
99522
+ const ch = str$1(item, "channel");
99377
99523
  return ch && ch.trim() !== "" ? ch.trim() : void 0;
99378
99524
  }
99379
99525
  function push(out, reading) {
@@ -99383,8 +99529,8 @@ function decodeChSoil(items, out) {
99383
99529
  for (const item of items) {
99384
99530
  const ch = channelOf(item);
99385
99531
  if (ch === void 0) continue;
99386
- const humidity = stripPercent(str(item, "humidity"));
99387
- const battery = str(item, "battery");
99532
+ const humidity = stripPercent(str$1(item, "humidity"));
99533
+ const battery = str$1(item, "battery");
99388
99534
  if (humidity) push(out, makeReading(`soilmoisture${ch}`, humidity));
99389
99535
  if (battery) push(out, makeReading(`soilbatt${ch}`, batteryPercent(battery, true)));
99390
99536
  }
@@ -99393,11 +99539,11 @@ function decodeChEc(items, out) {
99393
99539
  for (const item of items) {
99394
99540
  const ch = channelOf(item);
99395
99541
  if (ch === void 0) continue;
99396
- const humidity = stripPercent(str(item, "humidity"));
99397
- const temp = str(item, "temp");
99398
- const unit = str(item, "unit") ?? "C";
99399
- const ec = str(item, "ec");
99400
- const battery = str(item, "battery");
99542
+ const humidity = stripPercent(str$1(item, "humidity"));
99543
+ const temp = str$1(item, "temp");
99544
+ const unit = str$1(item, "unit") ?? "C";
99545
+ const ec = str$1(item, "ec");
99546
+ const battery = str$1(item, "battery");
99401
99547
  if (humidity) push(out, makeReading(`soilmoisture${ch}`, humidity));
99402
99548
  if (temp) push(out, makeReading(`soiltemp${ch}`, temp, unit));
99403
99549
  if (ec) push(out, makeReading(`soilec${ch}`, ec));
@@ -99408,9 +99554,9 @@ function decodeChAisle(items, gatewayTempUnit, out) {
99408
99554
  for (const item of items) {
99409
99555
  const ch = channelOf(item);
99410
99556
  if (ch === void 0) continue;
99411
- const temp = str(item, "temp");
99412
- const humidity = str(item, "humidity");
99413
- const battery = str(item, "battery");
99557
+ const temp = str$1(item, "temp");
99558
+ const humidity = str$1(item, "humidity");
99559
+ const battery = str$1(item, "battery");
99414
99560
  if (temp && temp !== "None") push(out, makeReading(`temp${ch}f`, temp, gatewayTempUnit));
99415
99561
  if (humidity && humidity !== "None") push(out, makeReading(`humidity${ch}`, stripPercent(humidity)));
99416
99562
  if (battery && battery !== "None") push(out, makeReading(`batt${ch}`, batteryPercent(battery, true)));
@@ -99420,8 +99566,8 @@ function decodeChTemp(items, gatewayTempUnit, out) {
99420
99566
  for (const item of items) {
99421
99567
  const ch = channelOf(item);
99422
99568
  if (ch === void 0) continue;
99423
- const temp = str(item, "temp");
99424
- const battery = str(item, "battery");
99569
+ const temp = str$1(item, "temp");
99570
+ const battery = str$1(item, "battery");
99425
99571
  if (temp && temp !== "None") push(out, makeReading(`tf_ch${ch}`, temp, gatewayTempUnit));
99426
99572
  if (battery && battery !== "None") push(out, makeReading(`tf_batt${ch}`, batteryPercent(battery, false)));
99427
99573
  }
@@ -99430,11 +99576,11 @@ function decodeChPm25(items, out) {
99430
99576
  for (const item of items) {
99431
99577
  const ch = channelOf(item);
99432
99578
  if (ch === void 0) continue;
99433
- const pm25 = str(item, "pm25") ?? str(item, "PM25");
99434
- const pm25_24h = str(item, "pm25_avg_24h") ?? str(item, "pm25_24h");
99435
- const realaqi = str(item, "PM25_RealAQI");
99436
- const aqi24h = str(item, "PM25_24HAQI");
99437
- const battery = str(item, "battery");
99579
+ const pm25 = str$1(item, "pm25") ?? str$1(item, "PM25");
99580
+ const pm25_24h = str$1(item, "pm25_avg_24h") ?? str$1(item, "pm25_24h");
99581
+ const realaqi = str$1(item, "PM25_RealAQI");
99582
+ const aqi24h = str$1(item, "PM25_24HAQI");
99583
+ const battery = str$1(item, "battery");
99438
99584
  if (pm25 && pm25 !== "None") push(out, makeReading(`pm25_ch${ch}`, pm25));
99439
99585
  if (pm25_24h && pm25_24h !== "None") push(out, makeReading(`pm25_avg_24h_ch${ch}`, pm25_24h));
99440
99586
  if (realaqi && realaqi !== "None") push(out, makeReading(`pm25_aqi_realtime_ch${ch}`, realaqi));
@@ -99446,8 +99592,8 @@ function decodeChLeaf(items, out) {
99446
99592
  for (const item of items) {
99447
99593
  const ch = channelOf(item);
99448
99594
  if (ch === void 0) continue;
99449
- const humidity = stripPercent(str(item, "humidity"));
99450
- const battery = str(item, "battery");
99595
+ const humidity = stripPercent(str$1(item, "humidity"));
99596
+ const battery = str$1(item, "battery");
99451
99597
  if (humidity) push(out, makeReading(`leafwetness_ch${ch}`, humidity));
99452
99598
  if (battery && battery !== "None") push(out, makeReading(`leaf_batt${ch}`, batteryPercent(battery, false)));
99453
99599
  }
@@ -99456,8 +99602,8 @@ function decodeChLeak(items, out) {
99456
99602
  for (const item of items) {
99457
99603
  const ch = channelOf(item);
99458
99604
  if (ch === void 0) continue;
99459
- const status = str(item, "status");
99460
- const battery = str(item, "battery");
99605
+ const status = str$1(item, "status");
99606
+ const battery = str$1(item, "battery");
99461
99607
  if (status !== void 0 && status !== "") {
99462
99608
  const leak = status.trim().toLowerCase() === "normal" ? "0" : "1";
99463
99609
  push(out, makeReading(`leak_ch${ch}`, leak));
@@ -99469,10 +99615,10 @@ function decodeChLds(items, out) {
99469
99615
  for (const item of items) {
99470
99616
  const ch = channelOf(item);
99471
99617
  if (ch === void 0) continue;
99472
- const air = str(item, "air");
99473
- const depth = str(item, "depth");
99474
- const voltage = str(item, "voltage");
99475
- const battery = str(item, "battery");
99618
+ const air = str$1(item, "air");
99619
+ const depth = str$1(item, "depth");
99620
+ const voltage = str$1(item, "voltage");
99621
+ const battery = str$1(item, "battery");
99476
99622
  if (air && air !== "None") push(out, makeReading(`lds_air_ch${ch}`, air));
99477
99623
  if (depth && depth !== "None") push(out, makeReading(`lds_depth_ch${ch}`, depth));
99478
99624
  if (voltage && voltage !== "None") push(out, makeReading(`lds_voltage_ch${ch}`, voltage));
@@ -99501,23 +99647,23 @@ var CO2_FIELDS = [
99501
99647
  ];
99502
99648
  function firstField(item, fields) {
99503
99649
  for (const f of fields) {
99504
- const v = str(item, f);
99650
+ const v = str$1(item, f);
99505
99651
  if (v && v !== "None") return v;
99506
99652
  }
99507
99653
  }
99508
99654
  function decodeCo2(items, out) {
99509
99655
  const item = items[0];
99510
99656
  if (item === void 0) return;
99511
- const temp = str(item, "temp");
99512
- const unit = str(item, "unit") ?? "C";
99657
+ const temp = str$1(item, "temp");
99658
+ const unit = str$1(item, "unit") ?? "C";
99513
99659
  if (temp) push(out, makeReading(unit === "C" ? "tf_co2c" : "tf_co2", temp, unit));
99514
- const humidity = stripPercent(str(item, "humidity"));
99660
+ const humidity = stripPercent(str$1(item, "humidity"));
99515
99661
  if (humidity) push(out, makeReading("humi_co2", humidity));
99516
99662
  for (const [fields, key] of CO2_FIELDS) {
99517
99663
  const v = firstField(item, fields);
99518
99664
  if (v !== void 0) push(out, makeReading(key, v));
99519
99665
  }
99520
- const battery = str(item, "battery");
99666
+ const battery = str$1(item, "battery");
99521
99667
  if (battery && battery !== "None") {
99522
99668
  const n = num(battery);
99523
99669
  push(out, makeReading("co2_batt", n !== void 0 ? String(Math.min(n * 20, 100)) : battery));
@@ -99529,12 +99675,12 @@ function push2(out, reading) {
99529
99675
  }
99530
99676
  function decodeCommonList(items, mapper, out) {
99531
99677
  for (const item of items) {
99532
- const id = str(item, "id");
99533
- const val = str(item, "val");
99534
- const unit = str(item, "unit");
99678
+ const id = str$1(item, "id");
99679
+ const val = str$1(item, "val");
99680
+ const unit = str$1(item, "unit");
99535
99681
  if (id && val !== void 0) push2(out, makeReading(id, val, unit));
99536
99682
  if (id === "0x03" && mapper.getHardwareId("wh26batt") !== void 0) {
99537
- const battery = str(item, "battery");
99683
+ const battery = str$1(item, "battery");
99538
99684
  if (battery !== void 0) push2(out, makeReading("wh26batt", battery === "0" ? "100" : "10"));
99539
99685
  }
99540
99686
  }
@@ -99542,12 +99688,12 @@ function decodeCommonList(items, mapper, out) {
99542
99688
  function decodeRain(items, mapper, out) {
99543
99689
  const rainHwId = mapper.getHardwareId("wh69batt") ?? mapper.getHardwareId("wh40batt");
99544
99690
  for (const item of items) {
99545
- const id = str(item, "id");
99546
- const val = str(item, "val");
99691
+ const id = str$1(item, "id");
99692
+ const val = str$1(item, "val");
99547
99693
  if (!id || val === void 0) continue;
99548
- push2(out, makeReading(id, val, str(item, "unit"), rainHwId));
99694
+ push2(out, makeReading(id, val, str$1(item, "unit"), rainHwId));
99549
99695
  if (id === "0x13") {
99550
- const battery = str(item, "battery");
99696
+ const battery = str$1(item, "battery");
99551
99697
  if (battery) {
99552
99698
  const n = num(battery);
99553
99699
  const pct = n !== void 0 && n > 1 ? String(n * 20) : battery === "0" ? "100" : "10";
@@ -99579,30 +99725,30 @@ function piezoKeysFor(mapper) {
99579
99725
  function decodePiezo(items, mapper, out) {
99580
99726
  const piezoHwId = mapper.getHardwareId("ws85batt") ?? mapper.getHardwareId("ws90batt") ?? mapper.getHardwareId("wh90batt");
99581
99727
  for (const item of items) {
99582
- const id = str(item, "id");
99583
- const val = str(item, "val");
99728
+ const id = str$1(item, "id");
99729
+ const val = str$1(item, "val");
99584
99730
  if (!id || val === void 0) continue;
99585
- push2(out, makeReading(id, val, str(item, "unit"), piezoHwId));
99731
+ push2(out, makeReading(id, val, str$1(item, "unit"), piezoHwId));
99586
99732
  if (id !== "0x13") continue;
99587
- const battery = str(item, "battery");
99733
+ const battery = str$1(item, "battery");
99588
99734
  if (!battery) continue;
99589
99735
  const keys = piezoKeysFor(mapper);
99590
99736
  const n = num(battery);
99591
99737
  push2(out, makeReading(keys.batt, n !== void 0 ? String(n * 20) : battery));
99592
- const voltage = str(item, "voltage");
99738
+ const voltage = str$1(item, "voltage");
99593
99739
  if (voltage) push2(out, makeReading(keys.volt, voltage));
99594
- const cap = str(item, keys.capField);
99740
+ const cap = str$1(item, keys.capField);
99595
99741
  if (cap) push2(out, makeReading(keys.capKey, cap));
99596
99742
  }
99597
99743
  }
99598
99744
  function decodeLightning(items, out) {
99599
99745
  const item = items[0];
99600
99746
  if (item === void 0) return;
99601
- const count = str(item, "count");
99747
+ const count = str$1(item, "count");
99602
99748
  if (count !== void 0) push2(out, makeReading("lightning_num", count));
99603
- const distance = str(item, "distance");
99749
+ const distance = str$1(item, "distance");
99604
99750
  if (distance !== void 0) push2(out, makeReading("lightning", distance.replace(" km", "").trim()));
99605
- const battery = str(item, "battery");
99751
+ const battery = str$1(item, "battery");
99606
99752
  if (battery !== void 0) {
99607
99753
  const n = num(battery);
99608
99754
  push2(out, makeReading("wh57batt", n !== void 0 ? String(n * 20) : battery));
@@ -99611,30 +99757,30 @@ function decodeLightning(items, out) {
99611
99757
  function decodeWh25(items, out) {
99612
99758
  const item = items[0];
99613
99759
  if (item === void 0) return;
99614
- const intemp = str(item, "intemp");
99615
- const unit = str(item, "unit") ?? "F";
99760
+ const intemp = str$1(item, "intemp");
99761
+ const unit = str$1(item, "unit") ?? "F";
99616
99762
  if (intemp !== void 0) push2(out, makeReading("tempinf", intemp, unit));
99617
- const inhumi = str(item, "inhumi");
99763
+ const inhumi = str$1(item, "inhumi");
99618
99764
  if (inhumi !== void 0) push2(out, makeReading("humidityin", stripPercent(inhumi)));
99619
- const abs = str(item, "abs");
99765
+ const abs = str$1(item, "abs");
99620
99766
  if (abs !== void 0) push2(out, makeReading("baromabsin", abs.replace(" hPa", "").trim()));
99621
- const rel = str(item, "rel");
99767
+ const rel = str$1(item, "rel");
99622
99768
  if (rel !== void 0) push2(out, makeReading("baromrelin", rel.replace(" hPa", "").trim()));
99623
99769
  }
99624
99770
  function decodeSoilCalibration(items, out) {
99625
99771
  for (const item of items) {
99626
- const ch = str(item, "ch");
99627
- const nowAd = str(item, "nowAd");
99772
+ const ch = str$1(item, "ch");
99773
+ const nowAd = str$1(item, "nowAd");
99628
99774
  if (ch && nowAd !== void 0) push2(out, makeReading(`soilad${ch}`, nowAd));
99629
99775
  }
99630
99776
  }
99631
99777
  function decodeLdsConfig(items, out) {
99632
99778
  for (const item of items) {
99633
- const ch = str(item, "ch");
99779
+ const ch = str$1(item, "ch");
99634
99780
  if (!ch) continue;
99635
- const level = str(item, "level");
99781
+ const level = str$1(item, "level");
99636
99782
  if (level !== void 0 && level !== "None") push2(out, makeReading(`lds_level_ch${ch}`, level));
99637
- const totalHeat = str(item, "total_heat");
99783
+ const totalHeat = str$1(item, "total_heat");
99638
99784
  if (totalHeat !== void 0 && totalHeat !== "None") push2(out, makeReading(`lds_total_heat_ch${ch}`, totalHeat));
99639
99785
  }
99640
99786
  }
@@ -101701,50 +101847,6 @@ function buildEcowittGatewayCandidates(input) {
101701
101847
  })];
101702
101848
  }
101703
101849
  //#endregion
101704
- //#region src/ecowitt-sensor-groups.ts
101705
- /** Resolve `deviceGroup` from a persisted container config (defaults to 'gateway'). */
101706
- function configDeviceGroup(config) {
101707
- const g = config["deviceGroup"];
101708
- return ECOWITT_DEVICE_GROUPS.find((group) => group === g) ?? "gateway";
101709
- }
101710
- /** Build the persisted sensor entry (incl. self-describing category) from a live Sensor. */
101711
- function sensorToEntry(s) {
101712
- return {
101713
- sensorId: s.id,
101714
- quantity: s.quantity,
101715
- ...s.channel !== void 0 ? { channel: s.channel } : {},
101716
- ...s.name !== void 0 ? { name: s.name } : {},
101717
- ...s.category !== void 0 ? { category: s.category } : {}
101718
- };
101719
- }
101720
- /** Bucket live sensors into their device group. */
101721
- function bucketSensorsByGroup(sensors) {
101722
- const byGroup = /* @__PURE__ */ new Map();
101723
- for (const group of ECOWITT_DEVICE_GROUPS) byGroup.set(group, []);
101724
- for (const s of sensors) {
101725
- const group = deviceGroupForSensor(s.category, s.channel);
101726
- byGroup.get(group)?.push(sensorToEntry(s));
101727
- }
101728
- return byGroup;
101729
- }
101730
- /** Structural equality of two persisted sensor lists (order-independent by id + name). */
101731
- function sensorsEqual(a, b) {
101732
- if (a.length !== b.length) return false;
101733
- const aById = /* @__PURE__ */ new Map();
101734
- for (const entry of a) if (typeof entry === "object" && entry !== null && "sensorId" in entry) {
101735
- const id = entry.sensorId;
101736
- if (typeof id === "string") {
101737
- const name = "name" in entry && typeof entry.name === "string" ? entry.name : void 0;
101738
- aById.set(id, name);
101739
- }
101740
- }
101741
- for (const entry of b) {
101742
- if (!aById.has(entry.sensorId)) return false;
101743
- if (aById.get(entry.sensorId) !== entry.name) return false;
101744
- }
101745
- return true;
101746
- }
101747
- //#endregion
101748
101850
  //#region src/ecowitt-adoption-provider.ts
101749
101851
  var DEVICES_FILTER = {
101750
101852
  id: "devices",
@@ -102048,6 +102150,56 @@ function ecowittCandidates(gateways, known = /* @__PURE__ */ new Set()) {
102048
102150
  });
102049
102151
  }
102050
102152
  //#endregion
102153
+ //#region src/ecowitt-duplicate-sensors.ts
102154
+ function str(config, key) {
102155
+ const v = config[key];
102156
+ return typeof v === "string" && v.length > 0 ? v : null;
102157
+ }
102158
+ /**
102159
+ * Sensors minted under the `gateway` group Container (id `gateway:<key>`) that are the SAME reading
102160
+ * as a hardware-owned child (`<hw>:<key>`) of the weather-station / CH1 Container of the same
102161
+ * gateway. Early snapshots carry no
102162
+ * model for a sensor, which nodewitt classifies as `gateway`, so a weather-station reading was
102163
+ * once minted under the gateway Container as well; the live readings reach only the sensor under
102164
+ * its own group. Only a duplicate is returned: a sensor that exists nowhere else is the
102165
+ * gateway's own and is never touched. `scanned` is the number of sensor children seen, so a
102166
+ * caller can tell "nothing to remove" from "children not spawned yet".
102167
+ */
102168
+ function findDuplicateGatewaySensors(rows) {
102169
+ const containers = /* @__PURE__ */ new Map();
102170
+ for (const row of rows) {
102171
+ if (row.type !== DeviceType.Container || row.config["system"] !== "ecowitt") continue;
102172
+ const gatewayId = str(row.config, "gatewayId");
102173
+ if (gatewayId === null) continue;
102174
+ containers.set(row.id, {
102175
+ gatewayId,
102176
+ gateway: configDeviceGroup(row.config) === "gateway"
102177
+ });
102178
+ }
102179
+ const ownedElsewhere = /* @__PURE__ */ new Set();
102180
+ const underGateway = [];
102181
+ let scanned = 0;
102182
+ for (const row of rows) {
102183
+ if (row.type !== DeviceType.Sensor || row.parentDeviceId === null) continue;
102184
+ const container = containers.get(row.parentDeviceId);
102185
+ const sensorId = str(row.config, "sensorId");
102186
+ if (container === void 0 || sensorId === null) continue;
102187
+ scanned += 1;
102188
+ const key = `${container.gatewayId}\u0000${sensorKeyOf(sensorId)}`;
102189
+ const owner = sensorOwnerOf(sensorId);
102190
+ if (container.gateway) {
102191
+ if (owner === "gateway") underGateway.push({
102192
+ id: row.id,
102193
+ key
102194
+ });
102195
+ } else if (owner !== "gateway" && owner !== "") ownedElsewhere.add(key);
102196
+ }
102197
+ return {
102198
+ duplicates: underGateway.filter((s) => ownedElsewhere.has(s.key)).map((s) => s.id),
102199
+ scanned
102200
+ };
102201
+ }
102202
+ //#endregion
102051
102203
  //#region src/ecowitt-reconciler.ts
102052
102204
  /**
102053
102205
  * Keeps one live client per enabled integration. Devices are removed ONLY on an explicit
@@ -102501,6 +102653,55 @@ var EcowittProviderAddon = class extends BaseDeviceProvider {
102501
102653
  const devices = this.ctx.kernel.devices;
102502
102654
  if (!reg || !devices) return;
102503
102655
  const byGroup = bucketSensorsByGroup(sensors);
102656
+ if (await this.reconcileEachContainer(gatewayKey, byGroup) || !this.duplicatesChecked.has(gatewayKey)) await this.removeDuplicateGatewaySensors(gatewayKey);
102657
+ }
102658
+ /** Gateway keys whose duplicate scan already saw spawned sensor children (idempotent anyway). */
102659
+ duplicatesChecked = /* @__PURE__ */ new Set();
102660
+ /**
102661
+ * Idempotent cleanup of the sensors minted under the `gateway` Container that duplicate a
102662
+ * sensor of the weather-station / CH1 Container (see {@link findDuplicateGatewaySensors}).
102663
+ * Goes through the normal device removal (tombstone); a read that failed removes nothing.
102664
+ */
102665
+ async removeDuplicateGatewaySensors(gatewayKey) {
102666
+ const reg = this.ctx.kernel.deviceRegistry;
102667
+ const devices = this.ctx.kernel.devices;
102668
+ if (!reg || !devices) return;
102669
+ const rows = [];
102670
+ for (const d of reg.getAllForAddon(this.addonId)) {
102671
+ if (d.type !== DeviceType.Container && d.type !== DeviceType.Sensor) continue;
102672
+ const config = await devices.loadConfig(d.id).catch(() => null);
102673
+ if (config === null) return;
102674
+ if (config["system"] !== "ecowitt" || config["gatewayId"] !== gatewayKey) continue;
102675
+ rows.push({
102676
+ id: d.id,
102677
+ type: d.type,
102678
+ parentDeviceId: d.parentDeviceId,
102679
+ config
102680
+ });
102681
+ }
102682
+ const { duplicates, scanned } = findDuplicateGatewaySensors(rows);
102683
+ if (scanned > 0) this.duplicatesChecked.add(gatewayKey);
102684
+ for (const id of duplicates) try {
102685
+ await devices.remove(id);
102686
+ this.ctx.logger.info("Ecowitt: duplicate sensor under the gateway container removed", {
102687
+ tags: { deviceId: id },
102688
+ meta: { gatewayKey }
102689
+ });
102690
+ } catch (err) {
102691
+ this.ctx.logger.warn("Ecowitt: duplicate sensor removal failed", {
102692
+ tags: { deviceId: id },
102693
+ meta: {
102694
+ gatewayKey,
102695
+ error: errMsg(err)
102696
+ }
102697
+ });
102698
+ }
102699
+ }
102700
+ async reconcileEachContainer(gatewayKey, byGroup) {
102701
+ const reg = this.ctx.kernel.deviceRegistry;
102702
+ const devices = this.ctx.kernel.devices;
102703
+ if (!reg || !devices) return false;
102704
+ let changed = false;
102504
102705
  for (const d of reg.getAllForAddon(this.addonId)) {
102505
102706
  if (d.parentDeviceId !== null || d.type !== DeviceType.Container) continue;
102506
102707
  const cfg = await devices.loadConfig(d.id).catch(() => ({}));
@@ -102508,6 +102709,7 @@ var EcowittProviderAddon = class extends BaseDeviceProvider {
102508
102709
  const nextSensors = byGroup.get(configDeviceGroup(cfg)) ?? [];
102509
102710
  if (sensorsEqual(Array.isArray(cfg["sensors"]) ? cfg["sensors"] : [], nextSensors)) continue;
102510
102711
  await devices.applyDerivedSpec(d.id, { config: { sensors: nextSensors } });
102712
+ changed = true;
102511
102713
  this.ctx.logger.info("Ecowitt: group sensors reconciled", {
102512
102714
  tags: { deviceId: d.id },
102513
102715
  meta: {
@@ -102516,6 +102718,7 @@ var EcowittProviderAddon = class extends BaseDeviceProvider {
102516
102718
  }
102517
102719
  });
102518
102720
  }
102721
+ return changed;
102519
102722
  }
102520
102723
  async adoptGatewayDevice(integrationId, gatewayKey, name) {
102521
102724
  const devices = this.ctx.kernel.devices;