zigbee-herdsman-converters 15.0.44 → 15.0.46

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.
@@ -4150,123 +4150,6 @@ const converters = {
4150
4150
  }
4151
4151
  },
4152
4152
  },
4153
- haozee_thermostat: {
4154
- cluster: 'manuSpecificTuya',
4155
- type: ['commandDataResponse', 'commandDataReport'],
4156
- convert: (model, msg, publish, options, meta) => {
4157
- const dpValue = tuya.firstDpValue(msg, meta, 'haozee_thermostat');
4158
- const dp = dpValue.dp; // First we get the data point ID
4159
- const value = tuya.getDataValue(dpValue);
4160
- const presetLookup = {0: 'auto', 1: 'manual', 2: 'off', 3: 'on'};
4161
- const systemModeLookup = {0: 'auto', 1: 'auto', 2: 'off', 3: 'heat'};
4162
- switch (dp) {
4163
- case tuya.dataPoints.haozeeSystemMode:
4164
- return {preset: presetLookup[value], system_mode: systemModeLookup[value]};
4165
- case tuya.dataPoints.haozeeHeatingSetpoint:
4166
- return {current_heating_setpoint: (value / 10).toFixed(1)};
4167
- case tuya.dataPoints.haozeeLocalTemp:
4168
- return {local_temperature: (value / 10).toFixed(1)};
4169
- case tuya.dataPoints.haozeeBoostHeatingCountdown:
4170
- // quick heating countdown - not supported by this device
4171
- return {boost_heating_countdown: value};
4172
- case tuya.dataPoints.haozeeWindowDetection:
4173
- // window check
4174
- return {window_detection: value ? 'ON' : 'OFF'};
4175
- case tuya.dataPoints.haozeeWindowState:
4176
- // window state
4177
- return {window: value ? 'OPEN' : 'CLOSED'};
4178
- case tuya.dataPoints.haozeeChildLock:
4179
- return {child_lock: value ? 'LOCK' : 'UNLOCK'};
4180
- case tuya.dataPoints.haozeeBattery:
4181
- // battery
4182
- return {battery: value};
4183
- case tuya.dataPoints.haozeeFaultAlarm:
4184
- return {error: value ? 'ON': 'OFF'};
4185
- case tuya.dataPoints.haozeeScheduleMonday:
4186
- // Monday
4187
- return {
4188
- 'monday_schedule': ' ' + value[1] + 'h:' + value[2] + 'm ' + value[4] / 10 + '°C' +
4189
- ', ' + value[5] + 'h:' + value[6] + 'm ' + value[8] / 10 + '°C' +
4190
- ', ' + value[9] + 'h:' + value[10] + 'm ' + value[12] / 10 + '°C' +
4191
- ', ' + value[13] + 'h:' + value[14] + 'm ' + value[16] / 10 + '°C ',
4192
- };
4193
- case tuya.dataPoints.haozeeScheduleTuesday:
4194
- // Tuesday
4195
- return {
4196
- 'tuesday_schedule': ' ' + value[1] + 'h:' + value[2] + 'm ' + value[4] / 10 + '°C' +
4197
- ', ' + value[5] + 'h:' + value[6] + 'm ' + value[8] / 10 + '°C' +
4198
- ', ' + value[9] + 'h:' + value[10] + 'm ' + value[12] / 10 + '°C' +
4199
- ', ' + value[13] + 'h:' + value[14] + 'm ' + value[16] / 10 + '°C ',
4200
- };
4201
- case tuya.dataPoints.haozeeScheduleWednesday:
4202
- // wednesday
4203
- return {
4204
- 'wednesday_schedule': ' ' + value[1] + 'h:' + value[2] + 'm ' + value[4] / 10 + '°C' +
4205
- ', ' + value[5] + 'h:' + value[6] + 'm ' + value[8] / 10 + '°C' +
4206
- ', ' + value[9] + 'h:' + value[10] + 'm ' + value[12] / 10 + '°C' +
4207
- ', ' + value[13] + 'h:' + value[14] + 'm ' + value[16] / 10 + '°C ',
4208
- };
4209
- case tuya.dataPoints.haozeeScheduleThursday:
4210
- // Thursday
4211
- return {
4212
- 'thursday_schedule': ' ' + value[1] + 'h:' + value[2] + 'm ' + value[4] / 10 + '°C' +
4213
- ', ' + value[5] + 'h:' + value[6] + 'm ' + value[8] / 10 + '°C' +
4214
- ', ' + value[9] + 'h:' + value[10] + 'm ' + value[12] / 10 + '°C' +
4215
- ', ' + value[13] + 'h:' + value[14] + 'm ' + value[16] / 10 + '°C ',
4216
- };
4217
- case tuya.dataPoints.haozeeScheduleFriday:
4218
- // Friday
4219
- return {
4220
- 'friday_schedule': ' ' + value[1] + 'h:' + value[2] + 'm ' + value[4] / 10 + '°C' +
4221
- ', ' + value[5] + 'h:' + value[6] + 'm ' + value[8] / 10 + '°C' +
4222
- ', ' + value[9] + 'h:' + value[10] + 'm ' + value[12] / 10 + '°C' +
4223
- ', ' + value[13] + 'h:' + value[14] + 'm ' + value[16] / 10 + '°C ',
4224
- };
4225
- case tuya.dataPoints.haozeeScheduleSaturday:
4226
- // Saturday
4227
- return {
4228
- 'saturday_schedule': ' ' + value[1] + 'h:' + value[2] + 'm ' + value[4] / 10 + '°C' +
4229
- ', ' + value[5] + 'h:' + value[6] + 'm ' + value[8] / 10 + '°C' +
4230
- ', ' + value[9] + 'h:' + value[10] + 'm ' + value[12] / 10 + '°C' +
4231
- ', ' + value[13] + 'h:' + value[14] + 'm ' + value[16] / 10 + '°C ',
4232
- };
4233
- case tuya.dataPoints.haozeeScheduleSunday:
4234
-
4235
- // Sunday
4236
- return {
4237
- 'sunday_schedule': ' ' + value[1] + 'h:' + value[2] + 'm ' + value[4] / 10 + '°C' +
4238
- ', ' + value[5] + 'h:' + value[6] + 'm ' + value[8] / 10 + '°C' +
4239
- ', ' + value[9] + 'h:' + value[10] + 'm ' + value[12] / 10 + '°C' +
4240
- ', ' + value[13] + 'h:' + value[14] + 'm ' + value[16] / 10 + '°C ',
4241
- };
4242
-
4243
- case tuya.dataPoints.haozeeRunningState:
4244
- // working status 0 - pause 1 -working
4245
- return {'heating': value ? 'ON' : 'OFF'};
4246
- case tuya.dataPoints.haozeeBoostHeating:
4247
- // rapid heating -> boolean - not supported by this device
4248
- return {'boost_heating': value ? 'ON' : 'OFF'};
4249
- case tuya.dataPoints.haozeeTempCalibration:
4250
- // temperature calibration
4251
- return {'local_temperature_calibration': ( value/10 ).toFixed(1)};
4252
- case tuya.dataPoints.haozeeValvePosition:
4253
- // valve position
4254
- return {'position': ( value/10 ).toFixed(1)};
4255
- case tuya.dataPoints.haozeeMinTemp:
4256
- // lower limit temperature
4257
- return {'min_temperature': ( value/10 ).toFixed(1)};
4258
- case tuya.dataPoints.haozeeMaxTemp:
4259
- // max limit temperature
4260
- return {'max_temperature': ( value/10 ).toFixed(1)};
4261
- case tuya.dataPoints.haozeeSoftVersion:
4262
- // software
4263
- break;
4264
- default:
4265
- meta.logger.warn(`zigbee-herdsman-converters:haozee: NOT RECOGNIZED DP #${
4266
- dp} with data ${JSON.stringify(dpValue)}`);
4267
- }
4268
- },
4269
- },
4270
4153
  tuya_air_quality: {
4271
4154
  cluster: 'manuSpecificTuya',
4272
4155
  type: ['commandDataReport', 'commandDataResponse'],
@@ -102,6 +102,12 @@ const converters = {
102
102
  await entity.read('genPowerCfg', ['batteryPercentageRemaining']);
103
103
  },
104
104
  },
