zigbee-herdsman-converters 25.70.0 → 25.72.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/devices/efekta.js +2 -2
  3. package/dist/devices/efekta.js.map +1 -1
  4. package/dist/devices/ikea.d.ts.map +1 -1
  5. package/dist/devices/ikea.js +21 -3
  6. package/dist/devices/ikea.js.map +1 -1
  7. package/dist/devices/lumi.d.ts.map +1 -1
  8. package/dist/devices/lumi.js +78 -10
  9. package/dist/devices/lumi.js.map +1 -1
  10. package/dist/devices/multir.d.ts.map +1 -1
  11. package/dist/devices/multir.js +15 -6
  12. package/dist/devices/multir.js.map +1 -1
  13. package/dist/devices/namron.d.ts.map +1 -1
  14. package/dist/devices/namron.js +100 -0
  15. package/dist/devices/namron.js.map +1 -1
  16. package/dist/devices/ozsmartthings.js +1 -1
  17. package/dist/devices/ozsmartthings.js.map +1 -1
  18. package/dist/devices/philips.d.ts.map +1 -1
  19. package/dist/devices/philips.js +201 -1
  20. package/dist/devices/philips.js.map +1 -1
  21. package/dist/devices/profalux.d.ts.map +1 -1
  22. package/dist/devices/profalux.js +13 -6
  23. package/dist/devices/profalux.js.map +1 -1
  24. package/dist/devices/slacky_diy.d.ts.map +1 -1
  25. package/dist/devices/slacky_diy.js +9 -1
  26. package/dist/devices/slacky_diy.js.map +1 -1
  27. package/dist/devices/smarli.d.ts.map +1 -1
  28. package/dist/devices/smarli.js +51 -0
  29. package/dist/devices/smarli.js.map +1 -1
  30. package/dist/devices/third_reality.d.ts.map +1 -1
  31. package/dist/devices/third_reality.js +19 -24
  32. package/dist/devices/third_reality.js.map +1 -1
  33. package/dist/devices/tuya.d.ts.map +1 -1
  34. package/dist/devices/tuya.js +145 -103
  35. package/dist/devices/tuya.js.map +1 -1
  36. package/dist/devices/woolley.d.ts.map +1 -1
  37. package/dist/devices/woolley.js +12 -0
  38. package/dist/devices/woolley.js.map +1 -1
  39. package/dist/devices/zigbeetlc.js +1 -1
  40. package/dist/devices/zigbeetlc.js.map +1 -1
  41. package/dist/lib/ikea.d.ts +5 -0
  42. package/dist/lib/ikea.d.ts.map +1 -1
  43. package/dist/lib/ikea.js +70 -1
  44. package/dist/lib/ikea.js.map +1 -1
  45. package/dist/lib/lumi.d.ts +42 -12
  46. package/dist/lib/lumi.d.ts.map +1 -1
  47. package/dist/lib/lumi.js +401 -116
  48. package/dist/lib/lumi.js.map +1 -1
  49. package/dist/models-index.json +1 -1
  50. package/package.json +1 -1
package/dist/lib/lumi.js CHANGED
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.toZigbee = exports.fromZigbee = exports.modernExtend = exports.lumiModernExtend = exports.manufacturerCode = exports.trv = exports.presence = exports.numericAttributes2Payload = exports.buffer2DataObject = void 0;
37
+ exports.w100EnsureDefaults = w100EnsureDefaults;
37
38
  const node_buffer_1 = require("node:buffer");
38
39
  const fz = __importStar(require("../converters/fromZigbee"));
39
40
  const exposes = __importStar(require("./exposes"));
