zigbee-herdsman-converters 14.0.511 → 14.0.514

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.
@@ -532,6 +532,24 @@ const converters = {
532
532
  }
533
533
  }
534
534
 
535
+ // onLevel - range 0x00 to 0xff - optional
536
+ // Any value outside of MinLevel to MaxLevel, including 0xff and 0x00, is interpreted as "previous".
537
+ if (msg.data.hasOwnProperty('onLevel') && (msg.data['onLevel'] !== undefined)) {
538
+ result.level_config.on_level = Number(msg.data['onLevel']);
539
+ if (result.level_config.on_level === 255) {
540
+ result.level_config.on_level = 'previous';
541
+ }
542
+ }
543
+
544
+ // options - 8-bit map
545
+ // bit 0: ExecuteIfOff - when 0, Move commands are ignored if the device is off;
546
+ // when 1, CurrentLevel can be changed while the device is off.
547
+ // bit 1: CoupleColorTempToLevel - when 1, changes to level also change color temperature.
548
+ // (What this means is not defined, but it's most likely to be "dim to warm".)
549
+ if (msg.data.hasOwnProperty('options') && msg.data['options'] !== undefined) {
550
+ result.level_config.execute_if_off = !!(Number(msg.data['options']) & 1);
551
+ }
552
+
535
553
  if (Object.keys(result.level_config).length > 0) {
536
554
  return result;
537
555
  }
@@ -1699,7 +1717,7 @@ const converters = {
1699
1717
  result.hysterersis = precisionRound(data[0x100A], 2) / 10;
1700
1718
  }
1701
1719
  if (data.hasOwnProperty(0x100B)) { // DisplayAutoOffEnable
1702
- const lookup = {0: 'enable', 1: 'disable'};
1720
+ const lookup = {0: 'enabled', 1: 'disabled'};
1703
1721
  result.display_auto_off_enabled = lookup[data[0x100B]];
1704
1722
  }
1705
1723
  if (data.hasOwnProperty(0x2001)) { // AlarmAirTempOverValue
@@ -8496,6 +8514,73 @@ const converters = {
8496
8514
  return result;
8497
8515
  },
8498
8516
  },
8517
+ ZG204ZL_lms: {
8518
+ cluster: 'manuSpecificTuya',
8519
+ type: ['commandDataResponse', 'commandDataReport'],
8520
+ convert: (model, msg, publish, options, meta) => {
8521
+ const result = {};
8522
+ for (const dpValue of msg.data.dpValues) {
8523
+ const dp = dpValue.dp;
8524
+ const value = tuya.getDataValue(dpValue);
8525
+ switch (dp) {
8526
+ case tuya.dataPoints.lmsState:
8527
+ result.occupancy = (value === 0);
8528
+ break;
8529
+ case tuya.dataPoints.lmsBattery:
8530
+ result.battery = value;
8531
+ break;
8532
+ case tuya.dataPoints.lmsSensitivity:
8533
+ result.sensitivity = {'0': 'low', '1': 'medium', '2': 'high'}[value];
8534
+ break;
8535
+ case tuya.dataPoints.lmsKeepTime:
8536
+ result.keep_time = {'0': '10', '1': '30', '2': '60', '3': '120'}[value];
8537
+ break;
8538
+ case tuya.dataPoints.lmsIlluminance:
8539
+ result.illuminance = value;
8540
+ break;
8541
+ default:
8542
+ meta.logger.warn(`zigbee-herdsman-converters:ZG204ZL_lms: NOT RECOGNIZED DP #${
8543
+ dp} with data ${JSON.stringify(dpValue)}`);
8544
+ }
8545
+ }
8546
+ return result;
8547
+ },
8548
+ },
8549
+ moes_cover: {
8550
+ cluster: 'manuSpecificTuya',
8551
+ type: ['commandDataResponse', 'commandDataReport'],
8552
+ options: [exposes.options.invert_cover()],
8553
+ convert: (model, msg, publish, options, meta) => {
8554
+ const dpValue = tuya.firstDpValue(msg, meta, 'moes_cover');
8555
+ const dp = dpValue.dp;
8556
+ const value = tuya.getDataValue(dpValue);
8557
+ let result = null;
8558
+ switch (dp) {
8559
+ case tuya.dataPoints.coverPosition: {
8560
+ const invert = !tuya.isCoverInverted(meta.device.manufacturerName) ?
8561
+ !options.invert_cover : options.invert_cover;
8562
+ const position = invert ? 100 - value : value;
8563
+ result = {position: position};
8564
+ break;
8565
+ }
8566
+ case tuya.dataPoints.state:
8567
+ result = {state: {0: 'OPEN', 1: 'STOP', 2: 'CLOSE'}[value], running: {0: true, 1: false, 2: true}[value]};
8568
+ break;
8569
+ case tuya.dataPoints.moesCoverBacklight:
8570
+ result = {backlight: {false: 'OFF', true: 'ON'}[value]};
8571
+ break;
8572
+ case tuya.dataPoints.moesCoverCalibration:
8573
+ result = {calibration: {0: 'ON', 1: 'OFF'}[value]};
8574
+ break;
8575
+ case tuya.dataPoints.moesCoverMotorReversal:
8576
+ result = {motor_reversal: {0: 'OFF', 1: 'ON'}[value]};
8577
+ break;
8578
+ default:
8579
+ meta.logger.warn(`fromZigbee.moes_cover: NOT RECOGNIZED DP ${dp} with data ${JSON.stringify(dpValue)}`);
8580
+ }
8581
+ return result;
8582
+ },
8583
+ },
8499
8584
  // #endregion
