zigbee-herdsman-converters 25.104.0 → 25.105.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.
@@ -4311,6 +4311,33 @@ exports.definitions = [
4311
4311
  ],
4312
4312
  },
4313
4313
  },
4314
+ {
4315
+ fingerprint: tuya.fingerprint("TS0301", ["_TZE210_inpjmc0h"]),
4316
+ model: "TS0301_dual_rail",
4317
+ vendor: "Tuya",
4318
+ description: "Top-down bottom-up dual motor shade",
4319
+ extend: [tuya.modernExtend.tuyaBase({ dp: true }), m.deviceEndpoints({ endpoints: { bottom: 1, top: 1 } })],
4320
+ exposes: [
4321
+ e.cover_position().withEndpoint("bottom").withDescription("Bottom rail"),
4322
+ e.cover_position().withEndpoint("top").withDescription("Top rail"),
4323
+ e.battery(),
4324
+ ],
4325
+ meta: {
4326
+ tuyaDatapoints: [
4327
+ // Bottom rail - DP109 is control, DP101 is set position, DP102 is current position
4328
+ [109, "state_bottom", tuya.valueConverterBasic.lookup({ OPEN: tuya.enum(0), CLOSE: tuya.enum(1), STOP: tuya.enum(2) })],
4329
+ [101, "position_bottom", tuya.valueConverter.coverPositionInverted],
4330
+ [102, "position_bottom", tuya.valueConverter.coverPositionInverted],
4331
+ // Top rail - DP1 is control, DP2 is set position, DP3 is current position
4332
+ // Note: State commands are reversed for Home Assistant compatibility with top-down operation
4333
+ [1, "state_top", tuya.valueConverterBasic.lookup({ OPEN: tuya.enum(1), CLOSE: tuya.enum(0), STOP: tuya.enum(2) })],
4334
+ [2, "position_top", tuya.valueConverter.coverPosition],
4335
+ [3, "position_top", tuya.valueConverter.coverPosition],
4336
+ // Battery
4337
+ [13, "battery", tuya.valueConverter.raw],
4338
+ ],
4339
+ },
4340
+ },
4314
4341
  {
4315
4342
  fingerprint: tuya.fingerprint("TS0601", [
4316
4343
  "_TZE200_aqnazj70",
@@ -5210,24 +5237,30 @@ exports.definitions = [
5210
5237
  },
5211
5238
  {
5212
5239
  fingerprint: tuya.fingerprint("TS0601", ["_TZE200_dzuqwsyg", "_TZE204_dzuqwsyg"]),
5213
- model: "BAC-003",
5240
+ model: "BAC-002-ALZB",
5214
5241
  vendor: "Tuya",
5215
5242
  description: "FCU thermostat temperature controller",
5216
5243
  extend: [tuya.modernExtend.tuyaBase({ dp: true, forceTimeUpdates: true, timeStart: "1970" })],
5217
5244
  options: [
5218
5245
  e
5219
- .enum("control_sequence_of_operation", ea.SET, ["cooling_only", "cooling_and_heating_4-pipes"])
5220
- .withDescription("Operating environment of the thermostat"),
5221
- e.binary("expose_device_state", ea.SET, true, false).withDescription("Expose device power state as a separate property when enabled."),
5246
+ .enum("control_sequence_of_operation", ea.SET, ["cooling_only", "cooling_and_heating"])
5247
+ .withLabel("Device Configuration")
5248
+ .withDescription("Report either cooling and fan or cooling, heating and fan capability."),
5249
+ e
5250
+ .binary("expose_device_state", ea.SET, true, false)
5251
+ .withLabel("Expose device switch")
5252
+ .withDescription("Expose a separate on/off switch, instead of including it in system mode."),
5253
+ e
5254
+ .binary("wake_before_power_transition", ea.SET, true, false)
5255
+ .withLabel("Wake before Power Transition")
5256
+ .withDescription("Send a wake-up command before turning the device on or/off, required for some firmware revisions."),
5222
5257
  ],
5223
5258
  exposes: (device, options) => {
5224
5259
  const system_modes = ["off", "cool", "heat", "fan_only"];
5225
- // Device can operate either in 2-pipe or 4-pipe configuration
5226
- // For 2-pipe configurations remove 'heat' mode
5227
- switch (options?.control_sequence_of_operation) {
5228
- case "cooling_only":
5229
- system_modes.splice(2, 1);
5230
- break;
5260
+ // Device can operate either in cooling or heating/cooling configuration
5261
+ // For cooling only configurations remove 'heat' mode
5262
+ if (options.control_sequence_of_operation === "cooling_only") {
5263
+ system_modes.splice(2, 1);
5231
5264
  }
5232
5265
  const exposes = [
5233
5266
  e
@@ -5237,25 +5270,19 @@ exports.definitions = [
5237
5270
  .withFanMode(["low", "medium", "high", "auto"], ea.STATE_SET)
5238
5271
  .withSetpoint("current_heating_setpoint", 5, 35, 1, ea.STATE_SET)
5239
5272
  .withPreset(["auto", "manual"])
5240
- .withLocalTemperatureCalibration(-3, 3, 1, ea.STATE_SET),
5273
+ .withLocalTemperatureCalibration(-9, 9, 1, ea.STATE_SET),
5241
5274
  e.child_lock(),
5242
- e
5243
- .composite("schedule", "schedule", ea.STATE_SET)
5244
- .withFeature(e.text("weekdays", ea.SET).withDescription('Schedule (1-5), 4 periods in format "hh:mm/tt".'))
5245
- .withFeature(e.text("saturday", ea.SET).withDescription('Schedule (6), 4 periods in format "hh:mm/tt".'))
5246
- .withFeature(e.text("sunday", ea.SET).withDescription('Schedule (7), 4 periods in format "hh:mm/tt".'))
5247
- .withDescription('Auto-mode schedule, 4 periods each per category. Example: "06:00/20 11:30/21 13:30/22 17:30/23.5".'),
5248
- e.max_temperature().withValueMin(35).withValueMax(45).withPreset("default", 35, "Default value"),
5249
- e
5250
- .numeric("deadzone_temperature", ea.STATE_SET)
5251
- .withUnit("°C")
5252
- .withValueMax(5)
5253
- .withValueMin(1)
5254
- .withValueStep(1)
5255
- .withPreset("default", 1, "Default value")
5256
- .withDescription("The delta between local_temperature and current_heating_setpoint to trigger activity"),
5275
+ e.max_temperature().withValueMin(35).withValueMax(45),
5276
+ e.numeric("deadzone_temperature", ea.STATE_SET).withUnit("°C").withValueMax(5).withValueMin(0),
5277
+ e.text("schedule_text", ea.STATE_SET).withDescription(`Weekly schedule in the format "HH:MM/TT HH:MM/TT ...".
5278
+ Example for 12 segments:
5279
+ "06:00/20 11:30/21 13:30/22 17:30/23 06:00/24 12:00/23 14:30/22 17:30/21 06:00/19 12:30/20 14:30/21 18:30/20".
5280
+ Each segment contains:
5281
+ - HH:MM: Time in 24-hour format.
5282
+ - TT: Temperature in °C.
5283
+ Ensure all 12 segments are defined and separated by spaces.`),
5257
5284
  ];
5258
- if (options?.expose_device_state === true) {
5285
+ if (options.expose_device_state === true) {
5259
5286
  exposes.unshift(e.binary("state", ea.STATE_SET, "ON", "OFF").withDescription("Turn the thermostat ON or OFF"));
5260
5287
  }
5261
5288
  return exposes;
@@ -5268,15 +5295,14 @@ exports.definitions = [
5268
5295
  "state",
5269
5296
  {
5270
5297
  to: async (v, meta) => {
5271
- if (meta.options?.expose_device_state === true) {
5298
+ if (meta.options.expose_device_state === true) {
5272
5299
  await tuya.sendDataPointBool(meta.device.endpoints[0], 1, utils.getFromLookup(v, { on: true, off: false }), "dataRequest", 1);
5273
5300
  }
5274
5301
  },
5275
5302
  from: (v, meta, options) => {
5276
5303
  meta.state.system_mode = v === true ? (meta.state.system_mode_device ?? "cool") : "off";
5277
- if (options?.expose_device_state === true) {
5304
+ if (options.expose_device_state === true)
5278
5305
  return v === true ? "ON" : "OFF";
5279
- }
5280
5306
  delete meta.state.state;
5281
5307
  },
5282
5308
  },
@@ -5285,22 +5311,29 @@ exports.definitions = [
5285
5311
  2,
5286
5312
  "system_mode",
5287
5313
  {
5288
- // Extend system_mode to support 'off' in addition to 'cool', 'heat' and 'fan_only'
5289
5314
  to: async (v, meta) => {
5290
- const entity = meta.device.endpoints[0];
5291
- // Power State
5292
- await tuya.sendDataPointBool(entity, 1, v !== "off", "dataRequest", 1);
5293
- switch (v) {
5294
- case "cool":
5295
- await tuya.sendDataPointEnum(entity, 2, 0, "dataRequest", 1);
5296
- break;
5297
- case "heat":
5298
- await tuya.sendDataPointEnum(entity, 2, 1, "dataRequest", 1);
5299
- break;
5300
- case "fan_only":
5301
- await tuya.sendDataPointEnum(entity, 2, 2, "dataRequest", 1);
5302
- break;
5315
+ const ep = meta.device.endpoints[0];
5316
+ if (v === "off") {
5317
+ if (meta.options.wake_before_power_transition === true) {
5318
+ await tuya.sendDataPointBool(ep, 1, true, "dataRequest", 1);
5319
+ await new Promise((r) => setTimeout(r, 120));
5320
+ }
5321
+ await tuya.sendDataPointBool(ep, 1, false, "dataRequest", 1);
5322
+ return;
5323
+ }
5324
+ if (meta.options.wake_before_power_transition === true) {
5325
+ if (meta.state.system_mode === "off") {
5326
+ await tuya.sendDataPointBool(ep, 1, true, "dataRequest", 1);
5327
+ await new Promise((r) => setTimeout(r, 120));
5328
+ }
5303
5329
  }
5330
+ await tuya.sendDataPointBool(ep, 1, true, "dataRequest", 1);
5331
+ if (v === "cool")
5332
+ await tuya.sendDataPointEnum(ep, 2, 0, "dataRequest", 1);
5333
+ if (v === "heat")
5334
+ await tuya.sendDataPointEnum(ep, 2, 1, "dataRequest", 1);
5335
+ if (v === "fan_only")
5336
+ await tuya.sendDataPointEnum(ep, 2, 2, "dataRequest", 1);
5304
5337
  },
5305
5338
  from: (v, meta) => {
5306
5339
  const modes = ["cool", "heat", "fan_only"];
@@ -5309,7 +5342,21 @@ exports.definitions = [
5309
5342
  },
5310
5343
  },
5311
5344
  ],
5312
- [4, "preset", tuya.valueConverterBasic.lookup({ manual: true, auto: false })],
5345
+ [
5346
+ 4,
5347
+ "preset",
5348
+ {
5349
+ to: async (v, meta) => {
5350
+ const ep = meta.device.endpoints[0];
5351
+ await tuya.sendDataPointBool(ep, 4, v === "manual");
5352
+ },
5353
+ from: (v, meta) => {
5354
+ const preset = v ? "manual" : "auto";
5355
+ meta.state.preset = preset;
5356
+ return preset;
5357
+ },
5358
+ },
5359
+ ],
5313
5360
  [16, "current_heating_setpoint", tuya.valueConverter.raw],
5314
5361
  [19, "max_temperature", tuya.valueConverter.raw],
5315
5362
  [24, "local_temperature", tuya.valueConverter.divideBy10],
@@ -5330,52 +5377,45 @@ exports.definitions = [
5330
5377
  101,
5331
5378
  "schedule",
5332
5379
  {
5333
- to: (v, meta) => {
5334
- const periods = (value) => {
5335
- const regex = /((?<h>[01][0-9]|2[0-3]):(?<m>[0-5][0-9])\/(?<t>[0-3][0-9](\.[0,5]|)))/gm;
5336
- const matches = [...value.matchAll(regex)];
5337
- if (matches.length === 4) {
5338
- return matches.reduce((arr, m) => {
5339
- arr.push(Number.parseInt(m.groups.h, 10));
5340
- arr.push(Number.parseInt(m.groups.m, 10));
5341
- arr.push(Number.parseFloat(m.groups.t) * 2);
5342
- return arr;
5343
- }, []);
5344
- }
5345
- logger_1.logger.warning("Ignoring invalid or incomplete schedule", NS);
5346
- };
5347
- const schedule = [...periods(v.weekdays), ...periods(v.saturday), ...periods(v.sunday)];
5348
- return schedule;
5349
- },
5350
5380
  from: (v, meta) => {
5351
- const format = (data) => {
5352
- return data.reduce((a, v, i) => {
5353
- switch (i % 3) {
5354
- // Hour
5355
- case 0:
5356
- return `${a}${i > 0 ? " " : ""}${v.toString().padStart(2, "0")}`;
5357
- // Minute
5358
- case 1:
5359
- return `${a}:${v.toString().padStart(2, "0")}`;
5360
- // Setpoint
5361
- case 2:
5362
- return `${a}/${v / 2}`;
5363
- default:
5364
- throw new Error(`Unexpected index ${i} in schedule data`);
5365
- }
5366
- }, "");
5367
- };
5368
- return {
5369
- weekdays: format(v.slice(0, 12)),
5370
- saturday: format(v.slice(1 * 12, 2 * 12)),
5371
- sunday: format(v.slice(2 * 12, 3 * 12)),
5372
- };
5381
+ const format = (data) => data.reduce((txt, val, i) => {
5382
+ if (i % 3 === 0)
5383
+ return `${txt}${i > 0 ? " " : ""}${val.toString().padStart(2, "0")}`;
5384
+ if (i % 3 === 1)
5385
+ return `${txt}:${val.toString().padStart(2, "0")}`;
5386
+ return `${txt}/${val / 2}`;
5387
+ }, "");
5388
+ const weekdays = format(v.slice(0, 12));
5389
+ const saturday = format(v.slice(12, 24));
5390
+ const sunday = format(v.slice(24, 36));
5391
+ const full = `${weekdays} ${saturday} ${sunday}`.trim();
5392
+ meta.state.schedule_text = full;
5393
+ return full;
5394
+ },
5395
+ },
5396
+ ],
5397
+ [
5398
+ 202,
5399
+ "schedule_text",
5400
+ {
5401
+ to: async (v, meta) => {
5402
+ const regex = /((?<h>[01][0-9]|2[0-3]):(?<m>[0-5][0-9])\/(?<t>[0-3]?[0-9](\.[0,5])?))/gm;
5403
+ const matches = [...v.matchAll(regex)];
5404
+ if (matches.length !== 12)
5405
+ return;
5406
+ const result = [];
5407
+ for (const m of matches) {
5408
+ result.push(Number(m.groups?.h));
5409
+ result.push(Number(m.groups?.m));
5410
+ result.push(Number(m.groups?.t) * 2);
5411
+ }
5412
+ await tuya.sendDataPointRaw(meta.device.endpoints[0], 101, Buffer.from(result));
5373
5413
  },
5374
5414
  },
5375
5415
  ],
5376
5416
  ],
5377
5417
  },
5378
- whiteLabel: [tuya.whitelabel("Tuya", "BAC-002-ALZB", "FCU thermostat temperature controller", ["_TZE200_dzuqwsyg"])],
5418
+ whiteLabel: [tuya.whitelabel("Tuya", "BAC-003", "FCU thermostat temperature controller", ["_TZE204_dzuqwsyg"])],
5379
5419
  },
5380
5420
  {
5381
5421
  fingerprint: tuya.fingerprint("TS0601", ["_TZE200_qq9mpfhw"]),
@@ -16042,7 +16082,10 @@ exports.definitions = [
16042
16082
  model: "BLE-YL01",
16043
16083
  vendor: "Tuya",
16044
16084
  description: "Smart WiFi Zigbee chlorine meter",
16045
- whiteLabel: [tuya.whitelabel("Tuya", "YK-S03", "Smart pH and Chlorine Tester for Swimming Pool", ["_TZE200_d9mzkhoq"])],
16085
+ whiteLabel: [
16086
+ tuya.whitelabel("Tuya", "YK-S03", "Smart pH and Chlorine Tester for Swimming Pool", ["_TZE200_d9mzkhoq"]),
16087
+ { model: "YY-1099L", vendor: "Tuya" },
16088
+ ],
16046
16089
  // Don't query too often. Values are not always updated. https://github.com/Koenkk/zigbee2mqtt/issues/18704
16047
16090
  extend: [
16048
16091
  tuya.modernExtend.tuyaBase({
@@ -22790,6 +22833,30 @@ exports.definitions = [
22790
22833
  ],
22791
22834
  },
22792
22835
  },
22836
+ {
22837
+ fingerprint: tuya.fingerprint("TS0601", ["_TZE200_fphxkxue"]),
22838
+ model: "ZVL-PRO",
22839
+ vendor: "Nova Digital",
22840
+ description: "Water walve",
22841
+ extend: [tuya.modernExtend.tuyaBase({ dp: true })],
22842
+ exposes: [
22843
+ e.switch().setAccess("state", ea.STATE_SET),
22844
+ e.battery(),
22845
+ e.numeric("countdown", ea.STATE_SET).withUnit("s").withValueMin(0).withValueMax(86400).withDescription("Countdown time in seconds"),
22846
+ e.enum("work_state", ea.STATE, ["auto", "manual", "idle"]).withDescription("Current state of operation"),
22847
+ e.numeric("water_once", ea.STATE).withUnit("L").withDescription("Consumption from the last watering"),
22848
+ ],
22849
+ meta: {
22850
+ tuyaDatapoints: [
22851
+ [1, "state", tuya.valueConverter.onOff],
22852
+ [7, "battery", tuya.valueConverter.raw],
22853
+ [11, "countdown", tuya.valueConverter.raw],
22854
+ [12, "work_state", tuya.valueConverter.raw],
22855
+ [5, "water_once", tuya.valueConverter.raw],
22856
+ [4, "fault", tuya.valueConverter.raw],
22857
+ ],
22858
+ },
22859
+ },
22793
22860
  {
22794
22861
  fingerprint: tuya.fingerprint("TS0601", ["_TZE204_wc2w9t1s"]),
22795
22862
  model: "BOT-R9V-ZB",