diff --git a/converters/fromZigbee.js b/converters/fromZigbee.js index a4758fd15d9e7..deb98a2467ffa 100644 --- a/converters/fromZigbee.js +++ b/converters/fromZigbee.js @@ -271,17 +271,17 @@ const converters = { const payload = {}; if (msg.data.hasOwnProperty('activePower')) { const multiplier = msg.endpoint.getClusterAttributeValue( - 'haElectricalMeasurement', 'acPowerMultiplier' + 'haElectricalMeasurement', 'acPowerMultiplier', ); const divisor = msg.endpoint.getClusterAttributeValue( - 'haElectricalMeasurement', 'acPowerDivisor' + 'haElectricalMeasurement', 'acPowerDivisor', ); const factor = multiplier && divisor ? multiplier / divisor : 1; payload.power = precisionRound(msg.data['activePower'] * factor, 2); } if (msg.data.hasOwnProperty('rmsCurrent')) { const multiplier = msg.endpoint.getClusterAttributeValue( - 'haElectricalMeasurement', 'acCurrentMultiplier' + 'haElectricalMeasurement', 'acCurrentMultiplier', ); const divisor = msg.endpoint.getClusterAttributeValue('haElectricalMeasurement', 'acCurrentDivisor'); const factor = multiplier && divisor ? multiplier / divisor : 1; @@ -289,7 +289,7 @@ const converters = { } if (msg.data.hasOwnProperty('rmsVoltage')) { const multiplier = msg.endpoint.getClusterAttributeValue( - 'haElectricalMeasurement', 'acVoltageMultiplier' + 'haElectricalMeasurement', 'acVoltageMultiplier', ); const divisor = msg.endpoint.getClusterAttributeValue('haElectricalMeasurement', 'acVoltageDivisor'); const factor = multiplier && divisor ? multiplier / divisor : 1; @@ -3242,52 +3242,60 @@ const converters = { }; }, }, - ZBT_CCTSwitch_D0001_cmdOnOff: { + CCTSwitch_D0001_on_off: { cluster: 'genOnOff', type: ['commandOn', 'commandOff'], convert: (model, msg, publish, options) => { - // the remote maintains state and sends two potential commands for the power button - // map both "on" and "off" to a consistent "button_1" - const deviceID = msg.device.ieeeAddr; - if (!store[deviceID]) { - store[deviceID] = {lastCmd: null, last_seq: -10}; - } - - const cmd = 'button_1'; - store[deviceID].lastSeq = msg.meta.zclTransactionSequenceNumber; - store[deviceID].lastCmd = cmd; - return {action: cmd}; + const cmd = msg.type === 'commandOn' ? 'on' : 'off'; + return {click: 'power', action: cmd}; }, }, - ZBT_CCTSwitch_D0001_moveToLevel: { + CCTSwitch_D0001_move_to_level_recall: { cluster: 'genLevelCtrl', type: ['commandMoveToLevel', 'commandMoveToLevelWithOnOff'], convert: (model, msg, publish, options) => { // wrap the messages from button2 and button4 into a single function // button2 always sends "commandMoveToLevel" // button4 sends two messages, with "commandMoveToLevelWithOnOff" coming first in the sequence - // so that's the one we key off of to indicate "button4" + // so that's the one we key off of to indicate "button4". we will NOT print it in that case, + // instead it will be returned as part of the second sequence with + // CCTSwitch_D0001_move_to_colortemp_recall below. const deviceID = msg.device.ieeeAddr; if (!store[deviceID]) { - store[deviceID] = {lastCmd: null, lastSeq: -10}; + store[deviceID] = {lastClk: null, lastSeq: -10, lastBrightness: null, + lastMoveLevel: null, lastColorTemp: null}; } + let clk = 'brightness'; let cmd = null; + const payload = {brightness: msg.data.level, transition: parseFloat(msg.data.transtime/10.0)}; if ( msg.type == 'commandMoveToLevel' ) { - cmd = 'button_2'; + // pressing the brightness button increments/decrements from 13-254. + // when it reaches the end (254) it will start decrementing by a step, + // and vice versa. + const direction = msg.data.level > store[deviceID].lastBrightness ? 'up' : 'down'; + cmd = `${clk}_${direction}`; + store[deviceID].lastBrightness = msg.data.level; } else if ( msg.type == 'commandMoveToLevelWithOnOff' ) { - cmd = 'button_4'; + // This is the 'start' of the 4th button sequence. + clk = 'memory'; + store[deviceID].lastMoveLevel = msg.data.level; + store[deviceID].lastClk = clk; } - store[deviceID].lastSeq = msg.meta.zclTransactionSequenceNumber; - store[deviceID].lastCmd = cmd; - return {action: cmd}; + if ( clk != 'memory' ) { + store[deviceID].lastSeq = msg.meta.zclTransactionSequenceNumber; + store[deviceID].lastClk = clk; + payload.click = clk; + payload.action = cmd; + return payload; + } }, }, - ZBT_CCTSwitch_D0001_moveToColorTemp: { + CCTSwitch_D0001_move_to_colortemp_recall: { cluster: 'lightingColorCtrl', - type: ['commandMoveToColorTemp'], + type: 'commandMoveToColorTemp', convert: (model, msg, publish, options) => { // both button3 and button4 send the command "commandMoveToColorTemp" // in order to distinguish between the buttons, use the sequence number and the previous command @@ -3296,32 +3304,101 @@ const converters = { // and we can ignore it entirely const deviceID = msg.device.ieeeAddr; if (!store[deviceID]) { - store[deviceID] = {lastCmd: null, lastSeq: -10}; + store[deviceID] = {lastClk: null, lastSeq: -10, lastBrightness: null, + lastMoveLevel: null, lastColorTemp: null}; } - const lastCmd = store[deviceID].lastCmd; + const lastClk = store[deviceID].lastClk; const lastSeq = store[deviceID].lastSeq; const seq = msg.meta.zclTransactionSequenceNumber; - let cmd = 'button_3'; + let clk = 'colortemp'; + const payload = {color_temp: msg.data.colortemp, transition: parseFloat(msg.data.transtime/10.0)}; // because the remote sends two commands for button4, we need to look at the previous command and // see if it was the recognized start command for button4 - if so, ignore this second command, // because it's not really button3, it's actually button4 - if ( lastCmd == 'button_4' ) { + if ( lastClk == 'memory' ) { + payload.click = lastClk; + payload.action = 'recall'; + payload.brightness = store[deviceID].lastMoveLevel; + // ensure the "last" message was really the message prior to this one // accounts for missed messages (gap >1) and for the remote's rollover from 127 to 0 if ( (seq == 0 && lastSeq == 127 ) || ( seq - lastSeq ) == 1 ) { - cmd = null; + clk = null; } + } else { + // pressing the color temp button increments/decrements from 153-370K. + // when it reaches the end (370) it will start decrementing by a step, + // and vice versa. + const direction = msg.data.colortemp > store[deviceID].lastColorTemp ? 'up' : 'down'; + const cmd = `${clk}_${direction}`; + payload.click = clk; + payload.action = cmd; + store[deviceID].lastColorTemp = msg.data.colortemp; } - if ( cmd != null ) { + if ( clk != null ) { store[deviceID].lastSeq = msg.meta.zclTransactionSequenceNumber; - store[deviceID].lastCmd = cmd; - return {action: cmd}; + store[deviceID].lastClk = clk; + return payload; } }, }, + CCTSwitch_D0001_brightness_updown_hold_release: { + cluster: 'genLevelCtrl', + type: ['commandMove', 'commandStop'], + convert: (model, msg, publish, options) => { + const deviceID = msg.device.ieeeAddr; + if (!store[deviceID]) { + store[deviceID] = {}; + } + const stop = msg.type === 'commandStop' ? true : false; + let direction = null; + const clk = 'brightness'; + const result = {click: clk}; + if (stop) { + direction = store[deviceID].direction; + const duration = Date.now() - store[deviceID].start; + result.action = `${clk}_${direction}_release`; + result.duration = duration; + } else { + direction = msg.data.movemode === 1 ? 'down' : 'up'; + result.action = `${clk}_${direction}_hold`; + // store button and start moment + store[deviceID].direction = direction; + store[deviceID].start = Date.now(); + } + return result; + }, + }, + CCTSwitch_D0001_colortemp_updown_hold_release: { + cluster: 'lightingColorCtrl', + type: 'commandMoveColorTemp', + convert: (model, msg, publish, options) => { + const deviceID = msg.device.ieeeAddr; + if (!store[deviceID]) { + store[deviceID] = {}; + } + const stop = msg.data.movemode === 0; + let direction = null; + const clk = 'colortemp'; + const result = {click: clk}; + if (stop) { + direction = store[deviceID].direction; + const duration = Date.now() - store[deviceID].start; + result.action = `${clk}_${direction}_release`; + result.duration = duration; + } else { + direction = msg.data.movemode === 3 ? 'down' : 'up'; + result.action = `${clk}_${direction}_hold`; + // store button and start moment + store[deviceID].direction = direction; + store[deviceID].start = Date.now(); + } + return result; + }, + }, // Ignore converters (these message dont need parsing). ignore_onoff_report: { diff --git a/converters/toZigbee.js b/converters/toZigbee.js index 03f890f98138c..84dd1fa546332 100644 --- a/converters/toZigbee.js +++ b/converters/toZigbee.js @@ -134,7 +134,7 @@ const converters = { 'genLevelCtrl', 'moveToLevel', {level: Math.round(Number(value) * 2.55).toString(), transtime: 0}, - getOptions(meta) + getOptions(meta), ); return {state: {position: value}, readAfterWriteTime: 0}; @@ -176,7 +176,7 @@ const converters = { 'ssIasWd', 'startWarning', {startwarninginfo: info, warningduration: values.duration}, - getOptions(meta) + getOptions(meta), ); }, }, @@ -206,14 +206,14 @@ const converters = { 'closuresWindowCovering', isPosition ? 'goToLiftPercentage' : 'goToTiltPercentage', isPosition ? {percentageliftvalue: value} : {percentagetiltvalue: value}, - getOptions(meta) + getOptions(meta), ); }, convertGet: async (entity, key, meta) => { const isPosition = (key === 'position'); await entity.read( 'closuresWindowCovering', - [isPosition ? 'currentPositionLiftPercentage' : 'currentPositionTiltPercentage'] + [isPosition ? 'currentPositionLiftPercentage' : 'currentPositionTiltPercentage'], ); }, }, @@ -291,7 +291,7 @@ const converters = { 'genLevelCtrl', 'moveToLevelWithOnOff', {level: Number(brightness), transtime: transition}, - getOptions(meta) + getOptions(meta), ); return { state: {state: brightness === 0 ? 'OFF' : 'ON', brightness: Number(brightness)}, @@ -898,7 +898,7 @@ const converters = { 'closuresDoorLock', `${value.toLowerCase()}Door`, {'pincodevalue': ''}, - getOptions(meta) + getOptions(meta), ); return {readAfterWriteTime: 200, state: {state: value.toUpperCase()}}; @@ -957,7 +957,7 @@ const converters = { } const result = await converters.light_brightness.convertSet( - meta.device.getEndpoint(15), key, value, meta + meta.device.getEndpoint(15), key, value, meta, ); return { state: {white_value: value, ...result.state, ...state}, diff --git a/devices.js b/devices.js index 9b513dd56158d..d614bf171a765 100755 --- a/devices.js +++ b/devices.js @@ -4337,6 +4337,13 @@ const devices = [ }, // EcoSmart + { + zigbeeModel: ['Ecosmart-ZBT-A19-CCT-Bulb'], + model: 'A9A19A60WESDZ02', + vendor: 'EcoSmart', + description: 'Tuneable white (A19)', + extend: generic.light_onoff_brightness_colortemp, + }, { zigbeeModel: ['zhaRGBW'], model: 'D1821', @@ -4374,7 +4381,11 @@ const devices = [ description: 'Four button remote control (included with EcoSmart smart bulbs)', supports: 'action', fromZigbee: [ - fz.ZBT_CCTSwitch_D0001_cmdOnOff, fz.ZBT_CCTSwitch_D0001_moveToLevel, fz.ZBT_CCTSwitch_D0001_moveToColorTemp, + fz.CCTSwitch_D0001_on_off, + fz.CCTSwitch_D0001_move_to_level_recall, + fz.CCTSwitch_D0001_move_to_colortemp_recall, + fz.CCTSwitch_D0001_colortemp_updown_hold_release, + fz.CCTSwitch_D0001_brightness_updown_hold_release, ], toZigbee: [], }, @@ -6313,5 +6324,5 @@ const devices = [ ]; module.exports = devices.map((device) => - device.extend ? Object.assign({}, device.extend, device) : device + device.extend ? Object.assign({}, device.extend, device) : device, );