@willieee802/zigbee-herdsman-converters 15.0.8-4.1
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/LICENSE +21 -0
- package/README.md +7 -0
- package/converters/fromZigbee.js +8439 -0
- package/converters/toZigbee.js +7162 -0
- package/devices/ITCommander.js +37 -0
- package/devices/RMC002.js +20 -0
- package/devices/acova.js +101 -0
- package/devices/acuity_brands_lighting.js +11 -0
- package/devices/adeo.js +256 -0
- package/devices/adurosmart.js +130 -0
- package/devices/aeotec.js +13 -0
- package/devices/airam.js +59 -0
- package/devices/ajax_online.js +49 -0
- package/devices/akuvox.js +28 -0
- package/devices/alchemy.js +18 -0
- package/devices/aldi.js +53 -0
- package/devices/alecto.js +98 -0
- package/devices/anchor.js +17 -0
- package/devices/atlantic.js +110 -0
- package/devices/atsmart.js +28 -0
- package/devices/aubess.js +21 -0
- package/devices/aurora_lighting.js +292 -0
- package/devices/automaton.js +44 -0
- package/devices/awox.js +184 -0
- package/devices/axis.js +22 -0
- package/devices/bankamp.js +11 -0
- package/devices/bega.js +22 -0
- package/devices/belkin.js +11 -0
- package/devices/bitron.js +334 -0
- package/devices/blaupunkt.js +27 -0
- package/devices/blitzwolf.js +47 -0
- package/devices/bosch.js +702 -0
- package/devices/brimate.js +15 -0
- package/devices/bseed.js +17 -0
- package/devices/bticino.js +118 -0
- package/devices/busch-jaeger.js +140 -0
- package/devices/byun.js +24 -0
- package/devices/calex.js +37 -0
- package/devices/candeo.js +49 -0
- package/devices/casaia.js +52 -0
- package/devices/centralite.js +359 -0
- package/devices/cleode.js +20 -0
- package/devices/cleverio.js +36 -0
- package/devices/climax.js +138 -0
- package/devices/commercial_electric.js +16 -0
- package/devices/connecte.js +46 -0
- package/devices/cree.js +11 -0
- package/devices/ctm.js +999 -0
- package/devices/current_products_corp.js +24 -0
- package/devices/custom_devices_diy.js +850 -0
- package/devices/cy-lighting.js +11 -0
- package/devices/danalock.js +26 -0
- package/devices/danfoss.js +340 -0
- package/devices/databyte.ch.js +55 -0
- package/devices/datek.js +246 -0
- package/devices/dawon_dns.js +338 -0
- package/devices/develco.js +868 -0
- package/devices/digi.js +14 -0
- package/devices/diyruz.js +305 -0
- package/devices/dlink.js +37 -0
- package/devices/dnake.js +11 -0
- package/devices/dresden_elektronik.js +47 -0
- package/devices/easyaccess.js +27 -0
- package/devices/eatonhalo_led.js +11 -0
- package/devices/echostar.js +25 -0
- package/devices/ecodim.js +145 -0
- package/devices/ecolink.js +21 -0
- package/devices/ecosmart.js +64 -0
- package/devices/ecozy.js +31 -0
- package/devices/edp.js +39 -0
- package/devices/eglo.js +25 -0
- package/devices/elko.js +176 -0
- package/devices/enbrighten.js +163 -0
- package/devices/enocean.js +52 -0
- package/devices/envilar.js +73 -0
- package/devices/essentialb.js +87 -0
- package/devices/eurotronic.js +48 -0
- package/devices/evanell.js +29 -0
- package/devices/evn.js +31 -0
- package/devices/evology.js +24 -0
- package/devices/evvr.js +17 -0
- package/devices/ewelink.js +184 -0
- package/devices/ezex.js +24 -0
- package/devices/fantem.js +77 -0
- package/devices/feibit.js +172 -0
- package/devices/fireangel.js +15 -0
- package/devices/frankever.js +23 -0
- package/devices/garza.js +11 -0
- package/devices/ge.js +111 -0
- package/devices/gewiss.js +48 -0
- package/devices/gidealed.js +11 -0
- package/devices/giderwel.js +11 -0
- package/devices/giex.js +222 -0
- package/devices/girier.js +19 -0
- package/devices/gledopto.js +756 -0
- package/devices/gmy.js +11 -0
- package/devices/gs.js +29 -0
- package/devices/halemeier.js +18 -0
- package/devices/hampton_bay.js +37 -0
- package/devices/heiman.js +805 -0
- package/devices/hej.js +107 -0
- package/devices/hfh.js +11 -0
- package/devices/hgkg.js +54 -0
- package/devices/hilux.js +11 -0
- package/devices/hive.js +524 -0
- package/devices/home_control_as.js +27 -0
- package/devices/hommyn.js +24 -0
- package/devices/honyar.js +29 -0
- package/devices/hornbach.js +53 -0
- package/devices/hzc.js +21 -0
- package/devices/hzc_electric.js +42 -0
- package/devices/icasa.js +121 -0
- package/devices/idinio.js +19 -0
- package/devices/ihorn.js +61 -0
- package/devices/ikea.js +1168 -0
- package/devices/ilightsin.js +11 -0
- package/devices/iluminize.js +262 -0
- package/devices/ilux.js +11 -0
- package/devices/immax.js +203 -0
- package/devices/innr.js +705 -0
- package/devices/inovelli.js +1315 -0
- package/devices/insta.js +110 -0
- package/devices/iolloi.js +20 -0
- package/devices/iotperfect.js +20 -0
- package/devices/iris.js +180 -0
- package/devices/istar.js +20 -0
- package/devices/jasco.js +55 -0
- package/devices/javis.js +37 -0
- package/devices/jethome.js +48 -0
- package/devices/jiawen.js +18 -0
- package/devices/jumitech.js +18 -0
- package/devices/jxuan.js +49 -0
- package/devices/kami.js +15 -0
- package/devices/keen_home.js +80 -0
- package/devices/klikaanklikuit.js +17 -0
- package/devices/kmpcil.js +148 -0
- package/devices/konke.js +141 -0
- package/devices/ksentry.js +17 -0
- package/devices/kurvia.js +17 -0
- package/devices/kwikset.js +120 -0
- package/devices/lanesto.js +11 -0
- package/devices/lds.js +11 -0
- package/devices/led_trading.js +69 -0
- package/devices/ledvance.js +353 -0
- package/devices/leedarson.js +130 -0
- package/devices/legrand.js +554 -0
- package/devices/lellki.js +146 -0
- package/devices/letsled.js +11 -0
- package/devices/letv.js +18 -0
- package/devices/leviton.js +142 -0
- package/devices/lg.js +18 -0
- package/devices/lidl.js +1000 -0
- package/devices/lifecontrol.js +92 -0
- package/devices/lightsolutions.js +37 -0
- package/devices/linkind.js +205 -0
- package/devices/livingwise.js +59 -0
- package/devices/livolo.js +286 -0
- package/devices/lixee.js +779 -0
- package/devices/lonsonho.js +218 -0
- package/devices/lubeez.js +18 -0
- package/devices/lupus.js +74 -0
- package/devices/lutron.js +32 -0
- package/devices/lux.js +40 -0
- package/devices/m-elec.js +18 -0
- package/devices/makegood.js +51 -0
- package/devices/matcall_bv.js +18 -0
- package/devices/meazon.js +47 -0
- package/devices/mercator.js +225 -0
- package/devices/miboxer.js +72 -0
- package/devices/micromatic.js +29 -0
- package/devices/moes.js +400 -0
- package/devices/mycket.js +11 -0
- package/devices/m/303/274ller_licht.js +220 -0
- package/devices/namron.js +774 -0
- package/devices/nanoleaf.js +11 -0
- package/devices/neo.js +86 -0
- package/devices/net2grid.js +29 -0
- package/devices/netvox.js +35 -0
- package/devices/niko.js +314 -0
- package/devices/ninja_blocks.js +24 -0
- package/devices/niviss.js +11 -0
- package/devices/nodon.js +109 -0
- package/devices/nordtronic.js +30 -0
- package/devices/nous.js +88 -0
- package/devices/novo.js +17 -0
- package/devices/nue_3a.js +417 -0
- package/devices/nyce.js +67 -0
- package/devices/onesti.js +59 -0
- package/devices/openlumi.js +22 -0
- package/devices/orvibo.js +515 -0
- package/devices/osram.js +519 -0
- package/devices/oujiabao.js +15 -0
- package/devices/owon.js +352 -0
- package/devices/ozsmartthings.js +11 -0
- package/devices/paul_neuhaus.js +96 -0
- package/devices/paulmann.js +158 -0
- package/devices/peq.js +24 -0
- package/devices/perenio.js +413 -0
- package/devices/philips.js +3118 -0
- package/devices/plaid.js +25 -0
- package/devices/plugwise.js +173 -0
- package/devices/popp.js +10 -0
- package/devices/profalux.js +47 -0
- package/devices/prolight.js +54 -0
- package/devices/qmotion.js +33 -0
- package/devices/qoto.js +113 -0
- package/devices/quotra.js +18 -0
- package/devices/rademacher.js +18 -0
- package/devices/rgb_genie.js +119 -0
- package/devices/robb.js +335 -0
- package/devices/roome.js +15 -0
- package/devices/rtx.js +90 -0
- package/devices/salus_controls.js +137 -0
- package/devices/samotech.js +93 -0
- package/devices/saswell.js +58 -0
- package/devices/scanproducts.js +32 -0
- package/devices/schlage.js +24 -0
- package/devices/schneider_electric.js +1039 -0
- package/devices/schwaiger.js +62 -0
- package/devices/seastar_intelligence.js +16 -0
- package/devices/securifi.js +39 -0
- package/devices/sengled.js +332 -0
- package/devices/sercomm.js +140 -0
- package/devices/shenzhen_homa.js +53 -0
- package/devices/shinasystem.js +686 -0
- package/devices/siglis.js +440 -0
- package/devices/sinope.js +1257 -0
- package/devices/siterwell.js +46 -0
- package/devices/skydance.js +112 -0
- package/devices/slv.js +27 -0
- package/devices/smart9.js +27 -0
- package/devices/smart_home_pty.js +18 -0
- package/devices/smartenit.js +42 -0
- package/devices/smartthings.js +495 -0
- package/devices/smartwings.js +24 -0
- package/devices/sohan_electric.js +13 -0
- package/devices/solaredge.js +11 -0
- package/devices/somgoms.js +47 -0
- package/devices/sonoff.js +262 -0
- package/devices/spotmau.js +36 -0
- package/devices/sprut.js +317 -0
- package/devices/stelpro.js +216 -0
- package/devices/sunricher.js +625 -0
- package/devices/swann.js +33 -0
- package/devices/sylvania.js +200 -0
- package/devices/tci.js +25 -0
- package/devices/technicolor.js +47 -0
- package/devices/terncy.js +64 -0
- package/devices/the_light_group.js +70 -0
- package/devices/third_reality.js +195 -0
- package/devices/titan_products.js +22 -0
- package/devices/tplink.js +42 -0
- package/devices/trust.js +114 -0
- package/devices/tubeszb.js +20 -0
- package/devices/tuya.js +4215 -0
- package/devices/ubisys.js +938 -0
- package/devices/uhome.js +25 -0
- package/devices/universal_electronics_inc.js +119 -0
- package/devices/urlighting.js +12 -0
- package/devices/useelink.js +52 -0
- package/devices/vbled.js +18 -0
- package/devices/vesternet.js +188 -0
- package/devices/viessmann.js +51 -0
- package/devices/villeroy_boch.js +18 -0
- package/devices/vimar.js +78 -0
- package/devices/visonic.js +80 -0
- package/devices/vrey.js +18 -0
- package/devices/wally.js +25 -0
- package/devices/waxman.js +45 -0
- package/devices/weiser.js +67 -0
- package/devices/weten.js +13 -0
- package/devices/wisdom.js +11 -0
- package/devices/woox.js +177 -0
- package/devices/wyze.js +23 -0
- package/devices/xiaomi.js +3223 -0
- package/devices/xinghuoyuan.js +11 -0
- package/devices/yale.js +227 -0
- package/devices/ynoa.js +68 -0
- package/devices/yookee.js +23 -0
- package/devices/ysrsai.js +26 -0
- package/devices/zemismart.js +254 -0
- package/devices/zen.js +34 -0
- package/devices/zipato.js +11 -0
- package/index.js +242 -0
- package/lib/color.js +784 -0
- package/lib/configureKey.js +944 -0
- package/lib/constants.js +316 -0
- package/lib/exposes.js +677 -0
- package/lib/extend.js +180 -0
- package/lib/kelvinToXy.js +1912 -0
- package/lib/legacy.js +2223 -0
- package/lib/light.js +111 -0
- package/lib/ota/OTA_URLs.md +119 -0
- package/lib/ota/common.js +476 -0
- package/lib/ota/index.js +10 -0
- package/lib/ota/inovelli.js +72 -0
- package/lib/ota/ledvance.js +52 -0
- package/lib/ota/lixee.js +57 -0
- package/lib/ota/salus.js +82 -0
- package/lib/ota/securifi.js +25 -0
- package/lib/ota/tradfri.js +45 -0
- package/lib/ota/ubisys.js +61 -0
- package/lib/ota/zigbeeOTA.js +161 -0
- package/lib/philips.js +667 -0
- package/lib/reporting.js +234 -0
- package/lib/store.js +57 -0
- package/lib/tuya.js +2027 -0
- package/lib/utils.js +527 -0
- package/lib/xiaomi.d.ts +11 -0
- package/lib/xiaomi.js +1288 -0
- package/lib/zosung.js +243 -0
- package/package.json +39 -0
package/lib/xiaomi.js
ADDED
|
@@ -0,0 +1,1288 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
batteryVoltageToPercentage,
|
|
5
|
+
calibrateAndPrecisionRoundOptions,
|
|
6
|
+
calibrateAndPrecisionRoundOptionsIsPercentual,
|
|
7
|
+
postfixWithEndpointName,
|
|
8
|
+
precisionRound,
|
|
9
|
+
getKey,
|
|
10
|
+
} = require('./utils');
|
|
11
|
+
|
|
12
|
+
const exposes = require('../lib/exposes');
|
|
13
|
+
const globalStore = require('../lib/store');
|
|
14
|
+
|
|
15
|
+
const buffer2DataObject = (meta, model, buffer) => {
|
|
16
|
+
const dataObject = {};
|
|
17
|
+
|
|
18
|
+
if (buffer !== null && Buffer.isBuffer(buffer)) {
|
|
19
|
+
// Xiaomi struct parsing
|
|
20
|
+
for (let i = 0; i < buffer.length - 1; i++) {
|
|
21
|
+
const index = buffer[i];
|
|
22
|
+
let value = null;
|
|
23
|
+
|
|
24
|
+
switch (buffer[i + 1]) {
|
|
25
|
+
case 16:
|
|
26
|
+
case 32:
|
|
27
|
+
// 0x10 ZclBoolean
|
|
28
|
+
// 0x20 Zcl8BitUint
|
|
29
|
+
value = buffer.readUInt8(i + 2);
|
|
30
|
+
i += 2;
|
|
31
|
+
break;
|
|
32
|
+
case 33:
|
|
33
|
+
// 0x21 Zcl16BitUint
|
|
34
|
+
value = buffer.readUInt16LE(i + 2);
|
|
35
|
+
i += 3;
|
|
36
|
+
break;
|
|
37
|
+
case 34:
|
|
38
|
+
// 0x22 Zcl24BitUint
|
|
39
|
+
value = buffer.readUIntLE(i + 2, 3);
|
|
40
|
+
i += 4;
|
|
41
|
+
break;
|
|
42
|
+
case 35:
|
|
43
|
+
// 0x23 Zcl32BitUint
|
|
44
|
+
value = buffer.readUInt32LE(i + 2);
|
|
45
|
+
i += 5;
|
|
46
|
+
break;
|
|
47
|
+
case 36:
|
|
48
|
+
// 0x24 Zcl40BitUint
|
|
49
|
+
value = buffer.readUIntLE(i + 2, 5);
|
|
50
|
+
i += 6;
|
|
51
|
+
break;
|
|
52
|
+
case 37:
|
|
53
|
+
// 0x25 Zcl48BitUint
|
|
54
|
+
value = buffer.readUIntLE(i + 2, 6);
|
|
55
|
+
i += 7;
|
|
56
|
+
break;
|
|
57
|
+
case 38:
|
|
58
|
+
// 0x26 Zcl56BitUint
|
|
59
|
+
value = buffer.readUIntLE(i + 2, 7);
|
|
60
|
+
i += 8;
|
|
61
|
+
break;
|
|
62
|
+
case 39:
|
|
63
|
+
// 0x27 Zcl64BitUint
|
|
64
|
+
value = buffer.readBigUInt64BE(i + 2);
|
|
65
|
+
i += 9;
|
|
66
|
+
break;
|
|
67
|
+
case 40:
|
|
68
|
+
// 0x28 Zcl8BitInt
|
|
69
|
+
value = buffer.readInt8(i + 2);
|
|
70
|
+
i += 2;
|
|
71
|
+
break;
|
|
72
|
+
case 41:
|
|
73
|
+
// 0x29 Zcl16BitInt
|
|
74
|
+
value = buffer.readInt16LE(i + 2);
|
|
75
|
+
i += 3;
|
|
76
|
+
break;
|
|
77
|
+
case 42:
|
|
78
|
+
// 0x2A Zcl24BitInt
|
|
79
|
+
value = buffer.readIntLE(i + 2, 3);
|
|
80
|
+
i += 4;
|
|
81
|
+
break;
|
|
82
|
+
case 43:
|
|
83
|
+
// 0x2B Zcl32BitInt
|
|
84
|
+
value = buffer.readInt32LE(i+2);
|
|
85
|
+
i += 5;
|
|
86
|
+
break;
|
|
87
|
+
case 44:
|
|
88
|
+
// 0x2C Zcl40BitInt
|
|
89
|
+
value = buffer.readIntLE(i + 2, 5);
|
|
90
|
+
i += 6;
|
|
91
|
+
break;
|
|
92
|
+
case 45:
|
|
93
|
+
// 0x2D Zcl48BitInt
|
|
94
|
+
value = buffer.readIntLE(i + 2, 6);
|
|
95
|
+
i += 7;
|
|
96
|
+
break;
|
|
97
|
+
case 46:
|
|
98
|
+
// 0x2E Zcl56BitInt
|
|
99
|
+
value = buffer.readIntLE(i + 2, 7);
|
|
100
|
+
i += 8;
|
|
101
|
+
break;
|
|
102
|
+
case 47:
|
|
103
|
+
// 0x2F Zcl64BitInt
|
|
104
|
+
value = buffer.readBigInt64BE(i + 2);
|
|
105
|
+
i += 9;
|
|
106
|
+
break;
|
|
107
|
+
case 57:
|
|
108
|
+
// 0x39 ZclSingleFloat
|
|
109
|
+
value = buffer.readFloatLE(i + 2);
|
|
110
|
+
i += 5;
|
|
111
|
+
break;
|
|
112
|
+
case 58:
|
|
113
|
+
// 0x3a ZclDoubleFloat
|
|
114
|
+
value = buffer.readDoubleLE(i + 2);
|
|
115
|
+
i += 5;
|
|
116
|
+
break;
|
|
117
|
+
case 66:
|
|
118
|
+
// 0x42 unknown, length taken from what seems correct in the logs, maybe is wrong
|
|
119
|
+
if (meta.logger) meta.logger.debug(`${model.zigbeeModel}: unknown vtype=${buffer[i+1]}, pos=${i+1}, moving length 1`);
|
|
120
|
+
i += 2;
|
|
121
|
+
break;
|
|
122
|
+
case 95:
|
|
123
|
+
// 0x5f unknown, length taken from what seems correct in the logs, maybe is wrong
|
|
124
|
+
if (meta.logger) meta.logger.debug(`${model.zigbeeModel}: unknown vtype=${buffer[i+1]}, pos=${i+1}, moving length 4`);
|
|
125
|
+
i += 5;
|
|
126
|
+
break;
|
|
127
|
+
default:
|
|
128
|
+
if (meta.logger) meta.logger.debug(`${model.zigbeeModel}: unknown vtype=${buffer[i + 1]}, pos=${i + 1}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (value != null) {
|
|
132
|
+
dataObject[index] = value;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (meta.logger) {
|
|
138
|
+
meta.logger.debug(`${model.zigbeeModel}: Processed buffer into data ${JSON.stringify(dataObject,
|
|
139
|
+
(key, value) => typeof value === 'bigint' ? value.toString() : value)}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
return dataObject;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const numericAttributes2Payload = async (msg, meta, model, options, dataObject) => {
|
|
147
|
+
let payload = {};
|
|
148
|
+
|
|
149
|
+
for (const [key, value] of Object.entries(dataObject)) {
|
|
150
|
+
switch (key) {
|
|
151
|
+
case '0':
|
|
152
|
+
payload.detection_period = value;
|
|
153
|
+
break;
|
|
154
|
+
case '1':
|
|
155
|
+
payload.voltage = value;
|
|
156
|
+
if (model.meta && model.meta.battery && model.meta.battery.voltageToPercentage) {
|
|
157
|
+
payload.battery = batteryVoltageToPercentage(value, model.meta.battery.voltageToPercentage);
|
|
158
|
+
}
|
|
159
|
+
break;
|
|
160
|
+
case '2':
|
|
161
|
+
if (['JT-BZ-01AQ/A'].includes(model.model)) {
|
|
162
|
+
payload.power_outage_count = value - 1;
|
|
163
|
+
}
|
|
164
|
+
break;
|
|
165
|
+
case '3':
|
|
166
|
+
if (['WXCJKG11LM', 'WXCJKG12LM', 'WXCJKG13LM', 'MCCGQ14LM', 'GZCGQ01LM', 'JY-GZ-01AQ', 'CTP-R01'].includes(model.model)) {
|
|
167
|
+
// The temperature value is constant 25 °C and does not change, so we ignore it
|
|
168
|
+
// https://github.com/Koenkk/zigbee2mqtt/issues/11126
|
|
169
|
+
// https://github.com/Koenkk/zigbee-herdsman-converters/pull/3585
|
|
170
|
+
// https://github.com/Koenkk/zigbee2mqtt/issues/13253
|
|
171
|
+
} else {
|
|
172
|
+
payload.device_temperature = calibrateAndPrecisionRoundOptions(value, options, 'device_temperature'); // 0x03
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
case '4':
|
|
176
|
+
payload.mode_switch = {4: 'anti_flicker_mode', 1: 'quick_mode'}[value];
|
|
177
|
+
break;
|
|
178
|
+
case '5':
|
|
179
|
+
payload.power_outage_count = value - 1;
|
|
180
|
+
break;
|
|
181
|
+
case '8':
|
|
182
|
+
if (['ZNLDP13LM'].includes(model.model)) {
|
|
183
|
+
// We don't know what the value means for these devices.
|
|
184
|
+
}
|
|
185
|
+
break;
|
|
186
|
+
case '9':
|
|
187
|
+
if (['ZNLDP13LM'].includes(model.model)) {
|
|
188
|
+
// We don't know what the value means for these devices.
|
|
189
|
+
}
|
|
190
|
+
break;
|
|
191
|
+
case '10':
|
|
192
|
+
if (['ZNLDP13LM', 'CTP-R01'].includes(model.model)) {
|
|
193
|
+
// We don't know what the value means for these devices.
|
|
194
|
+
} else {
|
|
195
|
+
payload.switch_type = {1: 'toggle', 2: 'momentary'}[value];
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
198
|
+
case '11':
|
|
199
|
+
if (['RTCGQ11LM'].includes(model.model)) {
|
|
200
|
+
payload.illuminance = calibrateAndPrecisionRoundOptions(value, options, 'illuminance');
|
|
201
|
+
// DEPRECATED: remove illuminance_lux here.
|
|
202
|
+
payload.illuminance_lux = calibrateAndPrecisionRoundOptions(value, options, 'illuminance_lux');
|
|
203
|
+
}
|
|
204
|
+
break;
|
|
205
|
+
case '12':
|
|
206
|
+
if (['ZNLDP13LM'].includes(model.model)) {
|
|
207
|
+
// We don't know what the value means for these devices.
|
|
208
|
+
}
|
|
209
|
+
break;
|
|
210
|
+
case '100':
|
|
211
|
+
if (['QBKG20LM', 'QBKG31LM', 'QBKG39LM', 'QBKG41LM', 'QBCZ15LM', 'LLKZMK11LM', 'QBKG12LM', 'QBKG03LM', 'QBKG25LM']
|
|
212
|
+
.includes(model.model)) {
|
|
213
|
+
let mapping;
|
|
214
|
+
switch (model.model) {
|
|
215
|
+
case 'QBCZ15LM':
|
|
216
|
+
mapping = 'relay';
|
|
217
|
+
break;
|
|
218
|
+
case 'LLKZMK11LM':
|
|
219
|
+
mapping = 'l1';
|
|
220
|
+
break;
|
|
221
|
+
default:
|
|
222
|
+
mapping = 'left';
|
|
223
|
+
}
|
|
224
|
+
payload[`state_${mapping}`] = value === 1 ? 'ON' : 'OFF';
|
|
225
|
+
} else if (['WXKG14LM', 'WXKG16LM', 'WXKG17LM'].includes(model.model)) {
|
|
226
|
+
payload.click_mode = {1: 'fast', 2: 'multi'}[value];
|
|
227
|
+
} else if (['WXCJKG11LM', 'WXCJKG12LM', 'WXCJKG13LM', 'ZNMS12LM', 'ZNCLBL01LM', 'RTCGQ12LM', 'RTCGQ13LM', 'RTCGQ14LM',
|
|
228
|
+
'RTCGQ15LM'].includes(model.model)) {
|
|
229
|
+
// We don't know what the value means for these devices.
|
|
230
|
+
// https://github.com/Koenkk/zigbee2mqtt/issues/11126
|
|
231
|
+
// https://github.com/Koenkk/zigbee2mqtt/issues/12279
|
|
232
|
+
} else if (['WSDCGQ01LM', 'WSDCGQ11LM', 'WSDCGQ12LM', 'VOCKQJK11LM'].includes(model.model)) {
|
|
233
|
+
// https://github.com/Koenkk/zigbee2mqtt/issues/798
|
|
234
|
+
// Sometimes the sensor publishes non-realistic vales, filter these
|
|
235
|
+
const temperature = parseFloat(value) / 100.0;
|
|
236
|
+
if (temperature > -65 && temperature < 65) {
|
|
237
|
+
payload.temperature = calibrateAndPrecisionRoundOptions(temperature, options, 'temperature');
|
|
238
|
+
}
|
|
239
|
+
} else if (['RTCGQ11LM'].includes(model.model)) {
|
|
240
|
+
// It contains the occupancy, but in z2m we use a custom timer to do it, so we ignore it
|
|
241
|
+
// payload.occupancy = value === 1;
|
|
242
|
+
} else if (['MCCGQ11LM', 'MCCGQ14LM'].includes(model.model)) {
|
|
243
|
+
payload.contact = value === 0;
|
|
244
|
+
} else if (['SJCGQ11LM'].includes(model.model)) {
|
|
245
|
+
// Ignore the message. It seems not reliable. See discussion here https://github.com/Koenkk/zigbee2mqtt/issues/12018
|
|
246
|
+
// payload.water_leak = value === 1;
|
|
247
|
+
} else if (['SJCGQ13LM'].includes(model.model)) {
|
|
248
|
+
payload.water_leak = value === 1;
|
|
249
|
+
} else if (['JTYJ-GD-01LM/BW'].includes(model.model)) {
|
|
250
|
+
payload.smoke_density = value;
|
|
251
|
+
} else if (['GZCGQ01LM'].includes(model.model)) {
|
|
252
|
+
// DEPRECATED: change illuminance_lux -> illuminance
|
|
253
|
+
payload.illuminance_lux = calibrateAndPrecisionRoundOptions(value, options, 'illuminance_lux');
|
|
254
|
+
} else {
|
|
255
|
+
payload.state = value === 1 ? 'ON' : 'OFF';
|
|
256
|
+
}
|
|
257
|
+
break;
|
|
258
|
+
case '101':
|
|
259
|
+
if (['QBKG20LM', 'QBKG31LM', 'QBKG39LM', 'QBKG41LM', 'QBCZ15LM', 'QBKG25LM', 'QBKG34LM', 'LLKZMK11LM', 'QBKG12LM', 'QBKG03LM']
|
|
260
|
+
.includes(model.model)) {
|
|
261
|
+
let mapping;
|
|
262
|
+
switch (model.model) {
|
|
263
|
+
case 'QBCZ15LM':
|
|
264
|
+
mapping = 'usb';
|
|
265
|
+
break;
|
|
266
|
+
case 'QBKG25LM':
|
|
267
|
+
case 'QBKG34LM':
|
|
268
|
+
mapping = 'center';
|
|
269
|
+
break;
|
|
270
|
+
case 'LLKZMK11LM':
|
|
271
|
+
mapping = 'l2';
|
|
272
|
+
break;
|
|
273
|
+
default:
|
|
274
|
+
mapping = 'right';
|
|
275
|
+
}
|
|
276
|
+
payload[`state_${mapping}`] = value === 1 ? 'ON' : 'OFF';
|
|
277
|
+
} else if (['RTCGQ12LM', 'RTCGQ14LM', 'RTCGQ15LM'].includes(model.model)) {
|
|
278
|
+
// Sometimes RTCGQ14LM reports high illuminance values in the dark
|
|
279
|
+
// https://github.com/Koenkk/zigbee2mqtt/issues/12596
|
|
280
|
+
const illuminance = value > 65000 ? 0 : value;
|
|
281
|
+
payload.illuminance = calibrateAndPrecisionRoundOptions(illuminance, options, 'illuminance');
|
|
282
|
+
} else if (['WSDCGQ01LM', 'WSDCGQ11LM', 'WSDCGQ12LM', 'VOCKQJK11LM'].includes(model.model)) {
|
|
283
|
+
// https://github.com/Koenkk/zigbee2mqtt/issues/798
|
|
284
|
+
// Sometimes the sensor publishes non-realistic vales, filter these
|
|
285
|
+
const humidity = parseFloat(value) / 100.0;
|
|
286
|
+
if (humidity >= 0 && humidity <= 100) {
|
|
287
|
+
payload.humidity = calibrateAndPrecisionRoundOptions(humidity, options, 'humidity');
|
|
288
|
+
}
|
|
289
|
+
} else if (['ZNJLBL01LM', 'ZNCLDJ12LM'].includes(model.model)) {
|
|
290
|
+
payload.battery = value;
|
|
291
|
+
} else if (['ZNCLBL01LM'].includes(model.model)) {
|
|
292
|
+
const battery = value / 2;
|
|
293
|
+
payload.battery = precisionRound(battery, 2);
|
|
294
|
+
} else if (['RTCZCGQ11LM'].includes(model.model)) {
|
|
295
|
+
payload.presence = {0: false, 1: true, 255: null}[value];
|
|
296
|
+
}
|
|
297
|
+
break;
|
|
298
|
+
case '102':
|
|
299
|
+
if (['QBKG25LM', 'QBKG34LM'].includes(model.model)) {
|
|
300
|
+
payload.state_right = value === 1 ? 'ON' : 'OFF';
|
|
301
|
+
} else if (['WSDCGQ01LM', 'WSDCGQ11LM'].includes(model.model)) {
|
|
302
|
+
payload.pressure = calibrateAndPrecisionRoundOptions(value/100.0, options, 'pressure');
|
|
303
|
+
} else if (['WSDCGQ12LM'].includes(model.model)) {
|
|
304
|
+
// This pressure value is ignored because it is less accurate than reported in the 'scaledValue' attribute
|
|
305
|
+
// of the 'msPressureMeasurement' cluster
|
|
306
|
+
} else if (['RTCZCGQ11LM'].includes(model.model)) {
|
|
307
|
+
if (meta.device.applicationVersion < 50) {
|
|
308
|
+
payload.presence_event = {0: 'enter', 1: 'leave', 2: 'left_enter', 3: 'right_leave', 4: 'right_enter',
|
|
309
|
+
5: 'left_leave', 6: 'approach', 7: 'away', 255: null}[value];
|
|
310
|
+
} else {
|
|
311
|
+
payload.motion_sensitivity = {1: 'low', 2: 'medium', 3: 'high'}[value];
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
break;
|
|
315
|
+
case '103':
|
|
316
|
+
if (['RTCZCGQ11LM'].includes(model.model)) {
|
|
317
|
+
payload.monitoring_mode = {0: 'undirected', 1: 'left_right'}[value];
|
|
318
|
+
}
|
|
319
|
+
break;
|
|
320
|
+
case '105':
|
|
321
|
+
if (['RTCGQ13LM'].includes(model.model)) {
|
|
322
|
+
payload.motion_sensitivity = {1: 'low', 2: 'medium', 3: 'high'}[value];
|
|
323
|
+
} else if (['RTCZCGQ11LM'].includes(model.model)) {
|
|
324
|
+
payload.approach_distance = {0: 'far', 1: 'medium', 2: 'near'}[value];
|
|
325
|
+
} else if (['RTCGQ14LM'].includes(model.model)) {
|
|
326
|
+
payload.detection_interval = value;
|
|
327
|
+
}
|
|
328
|
+
break;
|
|
329
|
+
case '106':
|
|
330
|
+
if (['RTCGQ14LM'].includes(model.model)) {
|
|
331
|
+
payload.motion_sensitivity = {1: 'low', 2: 'medium', 3: 'high'}[value];
|
|
332
|
+
}
|
|
333
|
+
break;
|
|
334
|
+
case '107':
|
|
335
|
+
if (['RTCGQ14LM'].includes(model.model)) {
|
|
336
|
+
payload.trigger_indicator = value === 1;
|
|
337
|
+
} else if (['ZNCLBL01LM'].includes(model.model)) {
|
|
338
|
+
const position = options.invert_cover ? 100 - value : value;
|
|
339
|
+
payload.position = position;
|
|
340
|
+
payload.state = options.invert_cover ? (position > 0 ? 'CLOSE' : 'OPEN') : (position > 0 ? 'OPEN' : 'CLOSE');
|
|
341
|
+
}
|
|
342
|
+
break;
|
|
343
|
+
case '149':
|
|
344
|
+
payload.energy = calibrateAndPrecisionRoundOptions(value, options, 'energy'); // 0x95
|
|
345
|
+
// Consumption is deprecated
|
|
346
|
+
payload.consumption = payload.energy;
|
|
347
|
+
break;
|
|
348
|
+
case '150':
|
|
349
|
+
if (!['JTYJ-GD-01LM/BW'].includes(model.model)) {
|
|
350
|
+
payload.voltage = calibrateAndPrecisionRoundOptions(value * 0.1, options, 'voltage'); // 0x96
|
|
351
|
+
}
|
|
352
|
+
break;
|
|
353
|
+
case '151':
|
|
354
|
+
if (['LLKZMK11LM'].includes(model.model)) {
|
|
355
|
+
payload.current = calibrateAndPrecisionRoundOptions(value, options, 'current');
|
|
356
|
+
} else {
|
|
357
|
+
payload.current = calibrateAndPrecisionRoundOptions(value * 0.001, options, 'current');
|
|
358
|
+
}
|
|
359
|
+
break;
|
|
360
|
+
case '152':
|
|
361
|
+
if (['DJT11LM'].includes(model.model)) {
|
|
362
|
+
// We don't know what implies for this device, it contains values like 30, 50,... that don't seem to change
|
|
363
|
+
} else {
|
|
364
|
+
payload.power = calibrateAndPrecisionRoundOptions(value, options, 'power'); // 0x98
|
|
365
|
+
}
|
|
366
|
+
break;
|
|
367
|
+
case '154':
|
|
368
|
+
if (['ZNLDP13LM'].includes(model.model)) {
|
|
369
|
+
// We don't know what the value means for these devices.
|
|
370
|
+
}
|
|
371
|
+
break;
|
|
372
|
+
case '159':
|
|
373
|
+
if (['JT-BZ-01AQ/A'].includes(model.model)) {
|
|
374
|
+
payload.gas_sensitivity = {1: '15%LEL', 2: '10%LEL'}[value];
|
|
375
|
+
}
|
|
376
|
+
break;
|
|
377
|
+
case '160':
|
|
378
|
+
if (['JT-BZ-01AQ/A'].includes(model.model)) {
|
|
379
|
+
payload.gas = value === 1;
|
|
380
|
+
} else if (['JY-GZ-01AQ'].includes(model.model)) {
|
|
381
|
+
payload.smoke = value === 1;
|
|
382
|
+
}
|
|
383
|
+
break;
|
|
384
|
+
case '161':
|
|
385
|
+
if (['JT-BZ-01AQ/A'].includes(model.model)) {
|
|
386
|
+
payload.gas_density = value;
|
|
387
|
+
} else if (['JY-GZ-01AQ'].includes(model.model)) {
|
|
388
|
+
payload.smoke_density = value;
|
|
389
|
+
payload.smoke_density_dbm = {0: 0, 1: 0.085, 2: 0.088, 3: 0.093, 4: 0.095, 5: 0.100, 6: 0.105, 7: 0.110,
|
|
390
|
+
8: 0.115, 9: 0.120, 10: 0.125}[value];
|
|
391
|
+
}
|
|
392
|
+
break;
|
|
393
|
+
case '162':
|
|
394
|
+
if (['JT-BZ-01AQ/A', 'JY-GZ-01AQ'].includes(model.model)) {
|
|
395
|
+
payload.test = value === 1;
|
|
396
|
+
}
|
|
397
|
+
break;
|
|
398
|
+
case '163':
|
|
399
|
+
if (['JT-BZ-01AQ/A', 'JY-GZ-01AQ'].includes(model.model)) {
|
|
400
|
+
payload.buzzer_manual_mute = value === 1;
|
|
401
|
+
}
|
|
402
|
+
break;
|
|
403
|
+
case '164':
|
|
404
|
+
if (['JT-BZ-01AQ/A'].includes(model.model)) {
|
|
405
|
+
payload.state = {0: 'work', 1: 'preparation'}[value];
|
|
406
|
+
} else if (['JY-GZ-01AQ'].includes(model.model)) {
|
|
407
|
+
payload.heartbeat_indicator = value === 1;
|
|
408
|
+
}
|
|
409
|
+
break;
|
|
410
|
+
case '165':
|
|
411
|
+
if (['JY-GZ-01AQ'].includes(model.model)) {
|
|
412
|
+
payload.linkage_alarm = value === 1;
|
|
413
|
+
}
|
|
414
|
+
break;
|
|
415
|
+
case '166':
|
|
416
|
+
if (['JT-BZ-01AQ/A'].includes(model.model)) {
|
|
417
|
+
payload.linkage_alarm = value === 1;
|
|
418
|
+
}
|
|
419
|
+
break;
|
|
420
|
+
case '240':
|
|
421
|
+
payload.flip_indicator_light = value === 1 ? 'ON' : 'OFF';
|
|
422
|
+
break;
|
|
423
|
+
case '247':
|
|
424
|
+
{
|
|
425
|
+
const dataObject247 = buffer2DataObject(meta, model, value);
|
|
426
|
+
if (['CTP-R01'].includes(model.model)) {
|
|
427
|
+
// execute pending soft switch of operation_mode, if exists
|
|
428
|
+
const opModeSwitchTask = globalStore.getValue(meta.device, 'opModeSwitchTask');
|
|
429
|
+
if (opModeSwitchTask) {
|
|
430
|
+
const {callback, newMode} = opModeSwitchTask;
|
|
431
|
+
try {
|
|
432
|
+
await callback();
|
|
433
|
+
payload.operation_mode = newMode;
|
|
434
|
+
globalStore.putValue(meta.device, 'opModeSwitchTask', null);
|
|
435
|
+
} catch (error) {
|
|
436
|
+
// do nothing when callback fails
|
|
437
|
+
}
|
|
438
|
+
} else {
|
|
439
|
+
payload.operation_mode = {0: 'action_mode', 1: 'scene_mode'}[dataObject247[155]];
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const payload247 = await numericAttributes2Payload(msg, meta, model, options, dataObject247);
|
|
443
|
+
payload = {...payload, ...payload247};
|
|
444
|
+
}
|
|
445
|
+
break;
|
|
446
|
+
case '258':
|
|
447
|
+
payload.detection_interval = value;
|
|
448
|
+
break;
|
|
449
|
+
case '268':
|
|
450
|
+
if (['RTCGQ13LM', 'RTCGQ14LM', 'RTCZCGQ11LM'].includes(model.model)) {
|
|
451
|
+
payload.motion_sensitivity = {1: 'low', 2: 'medium', 3: 'high'}[value];
|
|
452
|
+
} else if (['JT-BZ-01AQ/A'].includes(model.model)) {
|
|
453
|
+
payload.gas_sensitivity = {1: '15%LEL', 2: '10%LEL'}[value];
|
|
454
|
+
}
|
|
455
|
+
break;
|
|
456
|
+
case '276':
|
|
457
|
+
if (['VOCKQJK11LM'].includes(model.model)) {
|
|
458
|
+
payload.display_unit = getKey(VOCKQJK11LMDisplayUnit, value);
|
|
459
|
+
}
|
|
460
|
+
break;
|
|
461
|
+
case '293':
|
|
462
|
+
payload.click_mode = {1: 'fast', 2: 'multi'}[value];
|
|
463
|
+
break;
|
|
464
|
+
case '294':
|
|
465
|
+
if (['JT-BZ-01AQ/A', 'JY-GZ-01AQ'].includes(model.model)) {
|
|
466
|
+
payload.buzzer_manual_mute = value === 1;
|
|
467
|
+
}
|
|
468
|
+
break;
|
|
469
|
+
case '295':
|
|
470
|
+
if (['JT-BZ-01AQ/A', 'JY-GZ-01AQ'].includes(model.model)) {
|
|
471
|
+
payload.test = value === 1;
|
|
472
|
+
}
|
|
473
|
+
break;
|
|
474
|
+
case '313':
|
|
475
|
+
if (['JT-BZ-01AQ/A'].includes(model.model)) {
|
|
476
|
+
payload.state = {0: 'work', 1: 'preparation'}[value];
|
|
477
|
+
}
|
|
478
|
+
break;
|
|
479
|
+
case '314':
|
|
480
|
+
if (['JT-BZ-01AQ/A'].includes(model.model)) {
|
|
481
|
+
payload.gas = value === 1;
|
|
482
|
+
} else if (['JY-GZ-01AQ'].includes(model.model)) {
|
|
483
|
+
payload.smoke = value === 1;
|
|
484
|
+
}
|
|
485
|
+
break;
|
|
486
|
+
case '315':
|
|
487
|
+
if (['JT-BZ-01AQ/A'].includes(model.model)) {
|
|
488
|
+
payload.gas_density = value;
|
|
489
|
+
} else if (['JY-GZ-01AQ'].includes(model.model)) {
|
|
490
|
+
payload.smoke_density = value;
|
|
491
|
+
payload.smoke_density_dbm = {0: 0, 1: 0.085, 2: 0.088, 3: 0.093, 4: 0.095, 5: 0.100, 6: 0.105, 7: 0.110,
|
|
492
|
+
8: 0.115, 9: 0.120, 10: 0.125}[value];
|
|
493
|
+
}
|
|
494
|
+
break;
|
|
495
|
+
case '316':
|
|
496
|
+
if (['JY-GZ-01AQ'].includes(model.model)) {
|
|
497
|
+
payload.heartbeat_indicator = value === 1;
|
|
498
|
+
}
|
|
499
|
+
break;
|
|
500
|
+
case '317':
|
|
501
|
+
if (['JT-BZ-01AQ/A', 'JY-GZ-01AQ'].includes(model.model)) {
|
|
502
|
+
payload.buzzer_manual_alarm = value === 1;
|
|
503
|
+
}
|
|
504
|
+
break;
|
|
505
|
+
case '320':
|
|
506
|
+
if (['MCCGQ13LM'].includes(model.model)) {
|
|
507
|
+
payload.battery_cover = {0: 'CLOSE', 1: 'OPEN'}[value];
|
|
508
|
+
}
|
|
509
|
+
break;
|
|
510
|
+
case '322':
|
|
511
|
+
if (['RTCZCGQ11LM'].includes(model.model)) {
|
|
512
|
+
payload.presence = {0: false, 1: true, 255: null}[value];
|
|
513
|
+
}
|
|
514
|
+
break;
|
|
515
|
+
case '323':
|
|
516
|
+
if (['RTCZCGQ11LM'].includes(model.model)) {
|
|
517
|
+
payload.presence_event = {0: 'enter', 1: 'leave', 2: 'left_enter', 3: 'right_leave', 4: 'right_enter',
|
|
518
|
+
5: 'left_leave', 6: 'approach', 7: 'away'}[value];
|
|
519
|
+
}
|
|
520
|
+
break;
|
|
521
|
+
case '324':
|
|
522
|
+
if (['RTCZCGQ11LM'].includes(model.model)) {
|
|
523
|
+
payload.monitoring_mode = {0: 'undirected', 1: 'left_right'}[value];
|
|
524
|
+
}
|
|
525
|
+
break;
|
|
526
|
+
case '326':
|
|
527
|
+
if (['RTCZCGQ11LM'].includes(model.model)) {
|
|
528
|
+
payload.approach_distance = {0: 'far', 1: 'medium', 2: 'near'}[value];
|
|
529
|
+
}
|
|
530
|
+
break;
|
|
531
|
+
case '328':
|
|
532
|
+
if (['CTP-R01'].includes(model.model)) {
|
|
533
|
+
// detected hard switch of operation_mode (attribute 0x148[328])
|
|
534
|
+
payload.operation_mode = {0: 'action_mode', 1: 'scene_mode'}[msg.data[328]];
|
|
535
|
+
}
|
|
536
|
+
break;
|
|
537
|
+
case '329':
|
|
538
|
+
if (['CTP-R01'].includes(model.model)) {
|
|
539
|
+
// side_up attribute report (attribute 0x149[329])
|
|
540
|
+
payload.action = 'side_up';
|
|
541
|
+
payload.side = msg.data[329] + 1;
|
|
542
|
+
}
|
|
543
|
+
break;
|
|
544
|
+
case '331':
|
|
545
|
+
if (['JT-BZ-01AQ/A', 'JY-GZ-01AQ'].includes(model.model)) {
|
|
546
|
+
payload.linkage_alarm = value === 1;
|
|
547
|
+
}
|
|
548
|
+
break;
|
|
549
|
+
case '332':
|
|
550
|
+
if (['JT-BZ-01AQ/A', 'JY-GZ-01AQ'].includes(model.model)) {
|
|
551
|
+
payload.linkage_alarm_state = value === 1;
|
|
552
|
+
}
|
|
553
|
+
break;
|
|
554
|
+
case '338':
|
|
555
|
+
if (['RTCGQ14LM'].includes(model.model)) {
|
|
556
|
+
payload.trigger_indicator = value === 1;
|
|
557
|
+
}
|
|
558
|
+
break;
|
|
559
|
+
case '512':
|
|
560
|
+
if (['ZNCZ15LM', 'QBCZ14LM', 'QBCZ15LM', 'SP-EUC01'].includes(model.model)) {
|
|
561
|
+
payload.button_lock = value === 1 ? 'OFF' : 'ON';
|
|
562
|
+
} else {
|
|
563
|
+
const mode = {0x01: 'control_relay', 0x00: 'decoupled'}[value];
|
|
564
|
+
payload[postfixWithEndpointName('operation_mode', msg, model, meta)] = mode;
|
|
565
|
+
}
|
|
566
|
+
break;
|
|
567
|
+
case '513':
|
|
568
|
+
payload.power_outage_memory = value === 1;
|
|
569
|
+
break;
|
|
570
|
+
case '514':
|
|
571
|
+
payload.auto_off = value === 1;
|
|
572
|
+
break;
|
|
573
|
+
case '515':
|
|
574
|
+
payload.led_disabled_night = value === 1;
|
|
575
|
+
break;
|
|
576
|
+
case '519':
|
|
577
|
+
payload.consumer_connected = value === 1;
|
|
578
|
+
break;
|
|
579
|
+
case '523':
|
|
580
|
+
payload.overload_protection = precisionRound(value, 2);
|
|
581
|
+
break;
|
|
582
|
+
case '550':
|
|
583
|
+
payload.button_switch_mode = value === 1 ? 'relay_and_usb' : 'relay';
|
|
584
|
+
break;
|
|
585
|
+
case '1025':
|
|
586
|
+
if (['ZNCLBL01LM'].includes(model.model)) {
|
|
587
|
+
payload.hand_open = !value;
|
|
588
|
+
} else {
|
|
589
|
+
// next values update only when curtain finished initial setup and knows current position
|
|
590
|
+
payload.options = {...payload.options,
|
|
591
|
+
reverse_direction: value[2] == '\u0001',
|
|
592
|
+
hand_open: value[5] == '\u0000',
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
break;
|
|
596
|
+
case '1028':
|
|
597
|
+
payload = {...payload,
|
|
598
|
+
motor_state: (options.invert_cover ? {0: 'stopped', 1: 'closing', 2: 'opening'} :
|
|
599
|
+
{0: 'stopped', 1: 'opening', 2: 'closing'})[value],
|
|
600
|
+
running: !!value,
|
|
601
|
+
};
|
|
602
|
+
break;
|
|
603
|
+
case '1033':
|
|
604
|
+
if (['ZNJLBL01LM'].includes(model.model)) {
|
|
605
|
+
payload.charging_status = value === 1;
|
|
606
|
+
}
|
|
607
|
+
break;
|
|
608
|
+
case '1034':
|
|
609
|
+
if (['ZNJLBL01LM'].includes(model.model)) {
|
|
610
|
+
payload.battery = value;
|
|
611
|
+
}
|
|
612
|
+
break;
|
|
613
|
+
case '1035':
|
|
614
|
+
if (['ZNCLBL01LM'].includes(model.model)) {
|
|
615
|
+
payload.voltage = value;
|
|
616
|
+
}
|
|
617
|
+
break;
|
|
618
|
+
case '1055':
|
|
619
|
+
if (['ZNCLBL01LM'].includes(model.model)) {
|
|
620
|
+
payload.target_position = options.invert_cover ? 100 - value : value;
|
|
621
|
+
}
|
|
622
|
+
break;
|
|
623
|
+
case '1056':
|
|
624
|
+
if (['ZNCLBL01LM'].includes(model.model)) {
|
|
625
|
+
// This is the "target_state" attribute, which takes the following values: 0: 'OPEN', 1: 'CLOSE', 2: 'STOP'.
|
|
626
|
+
// It is not used because the values 0 and 1 are not always reported.
|
|
627
|
+
// https://github.com/Koenkk/zigbee-herdsman-converters/pull/4307
|
|
628
|
+
}
|
|
629
|
+
break;
|
|
630
|
+
case '1057':
|
|
631
|
+
if (['ZNCLBL01LM'].includes(model.model)) {
|
|
632
|
+
payload.motor_state = (options.invert_cover ? {0: 'opening', 1: 'closing', 2: 'stopped'} :
|
|
633
|
+
{0: 'closing', 1: 'opening', 2: 'stopped'})[value];
|
|
634
|
+
payload.running = value < 2 ? true : false;
|
|
635
|
+
}
|
|
636
|
+
break;
|
|
637
|
+
case '1061':
|
|
638
|
+
if (['ZNCLBL01LM'].includes(model.model)) {
|
|
639
|
+
payload.action = (options.invert_cover ? {1: 'manual_close', 2: 'manual_open'} :
|
|
640
|
+
{1: 'manual_open', 2: 'manual_close'})[value];
|
|
641
|
+
}
|
|
642
|
+
break;
|
|
643
|
+
case '1063':
|
|
644
|
+
if (['ZNCLBL01LM'].includes(model.model)) {
|
|
645
|
+
payload.hooks_lock = {0: 'UNLOCK', 1: 'LOCK'}[value];
|
|
646
|
+
}
|
|
647
|
+
break;
|
|
648
|
+
case '1064':
|
|
649
|
+
if (['ZNCLBL01LM'].includes(model.model)) {
|
|
650
|
+
payload.hooks_state = {0: 'unlocked', 1: 'locked', 2: 'locking', 3: 'unlocking'}[value];
|
|
651
|
+
payload.hooks_lock = {0: 'UNLOCK', 1: 'LOCK', 2: 'UNLOCK', 3: 'LOCK'}[value];
|
|
652
|
+
}
|
|
653
|
+
break;
|
|
654
|
+
case '1289':
|
|
655
|
+
payload.dimmer_mode = {3: 'rgbw', 1: 'dual_ct'}[value];
|
|
656
|
+
break;
|
|
657
|
+
case '65281':
|
|
658
|
+
{
|
|
659
|
+
const payload65281 = await numericAttributes2Payload(msg, meta, model, options, value);
|
|
660
|
+
payload = {...payload, ...payload65281};
|
|
661
|
+
}
|
|
662
|
+
break;
|
|
663
|
+
case '65282':
|
|
664
|
+
// This is a a complete structure with attributes, like element 0 for state, element 1 for voltage...
|
|
665
|
+
// At this moment we only extract what we are sure, for example, position 0 seems to be always 1 for a
|
|
666
|
+
// occupancy sensor, so we ignore it at this moment
|
|
667
|
+
if (['MCCGQ01LM'].includes(model.model)) {
|
|
668
|
+
payload.contact = value[0].elmVal === 0;
|
|
669
|
+
}
|
|
670
|
+
payload.voltage = value[1].elmVal;
|
|
671
|
+
if (model.meta && model.meta.battery && model.meta.battery.voltageToPercentage) {
|
|
672
|
+
payload.battery = batteryVoltageToPercentage(payload.voltage, model.meta.battery.voltageToPercentage);
|
|
673
|
+
}
|
|
674
|
+
payload.power_outage_count = value[4].elmVal - 1;
|
|
675
|
+
break;
|
|
676
|
+
case 'mode':
|
|
677
|
+
payload.operation_mode = ['command', 'event'][value];
|
|
678
|
+
break;
|
|
679
|
+
case 'modelId':
|
|
680
|
+
// We ignore it, but we add it here to not shown an unknown key in the log
|
|
681
|
+
break;
|
|
682
|
+
case 'illuminance':
|
|
683
|
+
// It contains the illuminance and occupancy, but in z2m we use a custom timer to do it, so we ignore it
|
|
684
|
+
break;
|
|
685
|
+
default:
|
|
686
|
+
if (meta.logger) meta.logger.debug(`${model.zigbeeModel}: unknown key ${key} with value ${value}`);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (meta.logger) meta.logger.debug(`${model.zigbeeModel}: Processed data into payload ${JSON.stringify(payload)}`);
|
|
691
|
+
|
|
692
|
+
return payload;
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
const VOCKQJK11LMDisplayUnit = {
|
|
696
|
+
'mgm3_celsius': 0x00, // mg/m³, °C (default)
|
|
697
|
+
'ppb_celsius': 0x01, // ppb, °C
|
|
698
|
+
'mgm3_fahrenheit': 0x10, // mg/m³, °F
|
|
699
|
+
'ppb_fahrenheit': 0x11, // ppb, °F
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
const numericAttributes2Options = (definition) => {
|
|
703
|
+
const supported = ['temperature', 'device_temperature', 'illuminance', 'illuminance_lux',
|
|
704
|
+
'pressure', 'power', 'current', 'voltage', 'energy', 'power'];
|
|
705
|
+
const precisionSupported = ['temperature', 'humidity', 'pressure', 'power', 'current', 'voltage', 'energy', 'power'];
|
|
706
|
+
const result = [];
|
|
707
|
+
for (const expose of definition.exposes) {
|
|
708
|
+
// only eletrical measurement voltage is supported, not battery
|
|
709
|
+
const isBatteryVoltage = expose.name === 'voltage' && definition.meta && definition.meta.battery;
|
|
710
|
+
if (supported.includes(expose.name) && !isBatteryVoltage) {
|
|
711
|
+
const type = calibrateAndPrecisionRoundOptionsIsPercentual(expose.name) ? 'percentual' : 'absolute';
|
|
712
|
+
result.push(exposes.options.calibration(expose.name, type));
|
|
713
|
+
if (precisionSupported.includes(expose.name)) {
|
|
714
|
+
result.push(exposes.options.precision(expose.name));
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return result;
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
// For RTCZCGQ11LM
|
|
723
|
+
/**
|
|
724
|
+
* @typedef {{
|
|
725
|
+
* x: number,
|
|
726
|
+
* y: number,
|
|
727
|
+
* }} AqaraFP1RegionZone
|
|
728
|
+
*/
|
|
729
|
+
const fp1Constants = {
|
|
730
|
+
region_event_key: 0x0151,
|
|
731
|
+
region_event_types: {
|
|
732
|
+
Enter: 1,
|
|
733
|
+
Leave: 2,
|
|
734
|
+
Occupied: 4,
|
|
735
|
+
Unoccupied: 8,
|
|
736
|
+
},
|
|
737
|
+
region_config_write_attribute: 0x0150,
|
|
738
|
+
region_config_write_attribute_type: 0x41,
|
|
739
|
+
region_config_cmds: {
|
|
740
|
+
/**
|
|
741
|
+
* Creates new region (or force replaces existing one)
|
|
742
|
+
* with new zones definition.
|
|
743
|
+
*/
|
|
744
|
+
create: 1,
|
|
745
|
+
/**
|
|
746
|
+
* Modifies existing region.
|
|
747
|
+
* Note: unused, as it seems to break existing regions
|
|
748
|
+
* (region stops reporting new detection events).
|
|
749
|
+
* Use "create" instead, as it replaces existing region with new one.
|
|
750
|
+
*/
|
|
751
|
+
modify: 2,
|
|
752
|
+
/**
|
|
753
|
+
* Deletes existing region.
|
|
754
|
+
*/
|
|
755
|
+
delete: 3,
|
|
756
|
+
},
|
|
757
|
+
region_config_regionId_min: 1,
|
|
758
|
+
region_config_regionId_max: 10,
|
|
759
|
+
region_config_zoneY_min: 1,
|
|
760
|
+
region_config_zoneY_max: 7,
|
|
761
|
+
region_config_zoneX_min: 1,
|
|
762
|
+
region_config_zoneX_max: 4,
|
|
763
|
+
region_config_cmd_suffix_upsert: 0xff,
|
|
764
|
+
region_config_cmd_suffix_delete: 0x00,
|
|
765
|
+
};
|
|
766
|
+
const fp1Mappers = {
|
|
767
|
+
aqara_fp1: {
|
|
768
|
+
region_event_type_names: {
|
|
769
|
+
[fp1Constants.region_event_types.Enter]: 'enter',
|
|
770
|
+
[fp1Constants.region_event_types.Leave]: 'leave',
|
|
771
|
+
[fp1Constants.region_event_types.Occupied]: 'occupied',
|
|
772
|
+
[fp1Constants.region_event_types.Unoccupied]: 'unoccupied',
|
|
773
|
+
},
|
|
774
|
+
},
|
|
775
|
+
};
|
|
776
|
+
const fp1 = {
|
|
777
|
+
constants: fp1Constants,
|
|
778
|
+
mappers: fp1Mappers,
|
|
779
|
+
/**
|
|
780
|
+
* @param {undefined | Set<number>} xCells
|
|
781
|
+
* @return {number}
|
|
782
|
+
*/
|
|
783
|
+
encodeXCellsDefinition: (xCells) => {
|
|
784
|
+
if (!xCells || !xCells.size) {
|
|
785
|
+
return 0;
|
|
786
|
+
}
|
|
787
|
+
return [...xCells.values()].reduce((accumulator, marker) => accumulator + fp1.encodeXCellIdx(marker), 0);
|
|
788
|
+
},
|
|
789
|
+
/**
|
|
790
|
+
* @param {number} cellXIdx
|
|
791
|
+
* @return {number}
|
|
792
|
+
*/
|
|
793
|
+
encodeXCellIdx: (cellXIdx) => {
|
|
794
|
+
return 2 ** (cellXIdx - 1);
|
|
795
|
+
},
|
|
796
|
+
// Note: let TypeScript infer the return type to enable union discrimination
|
|
797
|
+
// eslint-disable-next-line valid-jsdoc
|
|
798
|
+
/**
|
|
799
|
+
* @param {unknown} input
|
|
800
|
+
*/
|
|
801
|
+
parseAqaraFp1RegionDeleteInput: (input) => {
|
|
802
|
+
if (!input || typeof input !== 'object') {
|
|
803
|
+
return fp1.failure({reason: 'NOT_OBJECT'});
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (!('region_id' in input) || !fp1.isAqaraFp1RegionId(input.region_id)) {
|
|
807
|
+
return fp1.failure({reason: 'INVALID_REGION_ID'});
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
return {
|
|
811
|
+
/** @type true */
|
|
812
|
+
isSuccess: true,
|
|
813
|
+
payload: {
|
|
814
|
+
command: {
|
|
815
|
+
region_id: input.region_id,
|
|
816
|
+
},
|
|
817
|
+
},
|
|
818
|
+
};
|
|
819
|
+
},
|
|
820
|
+
// Note: let TypeScript infer the return type to enable union discrimination
|
|
821
|
+
// eslint-disable-next-line valid-jsdoc
|
|
822
|
+
/**
|
|
823
|
+
* @param {unknown} input
|
|
824
|
+
*/
|
|
825
|
+
parseAqaraFp1RegionUpsertInput: (input) => {
|
|
826
|
+
if (!input || typeof input !== 'object') {
|
|
827
|
+
return fp1.failure({reason: 'NOT_OBJECT'});
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (!('region_id' in input) || !fp1.isAqaraFp1RegionId(input.region_id)) {
|
|
831
|
+
return fp1.failure({reason: 'INVALID_REGION_ID'});
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if (!('zones' in input) || !Array.isArray(input.zones) || !input.zones.length) {
|
|
835
|
+
return fp1.failure({reason: 'ZONES_LIST_EMPTY'});
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (!input.zones.every(fp1.isAqaraFp1RegionZoneDefinition)) {
|
|
839
|
+
return fp1.failure({reason: 'INVALID_ZONES'});
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
return {
|
|
843
|
+
/** @type true */
|
|
844
|
+
isSuccess: true,
|
|
845
|
+
payload: {
|
|
846
|
+
command: {
|
|
847
|
+
region_id: input.region_id,
|
|
848
|
+
zones: input.zones,
|
|
849
|
+
},
|
|
850
|
+
},
|
|
851
|
+
};
|
|
852
|
+
},
|
|
853
|
+
// Note: this is valid typescript JSDoc
|
|
854
|
+
// eslint-disable-next-line valid-jsdoc
|
|
855
|
+
/**
|
|
856
|
+
* @param {unknown} value
|
|
857
|
+
* @returns {value is number}
|
|
858
|
+
*/
|
|
859
|
+
isAqaraFp1RegionId: (value) => {
|
|
860
|
+
return (
|
|
861
|
+
typeof value === 'number' &&
|
|
862
|
+
value >= fp1.constants.region_config_regionId_min &&
|
|
863
|
+
value <= fp1.constants.region_config_regionId_max
|
|
864
|
+
);
|
|
865
|
+
},
|
|
866
|
+
// Note: this is valid typescript JSDoc
|
|
867
|
+
// eslint-disable-next-line valid-jsdoc
|
|
868
|
+
/**
|
|
869
|
+
* @param {unknown} value
|
|
870
|
+
* @returns {value is AqaraFP1RegionZone}
|
|
871
|
+
*/
|
|
872
|
+
isAqaraFp1RegionZoneDefinition: (value) => {
|
|
873
|
+
return (
|
|
874
|
+
value &&
|
|
875
|
+
typeof value === 'object' &&
|
|
876
|
+
'x' in value &&
|
|
877
|
+
'y' in value &&
|
|
878
|
+
typeof value.x === 'number' &&
|
|
879
|
+
typeof value.y === 'number' &&
|
|
880
|
+
value.x >= fp1.constants.region_config_zoneX_min &&
|
|
881
|
+
value.x <= fp1.constants.region_config_zoneX_max &&
|
|
882
|
+
value.y >= fp1.constants.region_config_zoneY_min &&
|
|
883
|
+
value.y <= fp1.constants.region_config_zoneY_max
|
|
884
|
+
);
|
|
885
|
+
},
|
|
886
|
+
/**
|
|
887
|
+
* @template {Record<string, unknown>} ErrorType
|
|
888
|
+
* @param {ErrorType} error
|
|
889
|
+
* @return { { isSuccess: false, error: ErrorType } }
|
|
890
|
+
*/
|
|
891
|
+
failure: (error) => {
|
|
892
|
+
return {
|
|
893
|
+
isSuccess: false,
|
|
894
|
+
error,
|
|
895
|
+
};
|
|
896
|
+
},
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* @param {Buffer} buffer
|
|
901
|
+
* @param {number} offset
|
|
902
|
+
* @return {number}
|
|
903
|
+
*/
|
|
904
|
+
function readTemperature(buffer, offset) {
|
|
905
|
+
return buffer.readUint16BE(offset) / 100;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* @param {Buffer} buffer
|
|
910
|
+
* @param {number} offset
|
|
911
|
+
* @param {number} temperature
|
|
912
|
+
* @return {void}
|
|
913
|
+
*/
|
|
914
|
+
function writeTemperature(buffer, offset, temperature) {
|
|
915
|
+
buffer.writeUint16BE(temperature * 100, offset);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* @type {Day[]}
|
|
920
|
+
*/
|
|
921
|
+
const dayNames = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* @param {Buffer} buffer
|
|
925
|
+
* @param {number} offset
|
|
926
|
+
* @return {Day[]}
|
|
927
|
+
*/
|
|
928
|
+
function readDaySelection(buffer, offset) {
|
|
929
|
+
const selectedDays = [];
|
|
930
|
+
|
|
931
|
+
dayNames.forEach((day, index) => {
|
|
932
|
+
if ((buffer[offset] >> index + 1) % 2 !== 0) {
|
|
933
|
+
selectedDays.push(day);
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
return selectedDays;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* @param {Day[]} selectedDays
|
|
942
|
+
*/
|
|
943
|
+
function validateDaySelection(selectedDays) {
|
|
944
|
+
selectedDays.filter((selectedDay) => !dayNames.includes(selectedDay)).forEach((invalidValue) => {
|
|
945
|
+
throw new Error(`The value "${invalidValue}" is not a valid day (available values: ${dayNames.join(', ')})`);
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* @param {Buffer} buffer
|
|
951
|
+
* @param {number} offset
|
|
952
|
+
* @param {Day[]} selectedDays
|
|
953
|
+
*/
|
|
954
|
+
function writeDaySelection(buffer, offset, selectedDays) {
|
|
955
|
+
validateDaySelection(selectedDays);
|
|
956
|
+
|
|
957
|
+
const bitMap = dayNames.reduce((repeat, dayName, index) => {
|
|
958
|
+
const isDaySelected = selectedDays.includes(dayName);
|
|
959
|
+
return repeat | isDaySelected << index + 1;
|
|
960
|
+
}, 0);
|
|
961
|
+
|
|
962
|
+
buffer.writeUInt8(bitMap, offset);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const timeNextDayFlag = 1 << 15;
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* @param {Buffer} buffer
|
|
969
|
+
* @param {number} offset
|
|
970
|
+
* @return {number}
|
|
971
|
+
*/
|
|
972
|
+
function readTime(buffer, offset) {
|
|
973
|
+
const minutesWithDayFlag = buffer.readUint16BE(offset);
|
|
974
|
+
return minutesWithDayFlag & ~timeNextDayFlag;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* @param {number} time
|
|
979
|
+
* @return {void}
|
|
980
|
+
*/
|
|
981
|
+
function validateTime(time) {
|
|
982
|
+
const isPositiveInteger = (value) => typeof value === 'number' && Number.isInteger(value) && value >= 0;
|
|
983
|
+
|
|
984
|
+
if (!isPositiveInteger(time)) {
|
|
985
|
+
throw new Error(`Time must be a positive integer number`);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (time >= 24 * 60) {
|
|
989
|
+
throw new Error(`Time must be between 00:00 and 23:59`);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/**
|
|
994
|
+
* @param {Buffer} buffer
|
|
995
|
+
* @param {number} offset
|
|
996
|
+
* @param {number} time
|
|
997
|
+
* @param {boolean} isNextDay
|
|
998
|
+
* @return {void}
|
|
999
|
+
*/
|
|
1000
|
+
function writeTime(buffer, offset, time, isNextDay) {
|
|
1001
|
+
validateTime(time);
|
|
1002
|
+
|
|
1003
|
+
let minutesWithDayFlag = time;
|
|
1004
|
+
|
|
1005
|
+
if (isNextDay) {
|
|
1006
|
+
minutesWithDayFlag = minutesWithDayFlag | timeNextDayFlag;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
buffer.writeUint16BE(minutesWithDayFlag, offset);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* Formats a number of minutes into a user-readable 24-hour time notation in the form hh:mm.
|
|
1014
|
+
* @param {number} timeMinutes
|
|
1015
|
+
* @return {string}
|
|
1016
|
+
*/
|
|
1017
|
+
function formatTime(timeMinutes) {
|
|
1018
|
+
const hours = Math.floor(timeMinutes / 60);
|
|
1019
|
+
const minutes = timeMinutes % 60;
|
|
1020
|
+
return `${hours}:${String(minutes).padStart(2, '0')}`;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Parses a 24-hour time notation string in the form hh:mm into a number of minutes.
|
|
1025
|
+
* @param {string} timeString
|
|
1026
|
+
* @return {number}
|
|
1027
|
+
*/
|
|
1028
|
+
function parseTime(timeString) {
|
|
1029
|
+
const parts = timeString.split(':');
|
|
1030
|
+
|
|
1031
|
+
if (parts.length !== 2) {
|
|
1032
|
+
throw new Error(`Cannot parse time string ${timeString}`);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const hours = parseInt(parts[0]);
|
|
1036
|
+
const minutes = parseInt(parts[1]);
|
|
1037
|
+
|
|
1038
|
+
return hours * 60 + minutes;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
const stringifiedScheduleFragmentSeparator = '|';
|
|
1042
|
+
const stringifiedScheduleValueSeparator = ',';
|
|
1043
|
+
|
|
1044
|
+
const trv = {
|
|
1045
|
+
decodeFirmwareVersionString(value) {
|
|
1046
|
+
// Add prefix to follow Aqara's versioning schema: https://www.aqara.com/en/version/radiator-thermostat-e1
|
|
1047
|
+
const firmwareVersionPrefix = '0.0.0_';
|
|
1048
|
+
|
|
1049
|
+
// Reinterpret from LE integer to byte sequence(e.g., `[25,8,0,0]` corresponds to 0.0.0_0825)
|
|
1050
|
+
const buffer = Buffer.alloc(4);
|
|
1051
|
+
buffer.writeUInt32LE(value);
|
|
1052
|
+
const firmwareVersionNumber = buffer.reverse().subarray(1).join('');
|
|
1053
|
+
|
|
1054
|
+
return firmwareVersionPrefix + firmwareVersionNumber;
|
|
1055
|
+
},
|
|
1056
|
+
|
|
1057
|
+
decodePreset(value) {
|
|
1058
|
+
// Setup mode is the initial device state after powering it ("F11" on display) and not a real preset that can be deliberately
|
|
1059
|
+
// set by users, therefore it is exposed as a separate flag.
|
|
1060
|
+
return {
|
|
1061
|
+
setup: value === 3,
|
|
1062
|
+
preset: {2: 'away', 1: 'auto', 0: 'manual'}[value],
|
|
1063
|
+
};
|
|
1064
|
+
},
|
|
1065
|
+
|
|
1066
|
+
decodeHeartbeat(meta, model, messageBuffer) {
|
|
1067
|
+
const data = buffer2DataObject(meta, model, messageBuffer);
|
|
1068
|
+
const payload = {};
|
|
1069
|
+
|
|
1070
|
+
Object.entries(data).forEach(([key, value]) => {
|
|
1071
|
+
switch (parseInt(key)) {
|
|
1072
|
+
case 3:
|
|
1073
|
+
payload.device_temperature = value;
|
|
1074
|
+
break;
|
|
1075
|
+
case 5:
|
|
1076
|
+
payload.power_outage_count = value - 1;
|
|
1077
|
+
break;
|
|
1078
|
+
case 10:
|
|
1079
|
+
// unidentified number, e.g. 32274, 3847
|
|
1080
|
+
break;
|
|
1081
|
+
case 13:
|
|
1082
|
+
payload.firmware_version = trv.decodeFirmwareVersionString(value);
|
|
1083
|
+
break;
|
|
1084
|
+
case 17:
|
|
1085
|
+
// unidentified flag/enum, e.g. 1
|
|
1086
|
+
break;
|
|
1087
|
+
case 101:
|
|
1088
|
+
Object.assign(payload, trv.decodePreset(value));
|
|
1089
|
+
break;
|
|
1090
|
+
case 102:
|
|
1091
|
+
payload.local_temperature = value / 100;
|
|
1092
|
+
break;
|
|
1093
|
+
case 103:
|
|
1094
|
+
// This takes the following values:
|
|
1095
|
+
// - `occupied_heating_setpoint` if `system_mode` is `heat` and `preset` is `manual`
|
|
1096
|
+
// - `away_preset_temperature` if `system_mode` is `heat` and `preset` is `away`
|
|
1097
|
+
// - `5` if `system_mode` is `off`
|
|
1098
|
+
// It thus behaves similar to `occupied_heating_setpoint` except in `off` mode. Due to this difference,
|
|
1099
|
+
// this value is written to another property to avoid an inconsistency of the `occupied_heating_setpoint`.
|
|
1100
|
+
// TODO How to handle this value? Find better name?
|
|
1101
|
+
payload.internal_heating_setpoint = value / 100;
|
|
1102
|
+
break;
|
|
1103
|
+
case 104:
|
|
1104
|
+
payload.valve_alarm = value === 1;
|
|
1105
|
+
break;
|
|
1106
|
+
case 105:
|
|
1107
|
+
payload.battery = value;
|
|
1108
|
+
break;
|
|
1109
|
+
case 106:
|
|
1110
|
+
// unidentified flag/enum, e.g. 0
|
|
1111
|
+
break;
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
return payload;
|
|
1116
|
+
},
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* Decode a Zigbee schedule configuration message into a schedule configuration object.
|
|
1120
|
+
* @param {Buffer} buffer
|
|
1121
|
+
* @return {TrvScheduleConfig}
|
|
1122
|
+
*/
|
|
1123
|
+
decodeSchedule(buffer) {
|
|
1124
|
+
return {
|
|
1125
|
+
days: readDaySelection(buffer, 1),
|
|
1126
|
+
events: [
|
|
1127
|
+
{time: readTime(buffer, 2), temperature: readTemperature(buffer, 6)},
|
|
1128
|
+
{time: readTime(buffer, 8), temperature: readTemperature(buffer, 12)},
|
|
1129
|
+
{time: readTime(buffer, 14), temperature: readTemperature(buffer, 18)},
|
|
1130
|
+
{time: readTime(buffer, 20), temperature: readTemperature(buffer, 24)},
|
|
1131
|
+
],
|
|
1132
|
+
};
|
|
1133
|
+
},
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* @param {TrvScheduleConfig} schedule
|
|
1137
|
+
* @return {void}
|
|
1138
|
+
*/
|
|
1139
|
+
validateSchedule(schedule) {
|
|
1140
|
+
const eventCount = 4;
|
|
1141
|
+
|
|
1142
|
+
if (typeof schedule !== 'object') {
|
|
1143
|
+
throw new Error('The provided value must be a schedule object');
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
if (schedule.days == null || !Array.isArray(schedule.days) || schedule.days.length === 0) {
|
|
1147
|
+
throw new Error(`The schedule object must contain an array of days with at least one entry`);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
validateDaySelection(schedule.days);
|
|
1151
|
+
|
|
1152
|
+
if (schedule.events == null || !Array.isArray(schedule.events) || schedule.events.length !== eventCount) {
|
|
1153
|
+
throw new Error(`The schedule object must contain an array of ${eventCount} time/temperature events`);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
schedule.events.forEach((event) => {
|
|
1157
|
+
if (typeof event !== 'object') {
|
|
1158
|
+
throw new Error('The provided time/temperature event must be an object');
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
validateTime(event.time);
|
|
1162
|
+
|
|
1163
|
+
if (typeof event.temperature !== 'number') {
|
|
1164
|
+
throw new Error(`The provided time/temperature entry must contain a numeric temperature`);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
if (event.temperature < 5 || event.temperature > 30) {
|
|
1168
|
+
throw new Error(`The temperature must be between 5 and 30 °C`);
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
// Calculate time durations between events
|
|
1173
|
+
const durations = schedule.events
|
|
1174
|
+
.map((entry, index, entries) => {
|
|
1175
|
+
if (index === 0) {
|
|
1176
|
+
return 0;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
const time = entry.time;
|
|
1180
|
+
const fullDay = 24 * 60;
|
|
1181
|
+
const previousTime = entries[index - 1].time;
|
|
1182
|
+
const isNextDay = time < previousTime;
|
|
1183
|
+
|
|
1184
|
+
if (isNextDay) {
|
|
1185
|
+
return (fullDay - previousTime) + time;
|
|
1186
|
+
} else {
|
|
1187
|
+
return time - previousTime;
|
|
1188
|
+
}
|
|
1189
|
+
})
|
|
1190
|
+
// Remove first entry which is not a duration
|
|
1191
|
+
.slice(1);
|
|
1192
|
+
|
|
1193
|
+
const minDuration = 60;
|
|
1194
|
+
const hasInvalidDurations = durations.some((duration) => duration < minDuration);
|
|
1195
|
+
|
|
1196
|
+
if (hasInvalidDurations) {
|
|
1197
|
+
throw new Error(`The individual times must be at least 1 hour apart`);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
const maxTotalDuration = 24 * 60;
|
|
1201
|
+
const totalDuration = durations.reduce((total, duration) => total + duration, 0);
|
|
1202
|
+
|
|
1203
|
+
if (totalDuration > maxTotalDuration) {
|
|
1204
|
+
// this implicitly also makes sure that there is at most one "next day" switch
|
|
1205
|
+
throw new Error(`The start and end times must be at most 24 hours apart`);
|
|
1206
|
+
}
|
|
1207
|
+
},
|
|
1208
|
+
|
|
1209
|
+
/**
|
|
1210
|
+
* Encodes a schedule object into Zigbee message format.
|
|
1211
|
+
* @param {TrvScheduleConfig} schedule
|
|
1212
|
+
* @return {Buffer}
|
|
1213
|
+
*/
|
|
1214
|
+
encodeSchedule(schedule) {
|
|
1215
|
+
const buffer = Buffer.alloc(26);
|
|
1216
|
+
buffer.writeUInt8(0x04);
|
|
1217
|
+
|
|
1218
|
+
writeDaySelection(buffer, 1, schedule.days);
|
|
1219
|
+
|
|
1220
|
+
schedule.events.forEach((event, index, events) => {
|
|
1221
|
+
const offset = 2 + index * 6;
|
|
1222
|
+
const isNextDay = index > 0 && event.time < events[index - 1].time;
|
|
1223
|
+
|
|
1224
|
+
writeTime(buffer, offset, event.time, isNextDay);
|
|
1225
|
+
writeTemperature(buffer, offset + 4, event.temperature);
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
return buffer;
|
|
1229
|
+
},
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* Converts a schedule config object into a configuration string.
|
|
1233
|
+
* @param {TrvScheduleConfig} schedule
|
|
1234
|
+
* @return {string}
|
|
1235
|
+
*/
|
|
1236
|
+
stringifySchedule(schedule) {
|
|
1237
|
+
const stringifiedScheduleFragments = [schedule.days.join(stringifiedScheduleValueSeparator)];
|
|
1238
|
+
|
|
1239
|
+
for (const event of schedule.events) {
|
|
1240
|
+
const formattedTemperature = Number.isInteger(event.temperature) ?
|
|
1241
|
+
event.temperature.toFixed(1) : // add ".0" for usability to signal that floats can be used
|
|
1242
|
+
String(event.temperature);
|
|
1243
|
+
|
|
1244
|
+
const entryFragments = [formatTime(event.time), formattedTemperature];
|
|
1245
|
+
|
|
1246
|
+
stringifiedScheduleFragments.push(entryFragments.join(stringifiedScheduleValueSeparator));
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
return stringifiedScheduleFragments.join(stringifiedScheduleFragmentSeparator);
|
|
1250
|
+
},
|
|
1251
|
+
|
|
1252
|
+
/**
|
|
1253
|
+
* Parses a schedule configuration string into a configuration object.
|
|
1254
|
+
* @param {string} stringifiedSchedule
|
|
1255
|
+
* @return {TrvScheduleConfig}
|
|
1256
|
+
*/
|
|
1257
|
+
parseSchedule(stringifiedSchedule) {
|
|
1258
|
+
const schedule = {days: [], events: []};
|
|
1259
|
+
|
|
1260
|
+
if (!stringifiedSchedule) {
|
|
1261
|
+
return schedule;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
const stringifiedScheduleFragments = stringifiedSchedule.split(stringifiedScheduleFragmentSeparator);
|
|
1265
|
+
|
|
1266
|
+
stringifiedScheduleFragments.forEach((fragment, index) => {
|
|
1267
|
+
if (index === 0) {
|
|
1268
|
+
schedule.days.push(...fragment.split(stringifiedScheduleValueSeparator));
|
|
1269
|
+
} else {
|
|
1270
|
+
const entryFragments = fragment.split(stringifiedScheduleValueSeparator);
|
|
1271
|
+
const entry = {time: parseTime(entryFragments[0]), temperature: parseFloat(entryFragments[1])};
|
|
1272
|
+
schedule.events.push(entry);
|
|
1273
|
+
}
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
return schedule;
|
|
1277
|
+
},
|
|
1278
|
+
};
|
|
1279
|
+
|
|
1280
|
+
module.exports = {
|
|
1281
|
+
buffer2DataObject,
|
|
1282
|
+
numericAttributes2Payload,
|
|
1283
|
+
numericAttributes2Options,
|
|
1284
|
+
VOCKQJK11LMDisplayUnit,
|
|
1285
|
+
fp1,
|
|
1286
|
+
manufacturerCode: 0x115f,
|
|
1287
|
+
trv,
|
|
1288
|
+
};
|