@camstack/addon-provider-ecowitt 0.2.153 → 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/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.154] - 2026-11-01
11
+
12
+ ### Changed
13
+
14
+ - Ecowitt: weather-station and CH1 sensors are no longer duplicated under the gateway container; existing duplicates are removed on the next reading
15
+
10
16
  ## [0.2.153] - 2026-11-01
11
17
 
12
18
  ### Changed
package/dist/addon.js CHANGED
@@ -95229,6 +95229,73 @@ function deviceGroupForSensor(category, channel) {
95229
95229
  return "gateway";
95230
95230
  }
95231
95231
  //#endregion
95232
+ //#region src/ecowitt-sensor-groups.ts
95233
+ /** Resolve `deviceGroup` from a persisted container config (defaults to 'gateway'). */
95234
+ function configDeviceGroup(config) {
95235
+ const g = config["deviceGroup"];
95236
+ return ECOWITT_DEVICE_GROUPS.find((group) => group === g) ?? "gateway";
95237
+ }
95238
+ /** Build the persisted sensor entry (incl. self-describing category) from a live Sensor. */
95239
+ function sensorToEntry(s) {
95240
+ return {
95241
+ sensorId: s.id,
95242
+ quantity: s.quantity,
95243
+ ...s.channel !== void 0 ? { channel: s.channel } : {},
95244
+ ...s.name !== void 0 ? { name: s.name } : {},
95245
+ ...s.category !== void 0 ? { category: s.category } : {}
95246
+ };
95247
+ }
95248
+ /** Bucket live sensors into their device group. */
95249
+ function bucketSensorsByGroup(sensors) {
95250
+ const byGroup = /* @__PURE__ */ new Map();
95251
+ for (const group of ECOWITT_DEVICE_GROUPS) byGroup.set(group, []);
95252
+ for (const s of sensors) {
95253
+ const group = deviceGroupForSensor(s.category, s.channel);
95254
+ byGroup.get(group)?.push(sensorToEntry(s));
95255
+ }
95256
+ return byGroup;
95257
+ }
95258
+ /** Structural equality of two persisted sensor lists (order-independent by id + name). */
95259
+ function sensorsEqual(a, b) {
95260
+ if (a.length !== b.length) return false;
95261
+ const aById = /* @__PURE__ */ new Map();
95262
+ for (const entry of a) if (typeof entry === "object" && entry !== null && "sensorId" in entry) {
95263
+ const id = entry.sensorId;
95264
+ if (typeof id === "string") {
95265
+ const name = "name" in entry && typeof entry.name === "string" ? entry.name : void 0;
95266
+ aById.set(id, name);
95267
+ }
95268
+ }
95269
+ for (const entry of b) {
95270
+ if (!aById.has(entry.sensorId)) return false;
95271
+ if (aById.get(entry.sensorId) !== entry.name) return false;
95272
+ }
95273
+ return true;
95274
+ }
95275
+ /** The measurement key of a sensor id: the part after the first ':' (`8E:0x02` gives `0x02`). */
95276
+ function sensorKeyOf(sensorId) {
95277
+ const i = sensorId.indexOf(":");
95278
+ return i < 0 ? sensorId : sensorId.slice(i + 1);
95279
+ }
95280
+ /** The owner prefix of a sensor id (`8E` of `8E:0x02`, `gateway` for a gateway-owned reading). */
95281
+ function sensorOwnerOf(sensorId) {
95282
+ const i = sensorId.indexOf(":");
95283
+ return i < 0 ? "" : sensorId.slice(0, i);
95284
+ }
95285
+ /**
95286
+ * The persisted entries a Container of `group` may spawn as children. An early snapshot had no
95287
+ * model, so nodewitt called a weather-station reading `gateway:<key>`; once the sensor map is
95288
+ * known the same reading is `<hardwareId>:<key>`. An entry is dropped when its live sensor (the
95289
+ * exact id, or for a `gateway:` id the hardware-owned sensor of the same key) belongs to another
95290
+ * group. An entry the live set does not know is kept: unknown is not gone (D49).
95291
+ */
95292
+ function entriesForGroup(entries, group, live) {
95293
+ return entries.filter((entry) => {
95294
+ 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);
95295
+ return sensor === void 0 || deviceGroupForSensor(sensor.category, sensor.channel) === group;
95296
+ });
95297
+ }
95298
+ //#endregion
95232
95299
  //#region src/devices/ecowitt-sensor-device.ts