8500
8585
 
8501
8586
  // #region Ignore converters (these message dont need parsing).
@@ -535,12 +535,41 @@ const converters = {
535
535
  Object.assign(state, {current_level_startup: startUpCurrentLevelValue});
536
536
  }
537
537
 
538
+ // onLevel - range 0x00 to 0xff - optional
539
+ // Any value outside of MinLevel to MaxLevel, including 0xff and 0x00, is interpreted as "previous".
540
+ if (value.hasOwnProperty('on_level')) {
541
+ let onLevel = value.on_level;
542
+ if (typeof onLevel === 'string' && onLevel.toLowerCase() == 'previous') {
543
+ onLevel = 255;
544
+ } else {
545
+ onLevel = Number(onLevel);
546
+ }
547
+ if (onLevel > 255) onLevel = 254;
548
+ if (onLevel < 1) onLevel = 1;
549
+ await entity.write('genLevelCtrl', {onLevel}, utils.getOptions(meta.mapped, entity));
550
+ Object.assign(state, {on_level: onLevel == 255 ? 'previous' : onLevel});
551
+ }
552
+
553
+ // options - 8-bit map
554
+ // bit 0: ExecuteIfOff - when 0, Move commands are ignored if the device is off;
555
+ // when 1, CurrentLevel can be changed while the device is off.
556
+ // bit 1: CoupleColorTempToLevel - when 1, changes to level also change color temperature.
557
+ // (What this means is not defined, but it's most likely to be "dim to warm".)
558
+ if (value.hasOwnProperty('execute_if_off')) {
559
+ const executeIfOffValue = !!value.execute_if_off;
560
+ await entity.write('genLevelCtrl', {options: executeIfOffValue ? 1 : 0}, utils.getOptions(meta.mapped, entity));
561
+ Object.assign(state, {execute_if_off: executeIfOffValue});
562
+ }
563
+
538
564
  if (Object.keys(state).length > 0) {
539
565
  return {state: {level_config: state}};
540
566
  }
541
567
  },
