@camstack/addon-provider-homematic 1.2.118 → 1.2.119

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
@@ -48436,7 +48436,11 @@ var ALARM = [
48436
48436
  "alarm-armed",
48437
48437
  "alarm-disarmed",
48438
48438
  "alarm-arming",
48439
- "alarm-arm-refused"
48439
+ "alarm-arm-refused",
48440
+ "alarm-pending",
48441
+ "alarm-rearmed",
48442
+ "alarm-sensor-bypass",
48443
+ "alarm-check-failed"
48440
48444
  ];
48441
48445
  var COMBINED_ALARM = ["alarm-triggered"];
48442
48446
  var DEVICE_EVENTS = [
@@ -72619,7 +72623,12 @@ function readReachability(device) {
72619
72623
  * accessory-child classes reuse the SAME seeds and decoders without
72620
72624
  * duplicating them. Pure: no `BaseDevice` / facade dependency.
72621
72625
  */
72622
- /** Cold-start defaults so reads never miss a slice before the first push. */
72626
+ /**
72627
+ * Cold-start defaults so reads never miss a slice before the first push.
72628
+ * `lastChangedAt: 0` marks each as a PLACEHOLDER — the value is a guess, and
72629
+ * consumers read it as unknown. A replayed CCU value is stamped
72630
+ * `HM_SEEDED_CHANGED_AT` instead (`hm-change-stamp.ts`).
72631
+ */
72623
72632
  var COLD_SWITCH = {
72624
72633
  on: false,
72625
72634
  lastChangedAt: 0
@@ -73519,7 +73528,7 @@ var HmChildDevice = class extends BaseDevice {
73519
73528
  this.valueChangedUnsub = facade.on("valueChanged", (event) => {
73520
73529
  if (event.device !== this.address) return;
73521
73530
  if (!this.ownsEventChannel(event)) return;
73522
- handle(event);
73531
+ handle(event, "live");
73523
73532
  });
73524
73533
  this.seedTriggerUnsubs = [facade.on("ready", () => this.seedCurrentValues()), facade.on("connection", () => this.seedCurrentValues())];
73525
73534
  this.seedCurrentValues();
@@ -73618,6 +73627,11 @@ var HmChildDevice = class extends BaseDevice {
73618
73627
  * child's channel as a synthetic change — reusing the subclass decoders so the
73619
73628
  * slice reflects reality without waiting for a live change.
73620
73629
  *
73630
+ * A replay is a KNOWN value with an UNKNOWN change time, so it reaches the
73631
+ * handler as `origin: 'seed'` and the decoders never stamp it as a change
73632
+ * that happened now (`hm-change-stamp.ts`). `ts` stays the read time: it is
73633
+ * the truth for a measurement's `lastFetchedAt`.
73634
+ *
73621
73635
  * Channel-less children (event-emitter, update) are skipped: an event-emitter
73622
73636
  * would otherwise fire a spurious button press from a non-null `PRESS_*`
73623
73637
  * datapoint, and the update child reads firmware live in `getStatus`.
@@ -73650,7 +73664,7 @@ var HmChildDevice = class extends BaseDevice {
73650
73664
  value: dp.value,
73651
73665
  prevValue: null,
73652
73666
  ts: now
73653
- });
73667
+ }, "seed");
73654
73668
  }
73655
73669
  }
73656
73670
  }
@@ -73667,13 +73681,27 @@ var HmChildDevice = class extends BaseDevice {
73667
73681
  };
73668
73682
  //#endregion
73669
73683
  //#region src/homematic-decode.ts
73670
- /** HmIP `CONTROL_MODE` ENUM index → library climate mode. */
73684
+ /** HmIP `CONTROL_MODE` ENUM index → library climate mode. Numeric-arm
73685
+ * fallback only (see {@link fromHmControlMode}) — the wire index, used
73686
+ * when the value list did not resolve. */
73671
73687
  var HM_CONTROL_MODE_BY_INDEX = {
73672
73688
  0: "auto",
73673
73689
  1: "heat",
73674
73690
  2: "heat",
73675
73691
  3: "heat"
73676
73692
  };
73693
+ /**
73694
+ * HmIP `CONTROL_MODE` ENUM member (the CONVERTED value the library hands
73695
+ * over — `nodehomematic` resolves the value list, same as `LOCK_STATE` /
73696
+ * `ACTIVITY_STATE`, see {@link readLockState} / {@link readTravel}) →
73697
+ * library climate mode. Matched BY NAME, case-insensitively.
73698
+ */
73699
+ var HM_CONTROL_MODE_BY_NAME = {
73700
+ "AUTO-MODE": "auto",
73701
+ "MANU-MODE": "heat",
73702
+ "PARTY-MODE": "heat",
73703
+ "BOOST-MODE": "heat"
73704
+ };
73677
73705
  /** Map a CamStack `HvacMode` to the library's narrower climate mode. */