95233
95300
  /**
95234
95301
  * Persisted config every Ecowitt sensor child carries. `sensorId` resolves the
@@ -95566,7 +95633,7 @@ var EcowittContainerDevice = class extends BaseDevice {
95566
95633
  });
95567
95634
  }
95568
95635
  getAccessoryChildren() {
95569
- return this.config.values.sensors.map((sensor) => {
95636
+ return entriesForGroup(this.config.values.sensors, this.config.values.deviceGroup, ecowittFacades.getSensors(this.gatewayId)).map((sensor) => {
95570
95637
  const meta = {
95571
95638
  type: DeviceType.Sensor,
95572
95639
  role: roleForQuantity(sensor.quantity),
@@ -99413,7 +99480,7 @@ function parseValue(input) {
99413
99480
  raw
99414
99481
  };
99415
99482
  }
99416
- function str(item, field) {
99483
+ function str$1(item, field) {
99417
99484
  const v = item[field];
99418
99485
  if (typeof v === "string") return v;
99419
99486
  if (typeof v === "number") return String(v);
@@ -99453,7 +99520,7 @@ function makeReading(key, val, unit, forceId) {
99453
99520
  }
99454
99521
  }
99455
99522
  function channelOf(item) {
99456
- const ch = str(item, "channel");
99523
+ const ch = str$1(item, "channel");
99457
99524
  return ch && ch.trim() !== "" ? ch.trim() : void 0;
99458
99525
  }
99459
99526
  function push(out, reading) {
@@ -99463,8 +99530,8 @@ function decodeChSoil(items, out) {
99463
99530
  for (const item of items) {
99464
99531
  const ch = channelOf(item);
99465
99532
  if (ch === void 0) continue;
99466
- const humidity = stripPercent(str(item, "humidity"));
99467
- const battery = str(item, "battery");
99533
+ const humidity = stripPercent(str$1(item, "humidity"));
99534
+ const battery = str$1(item, "battery");
99468
99535
  if (humidity) push(out, makeReading(`soilmoisture${ch}`, humidity));
99469
99536
  if (battery) push(out, makeReading(`soilbatt${ch}`, batteryPercent(battery, true)));
99470
99537
  }
@@ -99473,11 +99540,11 @@ function decodeChEc(items, out) {
99473
99540
  for (const item of items) {
99474
99541
  const ch = channelOf(item);
99475
99542
  if (ch === void 0) continue;
99476
- const humidity = stripPercent(str(item, "humidity"));
99477
- const temp = str(item, "temp");
99478
- const unit = str(item, "unit") ?? "C";
99479
- const ec = str(item, "ec");
99480
- const battery = str(item, "battery");
99543
+ const humidity = stripPercent(str$1(item, "humidity"));
99544
+ const temp = str$1(item, "temp");
99545
+ const unit = str$1(item, "unit") ?? "C";
99546
+ const ec = str$1(item, "ec");
99547
+ const battery = str$1(item, "battery");
99481
99548
  if (humidity) push(out, makeReading(`soilmoisture${ch}`, humidity));
99482
99549
  if (temp) push(out, makeReading(`soiltemp${ch}`, temp, unit));
99483
99550
  if (ec) push(out, makeReading(`soilec${ch}`, ec));
@@ -99488,9 +99555,9 @@ function decodeChAisle(items, gatewayTempUnit, out) {
99488
99555
  for (const item of items) {
99489
99556
  const ch = channelOf(item);
99490
99557
  if (ch === void 0) continue;
99491
- const temp = str(item, "temp");
99492
- const humidity = str(item, "humidity");
99493
- const battery = str(item, "battery");
99558
+ const temp = str$1(item, "temp");
99559
+ const humidity = str$1(item, "humidity");
99560
+ const battery = str$1(item, "battery");
99494
99561
  if (temp && temp !== "None") push(out, makeReading(`temp${ch}f`, temp, gatewayTempUnit));
99495
99562
  if (humidity && humidity !== "None") push(out, makeReading(`humidity${ch}`, stripPercent(humidity)));
99496
99563
  if (battery && battery !== "None") push(out, makeReading(`batt${ch}`, batteryPercent(battery, true)));
@@ -99500,8 +99567,8 @@ function decodeChTemp(items, gatewayTempUnit, out) {
99500
99567
  for (const item of items) {
99501
99568
  const ch = channelOf(item);
99502
99569
  if (ch === void 0) continue;
99503
- const temp = str(item, "temp");
99504
- const battery = str(item, "battery");
99570
+ const temp = str$1(item, "temp");
99571
+ const battery = str$1(item, "battery");
99505
99572
  if (temp && temp !== "None") push(out, makeReading(`tf_ch${ch}`, temp, gatewayTempUnit));
99506
99573
  if (battery && battery !== "None") push(out, makeReading(`tf_batt${ch}`, batteryPercent(battery, false)));
99507
99574
  }
@@ -99510,11 +99577,11 @@ function decodeChPm25(items, out) {
99510
99577
  for (const item of items) {
99511
99578
  const ch = channelOf(item);
99512
99579
  if (ch === void 0) continue;
99513
- const pm25 = str(item, "pm25") ?? str(item, "PM25");
99514
- const pm25_24h = str(item, "pm25_avg_24h") ?? str(item, "pm25_24h");
99515
- const realaqi = str(item, "PM25_RealAQI");
99516
- const aqi24h = str(item, "PM25_24HAQI");
99517
- const battery = str(item, "battery");
99580
+ const pm25 = str$1(item, "pm25") ?? str$1(item, "PM25");
99581
+ const pm25_24h = str$1(item, "pm25_avg_24h") ?? str$1(item, "pm25_24h");
99582
+ const realaqi = str$1(item, "PM25_RealAQI");
99583
+ const aqi24h = str$1(item, "PM25_24HAQI");
99584
+ const battery = str$1(item, "battery");
99518
99585
  if (pm25 && pm25 !== "None") push(out, makeReading(`pm25_ch${ch}`, pm25));
99519
99586
  if (pm25_24h && pm25_24h !== "None") push(out, makeReading(`pm25_avg_24h_ch${ch}`, pm25_24h));
99520
99587
  if (realaqi && realaqi !== "None") push(out, makeReading(`pm25_aqi_realtime_ch${ch}`, realaqi));
@@ -99526,8 +99593,8 @@ function decodeChLeaf(items, out) {
99526
99593
  for (const item of items) {
99527
99594
  const ch = channelOf(item);
99528
99595
  if (ch === void 0) continue;
99529
- const humidity = stripPercent(str(item, "humidity"));
99530
- const battery = str(item, "battery");
99596
+ const humidity = stripPercent(str$1(item, "humidity"));
99597
+ const battery = str$1(item, "battery");
99531
99598
  if (humidity) push(out, makeReading(`leafwetness_ch${ch}`, humidity));
99532
99599
  if (battery && battery !== "None") push(out, makeReading(`leaf_batt${ch}`, batteryPercent(battery, false)));
99533
99600
  }
@@ -99536,8 +99603,8 @@ function decodeChLeak(items, out) {
99536
99603
  for (const item of items) {
99537
99604
  const ch = channelOf(item);
99538
99605
  if (ch === void 0) continue;
99539
- const status = str(item, "status");
99540
- const battery = str(item, "battery");
99606
+ const status = str$1(item, "status");
99607
+ const battery = str$1(item, "battery");
99541
99608
  if (status !== void 0 && status !== "") {
99542
99609
  const leak = status.trim().toLowerCase() === "normal" ? "0" : "1";
99543
99610
  push(out, makeReading(`leak_ch${ch}`, leak));
@@ -99549,10 +99616,10 @@ function decodeChLds(items, out) {
99549
99616
  for (const item of items) {
99550
99617
  const ch = channelOf(item);
99551
99618
  if (ch === void 0) continue;
99552
- const air = str(item, "air");
99553
- const depth = str(item, "depth");
99554
- const voltage = str(item, "voltage");
99555
- const battery = str(item, "battery");
99619
+ const air = str$1(item, "air");
99620
+ const depth = str$1(item, "depth");
99621
+ const voltage = str$1(item, "voltage");
99622
+ const battery = str$1(item, "battery");
99556
99623
  if (air && air !== "None") push(out, makeReading(`lds_air_ch${ch}`, air));
99557
99624
  if (depth && depth !== "None") push(out, makeReading(`lds_depth_ch${ch}`, depth));
99558
99625
  if (voltage && voltage !== "None") push(out, makeReading(`lds_voltage_ch${ch}`, voltage));
@@ -99581,23 +99648,23 @@ var CO2_FIELDS = [
99581
99648
  ];
99582
99649
  function firstField(item, fields) {
99583
99650
  for (const f of fields) {
99584
- const v = str(item, f);
99651
+ const v = str$1(item, f);
99585
99652
  if (v && v !== "None") return v;
99586
99653
  }
99587
99654
  }
99588
99655
  function decodeCo2(items, out) {
99589
99656
  const item = items[0];
99590
99657
  if (item === void 0) return;
99591
- const temp = str(item, "temp");
99592
- const unit = str(item, "unit") ?? "C";
99658
+ const temp = str$1(item, "temp");
99659
+ const unit = str$1(item, "unit") ?? "C";
99593
99660
  if (temp) push(out, makeReading(unit === "C" ? "tf_co2c" : "tf_co2", temp, unit));
99594
- const humidity = stripPercent(str(item, "humidity"));
99661
+ const humidity = stripPercent(str$1(item, "humidity"));
99595
99662
  if (humidity) push(out, makeReading("humi_co2", humidity));
99596
99663
  for (const [fields, key] of CO2_FIELDS) {
99597
99664
  const v = firstField(item, fields);
99598
99665
  if (v !== void 0) push(out, makeReading(key, v));
99599
99666
  }
99600
- const battery = str(item, "battery");
99667
+ const battery = str$1(item, "battery");
99601
99668
  if (battery && battery !== "None") {
99602
99669
  const n = num(battery);
99603
99670
  push(out, makeReading("co2_batt", n !== void 0 ? String(Math.min(n * 20, 100)) : battery));
@@ -99609,12 +99676,12 @@ function push2(out, reading) {
99609
99676
  }
99610
99677
  function decodeCommonList(items, mapper, out) {
99611
99678
  for (const item of items) {
99612
- const id = str(item, "id");
99613
- const val = str(item, "val");
99614
- const unit = str(item, "unit");
99679
+ const id = str$1(item, "id");
99680
+ const val = str$1(item, "val");
99681
+ const unit = str$1(item, "unit");
99615
99682
  if (id && val !== void 0) push2(out, makeReading(id, val, unit));
99616
99683
  if (id === "0x03" && mapper.getHardwareId("wh26batt") !== void 0) {
99617
- const battery = str(item, "battery");
99684
+ const battery = str$1(item, "battery");
99618
99685
  if (battery !== void 0) push2(out, makeReading("wh26batt", battery === "0" ? "100" : "10"));
99619
99686
  }
99620
99687
  }
@@ -99622,12 +99689,12 @@ function decodeCommonList(items, mapper, out) {
99622
99689
  function decodeRain(items, mapper, out) {
99623
99690
  const rainHwId = mapper.getHardwareId("wh69batt") ?? mapper.getHardwareId("wh40batt");
99624
99691
  for (const item of items) {
99625
- const id = str(item, "id");
99626
- const val = str(item, "val");
99692
+ const id = str$1(item, "id");
99693
+ const val = str$1(item, "val");
99627
99694
  if (!id || val === void 0) continue;
99628
- push2(out, makeReading(id, val, str(item, "unit"), rainHwId));
99695
+ push2(out, makeReading(id, val, str$1(item, "unit"), rainHwId));
99629
99696
  if (id === "0x13") {
99630
- const battery = str(item, "battery");
99697
+ const battery = str$1(item, "battery");
99631
99698
  if (battery) {
99632
99699
  const n = num(battery);
99633
99700
  const pct = n !== void 0 && n > 1 ? String(n * 20) : battery === "0" ? "100" : "10";
@@ -99659,30 +99726,30 @@ function piezoKeysFor(mapper) {
99659
99726
  function decodePiezo(items, mapper, out) {
99660
99727
  const piezoHwId = mapper.getHardwareId("ws85batt") ?? mapper.getHardwareId("ws90batt") ?? mapper.getHardwareId("wh90batt");
99661
99728
  for (const item of items) {
99662
- const id = str(item, "id");
99663
- const val = str(item, "val");
99729
+ const id = str$1(item, "id");
99730
+ const val = str$1(item, "val");
99664
99731
  if (!id || val === void 0) continue;
99665
- push2(out, makeReading(id, val, str(item, "unit"), piezoHwId));
99732
+ push2(out, makeReading(id, val, str$1(item, "unit"), piezoHwId));
99666
99733
  if (id !== "0x13") continue;
99667
- const battery = str(item, "battery");
99734
+ const battery = str$1(item, "battery");
99668
99735
  if (!battery) continue;
99669
99736
  const keys = piezoKeysFor(mapper);
99670
99737
  const n = num(battery);
99671
99738
  push2(out, makeReading(keys.batt, n !== void 0 ? String(n * 20) : battery));
99672
- const voltage = str(item, "voltage");
99739
+ const voltage = str$1(item, "voltage");
99673
99740
  if (voltage) push2(out, makeReading(keys.volt, voltage));
99674
- const cap = str(item, keys.capField);
99741
+ const cap = str$1(item, keys.capField);
99675
99742
  if (cap) push2(out, makeReading(keys.capKey, cap));
99676
99743
  }
99677
99744
  }
99678
99745
  function decodeLightning(items, out) {
99679
99746
  const item = items[0];
99680
99747
  if (item === void 0) return;
99681
- const count = str(item, "count");
99748
+ const count = str$1(item, "count");
99682
99749
  if (count !== void 0) push2(out, makeReading("lightning_num", count));
99683
- const distance = str(item, "distance");
99750
+ const distance = str$1(item, "distance");
99684
99751
  if (distance !== void 0) push2(out, makeReading("lightning", distance.replace(" km", "").trim()));
99685
- const battery = str(item, "battery");
99752
+ const battery = str$1(item, "battery");
99686
99753
  if (battery !== void 0) {
99687
99754
  const n = num(battery);
99688
99755
  push2(out, makeReading("wh57batt", n !== void 0 ? String(n * 20) : battery));
@@ -99691,30 +99758,30 @@ function decodeLightning(items, out) {
99691
99758
  function decodeWh25(items, out) {
99692
99759
  const item = items[0];
99693
99760
  if (item === void 0) return;
99694
- const intemp = str(item, "intemp");
99695
- const unit = str(item, "unit") ?? "F";
99761
+ const intemp = str$1(item, "intemp");
99762
+ const unit = str$1(item, "unit") ?? "F";
99696
99763
  if (intemp !== void 0) push2(out, makeReading("tempinf", intemp, unit));
99697
- const inhumi = str(item, "inhumi");
99764
+ const inhumi = str$1(item, "inhumi");
99698
99765
  if (inhumi !== void 0) push2(out, makeReading("humidityin", stripPercent(inhumi)));
99699
- const abs = str(item, "abs");
99766
+ const abs = str$1(item, "abs");
99700
99767
  if (abs !== void 0) push2(out, makeReading("baromabsin", abs.replace(" hPa", "").trim()));
99701
- const rel = str(item, "rel");
99768
+ const rel = str$1(item, "rel");
99702
99769
  if (rel !== void 0) push2(out, makeReading("baromrelin", rel.replace(" hPa", "").trim()));
99703
99770
  }
99704
99771
  function decodeSoilCalibration(items, out) {
99705
99772
  for (const item of items) {
99706
- const ch = str(item, "ch");
99707
- const nowAd = str(item, "nowAd");
99773
+ const ch = str$1(item, "ch");
99774
+ const nowAd = str$1(item, "nowAd");
99708
99775
  if (ch && nowAd !== void 0) push2(out, makeReading(`soilad${ch}`, nowAd));
99709
99776
  }
99710
99777
  }
99711
99778
  function decodeLdsConfig(items, out) {
99712
99779
  for (const item of items) {
99713
- const ch = str(item, "ch");
99780
+ const ch = str$1(item, "ch");
99714
99781
  if (!ch) continue;
99715
- const level = str(item, "level");
99782
+ const level = str$1(item, "level");
99716
99783
  if (level !== void 0 && level !== "None") push2(out, makeReading(`lds_level_ch${ch}`, level));
99717
- const totalHeat = str(item, "total_heat");
99784
+ const totalHeat = str$1(item, "total_heat");
99718
99785
  if (totalHeat !== void 0 && totalHeat !== "None") push2(out, makeReading(`lds_total_heat_ch${ch}`, totalHeat));
99719
99786
  }
99720
99787
  }
@@ -101781,50 +101848,6 @@ function buildEcowittGatewayCandidates(input) {
101781
101848
  })];
101782
101849
  }
101783
101850
  //#endregion
101784
- //#region src/ecowitt-sensor-groups.ts
101785
- /** Resolve `deviceGroup` from a persisted container config (defaults to 'gateway'). */
101786
- function configDeviceGroup(config) {
101787
- const g = config["deviceGroup"];
101788
- return ECOWITT_DEVICE_GROUPS.find((group) => group === g) ?? "gateway";
101789
- }
101790
- /** Build the persisted sensor entry (incl. self-describing category) from a live Sensor. */
101791
- function sensorToEntry(s) {
101792
- return {
101793
- sensorId: s.id,
101794
- quantity: s.quantity,
101795
- ...s.channel !== void 0 ? { channel: s.channel } : {},
101796
- ...s.name !== void 0 ? { name: s.name } : {},
101797
- ...s.category !== void 0 ? { category: s.category } : {}
101798
- };
101799
- }
101800
- /** Bucket live sensors into their device group. */
101801
- function bucketSensorsByGroup(sensors) {
101802
- const byGroup = /* @__PURE__ */ new Map();
101803
- for (const group of ECOWITT_DEVICE_GROUPS) byGroup.set(group, []);
101804
- for (const s of sensors) {
101805
- const group = deviceGroupForSensor(s.category, s.channel);
101806
- byGroup.get(group)?.push(sensorToEntry(s));
101807
- }
101808
- return byGroup;
101809
- }
101810
- /** Structural equality of two persisted sensor lists (order-independent by id + name). */
101811
- function sensorsEqual(a, b) {
101812
- if (a.length !== b.length) return false;
101813
- const aById = /* @__PURE__ */ new Map();
101814
- for (const entry of a) if (typeof entry === "object" && entry !== null && "sensorId" in entry) {
101815
- const id = entry.sensorId;
101816
- if (typeof id === "string") {
101817
- const name = "name" in entry && typeof entry.name === "string" ? entry.name : void 0;
101818
- aById.set(id, name);
101819
- }
101820
- }
101821
- for (const entry of b) {
101822
- if (!aById.has(entry.sensorId)) return false;
101823
- if (aById.get(entry.sensorId) !== entry.name) return false;
101824
- }
101825
- return true;
101826
- }
101827
- //#endregion
101828
101851
  //#region src/ecowitt-adoption-provider.ts
