@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.js CHANGED
@@ -5834,7 +5834,7 @@ var ZodIssueCode$1 = {
5834
5834
  var ZodFirstPartyTypeKind$1;
5835
5835
  ZodFirstPartyTypeKind$1 || (ZodFirstPartyTypeKind$1 = {});
5836
5836
  //#endregion
5837
- //#region ../types/dist/sleep-BHS4ImRX.mjs
5837
+ //#region ../types/dist/sleep-CSYuxssh.mjs
5838
5838
  /**
5839
5839
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5840
5840
  * window to float samples (D455).
@@ -9164,7 +9164,7 @@ var ProfileRtspEntrySchema = object({
9164
9164
  /**
9165
9165
  * The model weight formats a catalog entry can ship. Schema-free on purpose:
9166
9166
  * `types/models.ts` builds Zod schemas from it, but a runner-closure consumer
9167
- * (the model downloader in `@camstack/system`) needs only the list and must
9167
+ * (the model downloader in `@camstack/server/system`) needs only the list and must
9168
9168
  * reach it through `@camstack/types/addon` without building the cap schema
9169
9169
  * graph (D28).
9170
9170
  */
@@ -12553,6 +12553,71 @@ var EventOwnerTypeSchema = _enum(EVENT_OWNER_TYPES);
12553
12553
  var MediaPresenceOwnerKindSchema = _enum([...EVENT_OWNER_TYPES, "track"]);
12554
12554
  new Set(EVENT_OWNER_TYPES);
12555
12555
  /**
12556
+ * What a published package needs from the rest of the install (D989).
12557
+ *
12558
+ * One published framework package (`@camstack/server`) carries the kernel, the
12559
+ * builtins and the capability contracts, so "which addon runs on which
12560
+ * framework" is a question of version, and the answer is DECLARED by the
12561
+ * package that knows it:
12562
+ *
12563
+ * - an addon says the oldest server it runs on (`requires.server: ">=X"`);
12564
+ * - the server says the oldest addon it works with
12565
+ * (`requires.addons: { "@camstack/addon-x": ">=Y" }`) and the oldest shell
12566
+ * around it (`requires.wrapper: { desktop, image }`);
12567
+ * - a wrapper (the desktop app, the container starter) says which server
12568
+ * range it can host, and reports its own kind and version.
12569
+ *
12570
+ * The publish workflow STAMPS these after the version bump
12571
+ * (`scripts/ci/stamp-requires.mjs`); nobody types a number by hand.
12572
+ *
12573
+ * A requirement that cannot be checked is NOT met: a shell that reports no
12574
+ * version counts as below the minimum, and an unreadable running version is
12575
+ * named in the result (a failed read is never a pass).
12576
+ */
12577
+ /** `>=MAJOR.MINOR.PATCH[-prerelease]` — the only range shape a requirement uses. */
12578
+ var MinVersionSchema = string().regex(/^>=\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/, "a minimum is written \">=X.Y.Z\"");
12579
+ object({
12580
+ /** The oldest `@camstack/server` this package runs on. */
12581
+ server: MinVersionSchema.optional(),
12582
+ /** The oldest installed version of each named addon package. */
12583
+ addons: record(string(), MinVersionSchema).optional(),
12584
+ /** The oldest shell around the server, per shell kind. */
12585
+ wrapper: object({
12586
+ desktop: MinVersionSchema.optional(),
12587
+ image: MinVersionSchema.optional()
12588
+ }).optional()
12589
+ });
12590
+ /** One unmet requirement; each names the versions on both sides. */
12591
+ var UnmetRequirementSchema = discriminatedUnion("kind", [
12592
+ object({
12593
+ kind: literal("server-too-old"),
12594
+ required: string(),
12595
+ /** `null` — the running server version could not be read. */
12596
+ running: string().nullable()
12597
+ }),
12598
+ object({
12599
+ kind: literal("addon-too-old"),
12600
+ packageName: string(),
12601
+ required: string(),
12602
+ /** `null` — the addon is not installed (or its version is unreadable). */
12603
+ running: string().nullable()
12604
+ }),
12605
+ object({
12606
+ kind: literal("wrapper-too-old"),
12607
+ wrapper: _enum(["desktop", "image"]),
12608
+ required: string(),
12609
+ /** `null` — the wrapper reported no version. */
12610
+ running: string().nullable()
12611
+ })
12612
+ ]);
12613
+ var RequiredAddonUpdateSchema = object({
12614
+ packageName: string(),
12615
+ /** The minimum the target server declares. */
12616
+ required: string(),
12617
+ /** What this node has; `null` when the addon is not installed. */
12618
+ installed: string().nullable()
12619
+ });
12620
+ /**
12556
12621
  * Which integration instance a settings form is for. An addon that serves several instances
12557
12622
  * (`instanceMode: multiple`) can show a different form per instance — an NVR integration
12558
12623
  * needs its connection, the default integration needs the addon's own sections.
@@ -40827,7 +40892,8 @@ function normalizeUnit(raw) {
40827
40892
  if (lower === "dbm") return "dBm";
40828
40893
  if (lower === "dbfs") return "dBFS";
40829
40894
  if (lower === "inhg") return "inHg";
40830
- if (lower === "mm/h") return "mm/h";
40895
+ if (lower === "mm/h" || lower === "mm/hr") return "mm/h";
40896
+ if (lower === "in/h" || lower === "in/hr") return "in/h";
40831
40897
  if (lower === "w/m²" || lower === "w/m2") return "W/m²";
40832
40898
  if (lower === "°" || lower === "deg" || lower === "degree" || lower === "degrees") return "°";
40833
40899
  if (lower === "kbps") return "kbps";
@@ -43046,7 +43112,7 @@ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSche
43046
43112
  * rebuilds without manual reload.
43047
43113
  *
43048
43114
  * The hub-local builtin `addon-pages-aggregator` (see
43049
- * `@camstack/system/builtins/addon-pages-aggregator`) registers the
43115
+ * `@camstack/server/system/builtins/addon-pages-aggregator`) registers the
43050
43116
  * provider. Splitting the public aggregator from the raw collection
43051
43117
  * keeps both ends in codegen — there's no hand-written
43052
43118
  * `addon-pages.router.ts` wrapper anymore.
@@ -43346,7 +43412,7 @@ method(_void(), array(WidgetMetadataSchema).readonly(), { auth: "admin" });
43346
43412
  * manual reload — same scheme used by `addon-pages`.
43347
43413
  *
43348
43414
  * The hub-local builtin `addon-widgets-aggregator` (see
43349
- * `@camstack/system/builtins/addon-widgets-aggregator`) registers the
43415
+ * `@camstack/server/system/builtins/addon-widgets-aggregator`) registers the
43350
43416
  * provider. Splitting the public aggregator from the raw collection
43351
43417
  * keeps both ends in codegen — there's no hand-written wrapper.
43352
43418
  */
@@ -61546,7 +61612,18 @@ var ServerUpdateActionResultSchema = object({
61546
61612
  * store (D877), present when the pre-flight refused the update
61547
61613
  * (`reason: 'addon-would-refuse'`). Optional for version skew.
61548
61614
  */
61549
- blockers: array(UpdateBlockerSchema).optional()
61615
+ blockers: array(UpdateBlockerSchema).optional(),
61616
+ /**
61617
+ * The minimums the target server declares and this node does not meet
61618
+ * (D989), present when the update was refused with
61619
+ * `reason: 'requirements-unmet'`. Each names the versions on both sides.
61620
+ */
61621
+ unmet: array(UnmetRequirementSchema).optional(),
61622
+ /**
61623
+ * The addons this node has below the minimum the staged server declares —
61624
+ * the ones to update WITH the server. Present on an accepted update.
61625
+ */
61626
+ requiredAddonUpdates: array(RequiredAddonUpdateSchema).optional()
61550
61627
  });
61551
61628
  var LaunchOverridesSchema = object({
61552
61629
  /** The persisted heap ceiling in MB, or `null` when none is set. */
@@ -64090,7 +64167,9 @@ var PackageUpdateSchema = object({
64090
64167
  latestVersion: string(),
64091
64168
  category: _enum(["addon", "core"]),
64092
64169
  requiresRestart: boolean(),
64093
- isSystem: boolean()
64170
+ isSystem: boolean(),
64171
+ /** Held: what the newer version needs and this node does not meet (D989). */
64172
+ blockedBy: array(UnmetRequirementSchema).readonly().optional()
64094
64173
  });
64095
64174
  var PackageVersionInfoSchema = object({
64096
64175
  version: string(),
@@ -69373,7 +69452,7 @@ var lawnMowerControlCapability = {
69373
69452
  * clients — they reverse-connect to the hub. Exposing their interfaces
69374
69453
  * via the same surface would leak internal topology with no upside.
69375
69454
  *
69376
- * Implementation in `@camstack/system/builtins/local-network/`.
69455
+ * Implementation in `@camstack/server/system/builtins/local-network/`.
69377
69456
  */
69378
69457
  /** Coarse classification derived from the interface name + IP range. */
69379
69458
  var InterfaceKindEnum = _enum([
@@ -71261,7 +71340,7 @@ onStatusChanged: { data: object({
71261
71340
  * jitter, and observed/peak bandwidth per device + per client.
71262
71341
  *
71263
71342
  * Implementation lives in the server's `NetworkQualityService` (thin
71264
- * wrapper over the shared `NetworkQualityTracker` from `@camstack/system`).
71343
+ * wrapper over the shared `NetworkQualityTracker` from `@camstack/server/system`).
71265
71344
  * The provider is registered from `trpc.router.ts` against the existing
71266
71345
  * service instance — no addon owns this state.
71267
71346
  *
@@ -73412,7 +73491,7 @@ var ptzAutotrackCapability = {
73412
73491
  * - **Native**: the camera's own firmware autotrack, registered per device by
73413
73492
  * the driver when the probe says it exists (Reolink: yes; Hikvision:
73414
73493
  * model-dependent).
73415
- * - **Wrapper**: the emulated implementation (`@camstack/system`'s `autotrack` builtin),
73494
+ * - **Wrapper**: the emulated implementation (`@camstack/server/system`'s `autotrack` builtin),
73416
73495
  * which follows a subject with `ptz.move` pulses driven by the detection
73417
73496
  * pipeline. `defaultActive: false` — the operator binds it per camera in
73418
73497
  * the bindings UI.
@@ -95150,6 +95229,73 @@ function deviceGroupForSensor(category, channel) {
95150
95229
  return "gateway";
95151
95230
  }
95152
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
95153
95299
  //#region src/devices/ecowitt-sensor-device.ts
95154
95300
  /**
95155
95301
  * Persisted config every Ecowitt sensor child carries. `sensorId` resolves the
@@ -95487,7 +95633,7 @@ var EcowittContainerDevice = class extends BaseDevice {
95487
95633
  });
95488
95634
  }
95489
95635
  getAccessoryChildren() {
95490
- return this.config.values.sensors.map((sensor) => {
95636
+ return entriesForGroup(this.config.values.sensors, this.config.values.deviceGroup, ecowittFacades.getSensors(this.gatewayId)).map((sensor) => {
95491
95637
  const meta = {
95492
95638
  type: DeviceType.Sensor,
95493
95639
  role: roleForQuantity(sensor.quantity),
@@ -99334,7 +99480,7 @@ function parseValue(input) {
99334
99480
  raw
99335
99481
  };
99336
99482
  }
99337
- function str(item, field) {
99483
+ function str$1(item, field) {
99338
99484
  const v = item[field];
99339
99485
  if (typeof v === "string") return v;
99340
99486
  if (typeof v === "number") return String(v);
@@ -99374,7 +99520,7 @@ function makeReading(key, val, unit, forceId) {
99374
99520
  }
99375
99521
  }
99376
99522
  function channelOf(item) {
99377
- const ch = str(item, "channel");
99523
+ const ch = str$1(item, "channel");
99378
99524
  return ch && ch.trim() !== "" ? ch.trim() : void 0;
99379
99525
  }
99380
99526
  function push(out, reading) {
@@ -99384,8 +99530,8 @@ function decodeChSoil(items, out) {
99384
99530
  for (const item of items) {
99385
99531
  const ch = channelOf(item);
99386
99532
  if (ch === void 0) continue;
99387
- const humidity = stripPercent(str(item, "humidity"));
99388
- const battery = str(item, "battery");
99533
+ const humidity = stripPercent(str$1(item, "humidity"));
99534
+ const battery = str$1(item, "battery");
99389
99535
  if (humidity) push(out, makeReading(`soilmoisture${ch}`, humidity));
99390
99536
  if (battery) push(out, makeReading(`soilbatt${ch}`, batteryPercent(battery, true)));
99391
99537
  }
@@ -99394,11 +99540,11 @@ function decodeChEc(items, out) {
99394
99540
  for (const item of items) {
99395
99541
  const ch = channelOf(item);
99396
99542
  if (ch === void 0) continue;
99397
- const humidity = stripPercent(str(item, "humidity"));
99398
- const temp = str(item, "temp");
99399
- const unit = str(item, "unit") ?? "C";
99400
- const ec = str(item, "ec");
99401
- 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");
99402
99548
  if (humidity) push(out, makeReading(`soilmoisture${ch}`, humidity));
99403
99549
  if (temp) push(out, makeReading(`soiltemp${ch}`, temp, unit));
99404
99550
  if (ec) push(out, makeReading(`soilec${ch}`, ec));
@@ -99409,9 +99555,9 @@ function decodeChAisle(items, gatewayTempUnit, out) {
99409
99555
  for (const item of items) {
99410
99556
  const ch = channelOf(item);
99411
99557
  if (ch === void 0) continue;
99412
- const temp = str(item, "temp");
99413
- const humidity = str(item, "humidity");
99414
- 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");
99415
99561
  if (temp && temp !== "None") push(out, makeReading(`temp${ch}f`, temp, gatewayTempUnit));
99416
99562
  if (humidity && humidity !== "None") push(out, makeReading(`humidity${ch}`, stripPercent(humidity)));
99417
99563
  if (battery && battery !== "None") push(out, makeReading(`batt${ch}`, batteryPercent(battery, true)));
@@ -99421,8 +99567,8 @@ function decodeChTemp(items, gatewayTempUnit, out) {
99421
99567
  for (const item of items) {
99422
99568
  const ch = channelOf(item);
99423
99569
  if (ch === void 0) continue;
99424
- const temp = str(item, "temp");
99425
- const battery = str(item, "battery");
99570
+ const temp = str$1(item, "temp");
99571
+ const battery = str$1(item, "battery");
99426
99572
  if (temp && temp !== "None") push(out, makeReading(`tf_ch${ch}`, temp, gatewayTempUnit));
99427
99573
  if (battery && battery !== "None") push(out, makeReading(`tf_batt${ch}`, batteryPercent(battery, false)));
99428
99574
  }
@@ -99431,11 +99577,11 @@ function decodeChPm25(items, out) {
99431
99577
  for (const item of items) {
99432
99578
  const ch = channelOf(item);
99433
99579
  if (ch === void 0) continue;
99434
- const pm25 = str(item, "pm25") ?? str(item, "PM25");
99435
- const pm25_24h = str(item, "pm25_avg_24h") ?? str(item, "pm25_24h");
99436
- const realaqi = str(item, "PM25_RealAQI");
99437
- const aqi24h = str(item, "PM25_24HAQI");
99438
- 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");
99439
99585
  if (pm25 && pm25 !== "None") push(out, makeReading(`pm25_ch${ch}`, pm25));
99440
99586
  if (pm25_24h && pm25_24h !== "None") push(out, makeReading(`pm25_avg_24h_ch${ch}`, pm25_24h));
99441
99587
  if (realaqi && realaqi !== "None") push(out, makeReading(`pm25_aqi_realtime_ch${ch}`, realaqi));
@@ -99447,8 +99593,8 @@ function decodeChLeaf(items, out) {
99447
99593
  for (const item of items) {
99448
99594
  const ch = channelOf(item);
99449
99595
  if (ch === void 0) continue;
99450
- const humidity = stripPercent(str(item, "humidity"));
99451
- const battery = str(item, "battery");
99596
+ const humidity = stripPercent(str$1(item, "humidity"));
99597
+ const battery = str$1(item, "battery");
99452
99598
  if (humidity) push(out, makeReading(`leafwetness_ch${ch}`, humidity));
99453
99599
  if (battery && battery !== "None") push(out, makeReading(`leaf_batt${ch}`, batteryPercent(battery, false)));
99454
99600
  }
@@ -99457,8 +99603,8 @@ function decodeChLeak(items, out) {
99457
99603
  for (const item of items) {
99458
99604
  const ch = channelOf(item);
99459
99605
  if (ch === void 0) continue;
99460
- const status = str(item, "status");
99461
- const battery = str(item, "battery");
99606
+ const status = str$1(item, "status");
99607
+ const battery = str$1(item, "battery");
99462
99608
  if (status !== void 0 && status !== "") {
99463
99609
  const leak = status.trim().toLowerCase() === "normal" ? "0" : "1";
99464
99610
  push(out, makeReading(`leak_ch${ch}`, leak));
@@ -99470,10 +99616,10 @@ function decodeChLds(items, out) {
99470
99616
  for (const item of items) {
99471
99617
  const ch = channelOf(item);
99472
99618
  if (ch === void 0) continue;
99473
- const air = str(item, "air");
99474
- const depth = str(item, "depth");
99475
- const voltage = str(item, "voltage");
99476
- 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");
99477
99623
  if (air && air !== "None") push(out, makeReading(`lds_air_ch${ch}`, air));
99478
99624
  if (depth && depth !== "None") push(out, makeReading(`lds_depth_ch${ch}`, depth));
99479
99625
  if (voltage && voltage !== "None") push(out, makeReading(`lds_voltage_ch${ch}`, voltage));
@@ -99502,23 +99648,23 @@ var CO2_FIELDS = [
99502
99648
  ];
99503
99649
  function firstField(item, fields) {
99504
99650
  for (const f of fields) {
99505
- const v = str(item, f);
99651
+ const v = str$1(item, f);
99506
99652
  if (v && v !== "None") return v;
99507
99653
  }
99508
99654
  }
99509
99655
  function decodeCo2(items, out) {
99510
99656
  const item = items[0];
99511
99657
  if (item === void 0) return;
99512
- const temp = str(item, "temp");
99513
- const unit = str(item, "unit") ?? "C";
99658
+ const temp = str$1(item, "temp");
99659
+ const unit = str$1(item, "unit") ?? "C";
99514
99660
  if (temp) push(out, makeReading(unit === "C" ? "tf_co2c" : "tf_co2", temp, unit));
99515
- const humidity = stripPercent(str(item, "humidity"));
99661
+ const humidity = stripPercent(str$1(item, "humidity"));
99516
99662
  if (humidity) push(out, makeReading("humi_co2", humidity));
99517
99663
  for (const [fields, key] of CO2_FIELDS) {
99518
99664
  const v = firstField(item, fields);
99519
99665
  if (v !== void 0) push(out, makeReading(key, v));
99520
99666
  }
99521
- const battery = str(item, "battery");
99667
+ const battery = str$1(item, "battery");
99522
99668
  if (battery && battery !== "None") {
99523
99669
  const n = num(battery);
99524
99670
  push(out, makeReading("co2_batt", n !== void 0 ? String(Math.min(n * 20, 100)) : battery));
@@ -99530,12 +99676,12 @@ function push2(out, reading) {
99530
99676
  }
99531
99677
  function decodeCommonList(items, mapper, out) {
99532
99678
  for (const item of items) {
99533
- const id = str(item, "id");
99534
- const val = str(item, "val");
99535
- 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");
99536
99682
  if (id && val !== void 0) push2(out, makeReading(id, val, unit));
99537
99683
  if (id === "0x03" && mapper.getHardwareId("wh26batt") !== void 0) {
99538
- const battery = str(item, "battery");
99684
+ const battery = str$1(item, "battery");
99539
99685
  if (battery !== void 0) push2(out, makeReading("wh26batt", battery === "0" ? "100" : "10"));
99540
99686
  }
99541
99687
  }
@@ -99543,12 +99689,12 @@ function decodeCommonList(items, mapper, out) {
99543
99689
  function decodeRain(items, mapper, out) {
99544
99690
  const rainHwId = mapper.getHardwareId("wh69batt") ?? mapper.getHardwareId("wh40batt");
99545
99691
  for (const item of items) {
99546
- const id = str(item, "id");
99547
- const val = str(item, "val");
99692
+ const id = str$1(item, "id");
99693
+ const val = str$1(item, "val");
99548
99694
  if (!id || val === void 0) continue;
99549
- push2(out, makeReading(id, val, str(item, "unit"), rainHwId));
99695
+ push2(out, makeReading(id, val, str$1(item, "unit"), rainHwId));
99550
99696
  if (id === "0x13") {
99551
- const battery = str(item, "battery");
99697
+ const battery = str$1(item, "battery");
99552
99698
  if (battery) {
99553
99699
  const n = num(battery);
99554
99700
  const pct = n !== void 0 && n > 1 ? String(n * 20) : battery === "0" ? "100" : "10";
@@ -99580,30 +99726,30 @@ function piezoKeysFor(mapper) {
99580
99726
  function decodePiezo(items, mapper, out) {
99581
99727
  const piezoHwId = mapper.getHardwareId("ws85batt") ?? mapper.getHardwareId("ws90batt") ?? mapper.getHardwareId("wh90batt");
99582
99728
  for (const item of items) {
99583
- const id = str(item, "id");
99584
- const val = str(item, "val");
99729
+ const id = str$1(item, "id");
99730
+ const val = str$1(item, "val");
99585
99731
  if (!id || val === void 0) continue;
99586
- push2(out, makeReading(id, val, str(item, "unit"), piezoHwId));
99732
+ push2(out, makeReading(id, val, str$1(item, "unit"), piezoHwId));
99587
99733
  if (id !== "0x13") continue;
99588
- const battery = str(item, "battery");
99734
+ const battery = str$1(item, "battery");
99589
99735
  if (!battery) continue;
99590
99736
  const keys = piezoKeysFor(mapper);
99591
99737
  const n = num(battery);
99592
99738
  push2(out, makeReading(keys.batt, n !== void 0 ? String(n * 20) : battery));
99593
- const voltage = str(item, "voltage");
99739
+ const voltage = str$1(item, "voltage");
99594
99740
  if (voltage) push2(out, makeReading(keys.volt, voltage));
99595
- const cap = str(item, keys.capField);
99741
+ const cap = str$1(item, keys.capField);
99596
99742
  if (cap) push2(out, makeReading(keys.capKey, cap));
99597
99743
  }
99598
99744
  }
99599
99745
  function decodeLightning(items, out) {
99600
99746
  const item = items[0];
99601
99747
  if (item === void 0) return;
99602
- const count = str(item, "count");
99748
+ const count = str$1(item, "count");
99603
99749
  if (count !== void 0) push2(out, makeReading("lightning_num", count));
99604
- const distance = str(item, "distance");
99750
+ const distance = str$1(item, "distance");
99605
99751
  if (distance !== void 0) push2(out, makeReading("lightning", distance.replace(" km", "").trim()));
99606
- const battery = str(item, "battery");
99752
+ const battery = str$1(item, "battery");
99607
99753
  if (battery !== void 0) {
99608
99754
  const n = num(battery);
99609
99755
  push2(out, makeReading("wh57batt", n !== void 0 ? String(n * 20) : battery));
@@ -99612,30 +99758,30 @@ function decodeLightning(items, out) {
99612
99758
  function decodeWh25(items, out) {
99613
99759
  const item = items[0];
99614
99760
  if (item === void 0) return;
99615
- const intemp = str(item, "intemp");
99616
- const unit = str(item, "unit") ?? "F";
99761
+ const intemp = str$1(item, "intemp");
99762
+ const unit = str$1(item, "unit") ?? "F";
99617
99763
  if (intemp !== void 0) push2(out, makeReading("tempinf", intemp, unit));
99618
- const inhumi = str(item, "inhumi");
99764
+ const inhumi = str$1(item, "inhumi");
99619
99765
  if (inhumi !== void 0) push2(out, makeReading("humidityin", stripPercent(inhumi)));
99620
- const abs = str(item, "abs");
99766
+ const abs = str$1(item, "abs");
99621
99767
  if (abs !== void 0) push2(out, makeReading("baromabsin", abs.replace(" hPa", "").trim()));
99622
- const rel = str(item, "rel");
99768
+ const rel = str$1(item, "rel");
99623
99769
  if (rel !== void 0) push2(out, makeReading("baromrelin", rel.replace(" hPa", "").trim()));
99624
99770
  }
99625
99771
  function decodeSoilCalibration(items, out) {
99626
99772
  for (const item of items) {
99627
- const ch = str(item, "ch");
99628
- const nowAd = str(item, "nowAd");
99773
+ const ch = str$1(item, "ch");
99774
+ const nowAd = str$1(item, "nowAd");
99629
99775
  if (ch && nowAd !== void 0) push2(out, makeReading(`soilad${ch}`, nowAd));
99630
99776
  }
99631
99777
  }
99632
99778
  function decodeLdsConfig(items, out) {
99633
99779
  for (const item of items) {
99634
- const ch = str(item, "ch");
99780
+ const ch = str$1(item, "ch");
99635
99781
  if (!ch) continue;
99636
- const level = str(item, "level");
99782
+ const level = str$1(item, "level");
99637
99783
  if (level !== void 0 && level !== "None") push2(out, makeReading(`lds_level_ch${ch}`, level));
99638
- const totalHeat = str(item, "total_heat");
99784
+ const totalHeat = str$1(item, "total_heat");
99639
99785
  if (totalHeat !== void 0 && totalHeat !== "None") push2(out, makeReading(`lds_total_heat_ch${ch}`, totalHeat));
99640
99786
  }
99641
99787
  }
@@ -101702,50 +101848,6 @@ function buildEcowittGatewayCandidates(input) {
101702
101848
  })];
101703
101849
  }
101704
101850
  //#endregion
101705
- //#region src/ecowitt-sensor-groups.ts
101706
- /** Resolve `deviceGroup` from a persisted container config (defaults to 'gateway'). */
101707
- function configDeviceGroup(config) {
101708
- const g = config["deviceGroup"];
101709
- return ECOWITT_DEVICE_GROUPS.find((group) => group === g) ?? "gateway";
101710
- }
101711
- /** Build the persisted sensor entry (incl. self-describing category) from a live Sensor. */
101712
- function sensorToEntry(s) {
101713
- return {
101714
- sensorId: s.id,
101715
- quantity: s.quantity,
101716
- ...s.channel !== void 0 ? { channel: s.channel } : {},
101717
- ...s.name !== void 0 ? { name: s.name } : {},
101718
- ...s.category !== void 0 ? { category: s.category } : {}
101719
- };
101720
- }
101721
- /** Bucket live sensors into their device group. */
101722
- function bucketSensorsByGroup(sensors) {
101723
- const byGroup = /* @__PURE__ */ new Map();
101724
- for (const group of ECOWITT_DEVICE_GROUPS) byGroup.set(group, []);
101725
- for (const s of sensors) {
101726
- const group = deviceGroupForSensor(s.category, s.channel);
101727
- byGroup.get(group)?.push(sensorToEntry(s));
101728
- }
101729
- return byGroup;
101730
- }
101731
- /** Structural equality of two persisted sensor lists (order-independent by id + name). */
101732
- function sensorsEqual(a, b) {
101733
- if (a.length !== b.length) return false;
101734
- const aById = /* @__PURE__ */ new Map();
101735
- for (const entry of a) if (typeof entry === "object" && entry !== null && "sensorId" in entry) {
101736
- const id = entry.sensorId;
101737
- if (typeof id === "string") {
101738
- const name = "name" in entry && typeof entry.name === "string" ? entry.name : void 0;
101739
- aById.set(id, name);
101740
- }
101741
- }
101742
- for (const entry of b) {
101743
- if (!aById.has(entry.sensorId)) return false;
101744
- if (aById.get(entry.sensorId) !== entry.name) return false;
101745
- }
101746
- return true;
101747
- }
101748
- //#endregion
101749
101851
  //#region src/ecowitt-adoption-provider.ts
101750
101852
  var DEVICES_FILTER = {
101751
101853
  id: "devices",
@@ -102049,6 +102151,56 @@ function ecowittCandidates(gateways, known = /* @__PURE__ */ new Set()) {
102049
102151
  });
102050
102152
  }
102051
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
102052
102204
  //#region src/ecowitt-reconciler.ts
102053
102205
  /**
102054
102206
  * Keeps one live client per enabled integration. Devices are removed ONLY on an explicit
@@ -102502,6 +102654,55 @@ var EcowittProviderAddon = class extends BaseDeviceProvider {
102502
102654
  const devices = this.ctx.kernel.devices;
102503
102655
  if (!reg || !devices) return;
102504
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;
102505
102706
  for (const d of reg.getAllForAddon(this.addonId)) {
102506
102707
  if (d.parentDeviceId !== null || d.type !== DeviceType.Container) continue;
102507
102708
  const cfg = await devices.loadConfig(d.id).catch(() => ({}));
@@ -102509,6 +102710,7 @@ var EcowittProviderAddon = class extends BaseDeviceProvider {
102509
102710
  const nextSensors = byGroup.get(configDeviceGroup(cfg)) ?? [];
102510
102711
  if (sensorsEqual(Array.isArray(cfg["sensors"]) ? cfg["sensors"] : [], nextSensors)) continue;
102511
102712
  await devices.applyDerivedSpec(d.id, { config: { sensors: nextSensors } });
102713
+ changed = true;
102512
102714
  this.ctx.logger.info("Ecowitt: group sensors reconciled", {
102513
102715
  tags: { deviceId: d.id },
102514
102716
  meta: {
@@ -102517,6 +102719,7 @@ var EcowittProviderAddon = class extends BaseDeviceProvider {
102517
102719
  }
102518
102720
  });
102519
102721
  }
102722
+ return changed;
102520
102723
  }
102521
102724
  async adoptGatewayDevice(integrationId, gatewayKey, name) {
102522
102725
  const devices = this.ctx.kernel.devices;