73678
73706
  function toHmClimateMode(mode) {
73679
73707
  switch (mode) {
@@ -73683,8 +73711,21 @@ function toHmClimateMode(mode) {
73683
73711
  default: return "heat";
73684
73712
  }
73685
73713
  }
73686
- /** Map a library climate mode back to the CamStack `HvacMode`. */
73714
+ /**
73715
+ * Map a library climate mode back to the CamStack `HvacMode`.
73716
+ *
73717
+ * Like {@link readLockState} / {@link readTravel}, the library delivers a
73718
+ * CONVERTED ENUM string on the live CCU (`'AUTO-MODE'` etc) — testing
73719
+ * `typeof value === 'number'` alone meant a resolved `CONTROL_MODE` never
73720
+ * decoded at all. The numeric arm survives for a datapoint whose value
73721
+ * list did not resolve.
73722
+ */
73687
73723
  function fromHmControlMode(value) {
73724
+ if (typeof value === "string") {
73725
+ const mode = HM_CONTROL_MODE_BY_NAME[value.toUpperCase()];
73726
+ if (mode === void 0) return null;
73727
+ return mode === "auto" ? "auto" : "heat";
73728
+ }
73688
73729
  if (typeof value !== "number") return null;
73689
73730
  const mode = HM_CONTROL_MODE_BY_INDEX[value];
73690
73731
  if (mode === void 0) return null;
@@ -73787,16 +73828,47 @@ function readTravel(value) {
73787
73828
  return "unknown";
73788
73829
  }
73789
73830
  /**
73790
- * Decode a lock datapoint into a LockControlStatus patch. `LOCK_STATE`
73791
- * is an ENUM (0 unknown, 1 locked, 2 unlocked); `ACTIVITY_STATE`
73792
- * reflects the motor moving (3 = moving → locking/unlocking transient).
73831
+ * Read `LOCK_STATE`, in either form the library hands it over.
73832
+ *
73833
+ * Like the cover's travel ENUM (see {@link readTravel}), the library delivers a
73834
+ * CONVERTED ENUM — the value-list member, `UNKNOWN|LOCKED|UNLOCKED` on HmIP —
73835
+ * and only falls back to the wire index (0 unknown, 1 locked, 2 unlocked) when
73836
+ * the value list did not resolve. Testing `typeof value === 'number'` alone
73837
+ * meant a resolved lock never decoded at all.
73793
73838
  */
73794
- function decodeLock(previous, parameter, value, now) {
73795
- if (parameter === "LOCK_STATE" && typeof value === "number") return {
73796
- state: value === 1 ? "locked" : value === 2 ? "unlocked" : previous.state,
73839
+ function readLockState(value) {
73840
+ if (typeof value === "string") {
73841
+ const v = value.toUpperCase();
73842
+ if (v === "LOCKED") return "locked";
73843
+ if (v === "UNLOCKED") return "unlocked";
73844
+ return "unknown";
73845
+ }
73846
+ if (value === 1) return "locked";
73847
+ if (value === 2) return "unlocked";
73848
+ return "unknown";
73849
+ }
73850
+ /**
73851
+ * Decode a lock datapoint into a LockControlStatus patch, or `null` when the
73852
+ * datapoint carries nothing to write.
73853
+ *
73854
+ * An UNKNOWN `LOCK_STATE` is `null` — it never becomes a reading. It used to
73855
+ * keep `previous.state` and stamp it `now`: after a respawn `previous` is
73856
+ * `COLD_LOCK`, a placeholder `unlocked` (`lastChangedAt: 0`), and the CCU
73857
+ * saying "I don't know" turned it into a real, fresh unlock the alarm could
73858
+ * act on. Returning `null` leaves a placeholder a placeholder, and a real
73859
+ * reading exactly as it was, timestamp included (D49: unknown is never a
73860
+ * state). The caller logs the drop. Unlike the other decoders in this file,
73861
+ * no reading is ever derived from the previous slice, so there is no
73862
+ * `previous` parameter to carry.
73863
+ */
73864
+ function decodeLock(parameter, value, now) {
73865
+ if (parameter !== "LOCK_STATE") return null;
73866
+ const reading = readLockState(value);
73867
+ if (reading === "unknown") return null;
73868
+ return {
73869
+ state: reading,
73797
73870
  lastChangedAt: now
73798
73871
  };
73799
- return null;
73800
73872
  }
73801
73873
  /**
73802
73874
  * Decode a climate datapoint into a ClimateControlStatus patch.
@@ -73835,6 +73907,29 @@ function decodeClimate(previous, parameter, value, now) {
73835
73907
  };
73836
73908
  return null;
73837
73909
  }
73910
+ /**
73911
+ * The change stamp for one decoded update.
73912
+ *
73913
+ * `live` ⇒ `now`. `seed` ⇒ the previous stamp when the level did not move and
73914
+ * that stamp is a real one, otherwise {@link HM_SEEDED_CHANGED_AT} — never `0`
73915
+ * (the value is known) and never `now` (its change time is not).
73916
+ */
73917
+ function changeStamp(origin, now, previousChangedAt, levelUnchanged) {
73918
+ if (origin === "live") return now;
73919
+ if (levelUnchanged && previousChangedAt !== null && previousChangedAt > 0) return previousChangedAt;
73920
+ return 1;
73921
+ }
73922
+ /**
73923
+ * Re-stamp a decoder's result for its origin. Decoders stay pure and stamp
73924
+ * `now`; a seeded result is corrected here, so the rule lives in one place.
73925
+ */
73926
+ function stampForOrigin(origin, previous, next, levelUnchanged) {
73927
+ if (origin === "live") return next;
73928
+ return {
73929
+ ...next,
73930
+ lastChangedAt: changeStamp(origin, next.lastChangedAt, previous.lastChangedAt, levelUnchanged)
73931
+ };
73932
+ }
73838
73933
  //#endregion
73839
73934
  //#region src/devices/hm-actuator-device.ts
73840
73935
  /** Actuator child config: the owning channel + the custom-entity kind. */
@@ -73864,7 +73959,7 @@ var HmActuatorDevice = class extends HmChildDevice {
73864
73959
  super(ctx, hmActuatorSchema);
73865
73960
  this.kind = this.config.values.kind;
73866
73961
  this.registerActuatorCap();
73867
- this.attachValueChangedListener((event) => this.handleValueChanged(event));
73962
+ this.attachValueChangedListener((event, origin) => this.handleValueChanged(event, origin));
73868
73963
  }
73869
73964
  get actuatorChannel() {
73870
73965
  return this.config.values.channel;
@@ -73957,40 +74052,58 @@ var HmActuatorDevice = class extends HmChildDevice {
73957
74052
  this.runtimeState.setCapState("climate-control", COLD_CLIMATE);
73958
74053
  this.registeredCap = "climate-control";
73959
74054
  }
73960
- handleValueChanged(event) {
74055
+ handleValueChanged(event, origin) {
73961
74056
  const now = event.ts || Date.now();
73962
74057
  const { parameter, value } = event;
73963
74058
  const cap = this.registeredCap;
73964
74059
  if (cap === "switch" && parameter === "STATE" && typeof value === "boolean") {
74060
+ const prev = this.runtimeState.getCapState("switch") ?? COLD_SWITCH;
74061
+ const lastChangedAt = changeStamp(origin, now, prev.lastChangedAt, prev.on === value);
73965
74062
  this.runtimeState.setCapState("switch", {
73966
74063
  on: value,
73967
- lastChangedAt: now
74064
+ lastChangedAt
73968
74065
  });
73969
74066
  return;
73970
74067
  }
73971
- if (cap === "cover") {
73972
- const next = decodeCover(this.runtimeState.getCapState("cover") ?? COLD_COVER, parameter, value, now);
73973
- if (next === null && (parameter === "ACTIVITY_STATE" || parameter === "DIRECTION")) this.ctx.logger.warn("Homematic cover: travel value not understood — state unchanged", {
74068
+ if (cap === "cover") return this.applyCover(parameter, value, now, origin);
74069
+ if (cap === "lock-control") return this.applyLock(parameter, value, now, origin);
74070
+ if (cap === "climate-control") {
74071
+ const next = decodeClimate(this.runtimeState.getCapState("climate-control") ?? COLD_CLIMATE, parameter, value, now);
74072
+ if (next) this.runtimeState.setCapState("climate-control", next);
74073
+ }
74074
+ }
74075
+ applyCover(parameter, value, now, origin) {
74076
+ const prev = this.runtimeState.getCapState("cover") ?? COLD_COVER;
74077
+ const next = decodeCover(prev, parameter, value, now);
74078
+ if (next === null && (parameter === "ACTIVITY_STATE" || parameter === "DIRECTION")) this.ctx.logger.warn("Homematic cover: travel value not understood — state unchanged", {
74079
+ tags: { deviceId: this.id },
74080
+ meta: {
74081
+ deviceId: this.id,
74082
+ parameter,
74083
+ value,
74084
+ valueType: typeof value
74085
+ }
74086
+ });
74087
+ if (next === null) return;
74088
+ this.runtimeState.setCapState("cover", stampForOrigin(origin, prev, next, next.state === prev.state));
74089
+ }
74090
+ applyLock(parameter, value, now, origin) {
74091
+ const prev = this.runtimeState.getCapState("lock-control") ?? COLD_LOCK;
74092
+ const next = decodeLock(parameter, value, now);
74093
+ if (next === null) {
74094
+ if (parameter === "LOCK_STATE") this.ctx.logger.info("Homematic lock: LOCK_STATE unknown — state unchanged", {
73974
74095
  tags: { deviceId: this.id },
73975
74096
  meta: {
73976
- deviceId: this.id,
73977
- parameter,
73978
74097
  value,
73979
- valueType: typeof value
74098
+ valueType: typeof value,
74099
+ origin,
74100
+ keptState: prev.state,
74101
+ keptPlaceholder: prev.lastChangedAt === 0
73980
74102
  }
73981
74103
  });
73982
- if (next) this.runtimeState.setCapState("cover", next);
73983
74104
  return;
73984
74105
  }
73985
- if (cap === "lock-control") {
73986
- const next = decodeLock(this.runtimeState.getCapState("lock-control") ?? COLD_LOCK, parameter, value, now);
73987
- if (next) this.runtimeState.setCapState("lock-control", next);
73988
- return;
73989
- }
73990
- if (cap === "climate-control") {
73991
- const next = decodeClimate(this.runtimeState.getCapState("climate-control") ?? COLD_CLIMATE, parameter, value, now);
73992
- if (next) this.runtimeState.setCapState("climate-control", next);
73993
- }
74106
+ this.runtimeState.setCapState("lock-control", stampForOrigin(origin, prev, next, next.state === prev.state));
73994
74107
  }
73995
74108
  };
73996
74109
  //#endregion
@@ -74051,7 +74164,7 @@ var HmSensorDevice = class extends HmChildDevice {
74051
74164
  super(ctx, hmSensorSchema);
74052
74165
  this.caps = new Set(this.config.values.capabilities);
74053
74166
  this.registerSensorCaps();
74054
- this.attachValueChangedListener((event) => this.handleValueChanged(event));
74167
+ this.attachValueChangedListener((event, origin) => this.handleValueChanged(event, origin));
74055
74168
  }
74056
74169
  registerSensorCaps() {
74057
74170
  for (const cap of this.caps) switch (cap) {
@@ -74086,14 +74199,17 @@ var HmSensorDevice = class extends HmChildDevice {
74086
74199
  });
74087
74200
  this.runtimeState.setCapState("motion", COLD_MOTION);
74088
74201
  }
74089
- handleValueChanged(event) {
74202
+ handleValueChanged(event, origin) {
74090
74203
  const now = event.ts || Date.now();
74091
74204
  const { parameter, value } = event;
74092
- if (this.caps.has("motion") && parameter === "MOTION" && typeof value === "boolean") this.runtimeState.setCapState("motion", {
74093
- detected: value,
74094
- lastDetectedAt: value ? now : this.runtimeState.getCapState("motion")?.lastDetectedAt ?? null,
74095
- autoClearAfterMs: null
74096
- });
74205
+ if (this.caps.has("motion") && parameter === "MOTION" && typeof value === "boolean") {
74206
+ const prev = this.runtimeState.getCapState("motion") ?? COLD_MOTION;
74207
+ this.runtimeState.setCapState("motion", {
74208
+ detected: value,
74209
+ lastDetectedAt: value ? changeStamp(origin, now, prev.lastDetectedAt, prev.detected) : prev.lastDetectedAt,
74210
+ autoClearAfterMs: null
74211
+ });
74212
+ }
74097
74213
  this.decodeSensorPush(parameter, value, now);
74098
74214
  }
74099
74215
  decodeSensorPush(parameter, value, now) {
@@ -74265,7 +74381,7 @@ var HmDiagnosticDevice = class extends HmChildDevice {
74265
74381
  this.unit = cfg.unit;
74266
74382
  this.sourceParams = new Set(cfg.sourceParams.map((p) => p.toUpperCase()));
74267
74383
  this.registerDiagnosticCap();
74268
- this.attachValueChangedListener((event) => this.handleValueChanged(event));
74384
+ this.attachValueChangedListener((event, origin) => this.handleValueChanged(event, origin));
74269
74385
  }
74270
74386
  registerDiagnosticCap() {
74271
74387
  switch (this.capName) {
@@ -74285,18 +74401,20 @@ var HmDiagnosticDevice = class extends HmChildDevice {
74285
74401
  }
74286
74402
  }
74287
74403
  }
74288
- handleValueChanged(event) {
74404
+ handleValueChanged(event, origin) {
74289
74405
  const now = event.ts || Date.now();
74290
74406
  const parameter = event.parameter.toUpperCase();
74291
74407
  if (!this.sourceParams.has(parameter)) return;
74292
- this.applyValue(event.value, now);
74408
+ this.applyValue(event.value, now, origin);
74293
74409
  }
74294
- applyValue(value, now) {
74410
+ applyValue(value, now, origin) {
74295
74411
  if (this.capName === "binary") {
74296
74412
  if (typeof value !== "boolean") return;
74413
+ const prev = this.runtimeState.getCapState("binary") ?? COLD_BINARY;
74414
+ const lastChangedAt = changeStamp(origin, now, prev.lastChangedAt, prev.on === value);
74297
74415
  this.runtimeState.setCapState("binary", {
74298
74416
  on: value,
74299
- lastChangedAt: now
74417
+ lastChangedAt
74300
74418
  });
74301
74419
  return;
74302
74420
  }
package/dist/addon.mjs CHANGED
@@ -48437,7 +48437,11 @@ var ALARM = [
48437
48437
  "alarm-armed",
48438
48438
  "alarm-disarmed",
48439
48439
  "alarm-arming",
48440
- "alarm-arm-refused"
48440
+ "alarm-arm-refused",
48441
+ "alarm-pending",
48442
+ "alarm-rearmed",
48443
+ "alarm-sensor-bypass",
48444
+ "alarm-check-failed"
48441
48445
  ];
48442
48446
  var COMBINED_ALARM = ["alarm-triggered"];
48443
48447
  var DEVICE_EVENTS = [
@@ -72620,7 +72624,12 @@ function readReachability(device) {
72620
72624
  * accessory-child classes reuse the SAME seeds and decoders without
72621
72625
  * duplicating them. Pure: no `BaseDevice` / facade dependency.
72622
72626
  */
72623
- /** Cold-start defaults so reads never miss a slice before the first push. */
72627
+ /**
72628
+ * Cold-start defaults so reads never miss a slice before the first push.
72629
+ * `lastChangedAt: 0` marks each as a PLACEHOLDER — the value is a guess, and
72630
+ * consumers read it as unknown. A replayed CCU value is stamped
72631
+ * `HM_SEEDED_CHANGED_AT` instead (`hm-change-stamp.ts`).
72632
+ */
72624
72633
  var COLD_SWITCH = {
72625
72634
  on: false,
72626
72635
  lastChangedAt: 0
@@ -73520,7 +73529,7 @@ var HmChildDevice = class extends BaseDevice {
73520
73529
  this.valueChangedUnsub = facade.on("valueChanged", (event) => {
73521
73530
  if (event.device !== this.address) return;
73522
73531
  if (!this.ownsEventChannel(event)) return;
73523
- handle(event);
73532
+ handle(event, "live");
73524
73533
  });
73525
73534
  this.seedTriggerUnsubs = [facade.on("ready", () => this.seedCurrentValues()), facade.on("connection", () => this.seedCurrentValues())];
73526
73535
  this.seedCurrentValues();
@@ -73619,6 +73628,11 @@ var HmChildDevice = class extends BaseDevice {
73619
73628
  * child's channel as a synthetic change — reusing the subclass decoders so the
73620
73629
  * slice reflects reality without waiting for a live change.
73621
73630
  *
73631
+ * A replay is a KNOWN value with an UNKNOWN change time, so it reaches the
73632
+ * handler as `origin: 'seed'` and the decoders never stamp it as a change
73633
+ * that happened now (`hm-change-stamp.ts`). `ts` stays the read time: it is
73634
+ * the truth for a measurement's `lastFetchedAt`.
73635
+ *
73622
73636
  * Channel-less children (event-emitter, update) are skipped: an event-emitter
73623
73637
  * would otherwise fire a spurious button press from a non-null `PRESS_*`
73624
73638
  * datapoint, and the update child reads firmware live in `getStatus`.
@@ -73651,7 +73665,7 @@ var HmChildDevice = class extends BaseDevice {
73651
73665
  value: dp.value,
73652
73666
  prevValue: null,
73653
73667
  ts: now
73654
- });
73668
+ }, "seed");
73655
73669
  }
73656
73670
  }
73657
73671
  }
@@ -73668,13 +73682,27 @@ var HmChildDevice = class extends BaseDevice {
73668
73682
  };
73669
73683
  //#endregion
73670
73684
  //#region src/homematic-decode.ts
73671
- /** HmIP `CONTROL_MODE` ENUM index → library climate mode. */
73685
+ /** HmIP `CONTROL_MODE` ENUM index → library climate mode. Numeric-arm
73686
+ * fallback only (see {@link fromHmControlMode}) — the wire index, used
73687
+ * when the value list did not resolve. */
73672
73688
  var HM_CONTROL_MODE_BY_INDEX = {
73673
73689
  0: "auto",
73674
73690
  1: "heat",
73675
73691
  2: "heat",
73676
73692
  3: "heat"
73677
73693
  };
73694
+ /**
73695
+ * HmIP `CONTROL_MODE` ENUM member (the CONVERTED value the library hands
73696
+ * over — `nodehomematic` resolves the value list, same as `LOCK_STATE` /
73697
+ * `ACTIVITY_STATE`, see {@link readLockState} / {@link readTravel}) →
73698
+ * library climate mode. Matched BY NAME, case-insensitively.
73699
+ */
73700
+ var HM_CONTROL_MODE_BY_NAME = {
73701
+ "AUTO-MODE": "auto",
73702
+ "MANU-MODE": "heat",
73703
+ "PARTY-MODE": "heat",
73704
+ "BOOST-MODE": "heat"
73705
+ };
73678
73706
  /** Map a CamStack `HvacMode` to the library's narrower climate mode. */
73679
73707
  function toHmClimateMode(mode) {
73680
73708
  switch (mode) {
@@ -73684,8 +73712,21 @@ function toHmClimateMode(mode) {
73684
73712
  default: return "heat";
73685
73713
  }
73686
73714
  }
73687
- /** Map a library climate mode back to the CamStack `HvacMode`. */
73715
+ /**
73716
+ * Map a library climate mode back to the CamStack `HvacMode`.
73717
+ *
73718
+ * Like {@link readLockState} / {@link readTravel}, the library delivers a
73719
+ * CONVERTED ENUM string on the live CCU (`'AUTO-MODE'` etc) — testing
73720
+ * `typeof value === 'number'` alone meant a resolved `CONTROL_MODE` never
73721
+ * decoded at all. The numeric arm survives for a datapoint whose value
73722
+ * list did not resolve.
73723
+ */
73688
73724
  function fromHmControlMode(value) {
73725
+ if (typeof value === "string") {
73726
+ const mode = HM_CONTROL_MODE_BY_NAME[value.toUpperCase()];
73727
+ if (mode === void 0) return null;
73728
+ return mode === "auto" ? "auto" : "heat";
73729
+ }
73689
73730
  if (typeof value !== "number") return null;
73690
73731
  const mode = HM_CONTROL_MODE_BY_INDEX[value];
73691
73732
  if (mode === void 0) return null;
@@ -73788,16 +73829,47 @@ function readTravel(value) {
73788
73829
  return "unknown";
73789
73830
  }
73790
73831
  /**
73791
- * Decode a lock datapoint into a LockControlStatus patch. `LOCK_STATE`
73792
- * is an ENUM (0 unknown, 1 locked, 2 unlocked); `ACTIVITY_STATE`
73793
- * reflects the motor moving (3 = moving → locking/unlocking transient).
73832
+ * Read `LOCK_STATE`, in either form the library hands it over.
73833
+ *
73834
+ * Like the cover's travel ENUM (see {@link readTravel}), the library delivers a
73835
+ * CONVERTED ENUM — the value-list member, `UNKNOWN|LOCKED|UNLOCKED` on HmIP —
73836
+ * and only falls back to the wire index (0 unknown, 1 locked, 2 unlocked) when
73837
+ * the value list did not resolve. Testing `typeof value === 'number'` alone
73838
+ * meant a resolved lock never decoded at all.
73794
73839
  */
73795
- function decodeLock(previous, parameter, value, now) {
73796
- if (parameter === "LOCK_STATE" && typeof value === "number") return {
73797
- state: value === 1 ? "locked" : value === 2 ? "unlocked" : previous.state,
73840
+ function readLockState(value) {
73841
+ if (typeof value === "string") {
73842
+ const v = value.toUpperCase();
73843
+ if (v === "LOCKED") return "locked";
73844
+ if (v === "UNLOCKED") return "unlocked";
73845
+ return "unknown";
73846
+ }
73847
+ if (value === 1) return "locked";
73848
+ if (value === 2) return "unlocked";
73849
+ return "unknown";
73850
+ }
73851
+ /**
73852
+ * Decode a lock datapoint into a LockControlStatus patch, or `null` when the
73853
+ * datapoint carries nothing to write.
73854
+ *
73855
+ * An UNKNOWN `LOCK_STATE` is `null` — it never becomes a reading. It used to
73856
+ * keep `previous.state` and stamp it `now`: after a respawn `previous` is
73857
+ * `COLD_LOCK`, a placeholder `unlocked` (`lastChangedAt: 0`), and the CCU
73858
+ * saying "I don't know" turned it into a real, fresh unlock the alarm could
73859
+ * act on. Returning `null` leaves a placeholder a placeholder, and a real
73860
+ * reading exactly as it was, timestamp included (D49: unknown is never a
73861
+ * state). The caller logs the drop. Unlike the other decoders in this file,
73862
+ * no reading is ever derived from the previous slice, so there is no
73863
+ * `previous` parameter to carry.
73864
+ */
73865
+ function decodeLock(parameter, value, now) {
73866
+ if (parameter !== "LOCK_STATE") return null;
73867
+ const reading = readLockState(value);
73868
+ if (reading === "unknown") return null;
73869
+ return {
73870
+ state: reading,
73798
73871
  lastChangedAt: now
73799
73872
  };
73800
- return null;
73801
73873
  }
73802
73874
  /**
73803
73875
  * Decode a climate datapoint into a ClimateControlStatus patch.
@@ -73836,6 +73908,29 @@ function decodeClimate(previous, parameter, value, now) {
73836
73908
  };
73837
73909
  return null;
73838
73910
  }
73911
+ /**
73912
+ * The change stamp for one decoded update.
73913
+ *
73914
+ * `live` ⇒ `now`. `seed` ⇒ the previous stamp when the level did not move and
73915
+ * that stamp is a real one, otherwise {@link HM_SEEDED_CHANGED_AT} — never `0`
73916
+ * (the value is known) and never `now` (its change time is not).
73917
+ */
73918
+ function changeStamp(origin, now, previousChangedAt, levelUnchanged) {
73919
+ if (origin === "live") return now;
73920
+ if (levelUnchanged && previousChangedAt !== null && previousChangedAt > 0) return previousChangedAt;
73921
+ return 1;
73922
+ }
73923
+ /**
73924
+ * Re-stamp a decoder's result for its origin. Decoders stay pure and stamp
73925
+ * `now`; a seeded result is corrected here, so the rule lives in one place.
73926
+ */
73927
+ function stampForOrigin(origin, previous, next, levelUnchanged) {
73928
+ if (origin === "live") return next;
73929
+ return {
73930
+ ...next,
73931
+ lastChangedAt: changeStamp(origin, next.lastChangedAt, previous.lastChangedAt, levelUnchanged)
73932
+ };
73933
+ }
73839
73934
  //#endregion
73840
73935
  //#region src/devices/hm-actuator-device.ts
73841
73936
  /** Actuator child config: the owning channel + the custom-entity kind. */
@@ -73865,7 +73960,7 @@ var HmActuatorDevice = class extends HmChildDevice {
73865
73960
  super(ctx, hmActuatorSchema);
73866
73961
  this.kind = this.config.values.kind;
73867
73962
  this.registerActuatorCap();
73868
- this.attachValueChangedListener((event) => this.handleValueChanged(event));
73963
+ this.attachValueChangedListener((event, origin) => this.handleValueChanged(event, origin));
73869
73964
  }
73870
73965
  get actuatorChannel() {
73871
73966
  return this.config.values.channel;
@@ -73958,40 +74053,58 @@ var HmActuatorDevice = class extends HmChildDevice {
73958
74053
  this.runtimeState.setCapState("climate-control", COLD_CLIMATE);
73959
74054
  this.registeredCap = "climate-control";
73960
74055
  }
73961
- handleValueChanged(event) {
74056
+ handleValueChanged(event, origin) {
73962
74057
  const now = event.ts || Date.now();
73963
74058
  const { parameter, value } = event;
73964
74059
  const cap = this.registeredCap;
73965
74060
  if (cap === "switch" && parameter === "STATE" && typeof value === "boolean") {
74061
+ const prev = this.runtimeState.getCapState("switch") ?? COLD_SWITCH;
74062
+ const lastChangedAt = changeStamp(origin, now, prev.lastChangedAt, prev.on === value);
73966
74063
  this.runtimeState.setCapState("switch", {
73967
74064
  on: value,
73968
- lastChangedAt: now
74065
+ lastChangedAt
73969
74066
  });
73970
74067
  return;
73971
74068
  }
73972
- if (cap === "cover") {
73973
- const next = decodeCover(this.runtimeState.getCapState("cover") ?? COLD_COVER, parameter, value, now);
73974
- if (next === null && (parameter === "ACTIVITY_STATE" || parameter === "DIRECTION")) this.ctx.logger.warn("Homematic cover: travel value not understood — state unchanged", {
74069
+ if (cap === "cover") return this.applyCover(parameter, value, now, origin);
74070
+ if (cap === "lock-control") return this.applyLock(parameter, value, now, origin);
74071
+ if (cap === "climate-control") {
74072
+ const next = decodeClimate(this.runtimeState.getCapState("climate-control") ?? COLD_CLIMATE, parameter, value, now);
74073
+ if (next) this.runtimeState.setCapState("climate-control", next);
74074
+ }
74075
+ }
74076
+ applyCover(parameter, value, now, origin) {
74077
+ const prev = this.runtimeState.getCapState("cover") ?? COLD_COVER;
74078
+ const next = decodeCover(prev, parameter, value, now);
74079
+ if (next === null && (parameter === "ACTIVITY_STATE" || parameter === "DIRECTION")) this.ctx.logger.warn("Homematic cover: travel value not understood — state unchanged", {
74080
+ tags: { deviceId: this.id },
74081
+ meta: {
74082
+ deviceId: this.id,
74083
+ parameter,
74084
+ value,
74085
+ valueType: typeof value
74086
+ }
74087
+ });
74088
+ if (next === null) return;
74089
+ this.runtimeState.setCapState("cover", stampForOrigin(origin, prev, next, next.state === prev.state));
74090
+ }
74091
+ applyLock(parameter, value, now, origin) {
74092
+ const prev = this.runtimeState.getCapState("lock-control") ?? COLD_LOCK;
74093
+ const next = decodeLock(parameter, value, now);
74094
+ if (next === null) {
74095
+ if (parameter === "LOCK_STATE") this.ctx.logger.info("Homematic lock: LOCK_STATE unknown — state unchanged", {
73975
74096
  tags: { deviceId: this.id },
73976
74097
  meta: {
73977
- deviceId: this.id,
73978
- parameter,
73979
74098
  value,
73980
- valueType: typeof value
74099
+ valueType: typeof value,
74100
+ origin,
74101
+ keptState: prev.state,
74102
+ keptPlaceholder: prev.lastChangedAt === 0
73981
74103
  }
73982
74104
  });
73983
- if (next) this.runtimeState.setCapState("cover", next);
73984
74105
  return;
73985
74106
  }
73986
- if (cap === "lock-control") {
73987
- const next = decodeLock(this.runtimeState.getCapState("lock-control") ?? COLD_LOCK, parameter, value, now);
73988
- if (next) this.runtimeState.setCapState("lock-control", next);
73989
- return;
73990
- }
73991
- if (cap === "climate-control") {
73992
- const next = decodeClimate(this.runtimeState.getCapState("climate-control") ?? COLD_CLIMATE, parameter, value, now);
73993
- if (next) this.runtimeState.setCapState("climate-control", next);
73994
- }
74107
+ this.runtimeState.setCapState("lock-control", stampForOrigin(origin, prev, next, next.state === prev.state));
73995
74108
  }
73996
74109
  };
73997
74110
  //#endregion
@@ -74052,7 +74165,7 @@ var HmSensorDevice = class extends HmChildDevice {
74052
74165
  super(ctx, hmSensorSchema);
74053
74166
  this.caps = new Set(this.config.values.capabilities);
74054
74167
  this.registerSensorCaps();
74055
- this.attachValueChangedListener((event) => this.handleValueChanged(event));
74168
+ this.attachValueChangedListener((event, origin) => this.handleValueChanged(event, origin));
74056
74169
  }
74057
74170
  registerSensorCaps() {
74058
74171
  for (const cap of this.caps) switch (cap) {
@@ -74087,14 +74200,17 @@ var HmSensorDevice = class extends HmChildDevice {
74087
74200
  });
74088
74201
  this.runtimeState.setCapState("motion", COLD_MOTION);
74089
74202
  }
74090
- handleValueChanged(event) {
74203
+ handleValueChanged(event, origin) {
74091
74204
  const now = event.ts || Date.now();
74092
74205
  const { parameter, value } = event;
74093
- if (this.caps.has("motion") && parameter === "MOTION" && typeof value === "boolean") this.runtimeState.setCapState("motion", {
74094
- detected: value,
74095
- lastDetectedAt: value ? now : this.runtimeState.getCapState("motion")?.lastDetectedAt ?? null,
74096
- autoClearAfterMs: null
74097
- });
74206
+ if (this.caps.has("motion") && parameter === "MOTION" && typeof value === "boolean") {
74207
+ const prev = this.runtimeState.getCapState("motion") ?? COLD_MOTION;
74208
+ this.runtimeState.setCapState("motion", {
74209
+ detected: value,
74210
+ lastDetectedAt: value ? changeStamp(origin, now, prev.lastDetectedAt, prev.detected) : prev.lastDetectedAt,
74211
+ autoClearAfterMs: null
74212
+ });
74213
+ }
74098
74214
  this.decodeSensorPush(parameter, value, now);
74099
74215
  }
74100
74216
  decodeSensorPush(parameter, value, now) {
@@ -74266,7 +74382,7 @@ var HmDiagnosticDevice = class extends HmChildDevice {
74266
74382
  this.unit = cfg.unit;
74267
74383
  this.sourceParams = new Set(cfg.sourceParams.map((p) => p.toUpperCase()));
74268
74384
  this.registerDiagnosticCap();
74269
- this.attachValueChangedListener((event) => this.handleValueChanged(event));
74385
+ this.attachValueChangedListener((event, origin) => this.handleValueChanged(event, origin));
74270
74386
  }
74271
74387
  registerDiagnosticCap() {
74272
74388
  switch (this.capName) {
@@ -74286,18 +74402,20 @@ var HmDiagnosticDevice = class extends HmChildDevice {
74286
74402
  }
74287
74403
  }
74288
74404
  }
74289
- handleValueChanged(event) {
74405
+ handleValueChanged(event, origin) {
74290
74406
  const now = event.ts || Date.now();
74291
74407
  const parameter = event.parameter.toUpperCase();
74292
74408
  if (!this.sourceParams.has(parameter)) return;
74293
- this.applyValue(event.value, now);
74409
+ this.applyValue(event.value, now, origin);
74294
74410
  }
74295
- applyValue(value, now) {
74411
+ applyValue(value, now, origin) {
74296
74412
  if (this.capName === "binary") {
74297
74413
  if (typeof value !== "boolean") return;
74414
+ const prev = this.runtimeState.getCapState("binary") ?? COLD_BINARY;
74415
+ const lastChangedAt = changeStamp(origin, now, prev.lastChangedAt, prev.on === value);
74298
74416
  this.runtimeState.setCapState("binary", {
74299
74417
  on: value,
74300
- lastChangedAt: now
74418
+ lastChangedAt
74301
74419
  });
74302
74420
  return;
74303
74421
  }
package/dist/index.js CHANGED
@@ -427,7 +427,7 @@ var HomematicDevice = class extends require_addon.BaseDevice {
427
427
  if (next) this.runtimeState.setCapState("cover", next);
428
428
  }
429
429
  if (this.capChannels.has("lock-control")) {
430
- const next = require_addon.decodeLock(this.runtimeState.getCapState("lock-control") ?? COLD_LOCK, parameter, value, now);
430
+ const next = require_addon.decodeLock(parameter, value, now);
431
431
  if (next) this.runtimeState.setCapState("lock-control", next);
432
432
  }
433
433
  if (this.capChannels.has("climate-control")) {
package/dist/index.mjs CHANGED
@@ -426,7 +426,7 @@ var HomematicDevice = class extends BaseDevice {
426
426
  if (next) this.runtimeState.setCapState("cover", next);
427
427
  }
428
428
  if (this.capChannels.has("lock-control")) {
429
- const next = decodeLock(this.runtimeState.getCapState("lock-control") ?? COLD_LOCK, parameter, value, now);
429
+ const next = decodeLock(parameter, value, now);
430
430
  if (next) this.runtimeState.setCapState("lock-control", next);
431
431
  }
432
432
  if (this.capChannels.has("climate-control")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-homematic",
3
- "version": "1.2.118",
3
+ "version": "1.2.119",
4
4
  "description": "Homematic / HomematicIP (CCU3 / RaspberryMatic) device-provider addon for CamStack — wraps the nodehomematic library",
5
5
  "keywords": [
6
6
  "camstack",