105
+ battery_voltage: {
106
+ key: ['battery', 'voltage'],
107
+ convertGet: async (entity, key, meta) => {
108
+ await entity.read('genPowerCfg', ['batteryVoltage']);
109
+ },
110
+ },
105
111
  power_on_behavior: {
106
112
  key: ['power_on_behavior'],
107
113
  convertSet: async (entity, key, value, meta) => {
@@ -1260,7 +1266,12 @@ const converters = {
1260
1266
  if (val === undefined) {
1261
1267
  val = utils.getKey(constants.thermostatControlSequenceOfOperations, value, value, Number);
1262
1268
  }
1263
- await entity.write('hvacThermostat', {ctrlSeqeOfOper: val});
1269
+ const attributes = {ctrlSeqeOfOper: val};
1270
+ await entity.write('hvacThermostat', attributes);
1271
+ // NOTE: update the cluster attribute we store as this is used by
1272
+ // SMaBiT AV2010/32's dynamic expose function.
1273
+ entity.saveClusterAttributeKeyValue('hvacThermostat', attributes);
1274
+ return {readAfterWriteTime: 250, state: {control_sequence_of_operation: value}};
1264
1275
  },
1265
1276
  convertGet: async (entity, key, meta) => {
1266
1277
  await entity.read('hvacThermostat', ['ctrlSeqeOfOper']);
@@ -3474,79 +3485,6 @@ const converters = {
3474
3485
  return tuya.sendDataPointRaw(entity, tuya.dataPoints.moesSschedule, payload);
3475
3486
  },
3476
3487
  },
3477
- haozee_thermostat_preset: {
3478
- key: ['preset'],
3479
- convertSet: async (entity, key, value, meta) => {
3480
- const lookup = {'auto': 0, 'manual': 1, 'off': 2, 'on': 3};
3481
- await tuya.sendDataPointEnum(entity, tuya.dataPoints.haozeeSystemMode, lookup[value]);
3482
- },
3483
- },
3484
- haozee_thermostat_system_mode: {
3485
- key: ['system_mode'],
3486
- convertSet: async (entity, key, value, meta) => {
3487
- // mapping 'heat' system mode to 100% heating (same as preset 'ON'),
3488
- // mapping 'auto' system mode to heating up to the set point temperature (same as preset 'MANUAL')
3489
- // mapping 'off' system mode to idle (same as preset 'OFF')
3490
- // programmed schedule can be enabled by using preset mode 'AUTO' instead of 'MANUAL'
3491
- const lookup = {'auto': 1, 'off': 2, 'heat': 3};
3492
- await tuya.sendDataPointEnum(entity, tuya.dataPoints.haozeeSystemMode, lookup[value]);
3493
- },
3494
- },
3495
- haozee_thermostat_current_heating_setpoint: {
3496
- key: ['current_heating_setpoint'],
3497
- convertSet: async (entity, key, value, meta) => {
3498
- const temp = Math.round(value*10);
3499
- await tuya.sendDataPointValue(entity, tuya.dataPoints.haozeeHeatingSetpoint, temp);
3500
- },
3501
- },
3502
- haozee_thermostat_boost_heating: {
3503
- key: ['boost_heating'],
3504
- convertSet: async (entity, key, value, meta) => {
3505
- await tuya.sendDataPointBool(entity, tuya.dataPoints.haozeeBoostHeating, value === 'ON');
3506
- },
3507
- },
3508
- haozee_thermostat_boost_heating_countdown: {
3509
- key: ['boost_heating_countdown'],
3510
- convertSet: async (entity, key, value, meta) => {
3511
- await tuya.sendDataPointValue(entity, tuya.dataPoints.haozeeBoostHeatingCountdown, value);
3512
- },
3513
- },
3514
- haozee_thermostat_window_detection: {
3515
- key: ['window_detection'],
3516
- convertSet: async (entity, key, value, meta) => {
3517
- await tuya.sendDataPointBool(entity, tuya.dataPoints.haozeeWindowDetection, value === 'ON');
3518
- },
3519
- },
3520
- haozee_thermostat_child_lock: {
3521
- key: ['child_lock'],
3522
- convertSet: async (entity, key, value, meta) => {
3523
- await tuya.sendDataPointBool(entity, tuya.dataPoints.haozeeChildLock, value === 'LOCK');
3524
- },
3525
- },
3526
- haozee_thermostat_temperature_calibration: {
3527
- key: ['local_temperature_calibration'],
3528
- convertSet: async (entity, key, value, meta) => {
3529
- let temp = Math.round(value * 10);
3530
- if (temp < 0) {
3531
- temp = 0xFFFFFFFF + temp + 1;
3532
- }
3533
- await tuya.sendDataPointValue(entity, tuya.dataPoints.haozeeTempCalibration, temp);
3534
- },
3535
- },
3536
- haozee_thermostat_max_temperature: {
3537
- key: ['max_temperature'],
3538
- convertSet: async (entity, key, value, meta) => {
3539
- const temp = Math.round(value*10);
3540
- await tuya.sendDataPointValue(entity, tuya.dataPoints.haozeeMaxTemp, temp);
3541
- },
3542
- },
3543
- haozee_thermostat_min_temperature: {
3544
- key: ['min_temperature'],
3545
- convertSet: async (entity, key, value, meta) => {
3546
- const temp = Math.round(value*10);
3547
- await tuya.sendDataPointValue(entity, tuya.dataPoints.haozeeMinTemp, temp);
3548
- },
3549
- },
3550
3488
  hgkg_thermostat_standby: {
3551
3489
  key: ['system_mode'],
3552
3490
  convertSet: async (entity, key, value, meta) => {
package/devices/bitron.js CHANGED
@@ -4,6 +4,7 @@ const tz = require('../converters/toZigbee');
4
4
  const reporting = require('../lib/reporting');
5
5
  const extend = require('../lib/extend');
6
6
  const e = exposes.presets;
7
+ const ea = exposes.access;
7
8
 
8
9
  module.exports = [
9
10
  {
@@ -190,13 +191,43 @@ module.exports = [
190
191
  vendor: 'SMaBiT (Bitron Video)',
191
192
  description: 'Wireless wall thermostat with relay',
192
193
  fromZigbee: [fz.legacy.thermostat_att_report, fz.battery, fz.hvac_user_interface],
193
- toZigbee: [tz.thermostat_control_sequence_of_operation, tz.thermostat_occupied_heating_setpoint,
194
- tz.thermostat_occupied_cooling_setpoint, tz.thermostat_local_temperature_calibration, tz.thermostat_local_temperature,
195
- tz.thermostat_running_state, tz.thermostat_temperature_display_mode, tz.thermostat_keypad_lockout, tz.thermostat_system_mode],
196
- exposes: [e.battery(), exposes.climate().withSetpoint('occupied_heating_setpoint', 7, 30, 0.5).withLocalTemperature()
197
- .withSystemMode(['off', 'heat']).withRunningState(['idle', 'heat'])
198
- .withLocalTemperatureCalibration(), e.keypad_lockout()],
199
- meta: {battery: {voltageToPercentage: '3V_2500_3200'}},
194
+ toZigbee: [
195
+ tz.thermostat_control_sequence_of_operation, tz.thermostat_occupied_heating_setpoint,
196
+ tz.thermostat_occupied_cooling_setpoint, tz.thermostat_local_temperature_calibration,
197
+ tz.thermostat_local_temperature, tz.thermostat_running_state, tz.thermostat_temperature_display_mode,
198
+ tz.thermostat_keypad_lockout, tz.thermostat_system_mode, tz.battery_voltage,
199
+ ],
200
+ exposes: (device, options) => {
201
+ const dynExposes = [];
202
+ let ctrlSeqeOfOper = device ? device.getEndpoint(1).getClusterAttributeValue('hvacThermostat', 'ctrlSeqeOfOper') : null;
203
+ const modes = [];
204
+
205
+ // NOTE: ctrlSeqeOfOper defaults to 2 for this device (according to the manual)
206
+ if (ctrlSeqeOfOper == null) ctrlSeqeOfOper = 2;
207
+
208
+ // NOTE: set cool and/or heat support based on ctrlSeqeOfOper (see lib/constants -> thermostatControlSequenceOfOperations)
209
+ // WARN: a restart of zigbee2mqtt is required after changing ctrlSeqeOfOper for expose data to be re-calculated
210
+ if (ctrlSeqeOfOper >= 2) {
211
+ modes.push('heat');
212
+ }
213
+ if (ctrlSeqeOfOper < 2 || ctrlSeqeOfOper > 3) {
214
+ modes.push('cool');
215
+ }
216
+
217
+ dynExposes.push(exposes.climate()
218
+ .withSetpoint('occupied_heating_setpoint', 7, 30, 0.5)
219
+ .withLocalTemperature()
220
+ .withSystemMode(['off'].concat(modes))
221
+ .withRunningState(['idle'].concat(modes))
222
+ .withLocalTemperatureCalibration()
223
+ .withControlSequenceOfOperation(['heating_only', 'cooling_only'], ea.ALL));
224
+ dynExposes.push(e.keypad_lockout());
225
+ dynExposes.push(e.battery().withAccess(ea.STATE_GET));
226
+ dynExposes.push(e.battery_low());
227
+ dynExposes.push(e.linkquality());
228
+ return dynExposes;
229
+ },
230
+ meta: {battery: {voltageToPercentage: '3V_2500'}},
200
231
  configure: async (device, coordinatorEndpoint, logger) => {
201
232
  const endpoint = device.getEndpoint(1);
202
233
  const binds = [
@@ -204,12 +235,12 @@ module.exports = [
204
235
  ];
205
236
  await reporting.bind(endpoint, coordinatorEndpoint, binds);
206
237
  await reporting.thermostatTemperature(endpoint);
207
- await reporting.thermostatTemperatureCalibration(endpoint);
208
238
  await reporting.thermostatOccupiedHeatingSetpoint(endpoint);
209
239
  await reporting.thermostatOccupiedCoolingSetpoint(endpoint);
210
240
  await reporting.thermostatRunningState(endpoint);
211
241
  await reporting.batteryAlarmState(endpoint);
212
242
  await reporting.batteryVoltage(endpoint);
243
+ await endpoint.read('hvacThermostat', ['ctrlSeqeOfOper', 'localTemperatureCalibration']);
213
244
  },
214
245
  },
215
246
  {
package/devices/candeo.js CHANGED
@@ -6,7 +6,7 @@ const e = exposes.presets;
6
6
 
7
7
  module.exports = [
8
8
  {
9
- zigbeeModel: ['HK-DIM-A'],
9
+ zigbeeModel: ['HK-DIM-A', 'Candeo Zigbee Dimmer'],
10
10
  model: 'HK-DIM-A',
11
11
  vendor: 'Candeo',
12
12
  description: 'Zigbee LED dimmer smart switch',
@@ -129,7 +129,7 @@ module.exports = [
129
129
  // For some this fails so set manually
130
130
  // https://github.com/Koenkk/zigbee2mqtt/issues/3575
131
131
  endpoint.saveClusterAttributeKeyValue('haElectricalMeasurement', {
132
- acCurrentDivisor: 1000, acCurrentMultiplier: 1, powerMultiplier: 1, powerDivisor: 10});
132
+ acCurrentDivisor: 1000, acCurrentMultiplier: 1, acPowerMultiplier: 1, acPowerDivisor: 10});
133
133
  }
134
134
  await reporting.rmsVoltage(endpoint, {change: 2}); // Voltage reports in V
135
135
  await reporting.rmsCurrent(endpoint, {change: 10}); // Current reports in mA
@@ -473,7 +473,7 @@ module.exports = [
473
473
  await firstEndpoint.configureReporting('genPowerCfg', payload1);
474
474
  },
475
475
  exposes: [e.soil_moisture(), e.battery(), e.temperature(),
476
- exposes.numeric('report_delay', ea.STATE_SET).withUnit('Minutes').withValueMin(1).withValueMax(240)
476
+ exposes.numeric('report_delay', ea.STATE_SET).withUnit('min').withValueMin(1).withValueMax(240)
477
477
  .withDescription('Adjust Report Delay. Setting the time in minutes, by default 15 minutes')],
478
478
  },
479
479
  {
@@ -823,7 +823,7 @@ module.exports = [
823
823
  await reporting.batteryPercentageRemaining(endpoint, overides);
824
824
  },
825
825
  exposes: [e.battery(), e.temperature(), e.humidity(),
826
- exposes.numeric('report_delay', ea.STATE_SET).withUnit('Minutes')
826
+ exposes.numeric('report_delay', ea.STATE_SET).withUnit('min')
827
827
  .withDescription('Adjust Report Delay. Setting the time in minutes, by default 5 minutes')
828
828
  .withValueMin(1).withValueMax(60),
829
829
  exposes.binary('enable_temperature', ea.STATE_SET, 'ON', 'OFF').withDescription('Enable Temperature Control'),
@@ -2,6 +2,7 @@ const exposes = require('../lib/exposes');
2
2
  const fz = {...require('../converters/fromZigbee'), legacy: require('../lib/legacy').fromZigbee};
3
3
  const tz = require('../converters/toZigbee');
4
4
  const reporting = require('../lib/reporting');
5
+ const ota = require('../lib/ota');
5
6
  const e = exposes.presets;
6
7
 
7
8
  module.exports = [
@@ -13,6 +14,7 @@ module.exports = [
13
14
  fromZigbee: [fz.lock, fz.lock_operation_event, fz.battery, fz.lock_programming_event, fz.lock_pin_code_response],
14
15
  toZigbee: [tz.lock, tz.pincode_lock, tz.lock_userstatus],
15
16
  meta: {pinCodeCount: 20},
17
+ ota: ota.zigbeeOTA,
16
18
  configure: async (device, coordinatorEndpoint, logger) => {
17
19
  const endpoint = device.getEndpoint(1);
18
20
  await reporting.bind(endpoint, coordinatorEndpoint, ['closuresDoorLock', 'genPowerCfg']);
@@ -17,7 +17,7 @@ module.exports = [
17
17
  exposes: [e.switch().setAccess('state', ea.STATE_SET),
18
18
  exposes.numeric('threshold', exposes.access.STATE_SET).withValueMin(0).withValueMax(100).withUnit('%')
19
19
  .withDescription('Valve open percentage (multiple of 10)'),
20
- exposes.numeric('timer', exposes.access.STATE_SET).withValueMin(0).withValueMax(600).withUnit('minutes')
20
+ exposes.numeric('timer', exposes.access.STATE_SET).withValueMin(0).withValueMax(600).withUnit('min')
21
21
  .withDescription('Countdown timer in minutes')],
22
22
  },
23
23
  ];
package/devices/ikea.js CHANGED
@@ -12,7 +12,7 @@ const e = exposes.presets;
12
12
  const ea = exposes.access;
13
13
  const herdsman = require('zigbee-herdsman');
14
14
  const {
15
- calibrateAndPrecisionRoundOptions, postfixWithEndpointName, getMetaValue,
15
+ calibrateAndPrecisionRoundOptions, postfixWithEndpointName, getMetaValue, precisionRound,
16
16
  } = require('../lib/utils');
17
17
 
18
18
  const bulbOnEvent = async (type, data, device, options, state) => {
@@ -72,39 +72,6 @@ const configureRemote = async (device, coordinatorEndpoint, logger) => {
72
72
  await reporting.batteryPercentageRemaining(endpoint);
73
73
  };
74
74
 
75
- const fzLocal = {
76
- // The STYRBAR sends an on +- 500ms after the arrow release. We don't want to send the ON action in this case.
77
- // https://github.com/Koenkk/zigbee2mqtt/issues/13335
78
- STYRBAR_on: {
79
- cluster: 'genOnOff',
80
- type: 'commandOn',
81
- convert: (model, msg, publish, options, meta) => {
82
- if (utils.hasAlreadyProcessedMessage(msg, model)) return;
83
- const arrowReleaseAgo = Date.now() - globalStore.getValue(msg.endpoint, 'arrow_release', 0);
84
- if (arrowReleaseAgo > 700) {
85
- return {action: 'on'};
86
- }
87
- },
88
- },
89
- STYRBAR_arrow_release: {
90
- cluster: 'genScenes',
91
- type: 'commandTradfriArrowRelease',
92
- options: [exposes.options.legacy()],
93
- convert: (model, msg, publish, options, meta) => {
94
- if (utils.hasAlreadyProcessedMessage(msg, model)) return;
95
- globalStore.putValue(msg.endpoint, 'arrow_release', Date.now());
96
- const direction = globalStore.getValue(msg.endpoint, 'direction');
97
- if (direction) {
98
- globalStore.clearValue(msg.endpoint, 'direction');
99
- const duration = msg.data.value / 1000;
100
- const result = {action: `arrow_${direction}_release`, duration, action_duration: duration};
101
- if (!utils.isLegacyEnabled(options)) delete result.duration;
102
- return result;
103
- }
104
- },
105
- },
106
- };
107
-
108
75
  const tradfriExtend = {
109
76
  light_onoff_brightness: (options = {}) => ({
110
77
  ...extend.light_onoff_brightness(options),
@@ -214,6 +181,61 @@ const ikea = {
214
181
  return state;
215
182
  },
216
183
  },
184
+ remote_battery: {
185
+ cluster: 'genPowerCfg',
186
+ type: ['attributeReport', 'readResponse'],
187
+ convert: (model, msg, publish, options, meta) => {
188
+ const payload = {};
189
+ if (msg.data.hasOwnProperty('batteryPercentageRemaining')) {
190
+ // Some devices do not comply to the ZCL and report a
191
+ // batteryPercentageRemaining of 100 when the battery is full (should be 200).
192
+ //
193
+ // IKEA corrected this on newer remote fw version, but many people are still
194
+ // 2.2.010 which is the last version supporting group bindings. We try to be
195
+ // smart and pick the correct one for IKEA remotes.
196
+ let dontDividePercentage = false;
197
+ let percentage = msg.data['batteryPercentageRemaining'];
198
+ const fwVer = meta.device.softwareBuildID.split('.');
199
+ if ((fwVer[0] < 2) || (fwVer[0] == 2 && fwVer[1] <= 3)) {
200
+ dontDividePercentage = true;
201
+ }
202
+ percentage = dontDividePercentage ? percentage : percentage / 2;
203
+ payload.battery = precisionRound(percentage, 2);
204
+ }
205
+
206
+ return payload;
207
+ },
208
+ },
209
+ // The STYRBAR sends an on +- 500ms after the arrow release. We don't want to send the ON action in this case.
210
+ // https://github.com/Koenkk/zigbee2mqtt/issues/13335
211
+ styrbar_on: {
212
+ cluster: 'genOnOff',
213
+ type: 'commandOn',
214
+ convert: (model, msg, publish, options, meta) => {
215
+ if (utils.hasAlreadyProcessedMessage(msg, model)) return;
216
+ const arrowReleaseAgo = Date.now() - globalStore.getValue(msg.endpoint, 'arrow_release', 0);
217
+ if (arrowReleaseAgo > 700) {
218
+ return {action: 'on'};
219
+ }
220
+ },
221
+ },
222
+ styrbar_arrow_release: {
223
+ cluster: 'genScenes',
224
+ type: 'commandTradfriArrowRelease',
225
+ options: [exposes.options.legacy()],
226
+ convert: (model, msg, publish, options, meta) => {
227
+ if (utils.hasAlreadyProcessedMessage(msg, model)) return;
228
+ globalStore.putValue(msg.endpoint, 'arrow_release', Date.now());
229
+ const direction = globalStore.getValue(msg.endpoint, 'direction');
230
+ if (direction) {
231
+ globalStore.clearValue(msg.endpoint, 'direction');
232
+ const duration = msg.data.value / 1000;
233
+ const result = {action: `arrow_${direction}_release`, duration, action_duration: duration};
234
+ if (!utils.isLegacyEnabled(options)) delete result.duration;
235
+ return result;
236
+ }
237
+ },
238
+ },
217
239
  },
218
240
  tz: {
219
241
  air_purifier_fan_mode: {
@@ -623,12 +645,12 @@ module.exports = [
623
645
  model: 'E1524/E1810',
624
646
  description: 'TRADFRI remote control',
625
647
  vendor: 'IKEA',
626
- fromZigbee: [fz.battery, fz.E1524_E1810_toggle, fz.E1524_E1810_levelctrl, fz.ikea_arrow_click, fz.ikea_arrow_hold,
648
+ fromZigbee: [ikea.fz.remote_battery, fz.E1524_E1810_toggle, fz.E1524_E1810_levelctrl, fz.ikea_arrow_click, fz.ikea_arrow_hold,
627
649
  fz.ikea_arrow_release],
628
- exposes: [e.battery(), e.action(['arrow_left_click', 'arrow_left_hold', 'arrow_left_release', 'arrow_right_click',
629
- 'arrow_right_hold', 'arrow_right_release', 'brightness_down_click', 'brightness_down_hold', 'brightness_down_release',
630
- 'brightness_up_click', 'brightness_up_hold', 'brightness_up_release', 'toggle'])],
631
- toZigbee: [],
650
+ exposes: [e.battery().withAccess(ea.STATE_GET), e.action(['arrow_left_click', 'arrow_left_hold', 'arrow_left_release',
651
+ 'arrow_right_click', 'arrow_right_hold', 'arrow_right_release', 'brightness_down_click', 'brightness_down_hold',
652
+ 'brightness_down_release', 'brightness_up_click', 'brightness_up_hold', 'brightness_up_release', 'toggle'])],
653
+ toZigbee: [tz.battery_percentage_remaining],
632
654
  ota: ota.tradfri,
633
655
  // dontDividePercentage: true not needed with latest firmware
634
656
  // https://github.com/Koenkk/zigbee2mqtt/issues/16412
@@ -639,12 +661,12 @@ module.exports = [
639
661
  model: 'E2001/E2002',
640
662
  vendor: 'IKEA',
641
663
  description: 'STYRBAR remote control',
642
- fromZigbee: [fz.battery, fzLocal.STYRBAR_on, fz.command_off, fz.command_move, fz.command_stop, fz.ikea_arrow_click,
643
- fz.ikea_arrow_hold, fzLocal.STYRBAR_arrow_release],
644
- exposes: [e.battery(), e.action(['on', 'off', 'brightness_move_up', 'brightness_move_down',
664
+ fromZigbee: [ikea.fz.remote_battery, ikea.fz.styrbar_on, fz.command_off, fz.command_move, fz.command_stop, fz.ikea_arrow_click,
665
+ fz.ikea_arrow_hold, ikea.fz.styrbar_arrow_release],
666
+ exposes: [e.battery().withAccess(ea.STATE_GET), e.action(['on', 'off', 'brightness_move_up', 'brightness_move_down',
645
667
  'brightness_stop', 'arrow_left_click', 'arrow_right_click', 'arrow_left_hold',
646
668
  'arrow_right_hold', 'arrow_left_release', 'arrow_right_release'])],
647
- toZigbee: [],
669
+ toZigbee: [tz.battery_percentage_remaining],
648
670
  ota: ota.tradfri,
649
671
  meta: {battery: {dontDividePercentage: true}},
650
672
  configure: async (device, coordinatorEndpoint, logger) => {
@@ -659,10 +681,14 @@ module.exports = [
659
681
  model: 'E1743',
660
682
  vendor: 'IKEA',
661
683
  description: 'TRADFRI ON/OFF switch',
662
- fromZigbee: [fz.command_on, fz.legacy.genOnOff_cmdOn, fz.command_off, fz.legacy.genOnOff_cmdOff, fz.command_move, fz.battery,
663
- fz.legacy.E1743_brightness_up, fz.legacy.E1743_brightness_down, fz.command_stop, fz.legacy.E1743_brightness_stop],
664
- exposes: [e.battery(), e.action(['on', 'off', 'brightness_move_down', 'brightness_move_up', 'brightness_stop'])],
665
- toZigbee: [],
684
+ fromZigbee: [fz.command_on, fz.legacy.genOnOff_cmdOn, fz.command_off, fz.legacy.genOnOff_cmdOff, fz.command_move,
685
+ ikea.fz.remote_battery, fz.legacy.E1743_brightness_up, fz.legacy.E1743_brightness_down, fz.command_stop,
686
+ fz.legacy.E1743_brightness_stop],
687
+ exposes: [
688
+ e.battery().withAccess(ea.STATE_GET),
689
+ e.action(['on', 'off', 'brightness_move_down', 'brightness_move_up', 'brightness_stop']),
690
+ ],
691
+ toZigbee: [tz.battery_percentage_remaining],
666
692
  ota: ota.tradfri,
667
693
  meta: {disableActionGroup: true, battery: {dontDividePercentage: true}},
668
694
  configure: configureRemote,
@@ -672,9 +698,9 @@ module.exports = [
672
698
  model: 'E1841',
673
699
  vendor: 'IKEA',
674
700
  description: 'KNYCKLAN open/close remote water valve',
675
- fromZigbee: [fz.command_on, fz.command_off, fz.battery],
676
- exposes: [e.battery(), e.action(['on', 'off'])],
677
- toZigbee: [],
701
+ fromZigbee: [fz.command_on, fz.command_off, ikea.fz.remote_battery],
702
+ exposes: [e.battery().withAccess(ea.STATE_GET), e.action(['on', 'off'])],
703
+ toZigbee: [tz.battery_percentage_remaining],
678
704
  ota: ota.tradfri,
679
705
  meta: {disableActionGroup: true, battery: {dontDividePercentage: true}},
680
706
  configure: configureRemote,
@@ -699,9 +725,9 @@ module.exports = [
699
725
  model: 'E1812',
700
726
  vendor: 'IKEA',
701
727
  description: 'TRADFRI shortcut button',
702
- fromZigbee: [fz.command_on, fz.command_off, fz.command_move, fz.command_stop, fz.battery],
703
- exposes: [e.battery(), e.action(['on', 'off', 'brightness_move_up', 'brightness_stop'])],
704
- toZigbee: [],
728
+ fromZigbee: [fz.command_on, fz.command_off, fz.command_move, fz.command_stop, ikea.fz.remote_battery],
729
+ exposes: [e.battery().withAccess(ea.STATE_GET), e.action(['on', 'off', 'brightness_move_up', 'brightness_stop'])],
730
+ toZigbee: [tz.battery_percentage_remaining],
705
731
  ota: ota.tradfri,
706
732
  meta: {disableActionGroup: true, battery: {dontDividePercentage: true}},
707
733
  configure: async (device, coordinatorEndpoint, logger) => {
@@ -741,7 +767,6 @@ module.exports = [
741
767
  exposes.binary('illuminance_above_threshold', ea.STATE, true, false)
742
768
  .withDescription('Indicates whether the device detected bright light (works only in night mode)')],
743
769
  ota: ota.tradfri,
744
- meta: {battery: {dontDividePercentage: true}},
745
770
  configure: async (device, coordinatorEndpoint, logger) => {
746
771
  const endpoint = device.getEndpoint(1);
747
772
  await reporting.bind(endpoint, coordinatorEndpoint, ['genPowerCfg']);
@@ -839,10 +864,10 @@ module.exports = [
839
864
  model: 'E1766',
840
865
  vendor: 'IKEA',
841
866
  description: 'TRADFRI open/close remote',
842
- fromZigbee: [fz.battery, fz.command_cover_close, fz.legacy.cover_close, fz.command_cover_open, fz.legacy.cover_open,
867
+ fromZigbee: [ikea.fz.remote_battery, fz.command_cover_close, fz.legacy.cover_close, fz.command_cover_open, fz.legacy.cover_open,
843
868
  fz.command_cover_stop, fz.legacy.cover_stop],
844
- exposes: [e.battery(), e.action(['close', 'open', 'stop'])],
845
- toZigbee: [],
869
+ exposes: [e.battery().withAccess(ea.STATE_GET), e.action(['close', 'open', 'stop'])],
870
+ toZigbee: [tz.battery_percentage_remaining],
846
871
  meta: {battery: {dontDividePercentage: true}},
847
872
  ota: ota.tradfri,
848
873
  configure: configureRemote,
@@ -980,6 +1005,13 @@ module.exports = [
980
1005
  description: 'STOFTMOLN ceiling/wall lamp 24 warm light dimmable',
981
1006
  extend: tradfriExtend.light_onoff_brightness(),
982
1007
  },
1008
+ {
1009
+ zigbeeModel: ['STOFTMOLN ceiling/wall lamp WW10'],
1010
+ model: 'T2105',
1011
+ vendor: 'IKEA',
1012
+ description: 'STOFTMOLN ceiling/wall lamp 10 warm light dimmable',
1013
+ extend: tradfriExtend.light_onoff_brightness(),
1014
+ },
983
1015
  {
984
1016
  zigbeeModel: ['TRADFRIbulbPAR38WS900lm'],
985
1017
  model: 'LED2006R9',
@@ -28,6 +28,22 @@ const tzLocal = {
28
28
  };
29
29
 
30
30
  module.exports = [
31
+ {
32
+ zigbeeModel: [' Pocket remote\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000'+
33
+ '\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000'],
34
+ model: '067755',
35
+ vendor: 'Legrand',
36
+ description: 'Wireless and batteryless 4 scenes control',
37
+ fromZigbee: [fz.identify, fz.battery, fz.command_recall],
38
+ exposes: [e.battery(), e.action(['identify', 'recall_1_1'])],
39
+ toZigbee: [],
40
+ meta: {multiEndpoint: true, battery: {voltageToPercentage: '3V_2500'}, publishDuplicateTransaction: true},
41
+ onEvent: readInitialBatteryState,
42
+ configure: async (device, coordinatorEndpoint, logger) => {
43
+ const endpoint = device.getEndpoint(1);
44
+ await reporting.bind(endpoint, coordinatorEndpoint, ['genPowerCfg', 'genOnOff', 'genLevelCtrl']);
45
+ },
46
+ },
31
47
  {
32
48
  zigbeeModel: [' Dry contact\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000'+
33
49
  '\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000'],
@@ -183,6 +183,10 @@ module.exports = [
183
183
  await reporting.bind(endpoint, coordinatorEndpoint, ['genOnOff', 'genIdentify', 'manuSpecificTuya', 'hvacFanCtrl']);
184
184
  await reporting.onOff(endpoint);
185
185
  await reporting.fanMode(endpoint);
186
+
187
+ // Device ships with {fanModeSequence: 1} which restricts physical speed
188
+ // button to low/high. Set to 0 to allow low/med/high from physical press.
189
+ await endpoint.write('hvacFanCtrl', {fanModeSequence: 0});
186
190
  },
187
191
  },
188
192
  {
package/devices/moes.js CHANGED
@@ -308,9 +308,9 @@ module.exports = [
308
308
  'You can set up to 4 stages of temperature every for WEEKDAY ➀➁➂➃➄, SATURDAY ➅ and SUNDAY ➆.'),
309
309
  exposes.binary('boost_heating', ea.STATE_SET, 'ON', 'OFF').withDescription('Boost Heating: press and hold "+" for 3 seconds, ' +
310
310
  'the device will enter the boost heating mode, and the ▷╵◁ will flash. The countdown will be displayed in the APP'),
311
- exposes.numeric('boost_heating_countdown', ea.STATE).withUnit('minutes').withDescription('Countdown in minutes')
311
+ exposes.numeric('boost_heating_countdown', ea.STATE).withUnit('min').withDescription('Countdown in minutes')
312
312
  .withValueMin(0).withValueMax(15),
313
- exposes.numeric('boost_heating_countdown_time_set', ea.STATE_SET).withUnit('seconds')
313
+ exposes.numeric('boost_heating_countdown_time_set', ea.STATE_SET).withUnit('s')
314
314
  .withDescription('Boost Time Setting 100 sec - 900 sec, (default = 300 sec)').withValueMin(100)
315
315
  .withValueMax(900).withValueStep(100)],
316
316
  },