@camstack/addon-provider-ecowitt 0.1.5 → 0.1.7

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.
Files changed (3) hide show
  1. package/dist/addon.js +208 -41
  2. package/dist/addon.mjs +208 -41
  3. package/package.json +2 -2
package/dist/addon.js CHANGED
@@ -10082,15 +10082,18 @@ var humiditySensorCapability = {
10082
10082
  runtimeState: HumiditySensorStatusSchema
10083
10083
  };
10084
10084
  /**
10085
- * Image display cap. Models HA `image.*` entities a single still image
10086
- * exposed by an integration (a snapshot, a chart, a generated picture).
10085
+ * Image display cap. Models a single still image exposed by an integration
10086
+ * a snapshot, a chart, a generated picture, or a robot's cleaning-map render.
10087
10087
  *
10088
- * Read-only: there are no setters. The provider resolves the HA
10089
- * `entity_picture` (a relative, signed-token path) into an ABSOLUTE URL
10090
- * the browser loads directly the token stays in the query string so no
10091
- * auth header is required. The slice carries that URL plus the upstream
10092
- * last-updated timestamp; the image changes when the entity state (a
10093
- * timestamp) changes.
10088
+ * Read-only: there are no setters. The provider resolves whatever upstream
10089
+ * source it has into an ABSOLUTE URL the browser loads directly:
10090
+ * - HA `image.*` entities the `entity_picture` signed-token path
10091
+ * (token stays in the query string, so no auth header is needed);
10092
+ * - a Dreame/robot map the cloud/OSS map-image URL (or an addon
10093
+ * data-plane URL serving the rendered map bytes), exposed as its own
10094
+ * Image child device grouped under the robot's container.
10095
+ * The slice carries that URL plus the upstream last-updated timestamp; the
10096
+ * image changes when the source's last-updated marker changes.
10094
10097
  */
10095
10098
  var ImageStatusSchema = object({
10096
10099
  /** Absolute signed URL the browser loads directly. Null when the
@@ -10116,18 +10119,47 @@ var imageCapability = {
10116
10119
  */
10117
10120
  runtimeState: ImageStatusSchema
10118
10121
  };
10122
+ /**
10123
+ * Robotic lawn-mower cap. Models HA `lawn_mower.*` entities — anything
10124
+ * with a mowing lifecycle plus a dock action.
10125
+ *
10126
+ * Activity follows HA's canonical lawn-mower lifecycle: `idle` /
10127
+ * `mowing` / `paused` / `docked` / `error`. `batteryLevel` (0..100) is
10128
+ * nullable — some mowers don't report a battery percentage.
10129
+ *
10130
+ * `startMowing` begins a mowing run, `pause` halts it in place, and
10131
+ * `dock` sends the mower back to its charging station.
10132
+ */
10133
+ var LawnMowerActivitySchema = _enum([
10134
+ "idle",
10135
+ "mowing",
10136
+ "paused",
10137
+ "docked",
10138
+ "error"
10139
+ ]);
10140
+ /** Severity of the current device/error code — info (status), warning, error. */
10141
+ var DeviceCodeSeveritySchema = _enum([
10142
+ "info",
10143
+ "warning",
10144
+ "error"
10145
+ ]);
10119
10146
  var LawnMowerControlStatusSchema = object({
10120
10147
  /** Lifecycle activity of the mower. */
10121
- activity: _enum([
10122
- "idle",
10123
- "mowing",
10124
- "paused",
10125
- "docked",
10126
- "error"
10127
- ]),
10148
+ activity: LawnMowerActivitySchema,
10128
10149
  /** 0..100 battery percentage. Null when the device has no battery
10129
10150
  * reading. */
10130
10151
  batteryLevel: number().min(0).max(100).nullable(),
10152
+ /** 0..100 mowing-completion percentage of the current task, or null when no
10153
+ * task is active / progress is unavailable. */
10154
+ progressPercent: number().min(0).max(100).nullable(),
10155
+ /** Current device/event code (dynamic — mostly status, sometimes an error),
10156
+ * or null when unknown. */
10157
+ currentCode: number().nullable(),
10158
+ /** Human label for {@link currentCode}, or null when undecodable. */
10159
+ currentCodeLabel: string().nullable(),
10160
+ /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
10161
+ * attention; `info` is normal status. */
10162
+ severity: DeviceCodeSeveritySchema,
10131
10163
  /** Ms epoch when the slice was last updated. */
10132
10164
  lastChangedAt: number()
10133
10165
  });
@@ -12353,6 +12385,7 @@ var VacuumStateSchema = _enum([
12353
12385
  "paused",
12354
12386
  "returning",
12355
12387
  "docked",
12388
+ "drying",
12356
12389
  "error"
12357
12390
  ]);
12358
12391
  /**
@@ -12391,6 +12424,12 @@ var VacuumControlStatusSchema = object({
12391
12424
  detergent: TankStatusSchema.nullable(),
12392
12425
  /** Dust bin. Null when the hardware has no dust bin. */
12393
12426
  dustBin: TankStatusSchema.nullable(),
12427
+ /** 0..100 cleaning-completion percentage of the current task, or null. */
12428
+ progressPercent: number().min(0).max(100).nullable(),
12429
+ /** Current error code (0 / null = no error). */
12430
+ errorCode: number().nullable(),
12431
+ /** Human label for {@link errorCode}, or null when none / undecodable. */
12432
+ errorLabel: string().nullable(),
12394
12433
  /** Ms epoch when the slice was last updated. */
12395
12434
  lastChangedAt: number()
12396
12435
  });