101829
101852
  var DEVICES_FILTER = {
101830
101853
  id: "devices",
@@ -102128,6 +102151,56 @@ function ecowittCandidates(gateways, known = /* @__PURE__ */ new Set()) {
102128
102151
  });
102129
102152
  }
102130
102153
  //#endregion
102154
+ //#region src/ecowitt-duplicate-sensors.ts
102155
+ function str(config, key) {
102156
+ const v = config[key];
102157
+ return typeof v === "string" && v.length > 0 ? v : null;
102158
+ }
102159
+ /**
102160
+ * Sensors minted under the `gateway` group Container (id `gateway:<key>`) that are the SAME reading
102161
+ * as a hardware-owned child (`<hw>:<key>`) of the weather-station / CH1 Container of the same
102162
+ * gateway. Early snapshots carry no
102163
+ * model for a sensor, which nodewitt classifies as `gateway`, so a weather-station reading was
102164
+ * once minted under the gateway Container as well; the live readings reach only the sensor under
102165
+ * its own group. Only a duplicate is returned: a sensor that exists nowhere else is the
102166
+ * gateway's own and is never touched. `scanned` is the number of sensor children seen, so a
102167
+ * caller can tell "nothing to remove" from "children not spawned yet".
102168
+ */
102169
+ function findDuplicateGatewaySensors(rows) {
102170
+ const containers = /* @__PURE__ */ new Map();
102171
+ for (const row of rows) {
102172
+ if (row.type !== DeviceType.Container || row.config["system"] !== "ecowitt") continue;
102173
+ const gatewayId = str(row.config, "gatewayId");
102174
+ if (gatewayId === null) continue;
102175
+ containers.set(row.id, {
102176
+ gatewayId,
102177
+ gateway: configDeviceGroup(row.config) === "gateway"
102178
+ });
102179
+ }
102180
+ const ownedElsewhere = /* @__PURE__ */ new Set();
102181
+ const underGateway = [];
102182
+ let scanned = 0;
102183
+ for (const row of rows) {
102184
+ if (row.type !== DeviceType.Sensor || row.parentDeviceId === null) continue;
102185
+ const container = containers.get(row.parentDeviceId);
102186
+ const sensorId = str(row.config, "sensorId");
102187
+ if (container === void 0 || sensorId === null) continue;
102188
+ scanned += 1;
102189
+ const key = `${container.gatewayId}\u0000${sensorKeyOf(sensorId)}`;
102190
+ const owner = sensorOwnerOf(sensorId);
102191
+ if (container.gateway) {
102192
+ if (owner === "gateway") underGateway.push({
102193
+ id: row.id,
102194
+ key
102195
+ });
102196
+ } else if (owner !== "gateway" && owner !== "") ownedElsewhere.add(key);
102197
+ }
102198
+ return {
102199
+ duplicates: underGateway.filter((s) => ownedElsewhere.has(s.key)).map((s) => s.id),
102200
+ scanned
102201
+ };
102202
+ }
102203
+ //#endregion
102131
102204
  //#region src/ecowitt-reconciler.ts