@@ -425,6 +426,9 @@ msg, meta, model, options, dataObject) => {
425
426
  if (["QBKG25LM", "QBKG33LM", "QBKG34LM"].includes(model.model)) {
426
427
  payload.state_right = value === 1 ? "ON" : "OFF";
427
428
  }
429
+ else if (["TH-S04D"].includes(model.model)) {
430
+ payload.battery = value;
431
+ }
428
432
  else if (["WSDCGQ01LM", "WSDCGQ11LM"].includes(model.model)) {
429
433
  (0, utils_1.assertNumber)(value);
430
434
  payload.pressure = value / 100.0;
@@ -2744,6 +2748,79 @@ const feederDaysLookup = {
2744
2748
  85: "mon-wed-fri-sun",
2745
2749
  42: "tue-thu-sat",
2746
2750
  };
2751
+ const w100Defaults = {
2752
+ system_mode: "off",
2753
+ occupied_heating_setpoint: 15,
2754
+ fan_mode: "auto",
2755
+ unused: "0",
2756
+ thermostat_mode: "OFF",
2757
+ min_target_temp: 5,
2758
+ max_target_temp: 30,
2759
+ };
2760
+ function w100EnsureDefaults(meta) {
2761
+ const state = meta.state || {};
2762
+ const normalized = {
2763
+ system_mode: state.system_mode ?? w100Defaults.system_mode,
2764
+ occupied_heating_setpoint: state.occupied_heating_setpoint ?? w100Defaults.occupied_heating_setpoint,
2765
+ fan_mode: state.fan_mode ?? w100Defaults.fan_mode,
2766
+ unused: state.unused ?? w100Defaults.unused,
2767
+ thermostat_mode: state.thermostat_mode ?? w100Defaults.thermostat_mode,
2768
+ };
2769
+ meta.state = {
2770
+ ...state,
2771
+ ...normalized,
2772
+ };
2773
+ if (meta.device) {
2774
+ if (!meta.device.meta)
2775
+ meta.device.meta = {};
2776
+ if (!meta.device.meta.initialized) {
2777
+ meta.device.meta.initialized = true;
2778
+ }
2779
+ }
2780
+ return normalized;
2781
+ }
2782
+ async function w100SendPMTSD(entity, pmtsd, meta) {
2783
+ const { p, m, t, s, d } = pmtsd;
2784
+ const pmtsdStr = `P${p}_M${m}_T${t}_S${s}_D${d}`;
2785
+ const pmtsdBytes = Array.from(pmtsdStr).map((c) => c.charCodeAt(0));
2786
+ const pmtsdLen = pmtsdBytes.length;
2787
+ const fixedHeader = [
2788
+ 0xaa,
2789
+ 0x71,
2790
+ 0x1f,
2791
+ 0x44,
2792
+ 0x00,
2793
+ 0x00,
2794
+ 0x05,
2795
+ 0x41,
2796
+ 0x1c,
2797
+ 0x00,
2798
+ 0x00,
2799
+ 0x54,
2800
+ 0xef,
2801
+ 0x44,
2802
+ 0x80,
2803
+ 0x71,
2804
+ 0x1a,
2805
+ 0x08,
2806
+ 0x00,
2807
+ 0x08,
2808
+ 0x44,
2809
+ pmtsdLen,
2810
+ ];
2811
+ const counter = Math.floor(Math.random() * 256);
2812
+ fixedHeader[4] = counter;
2813
+ const fullPayload = [...fixedHeader, ...pmtsdBytes];
2814
+ const checksum = fullPayload.reduce((sum, b) => sum + b, 0) & 0xff;
2815
+ fixedHeader[5] = checksum;
2816
+ await entity.write(64704, { 65522: { value: node_buffer_1.Buffer.from(fullPayload), type: 65 } }, { manufacturerCode: 4447, disableDefaultResponse: true });
2817
+ logger_1.logger.info(`Aqara W100: PMTSD frame sent: ${pmtsdStr}`, NS);
2818
+ if (meta.device) {
2819
+ if (!meta.device.meta)
2820
+ meta.device.meta = {};
2821
+ meta.device.meta.lastPMTSDSend = Date.now();
2822
+ }
2823
+ }
2747
2824
  exports.fromZigbee = {
2748
2825
  // lumi generic
2749
2826
  lumi_basic: {
@@ -4039,82 +4116,179 @@ exports.fromZigbee = {
4039
4116
  return result;
4040
4117
  },
4041
4118
  },
4042
- w100_0844_req: {
4119
+ w100_specific: {
4043
4120
  cluster: "manuSpecificLumi",
4044
4121
  type: ["attributeReport", "readResponse"],
4045
- convert: (model, msg, publish, options, meta) => {
4046
- const attr = msg.data[65522];
4047
- if (!attr || !node_buffer_1.Buffer.isBuffer(attr))
4048
- return;
4049
- const endsWith = node_buffer_1.Buffer.from([0x08, 0x00, 0x08, 0x44]);
4050
- if (attr.slice(-4).equals(endsWith)) {
4051
- logger_1.logger.info(`Detected PMTSD request from device ${meta.device.ieeeAddr}`, NS);
4052
- return { action: "data_request" };
4122
+ convert: async (model, msg, publish, options, meta) => {
4123
+ const result = {};
4124
+ if (msg.data[65522]) {
4125
+ const base = w100EnsureDefaults(meta);
4126
+ Object.assign(result, base);
4127
+ const attr = msg.data[65522];
4128
+ if (!attr || !node_buffer_1.Buffer.isBuffer(attr)) {
4129
+ // defaults assigned
4130
+ }
4131
+ else {
4132
+ const endsWith = node_buffer_1.Buffer.from([0x08, 0x00, 0x08, 0x44]);
4133
+ const endsWithData = node_buffer_1.Buffer.from([0x08, 0x44]);
4134
+ if (attr.length >= 4 && attr.slice(-4).equals(endsWith)) {
4135
+ logger_1.logger.info(`Aqara W100: PMTSD request detected from device ${meta.device.ieeeAddr}`, NS);
4136
+ let modeValue = 0;
4137
+ if (base.system_mode === "off") {
4138
+ modeValue = meta.device.meta.lastActiveMode ?? 0;
4139
+ }
4140
+ else {
4141
+ const modeMap = { cool: 0, heat: 1, auto: 2 };
4142
+ modeValue = modeMap[base.system_mode] ?? 0;
4143
+ }
4144
+ const convertToNumber = (key, value) => {
4145
+ if (typeof value !== "string")
4146
+ return value;
4147
+ if (key === "fan_mode") {
4148
+ const speedMap = { auto: 0, low: 1, medium: 2, high: 3 };
4149
+ return speedMap[value.toLowerCase()] ?? 0;
4150
+ }
4151
+ if (key === "unused")
4152
+ return Number.parseInt(value, 10);
4153
+ return value;
4154
+ };
4155
+ const pmtsdValues = {
4156
+ p: base.system_mode === "off" ? 1 : 0,
4157
+ m: modeValue,
4158
+ t: typeof base.occupied_heating_setpoint === "string"
4159
+ ? Number.parseFloat(base.occupied_heating_setpoint)
4160
+ : base.occupied_heating_setpoint,
4161
+ s: convertToNumber("fan_mode", base.fan_mode) ?? 0,
4162
+ d: convertToNumber("unused", base.unused) ?? 0,
4163
+ };
4164
+ try {
4165
+ const endpoint = meta.device.getEndpoint(1);
4166
+ await w100SendPMTSD(endpoint, pmtsdValues, meta);
4167
+ }
4168
+ catch (error) {
4169
+ logger_1.logger.error(`Aqara W100: Failed to send PMTSD frame: ${error.message}`, NS);
4170
+ }
4171
+ result.action = "W100_PMTSD_request";
4172
+ }
4173
+ else {
4174
+ const idx = attr.indexOf(endsWithData);
4175
+ if (idx !== -1 && idx + 2 < attr.length) {
4176
+ const payloadLen = attr[idx + 2];
4177
+ const payloadStart = idx + 3;
4178
+ const payloadEnd = payloadStart + payloadLen;
4179
+ if (payloadEnd <= attr.length) {
4180
+ const payloadBytes = attr.slice(payloadStart, payloadEnd);
4181
+ let payloadAscii;
4182
+ try {
4183
+ payloadAscii = payloadBytes.toString("ascii");
4184
+ }
4185
+ catch {
4186
+ return;
4187
+ }
4188
+ const stateUpdate = {};
4189
+ const partsForCombined = [];
4190
+ const pairs = payloadAscii.split("_");
4191
+ const pmtsd = {
4192
+ p: base.system_mode === "off" ? 1 : 0,
4193
+ // biome-ignore lint/suspicious/noExplicitAny: ignored using `--suppress`
4194
+ m: meta.device.meta?.lastActiveMode ?? 0,
4195
+ t: 15,
4196
+ s: 0,
4197
+ d: 0,
4198
+ };
4199
+ if (base.system_mode !== "off") {
4200
+ const modeMap = { cool: 0, heat: 1, auto: 2 };
4201
+ pmtsd.m = modeMap[base.system_mode] ?? 0;
4202
+ }
4203
+ pairs.forEach((pair) => {
4204
+ if (pair.length >= 2) {
4205
+ const key = pair[0].toLowerCase();
4206
+ const value = pair.slice(1);
4207
+ let newKey;
4208
+ let stateKey;
4209
+ let displayValue = value;
4210
+ let processedValue = value;
4211
+ switch (key) {
4212
+ case "p":
4213
+ newKey = "PW";
4214
+ processedValue = Number.parseInt(value, 10);
4215
+ pmtsd.p = processedValue;
4216
+ break;
4217
+ case "m":
4218
+ newKey = "MW";
4219
+ processedValue = Number.parseInt(value, 10);
4220
+ pmtsd.m = processedValue;
4221
+ break;
4222
+ case "t":
4223
+ newKey = "TW";
4224
+ stateKey = "occupied_heating_setpoint";
4225
+ processedValue = Number.parseFloat(value);
4226
+ pmtsd.t = processedValue;
4227
+ displayValue = processedValue;
4228
+ break;
4229
+ case "s":
4230
+ newKey = "SW";
4231
+ stateKey = "fan_mode";
4232
+ processedValue = Number.parseInt(value, 10);
4233
+ pmtsd.s = processedValue;
4234
+ displayValue = ["auto", "low", "medium", "high"][processedValue];
4235
+ break;
4236
+ case "d":
4237
+ newKey = "DW";
4238
+ stateKey = "unused";
4239
+ processedValue = Number.parseInt(value, 10);
4240
+ pmtsd.d = processedValue;
4241
+ displayValue = String(processedValue);
4242
+ break;
4243
+ default:
4244
+ newKey = `${key.toUpperCase()}W`;
4245
+ }
4246
+ result[newKey] = value;
4247
+ if (stateKey) {
4248
+ stateUpdate[stateKey] = displayValue;
4249
+ result[stateKey] = displayValue;
4250
+ }
4251
+ partsForCombined.push(`${newKey}${value}`);
4252
+ }
4253
+ });
4254
+ const modeDisplay = ["cool", "heat", "auto"][pmtsd.m] || "cool";
4255
+ const systemMode = pmtsd.p === 1 ? "off" : modeDisplay;
4256
+ if (pmtsd.p === 0 && pmtsd.m !== undefined) {
4257
+ if (!meta.device.meta)
4258
+ meta.device.meta = {};
4259
+ // biome-ignore lint/suspicious/noExplicitAny: ignored using `--suppress`
4260
+ meta.device.meta.lastActiveMode = pmtsd.m;
4261
+ }
4262
+ stateUpdate.system_mode = systemMode;
4263
+ result.system_mode = systemMode;
4264
+ const date = new Date();
4265
+ const pad = (n) => n.toString().padStart(2, "0");
4266
+ const formattedDate = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
4267
+ const combinedString = partsForCombined.length
4268
+ ? `${formattedDate}_${partsForCombined.join("_")}`
4269
+ : `${formattedDate}`;
4270
+ result.PMTSD_from_W100_Data = combinedString;
4271
+ }
4272
+ }
4273
+ }
4274
+ }
4053
4275
  }
4276
+ return result;
4054
4277
  },
4055
4278
  },
4056
- pmtsd_from_w100: {
4057
- cluster: "manuSpecificLumi",
4279
+ w100_temperature: {
4280
+ cluster: "msTemperatureMeasurement",
4058
4281
  type: ["attributeReport", "readResponse"],
4059
4282
  convert: (model, msg, publish, options, meta) => {
4060
- const data = msg.data[65522];
4061
- if (!data || !node_buffer_1.Buffer.isBuffer(data))
4062
- return;
4063
- const endsWith = node_buffer_1.Buffer.from([0x08, 0x44]);
4064
- const idx = data.indexOf(endsWith);
4065
- if (idx === -1 || idx + 2 >= data.length)
4066
- return;
4067
- const payloadLen = data[idx + 2];
4068
- const payloadStart = idx + 3;
4069
- const payloadEnd = payloadStart + payloadLen;
4070
- if (payloadEnd > data.length)
4071
- return;
4072
- const payloadBytes = data.slice(payloadStart, payloadEnd);
4073
- let payloadAscii;
4074
- try {
4075
- payloadAscii = payloadBytes.toString("ascii");
4283
+ // W100 needs a custom converter to:
4284
+ // 1. Expose both 'temperature' and 'local_temperature' (needed for climate)
4285
+ // 2. Initialize device defaults (w100EnsureDefaults) needed for other converters
4286
+ const result = fz.temperature.convert(model, msg, publish, options, meta);
4287
+ if (result) {
4288
+ const temperature = Object.values(result)[0];
4289
+ const defaults = w100EnsureDefaults(meta);
4290
+ return { temperature, local_temperature: temperature, ...defaults };
4076
4291
  }
4077
- catch {
4078
- return;
4079
- }
4080
- const result = {};
4081
- const partsForCombined = [];
4082
- const pairs = payloadAscii.split("_");
4083
- pairs.forEach((p) => {
4084
- if (p.length >= 2) {
4085
- const key = p[0];
4086
- const value = p.slice(1);
4087
- let newKey;
4088
- switch (key) {
4089
- case "p":
4090
- newKey = "PW";
4091
- break;
4092
- case "m":
4093
- newKey = "MW";
4094
- break;
4095
- case "t":
4096
- newKey = "TW";
4097
- break;
4098
- case "s":
4099
- newKey = "SW";
4100
- break;
4101
- case "d":
4102
- newKey = "DW";
4103
- break;
4104
- default:
4105
- newKey = `${key.toUpperCase()}W`;
4106
- }
4107
- result[newKey] = value;
4108
- partsForCombined.push(`${newKey}${value}`);
4109
- }
4110
- });
4111
- const ts = Date.now();
4112
- const combinedString = partsForCombined.length ? `${ts}_${partsForCombined.join("_")}` : `${ts}`;
4113
- logger_1.logger.info(`Decoded PMTSD: ${JSON.stringify(result)} from ${meta.device.ieeeAddr}`, NS);
4114
- return {
4115
- ...result,
4116
- data: combinedString,
4117
- };
4118
4292
  },
4119
4293
  },
4120
4294
  };
@@ -5661,57 +5835,168 @@ exports.toZigbee = {
5661
5835
  throw new Error(`Not supported: '${key}'`);
5662
5836
  },
5663
5837
  },
5664
- pmtsd_to_w100: {
5665
- key: ["PMTSD_to_W100"],
5838
+ w100_pmtsd: {
5839
+ key: ["system_mode", "occupied_heating_setpoint", "fan_mode", "unused", "pmtsd_to_w100"],
5666
5840
  convertSet: async (entity, key, value, meta) => {
5667
- const { p, m, t, s, d } = value;
5668
- const pmtsdStr = `P${p}_M${m}_T${t}_S${s}_D${d}`;
5669
- const pmtsdBytes = Array.from(pmtsdStr).map((c) => c.charCodeAt(0));
5670
- const pmtsdLen = pmtsdBytes.length;
5671
- const fixedHeader = [
5672
- 0xaa,
5673
- 0x71,
5674
- 0x1f,
5675
- 0x44,
5676
- 0x00,
5677
- 0x00,
5678
- 0x05,
5679
- 0x41,
5680
- 0x1c,
5681
- 0x00,
5682
- 0x00,
5683
- 0x54,
5684
- 0xef,
5685
- 0x44,
5686
- 0x80,
5687
- 0x71,
5688
- 0x1a,
5689
- 0x08,
5690
- 0x00,
5691
- 0x08,
5692
- 0x44,
5693
- pmtsdLen,
5694
- ];
5695
- const counter = Math.floor(Math.random() * 256);
5696
- fixedHeader[4] = counter;
5697
- const fullPayload = [...fixedHeader, ...pmtsdBytes];
5698
- const checksum = fullPayload.reduce((sum, b) => sum + b, 0) & 0xff;
5699
- fullPayload[5] = checksum;
5700
- await entity.write(64704, { 65522: { value: node_buffer_1.Buffer.from(fullPayload), type: 65 } }, { manufacturerCode: 4447, disableDefaultResponse: true });
5701
- logger_1.logger.info(`PMTSD frame sent: ${pmtsdStr} (Counter: ${counter}, Checksum: ${checksum})`, NS);
5702
- return {};
5703
- },
5704
- },
5705
- thermostat_mode: {
5706
- key: ["mode"],
5841
+ const MIN_SEND_INTERVAL_MS = 5000;
5842
+ const base = w100EnsureDefaults(meta);
5843
+ let initialP = 0;
5844
+ let initialM = 0;
5845
+ if (base.system_mode === "off") {
5846
+ initialP = 1;
5847
+ initialM = meta.device?.meta?.lastActiveMode ?? 0;
5848
+ }
5849
+ else {
5850
+ initialP = 0;
5851
+ const modeMap = { cool: 0, heat: 1, auto: 2 };
5852
+ initialM = modeMap[base.system_mode] ?? 0;
5853
+ }
5854
+ let initialS = 0;
5855
+ const speedMap = { auto: 0, low: 1, medium: 2, high: 3 };
5856
+ initialS = speedMap[base.fan_mode] ?? 0;
5857
+ const pmtsd = {
5858
+ p: initialP,
5859
+ m: initialM,
5860
+ t: typeof base.occupied_heating_setpoint === "string"
5861
+ ? Number.parseFloat(base.occupied_heating_setpoint)
5862
+ : base.occupied_heating_setpoint,
5863
+ s: initialS,
5864
+ d: typeof base.unused === "string" ? Number.parseInt(base.unused, 10) : base.unused,
5865
+ };
5866
+ if (typeof pmtsd.t === "string")
5867
+ pmtsd.t = Number.parseFloat(pmtsd.t);
5868
+ if (typeof pmtsd.d === "string")
5869
+ pmtsd.d = Number.parseInt(pmtsd.d, 10);
5870
+ let hasChanged = false;
5871
+ if (key === "pmtsd_to_w100") {
5872
+ const val = value;
5873
+ if (val.P !== undefined)
5874
+ pmtsd.p = Number(val.P);
5875
+ if (val.M !== undefined)
5876
+ pmtsd.m = Number(val.M);
5877
+ if (val.T !== undefined)
5878
+ pmtsd.t = Number(val.T);
5879
+ if (val.S !== undefined)
5880
+ pmtsd.s = Number(val.S);
5881
+ if (val.D !== undefined)
5882
+ pmtsd.d = Number(val.D);
5883
+ hasChanged = true;
5884
+ }
5885
+ else {
5886
+ if (meta.state?.thermostat_mode !== "ON" && ["system_mode", "occupied_heating_setpoint", "fan_mode"].includes(key)) {
5887
+ logger_1.logger.warning(`Aqara W100: Ignoring ${key} command - thermostat_mode is not ON`, NS);
5888
+ return { state: {} };
5889
+ }
5890
+ switch (key) {
5891
+ case "system_mode": {
5892
+ if (value === "off") {
5893
+ if (pmtsd.p === 0 && pmtsd.m !== undefined) {
5894
+ if (!meta.device.meta)
5895
+ meta.device.meta = {};
5896
+ // biome-ignore lint/suspicious/noExplicitAny: ignored using `--suppress`
5897
+ meta.device.meta.lastActiveMode = pmtsd.m;
5898
+ }
5899
+ if (pmtsd.p !== 1) {
5900
+ pmtsd.p = 1;
5901
+ hasChanged = true;
5902
+ }
5903
+ }
5904
+ else {
5905
+ if (pmtsd.p !== 0) {
5906
+ pmtsd.p = 0;
5907
+ hasChanged = true;
5908
+ }
5909
+ const modeMap = { cool: 0, heat: 1, auto: 2 };
5910
+ const numValue = typeof value === "string" ? modeMap[value.toLowerCase()] : Number(value);
5911
+ if (numValue !== undefined && [0, 1, 2].includes(numValue)) {
5912
+ if (pmtsd.m !== numValue) {
5913
+ pmtsd.m = numValue;
5914
+ hasChanged = true;
5915
+ if (!meta.device.meta)
5916
+ meta.device.meta = {};
5917
+ // biome-ignore lint/suspicious/noExplicitAny: ignored using `--suppress`
5918
+ meta.device.meta.lastActiveMode = numValue;
5919
+ }
5920
+ }
5921
+ }
5922
+ break;
5923
+ }
5924
+ case "occupied_heating_setpoint": {
5925
+ const temp = Number.parseFloat(value);
5926
+ const minTarget = meta.options?.min_target_temp !== undefined ? Number(meta.options.min_target_temp) : w100Defaults.min_target_temp;
5927
+ const maxTarget = meta.options?.max_target_temp !== undefined ? Number(meta.options.max_target_temp) : w100Defaults.max_target_temp;
5928
+ if (Number.isNaN(temp) || temp < minTarget || temp > maxTarget)
5929
+ throw new Error(`occupied_heating_setpoint must be between ${minTarget} and ${maxTarget}`);
5930
+ if (pmtsd.t !== temp) {
5931
+ pmtsd.t = temp;
5932
+ hasChanged = true;
5933
+ }
5934
+ break;
5935
+ }
5936
+ case "fan_mode": {
5937
+ let numValue;
5938
+ if (typeof value === "string") {
5939
+ const speedMap = { auto: 0, low: 1, medium: 2, high: 3 };
5940
+ numValue = speedMap[value.toLowerCase()];
5941
+ }
5942
+ else {
5943
+ numValue = Number(value);
5944
+ }
5945
+ if (pmtsd.s !== numValue) {
5946
+ pmtsd.s = numValue;
5947
+ hasChanged = true;
5948
+ }
5949
+ break;
5950
+ }
5951
+ case "unused": {
5952
+ const numValue = typeof value === "string" ? Number.parseInt(value, 10) : Number(value);
5953
+ if (pmtsd.d !== numValue) {
5954
+ pmtsd.d = numValue;
5955
+ hasChanged = true;
5956
+ }
5957
+ break;
5958
+ }
5959
+ }
5960
+ }
5961
+ const modeDisplay = ["cool", "heat", "auto"][pmtsd.m] || "cool";
5962
+ const speedDisplay = ["auto", "low", "medium", "high"][pmtsd.s] || "auto";
5963
+ const stateUpdate = {
5964
+ state: {
5965
+ occupied_heating_setpoint: pmtsd.t,
5966
+ fan_mode: speedDisplay,
5967
+ system_mode: pmtsd.p === 1 ? "off" : modeDisplay,
5968
+ unused: String(pmtsd.d),
5969
+ },
5970
+ };
5971
+ const { p, m, t, s, d } = pmtsd;
5972
+ if (p === undefined || m === undefined || t === undefined || s === undefined || d === undefined)
5973
+ return stateUpdate;
5974
+ const now = Date.now();
5975
+ if (meta.device) {
5976
+ if (!meta.device.meta)
5977
+ meta.device.meta = {};
5978
+ }
5979
+ const lastSendTime = meta.device?.meta?.lastPMTSDSend || 0;
5980
+ const timeElapsed = now - lastSendTime;
5981
+ const shouldSend = hasChanged || timeElapsed >= MIN_SEND_INTERVAL_MS;
5982
+ if (!shouldSend)
5983
+ return stateUpdate;
5984
+ await w100SendPMTSD(entity, pmtsd, meta);
5985
+ return stateUpdate;
5986
+ },
5987
+ convertGet: async (entity, key, meta) => {
5988
+ await entity.read("manuSpecificLumi", [65522], { manufacturerCode: 4447 });
5989
+ },
5990
+ },
5991
+ w100_mode: {
5992
+ key: ["thermostat_mode"],
5707
5993
  convertSet: async (entity, key, value, meta) => {
5708
5994
  const deviceMac = meta.device.ieeeAddr.replace(/^0x/, "").toLowerCase();
5709
5995
  const hubMac = "54ef4480711a";
5710
5996
  function cleanMac(mac, expectedLen) {
5711
5997
  const cleaned = mac.replace(/[:-]/g, "");
5712
- if (cleaned.length !== expectedLen) {
5998
+ if (cleaned.length !== expectedLen)
5713
5999
  throw new Error(`MAC must be ${expectedLen} hex digits`);
5714
- }
5715
6000
  return cleaned;
5716
6001
  }
5717
6002
  const dev = node_buffer_1.Buffer.from(cleanMac(deviceMac, 16), "hex");
@@ -5735,13 +6020,13 @@ exports.toZigbee = {
5735
6020
  const seq = node_buffer_1.Buffer.from([Math.floor(Math.random() * 256)]);
5736
6021
  const control = node_buffer_1.Buffer.from([0x18]);
5737
6022
  frame = node_buffer_1.Buffer.concat([prefix, frameId, seq, control, dev]);
5738
- if (frame.length < 34) {
6023
+ if (frame.length < 34)
5739
6024
  frame = node_buffer_1.Buffer.concat([frame, node_buffer_1.Buffer.alloc(34 - frame.length, 0x00)]);
5740
- }
5741
6025
  }
5742
6026
  await entity.write(64704, { 65522: { value: frame, type: 0x41 } }, { manufacturerCode: 4447, disableDefaultResponse: true });
5743
- logger_1.logger.info(`Thermostat_Mode=${value} payload=${frame.toString("hex")}`, NS);
5744
- return {};
6027
+ logger_1.logger.info(`Aqara W100: thermostat_mode set to ${value}`, NS);
6028
+ const defaults = w100EnsureDefaults(meta);
6029
+ return { state: { ...defaults, thermostat_mode: value } };
5745
6030
  },
5746
6031
  },
5747
6032
  };