@@ -13357,9 +13396,29 @@ var BaseDevice = class {
13357
13396
  * is open (Reolink writes `hasPtz/hasIntercom`, Hikvision writes
13358
13397
  * `hasSupplementalLight/hasAlarmIo`, etc).
13359
13398
  *
13360
- * Default: no-op (driver had no probe to run).
13361
- */
13362
- async onProbe() {}
13399
+ * Default: nothing to probe mark the device PROBED (set `lastProbedAt`) so
13400
+ * the kernel treats it as ready immediately. A device that derives its shape
13401
+ * from a spec (a container, or an accessory sensor) rather than from a
13402
+ * hardware probe has no probe to "complete"; without stamping `lastProbedAt`
13403
+ * it would look perpetually un-probed — logging "Initial probe did not
13404
+ * complete" on every boot and spinning a pointless retry chain. Drivers that
13405
+ * DO probe override this and write their own `feature-probe` slice (including
13406
+ * `lastProbedAt`) once their probe actually succeeds.
13407
+ */
13408
+ async onProbe() {
13409
+ const base = this.runtimeState.getCapState("feature-probe") ?? {
13410
+ flags: {},
13411
+ deviceType: null,
13412
+ model: null,
13413
+ channelCount: null,
13414
+ lastProbedAt: 0,
13415
+ lastFetchedAt: 0
13416
+ };
13417
+ this.runtimeState.setCapState("feature-probe", {
13418
+ ...base,
13419
+ lastProbedAt: Date.now()
13420
+ });
13421
+ }
13363
13422
  /**
13364
13423
  * Phase 5 — fired after the device + its accessories are registered.
13365
13424
  * Drivers publish streams to the broker, kick off background tasks,
@@ -14821,17 +14880,32 @@ var ReleaseInputSchema = object({
14821
14880
  * the parent cascades into every accessory. */
14822
14881
  camDeviceId: number().int().nonnegative()
14823
14882
  });
14824
- var ResyncInputSchema = object({
14825
- /** Parent CamStack device id of an adopted device. The provider resolves its
14826
- * source (integration/broker + native id) and re-aligns the device's
14827
- * structural spec (type/role/capabilities/units) with the live mapping,
14828
- * rebuilding any child whose class changed while preserving operator edits. */
14829
- camDeviceId: number().int().nonnegative() });
14883
+ var ResyncInputSchema = object({
14884
+ /** Parent CamStack device id of an adopted device. The provider resolves its
14885
+ * source (integration/broker + native id) and re-aligns the device's
14886
+ * structural spec (type/role/capabilities/units) with the live mapping,
14887
+ * rebuilding any child whose class changed while preserving operator edits. */
14888
+ camDeviceId: number().int().nonnegative(),
14889
+ /** "Resync from zero" (#19). When true, the kernel PURGES every accessory
14890
+ * child of `camDeviceId` BEFORE the provider re-derives the device, so the
14891
+ * children are rebuilt fresh from source — correct names, coords, and units —
14892
+ * instead of being preserved by the incremental reconcile. Use to recover from
14893
+ * legacy generic/placeholder names that the normal name-precedence keeps frozen
14894
+ * (the operator's explicit reset). Push-driven integrations (no-op resync)
14895
+ * rebuild on their next snapshot; pull/command integrations rebuild in `resync`.
14896
+ * Operator edits on the PARENT (its name, layout, primary-child pick) survive —
14897
+ * only the children are torn down. Omitted/false ⇒ the normal incremental
14898
+ * re-sync that preserves children. */
14899
+ resetToSource: boolean().optional()
14900
+ });
14830
14901
  var ResyncResultSchema = object({
14831
14902
  /** True when the persisted spec actually changed (children may have been rebuilt). */
14832
14903
  changed: boolean(),
14833
14904
  /** Number of child devices rebuilt into a new class by this re-sync. */
14834
- rebuiltChildren: number().int().nonnegative()
14905
+ rebuiltChildren: number().int().nonnegative(),
14906
+ /** Number of accessory children torn down by a `resetToSource` purge before the
14907
+ * provider re-derived the device. 0/absent for a normal incremental re-sync. */
14908
+ removedChildren: number().int().nonnegative().optional()
14835
14909
  });
