@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 +162 -44
- package/dist/addon.mjs +162 -44
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
73791
|
-
*
|
|
73792
|
-
*
|
|
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
|
|
73795
|
-
if (
|
|
73796
|
-
|
|
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
|
|
74064
|
+
lastChangedAt
|
|
73968
74065
|
});
|
|
73969
74066
|
return;
|
|
73970
74067
|
}
|
|
73971
|
-
if (cap === "cover")
|
|
73972
|
-
|
|
73973
|
-
|
|
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
|
-
|
|
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")
|
|
74093
|
-
|
|
74094
|
-
|
|
74095
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
73792
|
-
*
|
|
73793
|
-
*
|
|
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
|
|
73796
|
-
if (
|
|
73797
|
-
|
|
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
|
|
74065
|
+
lastChangedAt
|
|
73969
74066
|
});
|
|
73970
74067
|
return;
|
|
73971
74068
|
}
|
|
73972
|
-
if (cap === "cover")
|
|
73973
|
-
|
|
73974
|
-
|
|
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
|
-
|
|
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")
|
|
74094
|
-
|
|
74095
|
-
|
|
74096
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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