102132
102205
  /**
102133
102206
  * Keeps one live client per enabled integration. Devices are removed ONLY on an explicit
@@ -102581,6 +102654,55 @@ var EcowittProviderAddon = class extends BaseDeviceProvider {
102581
102654
  const devices = this.ctx.kernel.devices;
102582
102655
  if (!reg || !devices) return;
102583
102656
  const byGroup = bucketSensorsByGroup(sensors);
102657
+ if (await this.reconcileEachContainer(gatewayKey, byGroup) || !this.duplicatesChecked.has(gatewayKey)) await this.removeDuplicateGatewaySensors(gatewayKey);
102658
+ }
102659
+ /** Gateway keys whose duplicate scan already saw spawned sensor children (idempotent anyway). */
102660
+ duplicatesChecked = /* @__PURE__ */ new Set();
102661
+ /**
102662
+ * Idempotent cleanup of the sensors minted under the `gateway` Container that duplicate a
102663
+ * sensor of the weather-station / CH1 Container (see {@link findDuplicateGatewaySensors}).
102664
+ * Goes through the normal device removal (tombstone); a read that failed removes nothing.
102665
+ */
102666
+ async removeDuplicateGatewaySensors(gatewayKey) {
102667
+ const reg = this.ctx.kernel.deviceRegistry;
102668
+ const devices = this.ctx.kernel.devices;
102669
+ if (!reg || !devices) return;
102670
+ const rows = [];
102671
+ for (const d of reg.getAllForAddon(this.addonId)) {
102672
+ if (d.type !== DeviceType.Container && d.type !== DeviceType.Sensor) continue;
102673
+ const config = await devices.loadConfig(d.id).catch(() => null);
102674
+ if (config === null) return;
102675
+ if (config["system"] !== "ecowitt" || config["gatewayId"] !== gatewayKey) continue;
102676
+ rows.push({
102677
+ id: d.id,
102678
+ type: d.type,
102679
+ parentDeviceId: d.parentDeviceId,
102680
+ config
102681
+ });
102682
+ }
102683
+ const { duplicates, scanned } = findDuplicateGatewaySensors(rows);
102684
+ if (scanned > 0) this.duplicatesChecked.add(gatewayKey);
102685
+ for (const id of duplicates) try {
102686
+ await devices.remove(id);
102687
+ this.ctx.logger.info("Ecowitt: duplicate sensor under the gateway container removed", {
102688
+ tags: { deviceId: id },
102689
+ meta: { gatewayKey }
102690
+ });
102691
+ } catch (err) {
102692
+ this.ctx.logger.warn("Ecowitt: duplicate sensor removal failed", {
102693
+ tags: { deviceId: id },
102694
+ meta: {
102695
+ gatewayKey,
102696
+ error: errMsg(err)
102697
+ }
102698
+ });
102699
+ }
102700
+ }
102701
+ async reconcileEachContainer(gatewayKey, byGroup) {
102702
+ const reg = this.ctx.kernel.deviceRegistry;
102703
+ const devices = this.ctx.kernel.devices;
102704
+ if (!reg || !devices) return false;
102705
+ let changed = false;
102584
102706
  for (const d of reg.getAllForAddon(this.addonId)) {
102585
102707
  if (d.parentDeviceId !== null || d.type !== DeviceType.Container) continue;
102586
102708
  const cfg = await devices.loadConfig(d.id).catch(() => ({}));
@@ -102588,6 +102710,7 @@ var EcowittProviderAddon = class extends BaseDeviceProvider {
102588
102710
  const nextSensors = byGroup.get(configDeviceGroup(cfg)) ?? [];
102589
102711
  if (sensorsEqual(Array.isArray(cfg["sensors"]) ? cfg["sensors"] : [], nextSensors)) continue;
102590
102712
  await devices.applyDerivedSpec(d.id, { config: { sensors: nextSensors } });
102713
+ changed = true;
102591
102714
  this.ctx.logger.info("Ecowitt: group sensors reconciled", {
102592
102715
  tags: { deviceId: d.id },
102593
102716
  meta: {
@@ -102596,6 +102719,7 @@ var EcowittProviderAddon = class extends BaseDeviceProvider {
102596
102719
  }
102597
102720
  });
102598
102721
  }
102722
+ return changed;
102599
102723
  }
102600
102724
  async adoptGatewayDevice(integrationId, gatewayKey, name) {
102601
102725
  const devices = this.ctx.kernel.devices;
package/dist/addon.mjs CHANGED
@@ -95228,6 +95228,73 @@ function deviceGroupForSensor(category, channel) {
95228
95228
  return "gateway";
95229
95229
  }
95230
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
95231
95298
  //#region src/devices/ecowitt-sensor-device.ts
95232
95299
  /**
95233
95300
  * Persisted config every Ecowitt sensor child carries. `sensorId` resolves the
@@ -95565,7 +95632,7 @@ var EcowittContainerDevice = class extends BaseDevice {
95565
95632
  });
95566
95633
  }
95567
95634
  getAccessoryChildren() {
95568
- return this.config.values.sensors.map((sensor) => {
95635
+ return entriesForGroup(this.config.values.sensors, this.config.values.deviceGroup, ecowittFacades.getSensors(this.gatewayId)).map((sensor) => {
95569
95636
  const meta = {
95570
95637
  type: DeviceType.Sensor,
95571
95638
  role: roleForQuantity(sensor.quantity),
@@ -99412,7 +99479,7 @@ function parseValue(input) {
99412
99479
  raw
99413
99480
  };
99414
99481
  }
99415
- function str(item, field) {
99482
+ function str$1(item, field) {
99416
99483
  const v = item[field];
99417
99484
  if (typeof v === "string") return v;
99418
99485
  if (typeof v === "number") return String(v);
@@ -99452,7 +99519,7 @@ function makeReading(key, val, unit, forceId) {
99452
99519
  }
99453
99520
  }
99454
99521
  function channelOf(item) {
99455
- const ch = str(item, "channel");
99522
+ const ch = str$1(item, "channel");
99456
99523
  return ch && ch.trim() !== "" ? ch.trim() : void 0;
99457
99524
  }
99458
99525
  function push(out, reading) {
@@ -99462,8 +99529,8 @@ function decodeChSoil(items, out) {
99462
99529
  for (const item of items) {
99463
99530
  const ch = channelOf(item);
99464
99531
  if (ch === void 0) continue;
99465
- const humidity = stripPercent(str(item, "humidity"));
99466
- const battery = str(item, "battery");
99532
+ const humidity = stripPercent(str$1(item, "humidity"));
99533
+ const battery = str$1(item, "battery");
99467
99534
  if (humidity) push(out, makeReading(`soilmoisture${ch}`, humidity));
99468
99535
  if (battery) push(out, makeReading(`soilbatt${ch}`, batteryPercent(battery, true)));
99469
99536
  }
@@ -99472,11 +99539,11 @@ function decodeChEc(items, out) {
99472
99539
  for (const item of items) {
99473
99540
  const ch = channelOf(item);
99474
99541
  if (ch === void 0) continue;
99475
- const humidity = stripPercent(str(item, "humidity"));
99476
- const temp = str(item, "temp");
99477
- const unit = str(item, "unit") ?? "C";
99478
- const ec = str(item, "ec");
99479
- 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");
99480
99547
  if (humidity) push(out, makeReading(`soilmoisture${ch}`, humidity));
99481
99548
  if (temp) push(out, makeReading(`soiltemp${ch}`, temp, unit));
99482
99549
  if (ec) push(out, makeReading(`soilec${ch}`, ec));
@@ -99487,9 +99554,9 @@ function decodeChAisle(items, gatewayTempUnit, out) {
99487
99554
  for (const item of items) {
99488
99555
  const ch = channelOf(item);
99489
99556
  if (ch === void 0) continue;
99490
- const temp = str(item, "temp");
99491
- const humidity = str(item, "humidity");
99492
- 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");
99493
99560
  if (temp && temp !== "None") push(out, makeReading(`temp${ch}f`, temp, gatewayTempUnit));
99494
99561
  if (humidity && humidity !== "None") push(out, makeReading(`humidity${ch}`, stripPercent(humidity)));
99495
99562
  if (battery && battery !== "None") push(out, makeReading(`batt${ch}`, batteryPercent(battery, true)));
@@ -99499,8 +99566,8 @@ function decodeChTemp(items, gatewayTempUnit, out) {
99499
99566
  for (const item of items) {
99500
99567
  const ch = channelOf(item);
99501
99568
  if (ch === void 0) continue;
99502
- const temp = str(item, "temp");
99503
- const battery = str(item, "battery");
99569
+ const temp = str$1(item, "temp");
99570
+ const battery = str$1(item, "battery");
99504
99571
  if (temp && temp !== "None") push(out, makeReading(`tf_ch${ch}`, temp, gatewayTempUnit));
99505
99572
  if (battery && battery !== "None") push(out, makeReading(`tf_batt${ch}`, batteryPercent(battery, false)));
99506
99573
  }
@@ -99509,11 +99576,11 @@ function decodeChPm25(items, out) {
99509
99576
  for (const item of items) {
99510
99577
  const ch = channelOf(item);
99511
99578
  if (ch === void 0) continue;
99512
- const pm25 = str(item, "pm25") ?? str(item, "PM25");
99513
- const pm25_24h = str(item, "pm25_avg_24h") ?? str(item, "pm25_24h");
99514
- const realaqi = str(item, "PM25_RealAQI");
99515
- const aqi24h = str(item, "PM25_24HAQI");
99516
- 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");
99517
99584
  if (pm25 && pm25 !== "None") push(out, makeReading(`pm25_ch${ch}`, pm25));
99518
99585
  if (pm25_24h && pm25_24h !== "None") push(out, makeReading(`pm25_avg_24h_ch${ch}`, pm25_24h));
99519
99586
  if (realaqi && realaqi !== "None") push(out, makeReading(`pm25_aqi_realtime_ch${ch}`, realaqi));
@@ -99525,8 +99592,8 @@ function decodeChLeaf(items, out) {
99525
99592
  for (const item of items) {
99526
99593
  const ch = channelOf(item);
99527
99594
  if (ch === void 0) continue;
99528
- const humidity = stripPercent(str(item, "humidity"));
99529
- const battery = str(item, "battery");
99595
+ const humidity = stripPercent(str$1(item, "humidity"));
99596
+ const battery = str$1(item, "battery");
99530
99597
  if (humidity) push(out, makeReading(`leafwetness_ch${ch}`, humidity));
99531
99598
  if (battery && battery !== "None") push(out, makeReading(`leaf_batt${ch}`, batteryPercent(battery, false)));
99532
99599
  }
@@ -99535,8 +99602,8 @@ function decodeChLeak(items, out) {
99535
99602
  for (const item of items) {
99536
99603
  const ch = channelOf(item);
99537
99604
  if (ch === void 0) continue;
99538
- const status = str(item, "status");
99539
- const battery = str(item, "battery");
99605
+ const status = str$1(item, "status");
99606
+ const battery = str$1(item, "battery");
99540
99607
  if (status !== void 0 && status !== "") {
99541
99608
  const leak = status.trim().toLowerCase() === "normal" ? "0" : "1";
99542
99609
  push(out, makeReading(`leak_ch${ch}`, leak));
@@ -99548,10 +99615,10 @@ function decodeChLds(items, out) {
99548
99615
  for (const item of items) {
99549
99616
  const ch = channelOf(item);
99550
99617
  if (ch === void 0) continue;
99551
- const air = str(item, "air");
99552
- const depth = str(item, "depth");
99553
- const voltage = str(item, "voltage");
99554
- 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");
99555
99622
  if (air && air !== "None") push(out, makeReading(`lds_air_ch${ch}`, air));
99556
99623
  if (depth && depth !== "None") push(out, makeReading(`lds_depth_ch${ch}`, depth));
99557
99624
  if (voltage && voltage !== "None") push(out, makeReading(`lds_voltage_ch${ch}`, voltage));
@@ -99580,23 +99647,23 @@ var CO2_FIELDS = [
99580
99647
  ];
99581
99648
  function firstField(item, fields) {
99582
99649
  for (const f of fields) {
99583
- const v = str(item, f);
99650
+ const v = str$1(item, f);
99584
99651
  if (v && v !== "None") return v;
99585
99652
  }
99586
99653
  }
99587
99654
  function decodeCo2(items, out) {
99588
99655
  const item = items[0];
99589
99656
  if (item === void 0) return;
99590
- const temp = str(item, "temp");
99591
- const unit = str(item, "unit") ?? "C";
99657
+ const temp = str$1(item, "temp");
99658
+ const unit = str$1(item, "unit") ?? "C";
99592
99659
  if (temp) push(out, makeReading(unit === "C" ? "tf_co2c" : "tf_co2", temp, unit));
99593
- const humidity = stripPercent(str(item, "humidity"));
99660
+ const humidity = stripPercent(str$1(item, "humidity"));
99594
99661
  if (humidity) push(out, makeReading("humi_co2", humidity));
99595
99662
  for (const [fields, key] of CO2_FIELDS) {
99596
99663
  const v = firstField(item, fields);
99597
99664
  if (v !== void 0) push(out, makeReading(key, v));
99598
99665
  }
99599
- const battery = str(item, "battery");
99666
+ const battery = str$1(item, "battery");
99600
99667
  if (battery && battery !== "None") {
99601
99668
  const n = num(battery);
99602
99669
  push(out, makeReading("co2_batt", n !== void 0 ? String(Math.min(n * 20, 100)) : battery));
@@ -99608,12 +99675,12 @@ function push2(out, reading) {
99608
99675
  }
99609
99676
  function decodeCommonList(items, mapper, out) {
99610
99677
  for (const item of items) {
99611
- const id = str(item, "id");
99612
- const val = str(item, "val");
99613
- 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");
99614
99681
  if (id && val !== void 0) push2(out, makeReading(id, val, unit));
99615
99682
  if (id === "0x03" && mapper.getHardwareId("wh26batt") !== void 0) {
99616
- const battery = str(item, "battery");
99683
+ const battery = str$1(item, "battery");
99617
99684
  if (battery !== void 0) push2(out, makeReading("wh26batt", battery === "0" ? "100" : "10"));
99618
99685
  }
99619
99686
  }
@@ -99621,12 +99688,12 @@ function decodeCommonList(items, mapper, out) {
99621
99688
  function decodeRain(items, mapper, out) {
99622
99689
  const rainHwId = mapper.getHardwareId("wh69batt") ?? mapper.getHardwareId("wh40batt");
99623
99690
  for (const item of items) {
99624
- const id = str(item, "id");
99625
- const val = str(item, "val");
99691
+ const id = str$1(item, "id");
99692
+ const val = str$1(item, "val");
99626
99693
  if (!id || val === void 0) continue;
99627
- push2(out, makeReading(id, val, str(item, "unit"), rainHwId));
99694
+ push2(out, makeReading(id, val, str$1(item, "unit"), rainHwId));
99628
99695
  if (id === "0x13") {
99629
- const battery = str(item, "battery");
99696
+ const battery = str$1(item, "battery");
99630
99697
  if (battery) {
99631
99698
  const n = num(battery);
99632
99699
  const pct = n !== void 0 && n > 1 ? String(n * 20) : battery === "0" ? "100" : "10";
@@ -99658,30 +99725,30 @@ function piezoKeysFor(mapper) {
99658
99725
  function decodePiezo(items, mapper, out) {
99659
99726
  const piezoHwId = mapper.getHardwareId("ws85batt") ?? mapper.getHardwareId("ws90batt") ?? mapper.getHardwareId("wh90batt");
99660
99727
  for (const item of items) {
99661
- const id = str(item, "id");
99662
- const val = str(item, "val");
99728
+ const id = str$1(item, "id");
99729
+ const val = str$1(item, "val");
99663
99730
  if (!id || val === void 0) continue;
99664
- push2(out, makeReading(id, val, str(item, "unit"), piezoHwId));
99731
+ push2(out, makeReading(id, val, str$1(item, "unit"), piezoHwId));
99665
99732
  if (id !== "0x13") continue;
99666
- const battery = str(item, "battery");
99733
+ const battery = str$1(item, "battery");
99667
99734
  if (!battery) continue;
99668
99735
  const keys = piezoKeysFor(mapper);
99669
99736
  const n = num(battery);
99670
99737
  push2(out, makeReading(keys.batt, n !== void 0 ? String(n * 20) : battery));
99671
- const voltage = str(item, "voltage");
99738
+ const voltage = str$1(item, "voltage");
99672
99739
  if (voltage) push2(out, makeReading(keys.volt, voltage));
99673
- const cap = str(item, keys.capField);
99740
+ const cap = str$1(item, keys.capField);
99674
99741
  if (cap) push2(out, makeReading(keys.capKey, cap));
99675
99742
  }
99676
99743
  }
99677
99744
  function decodeLightning(items, out) {
99678
99745
  const item = items[0];
99679
99746
  if (item === void 0) return;
99680
- const count = str(item, "count");
99747
+ const count = str$1(item, "count");
99681
99748
  if (count !== void 0) push2(out, makeReading("lightning_num", count));
99682
- const distance = str(item, "distance");
99749
+ const distance = str$1(item, "distance");
99683
99750
  if (distance !== void 0) push2(out, makeReading("lightning", distance.replace(" km", "").trim()));
99684
- const battery = str(item, "battery");
99751
+ const battery = str$1(item, "battery");
99685
99752
  if (battery !== void 0) {
99686
99753
  const n = num(battery);
99687
99754
  push2(out, makeReading("wh57batt", n !== void 0 ? String(n * 20) : battery));
@@ -99690,30 +99757,30 @@ function decodeLightning(items, out) {
99690
99757
  function decodeWh25(items, out) {
99691
99758
  const item = items[0];
99692
99759
  if (item === void 0) return;
99693
- const intemp = str(item, "intemp");
99694
- const unit = str(item, "unit") ?? "F";
99760
+ const intemp = str$1(item, "intemp");
99761
+ const unit = str$1(item, "unit") ?? "F";
99695
99762
  if (intemp !== void 0) push2(out, makeReading("tempinf", intemp, unit));
99696
- const inhumi = str(item, "inhumi");
99763
+ const inhumi = str$1(item, "inhumi");
99697
99764
  if (inhumi !== void 0) push2(out, makeReading("humidityin", stripPercent(inhumi)));
99698
- const abs = str(item, "abs");
99765
+ const abs = str$1(item, "abs");
99699
99766
  if (abs !== void 0) push2(out, makeReading("baromabsin", abs.replace(" hPa", "").trim()));
99700
- const rel = str(item, "rel");
99767
+ const rel = str$1(item, "rel");
99701
99768
  if (rel !== void 0) push2(out, makeReading("baromrelin", rel.replace(" hPa", "").trim()));
99702
99769
  }
99703
99770
  function decodeSoilCalibration(items, out) {
99704
99771
  for (const item of items) {
99705
- const ch = str(item, "ch");
99706
- const nowAd = str(item, "nowAd");
99772
+ const ch = str$1(item, "ch");
99773
+ const nowAd = str$1(item, "nowAd");
99707
99774
  if (ch && nowAd !== void 0) push2(out, makeReading(`soilad${ch}`, nowAd));
99708
99775
  }
99709
99776
  }
99710
99777
  function decodeLdsConfig(items, out) {
99711
99778
  for (const item of items) {
99712
- const ch = str(item, "ch");
99779
+ const ch = str$1(item, "ch");
99713
99780
  if (!ch) continue;
99714
- const level = str(item, "level");
99781
+ const level = str$1(item, "level");
99715
99782
  if (level !== void 0 && level !== "None") push2(out, makeReading(`lds_level_ch${ch}`, level));
99716
- const totalHeat = str(item, "total_heat");
99783
+ const totalHeat = str$1(item, "total_heat");
99717
99784
  if (totalHeat !== void 0 && totalHeat !== "None") push2(out, makeReading(`lds_total_heat_ch${ch}`, totalHeat));
99718
99785
  }
99719
99786
  }
@@ -101780,50 +101847,6 @@ function buildEcowittGatewayCandidates(input) {
101780
101847
  })];
101781
101848
  }
101782
101849
  //#endregion
101783
- //#region src/ecowitt-sensor-groups.ts
101784
- /** Resolve `deviceGroup` from a persisted container config (defaults to 'gateway'). */
101785
- function configDeviceGroup(config) {
101786
- const g = config["deviceGroup"];
101787
- return ECOWITT_DEVICE_GROUPS.find((group) => group === g) ?? "gateway";
101788
- }
101789
- /** Build the persisted sensor entry (incl. self-describing category) from a live Sensor. */
101790
- function sensorToEntry(s) {
101791
- return {
101792
- sensorId: s.id,
101793
- quantity: s.quantity,
101794
- ...s.channel !== void 0 ? { channel: s.channel } : {},
101795
- ...s.name !== void 0 ? { name: s.name } : {},
101796
- ...s.category !== void 0 ? { category: s.category } : {}
101797
- };
101798
- }
101799
- /** Bucket live sensors into their device group. */
101800
- function bucketSensorsByGroup(sensors) {
101801
- const byGroup = /* @__PURE__ */ new Map();
101802
- for (const group of ECOWITT_DEVICE_GROUPS) byGroup.set(group, []);
101803
- for (const s of sensors) {
101804
- const group = deviceGroupForSensor(s.category, s.channel);
101805
- byGroup.get(group)?.push(sensorToEntry(s));
101806
- }
101807
- return byGroup;
101808
- }
101809
- /** Structural equality of two persisted sensor lists (order-independent by id + name). */
101810
- function sensorsEqual(a, b) {
101811
- if (a.length !== b.length) return false;
101812
- const aById = /* @__PURE__ */ new Map();
101813
- for (const entry of a) if (typeof entry === "object" && entry !== null && "sensorId" in entry) {
101814
- const id = entry.sensorId;
101815
- if (typeof id === "string") {
101816
- const name = "name" in entry && typeof entry.name === "string" ? entry.name : void 0;
101817
- aById.set(id, name);
101818
- }
101819
- }
101820
- for (const entry of b) {
101821
- if (!aById.has(entry.sensorId)) return false;
101822
- if (aById.get(entry.sensorId) !== entry.name) return false;
101823
- }
101824
- return true;
101825
- }
101826
- //#endregion
101827
101850
  //#region src/ecowitt-adoption-provider.ts
101828
101851
  var DEVICES_FILTER = {
101829
101852
  id: "devices",
@@ -102127,6 +102150,56 @@ function ecowittCandidates(gateways, known = /* @__PURE__ */ new Set()) {
102127
102150
  });
102128
102151
  }
102129
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
102130
102203
  //#region src/ecowitt-reconciler.ts
102131
102204
  /**
102132
102205
  * Keeps one live client per enabled integration. Devices are removed ONLY on an explicit
@@ -102580,6 +102653,55 @@ var EcowittProviderAddon = class extends BaseDeviceProvider {
102580
102653
  const devices = this.ctx.kernel.devices;
102581
102654
  if (!reg || !devices) return;
102582
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;
102583
102705
  for (const d of reg.getAllForAddon(this.addonId)) {
102584
102706
  if (d.parentDeviceId !== null || d.type !== DeviceType.Container) continue;
102585
102707
  const cfg = await devices.loadConfig(d.id).catch(() => ({}));
@@ -102587,6 +102709,7 @@ var EcowittProviderAddon = class extends BaseDeviceProvider {
102587
102709
  const nextSensors = byGroup.get(configDeviceGroup(cfg)) ?? [];
102588
102710
  if (sensorsEqual(Array.isArray(cfg["sensors"]) ? cfg["sensors"] : [], nextSensors)) continue;
102589
102711
  await devices.applyDerivedSpec(d.id, { config: { sensors: nextSensors } });
102712
+ changed = true;
102590
102713
  this.ctx.logger.info("Ecowitt: group sensors reconciled", {
102591
102714
  tags: { deviceId: d.id },
102592
102715
  meta: {
@@ -102595,6 +102718,7 @@ var EcowittProviderAddon = class extends BaseDeviceProvider {
102595
102718
  }
102596
102719
  });
102597
102720
  }
102721
+ return changed;
102598
102722
  }
102599
102723
  async adoptGatewayDevice(integrationId, gatewayKey, name) {
102600
102724
  const devices = this.ctx.kernel.devices;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-ecowitt",
3
- "version": "0.2.153",
3
+ "version": "0.2.154",
4
4
  "description": "Ecowitt weather-station device-provider addon for CamStack — wraps the @apocaliss92/nodewitt local-poll / push client",
5
5
  "keywords": [
6
6
  "camstack",
@@ -59,7 +59,7 @@
59
59
  }
60
60
  ],
61
61
  "requires": {
62
- "server": ">=1.2.719"
62
+ "server": ">=1.2.722"
63
63
  }
64
64
  },
65
65
  "files": [