542
568
  convertGet: async (entity, key, meta) => {
543
- for (const attribute of ['onOffTransitionTime', 'onTransitionTime', 'offTransitionTime', 'startUpCurrentLevel']) {
569
+ for (const attribute of [
570
+ 'onOffTransitionTime', 'onTransitionTime', 'offTransitionTime', 'startUpCurrentLevel',
571
+ 'onLevel', 'options',
572
+ ]) {
544
573
  try {
545
574
  await entity.read('genLevelCtrl', [attribute]);
546
575
  } catch (ex) {
@@ -798,7 +827,7 @@ const converters = {
798
827
  const {message} = meta;
799
828
  const transition = utils.getTransition(entity, 'brightness', meta);
800
829
  const turnsOffAtBrightness1 = utils.getMetaValue(entity, meta.mapped, 'turnsOffAtBrightness1', 'allEqual', false);
801
- let state = message.hasOwnProperty('state') ? message.state.toLowerCase() : undefined;
830
+ let state = message.hasOwnProperty('state') ? (message.state === null ? null : message.state.toLowerCase()) : undefined;
802
831
  let brightness = undefined;
803
832
  if (message.hasOwnProperty('brightness')) {
804
833
  brightness = Number(message.brightness);
@@ -806,91 +835,106 @@ const converters = {
806
835
  brightness = utils.mapNumberRange(Number(message.brightness_percent), 0, 100, 0, 255);
807
836
  }
808
837
 
809
- if (brightness !== undefined && (isNaN(brightness) || brightness < 0 || brightness > 255)) {
810
- // Allow 255 value, changing this to 254 would be a breaking change.
838
+ if (brightness === 255) {
839
+ // Allow 255 for backwards compatibility.
840
+ brightness = 254;
841
+ }
842
+
843
+ if (brightness !== undefined && (isNaN(brightness) || brightness < 0 || brightness > 254)) {
811
844
  throw new Error(`Brightness value of message: '${JSON.stringify(message)}' invalid, must be a number >= 0 and =< 254`);
812
845
  }
813
846
 
814
- if (state !== undefined && ['on', 'off', 'toggle'].includes(state) === false) {
847
+ if (state !== undefined && state !== null && ['on', 'off', 'toggle'].includes(state) === false) {
815
848
  throw new Error(`State value of message: '${JSON.stringify(message)}' invalid, must be 'ON', 'OFF' or 'TOGGLE'`);
816
849
  }
817
850
 
818
- if (state === 'toggle' || state === 'off' || (brightness === undefined && state === 'on')) {
819
- if (transition.specified) {
820
- if (state === 'toggle') {
821
- state = meta.state.state === 'ON' ? 'off' : 'on';
822
- }
823
-
824
- if (state === 'off' && meta.state.brightness && meta.state.state === 'ON') {
825
- // https://github.com/Koenkk/zigbee2mqtt/issues/2850#issuecomment-580365633
826
- // We need to remember the state before turning the device off as we need to restore
827
- // it once we turn it on again.
828
- // We cannot rely on the meta.state as when reporting is enabled the bulb will reports
829
- // it brightness while decreasing the brightness.
830
- globalStore.putValue(entity, 'brightness', meta.state.brightness);
831
- globalStore.putValue(entity, 'turnedOffWithTransition', true);
832
- }
833
-
834
- const fallbackLevel = utils.getObjectProperty(meta.state, 'brightness', 254);
835
- let level = state === 'off' ? 0 : globalStore.getValue(entity, 'brightness', fallbackLevel);
836
- if (state === 'on' && level === 0) level = turnsOffAtBrightness1 ? 2 : 1;
837
-
838
- const payload = {level, transtime: transition.time};
839
- await entity.command('genLevelCtrl', 'moveToLevelWithOnOff', payload, utils.getOptions(meta.mapped, entity));
840
- const result = {state: {state: state.toUpperCase()}};
841
- if (state === 'on') result.state.brightness = level;
842
- return result;
843
- } else {
844
- if (state === 'on' && globalStore.getValue(entity, 'turnedOffWithTransition') === true) {
845
- /**
846
- * In case the bulb it turned OFF with a transition and turned ON WITHOUT
847
- * a transition, the brightness is not recovered as it turns on with brightness 1.
848
- * https://github.com/Koenkk/zigbee-herdsman-converters/issues/1073
849
- */
850
- globalStore.putValue(entity, 'turnedOffWithTransition', false);
851
- await entity.command(
852
- 'genLevelCtrl',
853
- 'moveToLevelWithOnOff',
854
- {level: globalStore.getValue(entity, 'brightness'), transtime: 0},
855
- utils.getOptions(meta.mapped, entity),
856
- );
857
- return {state: {state: 'ON'}, readAfterWriteTime: transition * 100};
858
- } else {
859
- // Store brightness where the bulb was turned off with as we need it when the bulb is turned on
860
- // with transition.
861
- if (meta.state.hasOwnProperty('brightness') && state === 'off') {
862
- globalStore.putValue(entity, 'brightness', meta.state.brightness);
863
- globalStore.putValue(entity, 'turnedOffWithTransition', false);
864
- }
865
-
866
- const result = await converters.on_off.convertSet(entity, 'state', state, meta);
867
- result.readAfterWriteTime = 0;
868
- if (result.state && result.state.state === 'ON' && meta.state.brightness === 0) {
869
- result.state.brightness = 1;
851
+ if ((state === undefined || state === null) && brightness === undefined) {
852
+ throw new Error(`At least one of "brightness" or "state" must have a value: '${JSON.stringify(message)}'`);
853
+ }
854
+
855
+ // Infer state from desired brightness if unset. Ideally we'd want to keep it as it is, but this code has always
856
+ // used 'MoveToLevelWithOnOff' so that'd break backwards compatibility. To keep the state, the user
857
+ // has to explicitly set it to null.
858
+ if (state === undefined) {
859
+ // Also write to `meta.message.state` in case we delegate to the `on_off` converter.
860
+ state = meta.message.state = brightness === 0 ? 'off' : 'on';
861
+ }
862
+
863
+ const targetState = state === 'toggle' ? (meta.state.state === 'ON' ? 'off' : 'on') : state;
864
+ if (targetState === 'off') {
865
+ // Simulate 'Off' with transition via 'MoveToLevelWithOnOff', otherwise just use 'Off'.
866
+ // TODO: if this is a group where some members don't support Level Control, turning them off
867
+ // with transition may have no effect. (Some devices, such as Envilar ZG302-BOX-RELAY, handle
868
+ // 'MoveToLevelWithOnOff' despite not supporting the cluster; others, like the LEDVANCE SMART+
869
+ // plug, do not.)
870
+ brightness = transition.specified || brightness === 0 ? 0 : undefined;
871
+ if (meta.state.hasOwnProperty('brightness') && meta.state.state === 'ON') {
872
+ // The light's current level gets clobbered in two cases:
873
+ // 1. when 'Off' has a transition, in which case it is really 'MoveToLevelWithOnOff'
874
+ // https://github.com/Koenkk/zigbee-herdsman-converters/issues/1073
875
+ // 2. when 'OnLevel' is set: "If OnLevel is not defined, set the CurrentLevel to the stored level."
876
+ // https://github.com/Koenkk/zigbee2mqtt/issues/2850#issuecomment-580365633
877
+ // We need to remember current brightness in case the next 'On' does not provide it. `meta` is not reliable
878
+ // here, as it will get clobbered too if reporting is configured.
879
+ globalStore.putValue(entity, 'brightness', meta.state.brightness);
880
+ globalStore.putValue(entity, 'turnedOffWithTransition', brightness !== undefined);
881
+ }
882
+ } else if (targetState === 'on' && brightness === undefined) {
883
+ // Simulate 'On' with transition via 'MoveToLevelWithOnOff', or restore the level from before
884
+ // it was clobbered by a previous transition to off; otherwise just use 'On'.
885
+ // TODO: same problem as above.
886
+ // TODO: if transition is not specified, should use device default (OnTransitionTime), not 0.
887
+ if (transition.specified || globalStore.getValue(entity, 'turnedOffWithTransition') === true) {
888
+ const current = utils.getObjectProperty(meta.state, 'brightness', 254);
889
+ brightness = globalStore.getValue(entity, 'brightness', current);
890
+ try {
891
+ const attributeRead = await entity.read('genLevelCtrl', ['onLevel']);
892
+ // TODO: for groups, `read` does not wait for responses. If it did, we could still issue a single
893
+ // command if all values of `OnLevel` are equal, or split into one command per device if not.
894
+ if (attributeRead !== undefined && attributeRead['onLevel'] != 255) {
895
+ brightness = attributeRead['onLevel'];
870
896
  }
871
-
872
- return result;
897
+ } catch (e) {
898
+ // OnLevel not supported
873
899
  }
874
900
  }
875
- } else {
876
- brightness = Math.min(254, brightness);
877
- if (brightness === 1 && turnsOffAtBrightness1) {
878
- brightness = 2;
901
+ }
902
+
903
+ if (brightness === undefined) {
904
+ const result = await converters.on_off.convertSet(entity, 'state', state, meta);
905
+ result.readAfterWriteTime = 0;
906
+ if (result.state && result.state.state === 'ON' && meta.state.brightness === 0) {
907
+ result.state.brightness = 1;
879
908
  }
909
+ return result;
910
+ }
911
+
912
+ if (brightness === 0 && (targetState === 'on' || state === null)) {
913
+ brightness = 1;
914
+ }
915
+ if (brightness === 1 && turnsOffAtBrightness1) {
916
+ brightness = 2;
917
+ }
880
918
 
919
+ if (targetState !== 'off') {
881
920
  globalStore.putValue(entity, 'brightness', brightness);
882
- await entity.command(
883
- 'genLevelCtrl',
884
- 'moveToLevelWithOnOff',
885
- {level: Number(brightness), transtime: transition.time},
886
- utils.getOptions(meta.mapped, entity),
887
- );
921
+ globalStore.clearValue(entity, 'turnedOffWithTransition');
922
+ }
923
+ await entity.command(
924
+ 'genLevelCtrl',
925
+ state === null ? 'moveToLevel' : 'moveToLevelWithOnOff',
926
+ {level: Number(brightness), transtime: transition.time},
927
+ utils.getOptions(meta.mapped, entity),
928
+ );
888
929
 
889
- return {
890
- state: {state: brightness === 0 ? 'OFF' : 'ON', brightness: Number(brightness)},
891
- readAfterWriteTime: transition.time * 100,
892
- };
930
+ const result = {state: {}, readAfterWriteTime: transition.time * 100};
931
+ if (brightness !== 0) {
932
+ result.state.brightness = Number(brightness);
893
933
  }
934
+ if (state !== null) {
935
+ result.state.state = brightness === 0 ? 'OFF' : 'ON';
936
+ }
937
+ return result;
894
938
  },
895
939
  convertGet: async (entity, key, meta) => {
896
940
  if (key === 'brightness') {
@@ -3077,10 +3121,10 @@ const converters = {
3077
3121
  const payload = {0x1009: {value: value * 2, type: 0x20}};
3078
3122
  await entity.write('hvacThermostat', payload, manufacturerOptions.sunricher);
3079
3123
  } else if (key==='hysterersis') {
3080
- const payload = {0x100A: {value: value* 10, type: 0x20}};
3124
+ const payload = {0x100A: {value: value * 10, type: 0x20}};
3081
3125
  await entity.write('hvacThermostat', payload, manufacturerOptions.sunricher);
3082
3126
  } else if (key==='display_auto_off_enabled') {
3083
- const lookup = {'enable': 0, 'disabled': 1};
3127
+ const lookup = {'enabled': 0, 'disabled': 1};
3084
3128
  const payload = {0x100B: {value: lookup[value], type: herdsman.Zcl.DataType.enum8}};
3085
3129
  await entity.write('hvacThermostat', payload, manufacturerOptions.sunricher);
3086
3130
  } else if (key==='alarm_airtemp_overvalue') {
@@ -7421,6 +7465,70 @@ const converters = {
7421
7465
  }
7422
7466
  },
7423
7467
  },
7468
+ ZG204ZL_lms: {
7469
+ key: ['sensitivity', 'keep_time'],
7470
+ convertSet: async (entity, key, value, meta) => {
7471
+ switch (key) {
7472
+ case 'sensitivity':
7473
+ await tuya.sendDataPointEnum(entity, tuya.dataPoints.lmsSensitivity, {'low': 0, 'medium': 1, 'high': 2}[value]);
7474
+ break;
7475
+ case 'keep_time':
7476
+ await tuya.sendDataPointEnum(entity, tuya.dataPoints.lmsKeepTime, {'10': 0, '30': 1, '60': 2, '120': 3}[value]);
7477
+ break;
7478
+ default: // Unknown key
7479
+ meta.logger.warn(`tz.ZG204ZL_lms: Unhandled key ${key}`);
7480
+ }
7481
+ },
7482
+ convertGet: async (entity, key, meta) => {
7483
+ switch (key) {
7484
+ case 'sensitivity':
7485
+ await tuya.sendDataPointEnum(entity, tuya.dataPoints.lmsSensitivity, 0, 'dataQuery' );
7486
+ break;
7487
+ case 'keep_time':
7488
+ await tuya.sendDataPointEnum(entity, tuya.dataPoints.lmsKeepTime, 0, 'dataQuery' );
7489
+ break;
7490
+ default: // Unknown key
7491
+ meta.logger.warn(`Unhandled key toZigbee.ZG204ZL_lms.convertGet ${key}`);
7492
+ }
7493
+ },
7494
+ },
7495
+ moes_cover: {
7496
+ key: ['backlight', 'calibration', 'motor_reversal', 'state', 'position'],
7497
+ options: [exposes.options.invert_cover()],
7498
+ convertSet: async (entity, key, value, meta) => {
7499
+ switch (key) {
7500
+ case 'position':
7501
+ if (value >= 0 && value <= 100) {
7502
+ const invert = !tuya.isCoverInverted(meta.device.manufacturerName) ?
7503
+ !meta.options.invert_cover : meta.options.invert_cover;
7504
+ const position = invert ? 100 - value : value;
7505
+ await tuya.sendDataPointValue(entity, tuya.dataPoints.coverPosition, position);
7506
+ return {position: value};
7507
+ }
7508
+ break;
7509
+ case 'state': {
7510
+ const state = {'OPEN': 0, 'STOP': 1, 'CLOSE': 2}[value.toUpperCase()];
7511
+ await tuya.sendDataPointEnum(entity, tuya.dataPoints.state, state);
7512
+ break;
7513
+ }
7514
+ case 'backlight': {
7515
+ const backlight = value.toUpperCase() === 'ON' ? true : false;
7516
+ await tuya.sendDataPointBool(entity, tuya.dataPoints.moesCoverBacklight, backlight);
7517
+ return {backlight: value};
7518
+ }
7519
+ case 'calibration': {
7520
+ const calibration = value.toUpperCase() === 'ON' ? 0 : 1;
7521
+ await tuya.sendDataPointEnum(entity, tuya.dataPoints.moesCoverCalibration, calibration);
7522
+ break;
7523
+ }
7524
+ case 'motor_reversal': {
7525
+ const motorReversal = value.toUpperCase() === 'ON' ? 1 : 0;
7526
+ await tuya.sendDataPointEnum(entity, tuya.dataPoints.moesCoverMotorReversal, motorReversal);
7527
+ return {motor_reversal: value};
7528
+ }
7529
+ }
7530
+ },
7531
+ },
7424
7532
  // #endregion
7425
7533
 
7426
7534
  // #region Ignore converters
@@ -22,5 +22,6 @@ module.exports = [
22
22
  exposes: [e.lock(), e.battery(), e.sound_volume(),
23
23
  e.action(['zigbee_unlock', 'lock', 'rfid_unlock', 'keypad_unlock']),
24
24
  exposes.binary('auto_relock', ea.STATE_SET, true, false).withDescription('Auto relock after 7 seconds.')],
25
+ whiteLabel: [{vendor: 'Datek Wireless', model: 'EasyCode903G2.1'}],
25
26
  },
26
27
  ];
package/devices/ikea.js CHANGED
@@ -36,9 +36,12 @@ const bulbOnEvent = async (type, data, device, options, state) => {
36
36
 
37
37
  // NOTE: execute_if_off default is false
38
38
  // we only restore if true, to save unneeded network writes
39
- if (state.color_options.execute_if_off === true) {
39
+ if (state.color_options !== undefined && state.color_options.execute_if_off === true) {
40
40
  device.endpoints[0].write('lightingColorCtrl', {'options': 1});
41
41
  }
42
+ if (state.level_config !== undefined && state.level_config.execute_if_off === true) {
43
+ device.endpoints[0].write('genLevelCtrl', {'options': 1});
44
+ }
42
45
  }
43
46
  };
44
47