14836
14910
  var deviceAdoptionCapability = {
14837
14911
  name: "device-adoption",
@@ -16596,6 +16670,11 @@ var DeviceMetaSchema = object({
16596
16670
  addonId: string(),
16597
16671
  type: string(),
16598
16672
  name: string(),
16673
+ /** True once an operator explicitly renamed the device via `setName`. Drives
16674
+ * reconcile name-precedence (preserve operator name vs adopt fresh provider
16675
+ * name). Absent ⇒ treated as user-named (PRESERVE) for legacy rows. See
16676
+ * `DeviceMeta.userNamed`. */
16677
+ userNamed: boolean().optional(),
16599
16678
  location: string().nullable(),
16600
16679
  disabled: boolean(),
16601
16680
  parentDeviceId: number().nullable(),
@@ -16906,6 +16985,9 @@ method(object({
16906
16985
  }), method(ReleaseInputSchema.extend({ addonId: string() }), _void(), {
16907
16986
  kind: "mutation",
16908
16987
  auth: "admin"
16988
+ }), method(ResyncInputSchema, ResyncResultSchema, {
16989
+ kind: "mutation",
16990
+ auth: "admin"
16909
16991
  }), method(object({
16910
16992
  deviceId: number(),
16911
16993
  key: string(),
@@ -20668,6 +20750,12 @@ Object.freeze({
20668
20750
  addonId: null,
20669
20751
  access: "create"
20670
20752
  },
20753
+ "deviceManager.adoptionResync": {
20754
+ capName: "device-manager",
20755
+ capScope: "system",
20756
+ addonId: null,
20757
+ access: "create"
20758
+ },
20671
20759
  "deviceManager.allocateDeviceId": {
20672
20760
  capName: "device-manager",
20673
20761
  capScope: "system",
@@ -44576,7 +44664,8 @@ function classifyKey(key) {
44576
44664
  const hex = lookupHexId(key);
44577
44665
  if (hex !== void 0) return {
44578
44666
  kind: "measurement",
44579
- quantity: hex.quantity
44667
+ quantity: hex.quantity,
44668
+ name: hex.name
44580
44669
  };
44581
44670
  const named = Object.prototype.hasOwnProperty.call(NAMED_QUANTITY, key) ? NAMED_QUANTITY[key] : void 0;
44582
44671
  if (named !== void 0) return {
@@ -44612,6 +44701,7 @@ var Station = class {
44612
44701
  ...owner !== void 0 ? { hardwareId: owner } : {},
44613
44702
  ...info?.model ? { model: info.model } : {},
44614
44703
  ...info?.channel !== void 0 ? { channel: info.channel } : {},
44704
+ ...cls.name !== void 0 ? { name: cls.name } : {},
44615
44705
  quantity: cls.quantity,
44616
44706
  value: r.value,
44617
44707
  unit: r.unit,
@@ -44645,6 +44735,7 @@ var Station = class {
44645
44735
  this.upsert(id, {
44646
44736
  id,
44647
44737
  ...r.channel !== void 0 ? { channel: r.channel } : {},
44738
+ ...cls.name !== void 0 ? { name: cls.name } : {},
44648
44739
  quantity: cls.quantity,
44649
44740
  value: r.value,
44650
44741
  unit: r.unit,
@@ -45670,17 +45761,84 @@ function valueFieldForCap(cap) {
45670
45761
  }
45671
45762
  }
45672
45763
  /**
45673
- * Build a human label for a sensor from its quantity + optional channel.
45674
- * `temperature` + channel 2 → `Temperature CH2`; channel-less → `Temperature`.
45764
+ * Build a human label for a sensor. The label is resolved in priority order so
45765
+ * SAME-QUANTITY sensors are always distinguishable:
45675
45766
  *
45676
- * Accepts a plain `string` (not just {@link Quantity}) so a persisted config
45677
- * value flows through without a cast; an unknown quantity falls back to its raw
45678
- * string label.
45767
+ * 1. The SPECIFIC measurement `name` the library resolved from its hex-id
45768
+ * table (e.g. "Outdoor Temperature", "Wind Gust") present for the poll /
45769
+ * hex-id path.
45770
+ * 2. A specific name derived from the raw gateway FIELD KEY (e.g. `dailyrainin`
45771
+ * → "Daily Rain", `tempinf` → "Indoor Temperature") — covers the push /
45772
+ * named-and-pattern-key path where the library leaves `name` absent and
45773
+ * every same-quantity reading would otherwise collapse to one generic
45774
+ * label (five rain readings all "Precipitation", indoor+outdoor temp both
45775
+ * "Temperature", etc.). See {@link FIELD_KEY_LABEL}.
45776
+ * 3. The generic quantity label as a last resort.
45777
+ *
45778
+ * A channel suffix is appended when present (`temperature` + channel 2 →
45779
+ * `Temperature CH2`).
45780
+ *
45781
+ * `quantity`/`name` accept a plain `string` (not just {@link Quantity}) so a
45782
+ * persisted config value flows through without a cast; an unknown quantity falls
45783
+ * back to its raw string label. `sensorId` is the nodewitt `Sensor.id`
45784
+ * (`<owner>:<key>` or `<owner>:ch<n>:<key>`); the field key is its final
45785
+ * colon-segment.
45679
45786
  */
45680
- function sensorLabel(quantity, channel) {
45681
- const base = isKnownQuantity(quantity) ? QUANTITY_LABEL[quantity] : quantity;
45787
+ function sensorLabel(quantity, channel, name, sensorId) {
45788
+ const base = labelFromFieldKey(sensorId) ?? (name !== void 0 && name.length > 0 ? name : isKnownQuantity(quantity) ? QUANTITY_LABEL[quantity] : quantity);
45682
45789
  return channel !== void 0 ? `${base} CH${channel}` : base;
45683
45790
  }
45791
+ /**
45792
+ * Resolve a specific human label from a nodewitt sensor id by extracting its raw
45793
+ * gateway field key (the final colon-segment of `<owner>[:ch<n>]:<key>`) and
45794
+ * looking it up in {@link FIELD_KEY_LABEL}. Returns `undefined` when no id is
45795
+ * given or the key is unrecognised, so the caller falls through to the generic
45796
+ * quantity label.
45797
+ */
45798
+ function labelFromFieldKey(sensorId) {
45799
+ if (sensorId === void 0) return void 0;
45800
+ const key = sensorId.slice(sensorId.lastIndexOf(":") + 1);
45801
+ return Object.prototype.hasOwnProperty.call(FIELD_KEY_LABEL, key) ? FIELD_KEY_LABEL[key] : void 0;
45802
+ }
45803
+ /**
45804
+ * Specific human labels for the raw Ecowitt gateway field keys that the library
45805
+ * classifies via its named / pattern tables (the push path) and therefore leaves
45806
+ * WITHOUT a `name`. Mirrors the vocabulary the library's hex-id table already
45807
+ * uses for the poll path, so a station reached over either transport reads the
45808
+ * same. Channel-suffixed keys (`temp3f`, `humidity2`, …) are handled by the
45809
+ * `channel` argument and intentionally omitted here.
45810
+ */
45811
+ var FIELD_KEY_LABEL = {
45812
+ tempinf: "Indoor Temperature",
45813
+ tempf: "Outdoor Temperature",
45814
+ humidityin: "Indoor Humidity",
45815
+ humidity: "Outdoor Humidity",
45816
+ baromrelin: "Relative Pressure",
45817
+ baromabsin: "Absolute Pressure",
45818
+ windspeedmph: "Wind Speed",
45819
+ windgustmph: "Wind Gust",
45820
+ maxdailygust: "Max Daily Gust",
45821
+ winddir: "Wind Direction",
45822
+ solarradiation: "Solar Radiation",
45823
+ uv: "UV Index",
45824
+ lightning: "Lightning Distance",
45825
+ lightning_num: "Lightning Count",
45826
+ rainratein: "Rain Rate",
45827
+ eventrainin: "Rain Event",
45828
+ hourlyrainin: "Hourly Rain",
45829
+ dailyrainin: "Daily Rain",
45830
+ weeklyrainin: "Weekly Rain",
45831
+ monthlyrainin: "Monthly Rain",
45832
+ yearlyrainin: "Yearly Rain",
45833
+ totalrainin: "Total Rain",
45834
+ rrain_piezo: "Rain Rate",
45835
+ erain_piezo: "Rain Event",
45836
+ hrain_piezo: "Hourly Rain",
45837
+ drain_piezo: "Daily Rain",
45838
+ wrain_piezo: "Weekly Rain",
45839
+ mrain_piezo: "Monthly Rain",
45840
+ yrain_piezo: "Yearly Rain"
45841
+ };
45684
45842
  /** Type guard: is `q` one of the known {@link Quantity} union members? */
45685
45843
  function isKnownQuantity(q) {
45686
45844
  return Object.prototype.hasOwnProperty.call(QUANTITY_LABEL, q);
@@ -45712,7 +45870,7 @@ var QUANTITY_LABEL = {
45712
45870
  function buildEcowittGatewayCandidate(input) {
45713
45871
  const children = input.sensors.map((sensor) => ({
45714
45872
  childNativeId: sensor.id,
45715
- name: sensorLabel(sensor.quantity, sensor.channel),
45873
+ name: sensorLabel(sensor.quantity, sensor.channel, sensor.name, sensor.id),
45716
45874
  type: DeviceType.Sensor,
45717
45875
  status: "online",
45718
45876
  metadata: sensor.model !== void 0 ? { model: sensor.model } : {},
@@ -46187,7 +46345,8 @@ function clampForCap(cap, value) {
46187
46345
  var ecowittSensorEntrySchema = object({
46188
46346
  sensorId: string(),
46189
46347
  quantity: string(),
46190
- channel: number().optional()
46348
+ channel: number().optional(),
46349
+ name: string().optional()
46191
46350
  });
46192
46351
  /**
46193
46352
  * Persisted config for an Ecowitt gateway Container. `brokerId` selects the
@@ -46230,7 +46389,7 @@ var EcowittContainerDevice = class extends BaseDevice {
46230
46389
  const meta = {
46231
46390
  type: DeviceType.Sensor,
46232
46391
  role: roleForQuantity(sensor.quantity),
46233
- name: sensorLabel(sensor.quantity, sensor.channel),
46392
+ name: sensorLabel(sensor.quantity, sensor.channel, sensor.name, sensor.sensorId),
46234
46393
  linkDeviceId: this.id,
46235
46394
  ...this.integrationId !== void 0 ? { integrationId: this.integrationId } : {}
46236
46395
  };
@@ -46385,7 +46544,8 @@ var EcowittProviderAddon = class extends BaseDeviceProvider {
46385
46544
  const nextSensors = sensors.map((s) => ({
46386
46545
  sensorId: s.id,
46387
46546
  quantity: s.quantity,
46388
- ...s.channel !== void 0 ? { channel: s.channel } : {}
46547
+ ...s.channel !== void 0 ? { channel: s.channel } : {},
46548
+ ...s.name !== void 0 ? { name: s.name } : {}
46389
46549
  }));
46390
46550
  const cfg = await devices.loadConfig(containerId).catch(() => ({}));
46391
46551
  if (sensorsEqual(Array.isArray(cfg["sensors"]) ? cfg["sensors"] : [], nextSensors)) return;
@@ -46506,7 +46666,8 @@ var EcowittProviderAddon = class extends BaseDeviceProvider {
46506
46666
  sensors: ecowittFacades.getSensors(brokerId).map((s) => ({
46507
46667
  sensorId: s.id,
46508
46668
  quantity: s.quantity,
46509
- ...s.channel !== void 0 ? { channel: s.channel } : {}
46669
+ ...s.channel !== void 0 ? { channel: s.channel } : {},
46670
+ ...s.name !== void 0 ? { name: s.name } : {}
46510
46671
  })),
46511
46672
  system: "ecowitt",
46512
46673
  integrationId,
@@ -46574,12 +46735,18 @@ var EcowittProviderAddon = class extends BaseDeviceProvider {
46574
46735
  /** Structural equality of two persisted sensor lists (order-independent by id). */
46575
46736
  function sensorsEqual(a, b) {
46576
46737
  if (a.length !== b.length) return false;
46577
- const aIds = /* @__PURE__ */ new Set();
46738
+ const aById = /* @__PURE__ */ new Map();
46578
46739
  for (const entry of a) if (typeof entry === "object" && entry !== null && "sensorId" in entry) {
46579
46740
  const id = entry.sensorId;
46580
- if (typeof id === "string") aIds.add(id);
46741
+ if (typeof id === "string") {
46742
+ const name = "name" in entry && typeof entry.name === "string" ? entry.name : void 0;
46743
+ aById.set(id, name);
46744
+ }
46745
+ }
46746
+ for (const entry of b) {
46747
+ if (!aById.has(entry.sensorId)) return false;
46748
+ if (aById.get(entry.sensorId) !== entry.name) return false;
46581
46749
  }
46582
- for (const entry of b) if (!aIds.has(entry.sensorId)) return false;
46583
46750
  return true;
46584
46751
  }
46585
46752
  //#endregion
package/dist/addon.mjs CHANGED
@@ -10083,15 +10083,18 @@ var humiditySensorCapability = {
10083
10083
  runtimeState: HumiditySensorStatusSchema
10084
10084
  };
10085
10085
  /**
10086
- * Image display cap. Models HA `image.*` entities a single still image
10087
- * exposed by an integration (a snapshot, a chart, a generated picture).
10086
+ * Image display cap. Models a single still image exposed by an integration
10087
+ * a snapshot, a chart, a generated picture, or a robot's cleaning-map render.
10088
10088
  *
10089
- * Read-only: there are no setters. The provider resolves the HA
10090
- * `entity_picture` (a relative, signed-token path) into an ABSOLUTE URL
10091
- * the browser loads directly the token stays in the query string so no
10092
- * auth header is required. The slice carries that URL plus the upstream
10093
- * last-updated timestamp; the image changes when the entity state (a
10094
- * timestamp) changes.
10089
+ * Read-only: there are no setters. The provider resolves whatever upstream
10090
+ * source it has into an ABSOLUTE URL the browser loads directly:
10091
+ * - HA `image.*` entities the `entity_picture` signed-token path
10092
+ * (token stays in the query string, so no auth header is needed);
10093
+ * - a Dreame/robot map the cloud/OSS map-image URL (or an addon
10094
+ * data-plane URL serving the rendered map bytes), exposed as its own
10095
+ * Image child device grouped under the robot's container.
10096
+ * The slice carries that URL plus the upstream last-updated timestamp; the
10097
+ * image changes when the source's last-updated marker changes.
10095
10098
  */
10096
10099
  var ImageStatusSchema = object({
10097
10100
  /** Absolute signed URL the browser loads directly. Null when the
@@ -10117,18 +10120,47 @@ var imageCapability = {
10117
10120
  */
10118
10121
  runtimeState: ImageStatusSchema
10119
10122
  };
10123
+ /**
10124
+ * Robotic lawn-mower cap. Models HA `lawn_mower.*` entities — anything
10125
+ * with a mowing lifecycle plus a dock action.
10126
+ *
10127
+ * Activity follows HA's canonical lawn-mower lifecycle: `idle` /
10128
+ * `mowing` / `paused` / `docked` / `error`. `batteryLevel` (0..100) is
10129
+ * nullable — some mowers don't report a battery percentage.
10130
+ *
10131
+ * `startMowing` begins a mowing run, `pause` halts it in place, and
10132
+ * `dock` sends the mower back to its charging station.
10133
+ */
10134
+ var LawnMowerActivitySchema = _enum([
10135
+ "idle",
10136
+ "mowing",
10137
+ "paused",
10138
+ "docked",
10139
+ "error"
10140
+ ]);
10141
+ /** Severity of the current device/error code — info (status), warning, error. */
10142
+ var DeviceCodeSeveritySchema = _enum([
10143
+ "info",
10144
+ "warning",
10145
+ "error"
10146
+ ]);
10120
10147
  var LawnMowerControlStatusSchema = object({
10121
10148
  /** Lifecycle activity of the mower. */
10122
- activity: _enum([
10123
- "idle",
10124
- "mowing",
10125
- "paused",
10126
- "docked",
10127
- "error"
10128
- ]),
10149
+ activity: LawnMowerActivitySchema,
10129
10150
  /** 0..100 battery percentage. Null when the device has no battery
10130
10151
  * reading. */
10131
10152
  batteryLevel: number().min(0).max(100).nullable(),
10153
+ /** 0..100 mowing-completion percentage of the current task, or null when no
10154
+ * task is active / progress is unavailable. */
10155
+ progressPercent: number().min(0).max(100).nullable(),
10156
+ /** Current device/event code (dynamic — mostly status, sometimes an error),
10157
+ * or null when unknown. */
10158
+ currentCode: number().nullable(),
10159
+ /** Human label for {@link currentCode}, or null when undecodable. */
10160
+ currentCodeLabel: string().nullable(),
10161
+ /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
10162
+ * attention; `info` is normal status. */
10163
+ severity: DeviceCodeSeveritySchema,
10132
10164
  /** Ms epoch when the slice was last updated. */
10133
10165
  lastChangedAt: number()
10134
10166
  });
@@ -12354,6 +12386,7 @@ var VacuumStateSchema = _enum([
12354
12386
  "paused",
12355
12387
  "returning",
12356
12388
  "docked",
12389
+ "drying",
12357
12390
  "error"
12358
12391
  ]);
12359
12392
  /**
@@ -12392,6 +12425,12 @@ var VacuumControlStatusSchema = object({
12392
12425
  detergent: TankStatusSchema.nullable(),
12393
12426
  /** Dust bin. Null when the hardware has no dust bin. */
12394
12427
  dustBin: TankStatusSchema.nullable(),
12428
+ /** 0..100 cleaning-completion percentage of the current task, or null. */
12429
+ progressPercent: number().min(0).max(100).nullable(),
12430
+ /** Current error code (0 / null = no error). */
12431
+ errorCode: number().nullable(),
12432
+ /** Human label for {@link errorCode}, or null when none / undecodable. */
12433
+ errorLabel: string().nullable(),
12395
12434
  /** Ms epoch when the slice was last updated. */
12396
12435
  lastChangedAt: number()
12397
12436
  });
@@ -13358,9 +13397,29 @@ var BaseDevice = class {
13358
13397
  * is open (Reolink writes `hasPtz/hasIntercom`, Hikvision writes
13359
13398
  * `hasSupplementalLight/hasAlarmIo`, etc).
13360
13399
  *
13361
- * Default: no-op (driver had no probe to run).
13362
- */
13363
- async onProbe() {}
13400
+ * Default: nothing to probe mark the device PROBED (set `lastProbedAt`) so
13401
+ * the kernel treats it as ready immediately. A device that derives its shape
13402
+ * from a spec (a container, or an accessory sensor) rather than from a
13403
+ * hardware probe has no probe to "complete"; without stamping `lastProbedAt`
13404
+ * it would look perpetually un-probed — logging "Initial probe did not
13405
+ * complete" on every boot and spinning a pointless retry chain. Drivers that
13406
+ * DO probe override this and write their own `feature-probe` slice (including
13407
+ * `lastProbedAt`) once their probe actually succeeds.
13408
+ */
13409
+ async onProbe() {
13410
+ const base = this.runtimeState.getCapState("feature-probe") ?? {
13411
+ flags: {},
13412
+ deviceType: null,
13413
+ model: null,
13414
+ channelCount: null,
13415
+ lastProbedAt: 0,
13416
+ lastFetchedAt: 0
13417
+ };
13418
+ this.runtimeState.setCapState("feature-probe", {
13419
+ ...base,
13420
+ lastProbedAt: Date.now()
13421
+ });
13422
+ }
13364
13423
  /**
13365
13424
  * Phase 5 — fired after the device + its accessories are registered.
13366
13425
  * Drivers publish streams to the broker, kick off background tasks,
@@ -14822,17 +14881,32 @@ var ReleaseInputSchema = object({
14822
14881
  * the parent cascades into every accessory. */
14823
14882
  camDeviceId: number().int().nonnegative()
14824
14883
  });
14825
- var ResyncInputSchema = object({
14826
- /** Parent CamStack device id of an adopted device. The provider resolves its
14827
- * source (integration/broker + native id) and re-aligns the device's
14828
- * structural spec (type/role/capabilities/units) with the live mapping,
14829
- * rebuilding any child whose class changed while preserving operator edits. */
14830
- camDeviceId: number().int().nonnegative() });
14884
+ var ResyncInputSchema = object({
14885
+ /** Parent CamStack device id of an adopted device. The provider resolves its
14886
+ * source (integration/broker + native id) and re-aligns the device's
14887
+ * structural spec (type/role/capabilities/units) with the live mapping,
14888
+ * rebuilding any child whose class changed while preserving operator edits. */
14889
+ camDeviceId: number().int().nonnegative(),
14890
+ /** "Resync from zero" (#19). When true, the kernel PURGES every accessory
14891
+ * child of `camDeviceId` BEFORE the provider re-derives the device, so the
14892
+ * children are rebuilt fresh from source — correct names, coords, and units —
14893
+ * instead of being preserved by the incremental reconcile. Use to recover from
14894
+ * legacy generic/placeholder names that the normal name-precedence keeps frozen
14895
+ * (the operator's explicit reset). Push-driven integrations (no-op resync)
14896
+ * rebuild on their next snapshot; pull/command integrations rebuild in `resync`.
14897
+ * Operator edits on the PARENT (its name, layout, primary-child pick) survive —
14898
+ * only the children are torn down. Omitted/false ⇒ the normal incremental
14899
+ * re-sync that preserves children. */
14900
+ resetToSource: boolean().optional()
14901
+ });
14831
14902
  var ResyncResultSchema = object({
14832
14903
  /** True when the persisted spec actually changed (children may have been rebuilt). */
14833
14904
  changed: boolean(),
14834
14905
  /** Number of child devices rebuilt into a new class by this re-sync. */
14835
- rebuiltChildren: number().int().nonnegative()
14906
+ rebuiltChildren: number().int().nonnegative(),
14907
+ /** Number of accessory children torn down by a `resetToSource` purge before the
14908
+ * provider re-derived the device. 0/absent for a normal incremental re-sync. */
14909
+ removedChildren: number().int().nonnegative().optional()
14836
14910
  });
14837
14911
  var deviceAdoptionCapability = {
14838
14912
  name: "device-adoption",
@@ -16597,6 +16671,11 @@ var DeviceMetaSchema = object({
16597
16671
  addonId: string(),
16598
16672
  type: string(),
16599
16673
  name: string(),
16674
+ /** True once an operator explicitly renamed the device via `setName`. Drives
16675
+ * reconcile name-precedence (preserve operator name vs adopt fresh provider
16676
+ * name). Absent ⇒ treated as user-named (PRESERVE) for legacy rows. See
16677
+ * `DeviceMeta.userNamed`. */
16678
+ userNamed: boolean().optional(),
16600
16679
  location: string().nullable(),
16601
16680
  disabled: boolean(),
16602
16681
  parentDeviceId: number().nullable(),
@@ -16907,6 +16986,9 @@ method(object({
16907
16986
  }), method(ReleaseInputSchema.extend({ addonId: string() }), _void(), {
16908
16987
  kind: "mutation",
16909
16988
  auth: "admin"
16989
+ }), method(ResyncInputSchema, ResyncResultSchema, {
16990
+ kind: "mutation",
16991
+ auth: "admin"
16910
16992
  }), method(object({
16911
16993
  deviceId: number(),
16912
16994
  key: string(),
@@ -20669,6 +20751,12 @@ Object.freeze({
20669
20751
  addonId: null,
20670
20752
  access: "create"
20671
20753
  },
20754
+ "deviceManager.adoptionResync": {
20755
+ capName: "device-manager",
20756
+ capScope: "system",
20757
+ addonId: null,
20758
+ access: "create"
20759
+ },
20672
20760
  "deviceManager.allocateDeviceId": {
20673
20761
  capName: "device-manager",
20674
20762
  capScope: "system",
@@ -44577,7 +44665,8 @@ function classifyKey(key) {
44577
44665
  const hex = lookupHexId(key);
44578
44666
  if (hex !== void 0) return {
44579
44667
  kind: "measurement",
44580
- quantity: hex.quantity
44668
+ quantity: hex.quantity,
44669
+ name: hex.name
44581
44670
  };
44582
44671
  const named = Object.prototype.hasOwnProperty.call(NAMED_QUANTITY, key) ? NAMED_QUANTITY[key] : void 0;
44583
44672
  if (named !== void 0) return {
@@ -44613,6 +44702,7 @@ var Station = class {
44613
44702
  ...owner !== void 0 ? { hardwareId: owner } : {},
44614
44703
  ...info?.model ? { model: info.model } : {},
44615
44704
  ...info?.channel !== void 0 ? { channel: info.channel } : {},
44705
+ ...cls.name !== void 0 ? { name: cls.name } : {},
44616
44706
  quantity: cls.quantity,
44617
44707
  value: r.value,
44618
44708
  unit: r.unit,
@@ -44646,6 +44736,7 @@ var Station = class {
44646
44736
  this.upsert(id, {
44647
44737
  id,
44648
44738
  ...r.channel !== void 0 ? { channel: r.channel } : {},
44739
+ ...cls.name !== void 0 ? { name: cls.name } : {},
44649
44740
  quantity: cls.quantity,
44650
44741
  value: r.value,
44651
44742
  unit: r.unit,
@@ -45671,17 +45762,84 @@ function valueFieldForCap(cap) {
45671
45762
  }
45672
45763
  }
45673
45764
  /**
45674
- * Build a human label for a sensor from its quantity + optional channel.
45675
- * `temperature` + channel 2 → `Temperature CH2`; channel-less → `Temperature`.
45765
+ * Build a human label for a sensor. The label is resolved in priority order so
45766
+ * SAME-QUANTITY sensors are always distinguishable:
45676
45767
  *
45677
- * Accepts a plain `string` (not just {@link Quantity}) so a persisted config
45678
- * value flows through without a cast; an unknown quantity falls back to its raw
45679
- * string label.
45768
+ * 1. The SPECIFIC measurement `name` the library resolved from its hex-id
45769
+ * table (e.g. "Outdoor Temperature", "Wind Gust") present for the poll /
45770
+ * hex-id path.
45771
+ * 2. A specific name derived from the raw gateway FIELD KEY (e.g. `dailyrainin`
45772
+ * → "Daily Rain", `tempinf` → "Indoor Temperature") — covers the push /
45773
+ * named-and-pattern-key path where the library leaves `name` absent and
45774
+ * every same-quantity reading would otherwise collapse to one generic
45775
+ * label (five rain readings all "Precipitation", indoor+outdoor temp both
45776
+ * "Temperature", etc.). See {@link FIELD_KEY_LABEL}.
45777
+ * 3. The generic quantity label as a last resort.
45778
+ *
45779
+ * A channel suffix is appended when present (`temperature` + channel 2 →
45780
+ * `Temperature CH2`).
45781
+ *
45782
+ * `quantity`/`name` accept a plain `string` (not just {@link Quantity}) so a
45783
+ * persisted config value flows through without a cast; an unknown quantity falls
45784
+ * back to its raw string label. `sensorId` is the nodewitt `Sensor.id`
45785
+ * (`<owner>:<key>` or `<owner>:ch<n>:<key>`); the field key is its final
45786
+ * colon-segment.
45680
45787
  */
45681
- function sensorLabel(quantity, channel) {
45682
- const base = isKnownQuantity(quantity) ? QUANTITY_LABEL[quantity] : quantity;
45788
+ function sensorLabel(quantity, channel, name, sensorId) {
45789
+ const base = labelFromFieldKey(sensorId) ?? (name !== void 0 && name.length > 0 ? name : isKnownQuantity(quantity) ? QUANTITY_LABEL[quantity] : quantity);
45683
45790
  return channel !== void 0 ? `${base} CH${channel}` : base;
45684
45791
  }
45792
+ /**
45793
+ * Resolve a specific human label from a nodewitt sensor id by extracting its raw
45794
+ * gateway field key (the final colon-segment of `<owner>[:ch<n>]:<key>`) and
45795
+ * looking it up in {@link FIELD_KEY_LABEL}. Returns `undefined` when no id is
45796
+ * given or the key is unrecognised, so the caller falls through to the generic
45797
+ * quantity label.
45798
+ */
45799
+ function labelFromFieldKey(sensorId) {
45800
+ if (sensorId === void 0) return void 0;
45801
+ const key = sensorId.slice(sensorId.lastIndexOf(":") + 1);
45802
+ return Object.prototype.hasOwnProperty.call(FIELD_KEY_LABEL, key) ? FIELD_KEY_LABEL[key] : void 0;
45803
+ }
45804
+ /**
45805
+ * Specific human labels for the raw Ecowitt gateway field keys that the library
45806
+ * classifies via its named / pattern tables (the push path) and therefore leaves
45807
+ * WITHOUT a `name`. Mirrors the vocabulary the library's hex-id table already
45808
+ * uses for the poll path, so a station reached over either transport reads the
45809
+ * same. Channel-suffixed keys (`temp3f`, `humidity2`, …) are handled by the
45810
+ * `channel` argument and intentionally omitted here.
45811
+ */
45812
+ var FIELD_KEY_LABEL = {
45813
+ tempinf: "Indoor Temperature",
45814
+ tempf: "Outdoor Temperature",
45815
+ humidityin: "Indoor Humidity",
45816
+ humidity: "Outdoor Humidity",
45817
+ baromrelin: "Relative Pressure",
45818
+ baromabsin: "Absolute Pressure",
45819
+ windspeedmph: "Wind Speed",
45820
+ windgustmph: "Wind Gust",
45821
+ maxdailygust: "Max Daily Gust",
45822
+ winddir: "Wind Direction",
45823
+ solarradiation: "Solar Radiation",
45824
+ uv: "UV Index",
45825
+ lightning: "Lightning Distance",
45826
+ lightning_num: "Lightning Count",
45827
+ rainratein: "Rain Rate",
45828
+ eventrainin: "Rain Event",
45829
+ hourlyrainin: "Hourly Rain",
45830
+ dailyrainin: "Daily Rain",
45831
+ weeklyrainin: "Weekly Rain",
45832
+ monthlyrainin: "Monthly Rain",
45833
+ yearlyrainin: "Yearly Rain",
45834
+ totalrainin: "Total Rain",
45835
+ rrain_piezo: "Rain Rate",
45836
+ erain_piezo: "Rain Event",
45837
+ hrain_piezo: "Hourly Rain",
45838
+ drain_piezo: "Daily Rain",
45839
+ wrain_piezo: "Weekly Rain",
45840
+ mrain_piezo: "Monthly Rain",
45841
+ yrain_piezo: "Yearly Rain"
45842
+ };
45685
45843
  /** Type guard: is `q` one of the known {@link Quantity} union members? */
45686
45844
  function isKnownQuantity(q) {
45687
45845
  return Object.prototype.hasOwnProperty.call(QUANTITY_LABEL, q);
@@ -45713,7 +45871,7 @@ var QUANTITY_LABEL = {
45713
45871
  function buildEcowittGatewayCandidate(input) {
45714
45872
  const children = input.sensors.map((sensor) => ({
45715
45873
  childNativeId: sensor.id,
45716
- name: sensorLabel(sensor.quantity, sensor.channel),
45874
+ name: sensorLabel(sensor.quantity, sensor.channel, sensor.name, sensor.id),
45717
45875
  type: DeviceType.Sensor,
45718
45876
  status: "online",
45719
45877
  metadata: sensor.model !== void 0 ? { model: sensor.model } : {},
@@ -46188,7 +46346,8 @@ function clampForCap(cap, value) {
46188
46346
  var ecowittSensorEntrySchema = object({
46189
46347
  sensorId: string(),
46190
46348
  quantity: string(),
46191
- channel: number().optional()
46349
+ channel: number().optional(),
46350
+ name: string().optional()
46192
46351
  });
46193
46352
  /**
46194
46353
  * Persisted config for an Ecowitt gateway Container. `brokerId` selects the
@@ -46231,7 +46390,7 @@ var EcowittContainerDevice = class extends BaseDevice {
46231
46390
  const meta = {
46232
46391
  type: DeviceType.Sensor,
46233
46392
  role: roleForQuantity(sensor.quantity),
46234
- name: sensorLabel(sensor.quantity, sensor.channel),
46393
+ name: sensorLabel(sensor.quantity, sensor.channel, sensor.name, sensor.sensorId),
46235
46394
  linkDeviceId: this.id,
46236
46395
  ...this.integrationId !== void 0 ? { integrationId: this.integrationId } : {}
46237
46396
  };
@@ -46386,7 +46545,8 @@ var EcowittProviderAddon = class extends BaseDeviceProvider {
46386
46545
  const nextSensors = sensors.map((s) => ({
46387
46546
  sensorId: s.id,
46388
46547
  quantity: s.quantity,
46389
- ...s.channel !== void 0 ? { channel: s.channel } : {}
46548
+ ...s.channel !== void 0 ? { channel: s.channel } : {},
46549
+ ...s.name !== void 0 ? { name: s.name } : {}
46390
46550
  }));
46391
46551
  const cfg = await devices.loadConfig(containerId).catch(() => ({}));
46392
46552
  if (sensorsEqual(Array.isArray(cfg["sensors"]) ? cfg["sensors"] : [], nextSensors)) return;
@@ -46507,7 +46667,8 @@ var EcowittProviderAddon = class extends BaseDeviceProvider {
46507
46667
  sensors: ecowittFacades.getSensors(brokerId).map((s) => ({
46508
46668
  sensorId: s.id,
46509
46669
  quantity: s.quantity,
46510
- ...s.channel !== void 0 ? { channel: s.channel } : {}
46670
+ ...s.channel !== void 0 ? { channel: s.channel } : {},
46671
+ ...s.name !== void 0 ? { name: s.name } : {}
46511
46672
  })),
46512
46673
  system: "ecowitt",
46513
46674
  integrationId,
@@ -46575,12 +46736,18 @@ var EcowittProviderAddon = class extends BaseDeviceProvider {
46575
46736
  /** Structural equality of two persisted sensor lists (order-independent by id). */
46576
46737
  function sensorsEqual(a, b) {
46577
46738
  if (a.length !== b.length) return false;
46578
- const aIds = /* @__PURE__ */ new Set();
46739
+ const aById = /* @__PURE__ */ new Map();
46579
46740
  for (const entry of a) if (typeof entry === "object" && entry !== null && "sensorId" in entry) {
46580
46741
  const id = entry.sensorId;
46581
- if (typeof id === "string") aIds.add(id);
46742
+ if (typeof id === "string") {
46743
+ const name = "name" in entry && typeof entry.name === "string" ? entry.name : void 0;
46744
+ aById.set(id, name);
46745
+ }
46746
+ }
46747
+ for (const entry of b) {
46748
+ if (!aById.has(entry.sensorId)) return false;
46749
+ if (aById.get(entry.sensorId) !== entry.name) return false;
46582
46750
  }
46583
- for (const entry of b) if (!aIds.has(entry.sensorId)) return false;
46584
46751
  return true;
46585
46752
  }
46586
46753
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-ecowitt",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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",
@@ -71,7 +71,7 @@
71
71
  "publish": "npm publish --access public"
72
72
  },
73
73
  "devDependencies": {
74
- "@apocaliss92/nodewitt": "^1.1.1",
74
+ "@apocaliss92/nodewitt": "^1.2.0",
75
75
  "@camstack/types": "*",
76
76
  "typescript": "~6.0.3",
77
77
  "vite": "^8.0.11",