UNPKG

zigbee-herdsman-converters

Version:

Collection of device converters to be used with zigbee-herdsman

952 lines • 215 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); const zigbee_herdsman_1 = require("zigbee-herdsman"); const globalStore = __importStar(require("../lib/store")); const constants = __importStar(require("../lib/constants")); const libColor = __importStar(require("../lib/color")); const utils = __importStar(require("../lib/utils")); const light = __importStar(require("../lib/light")); const legacy = __importStar(require("../lib/legacy")); const exposes = __importStar(require("../lib/exposes")); const logger_1 = require("../lib/logger"); const NS = 'zhc:tz'; const manufacturerOptions = { sunricher: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.SHENZHEN_SUNRICHER_TECHNOLOGY_LTD }, lumi: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.LUMI_UNITED_TECHOLOGY_LTD_SHENZHEN, disableDefaultResponse: true }, eurotronic: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.NXP_SEMICONDUCTORS }, danfoss: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.DANFOSS_A_S }, hue: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V }, ikea: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.IKEA_OF_SWEDEN }, sinope: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.SINOPE_TECHNOLOGIES }, tint: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.MUELLER_LICHT_INTERNATIONAL_INC }, legrand: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.LEGRAND_GROUP, disableDefaultResponse: true }, viessmann: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.VIESSMANN_ELEKTRONIK_GMBH }, nodon: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.NODON }, }; const converters1 = { on_off: { key: ['state', 'on_time', 'off_wait_time'], convertSet: async (entity, key, value, meta) => { const state = utils.isString(meta.message.state) ? meta.message.state.toLowerCase() : null; utils.validateValue(state, ['toggle', 'off', 'on']); if (state === 'on' && (meta.message.hasOwnProperty('on_time') || meta.message.hasOwnProperty('off_wait_time'))) { const onTime = meta.message.hasOwnProperty('on_time') ? meta.message.on_time : 0; const offWaitTime = meta.message.hasOwnProperty('off_wait_time') ? meta.message.off_wait_time : 0; if (typeof onTime !== 'number') { throw Error('The on_time value must be a number!'); } if (typeof offWaitTime !== 'number') { throw Error('The off_wait_time value must be a number!'); } const payload = { ctrlbits: 0, ontime: Math.round(onTime * 10), offwaittime: Math.round(offWaitTime * 10) }; await entity.command('genOnOff', 'onWithTimedOff', payload, utils.getOptions(meta.mapped, entity)); } else { await entity.command('genOnOff', state, {}, utils.getOptions(meta.mapped, entity)); if (state === 'toggle') { const currentState = meta.state[`state${meta.endpoint_name ? `_${meta.endpoint_name}` : ''}`]; return currentState ? { state: { state: currentState === 'OFF' ? 'ON' : 'OFF' } } : {}; } else { return { state: { state: state.toUpperCase() } }; } } }, convertGet: async (entity, key, meta) => { await entity.read('genOnOff', ['onOff']); }, }, light_color: { key: ['color'], options: [exposes.options.color_sync(), exposes.options.transition()], convertSet: async (entity, key, value, meta) => { let command; const newColor = libColor.Color.fromConverterArg(value); const newState = {}; const zclData = { transtime: utils.getTransition(entity, key, meta).time }; const supportsHueAndSaturation = utils.getMetaValue(entity, meta.mapped, 'supportsHueAndSaturation', 'allEqual', true); const supportsEnhancedHue = utils.getMetaValue(entity, meta.mapped, 'supportsEnhancedHue', 'allEqual', true); if (newColor.isHSV() && !supportsHueAndSaturation) { // The color we got is HSV but the bulb does not support Hue/Saturation mode throw new Error('This light does not support Hue/Saturation, please use X/Y instead.'); } else if (newColor.isRGB() || newColor.isXY()) { // Convert RGB to XY color mode because Zigbee doesn't support RGB (only x/y and hue/saturation) const xy = newColor.isRGB() ? newColor.rgb.gammaCorrected().toXY().rounded(4) : newColor.xy; // Some bulbs e.g. RB 185 C don't turn to red (they don't respond at all) when x: 0.701 and y: 0.299 // is send. These values are e.g. send by Home Assistant when clicking red in the color wheel. // If we slightly modify these values the bulb will respond. // https://github.com/home-assistant/home-assistant/issues/31094 if (utils.getMetaValue(entity, meta.mapped, 'applyRedFix', 'allEqual', false) && xy.x == 0.701 && xy.y === 0.299) { xy.x = 0.7006; xy.y = 0.2993; } newState.color_mode = constants.colorModeLookup[1]; newState.color = xy.toObject(); zclData.colorx = utils.mapNumberRange(xy.x, 0, 1, 0, 65535); zclData.colory = utils.mapNumberRange(xy.y, 0, 1, 0, 65535); command = 'moveToColor'; } else if (newColor.isHSV()) { const hsv = newColor.hsv; const hsvCorrected = hsv.colorCorrected(meta); newState.color_mode = constants.colorModeLookup[0]; newState.color = hsv.toObject(false); if (hsv.hue !== null) { if (supportsEnhancedHue) { zclData.enhancehue = utils.mapNumberRange(hsvCorrected.hue, 0, 360, 0, 65535); } else { zclData.hue = utils.mapNumberRange(hsvCorrected.hue, 0, 360, 0, 254); } // @ts-expect-error zclData.direction = value.direction || 0; } if (hsv.saturation != null) { zclData.saturation = utils.mapNumberRange(hsvCorrected.saturation, 0, 100, 0, 254); } if (hsv.value !== null) { // fallthrough to genLevelCtrl // @ts-expect-error value.brightness = utils.mapNumberRange(hsvCorrected.value, 0, 100, 0, 254); } if (hsv.hue !== null && hsv.saturation !== null) { if (supportsEnhancedHue) { command = 'enhancedMoveToHueAndSaturation'; } else { command = 'moveToHueAndSaturation'; } } else if (hsv.hue !== null) { if (supportsEnhancedHue) { command = 'enhancedMoveToHue'; } else { command = 'moveToHue'; } } else if (hsv.saturation !== null) { command = 'moveToSaturation'; } } if (value.hasOwnProperty('brightness')) { await entity.command('genLevelCtrl', 'moveToLevelWithOnOff', // @ts-expect-error { level: Number(value.brightness), transtime: utils.getTransition(entity, key, meta).time }, utils.getOptions(meta.mapped, entity)); } await entity.command('lightingColorCtrl', command, zclData, utils.getOptions(meta.mapped, entity)); return { state: libColor.syncColorState(newState, meta.state, entity, meta.options), readAfterWriteTime: zclData.transtime * 100 }; }, convertGet: async (entity, key, meta) => { await entity.read('lightingColorCtrl', light.readColorAttributes(entity, meta)); }, }, light_colortemp: { key: ['color_temp', 'color_temp_percent'], options: [exposes.options.color_sync(), exposes.options.transition()], convertSet: async (entity, key, value, meta) => { const [colorTempMin, colorTempMax] = light.findColorTempRange(entity); const preset = { 'warmest': colorTempMax, 'warm': 454, 'neutral': 370, 'cool': 250, 'coolest': colorTempMin }; if (key === 'color_temp_percent') { utils.assertNumber(value); value = utils.mapNumberRange(value, 0, 100, ((colorTempMin != null) ? colorTempMin : 154), ((colorTempMax != null) ? colorTempMax : 500)).toString(); } if (utils.isString(value) && value in preset) { value = utils.getFromLookup(value, preset); } value = Number(value); // ensure value within range utils.assertNumber(value); value = light.clampColorTemp(value, colorTempMin, colorTempMax); const payload = { colortemp: value, transtime: utils.getTransition(entity, key, meta).time }; await entity.command('lightingColorCtrl', 'moveToColorTemp', payload, utils.getOptions(meta.mapped, entity)); return { state: libColor.syncColorState({ 'color_mode': constants.colorModeLookup[2], 'color_temp': value }, meta.state, entity, meta.options), readAfterWriteTime: payload.transtime * 100, }; }, convertGet: async (entity, key, meta) => { await entity.read('lightingColorCtrl', ['colorMode', 'colorTemperature']); }, }, }; const converters2 = { // #region Generic converters read: { key: ['read'], convertSet: async (entity, key, value, meta) => { utils.assertObject(value, key); const result = await entity.read(value.cluster, value.attributes, (value.hasOwnProperty('options') ? value.options : {})); logger_1.logger.info(`Read result of '${value.cluster}': ${JSON.stringify(result)}`, NS); if (value.hasOwnProperty('state_property')) { return { state: { [value.state_property]: result } }; } }, }, write: { key: ['write'], convertSet: async (entity, key, value, meta) => { utils.assertObject(value, key); const options = utils.getOptions(meta.mapped, entity); if (value.hasOwnProperty('options')) { Object.assign(options, value.options); } await entity.write(value.cluster, value.payload, options); logger_1.logger.info(`Wrote '${JSON.stringify(value.payload)}' to '${value.cluster}'`, NS); }, }, command: { key: ['command'], convertSet: async (entity, key, value, meta) => { utils.assertObject(value, key); const options = utils.getOptions(meta.mapped, entity); await entity.command(value.cluster, value.command, (value.hasOwnProperty('payload') ? value.payload : {}), options); logger_1.logger.info(`Invoked '${value.cluster}.${value.command}' with payload '${JSON.stringify(value.payload)}'`, NS); }, }, factory_reset: { key: ['reset'], convertSet: async (entity, key, value, meta) => { await entity.command('genBasic', 'resetFactDefault', {}, utils.getOptions(meta.mapped, entity)); }, }, identify: { key: ['identify'], options: [exposes.options.identify_timeout()], convertSet: async (entity, key, value, meta) => { // External value takes priority over options for compatibility const identifyTimeout = value ?? meta.options.identify_timeout ?? 3; await entity.command('genIdentify', 'identify', { identifytime: identifyTimeout }, utils.getOptions(meta.mapped, entity)); }, }, zcl_command: { key: ['zclcommand'], convertSet: async (entity, key, value, meta) => { utils.assertObject(value, key); const payload = (value.hasOwnProperty('payload') ? value.payload : {}); utils.assertEndpoint(entity); await entity.zclCommand(value.cluster, value.command, payload, (value.hasOwnProperty('options') ? value.options : {})); logger_1.logger.info(`Invoked ZCL command ${value.cluster}.${value.command} with payload '${JSON.stringify(payload)}'`, NS); }, }, arm_mode: { key: ['arm_mode'], convertSet: async (entity, key, value, meta) => { utils.assertEndpoint(entity); utils.assertObject(value, key); if (Array.isArray(meta.mapped)) throw new Error(`Not supported for groups`); const isNotification = value.hasOwnProperty('transaction'); const modeSrc = isNotification ? constants.armNotification : constants.armMode; const mode = utils.getKey(modeSrc, value.mode, undefined, Number); if (mode === undefined) { throw new Error(`Unsupported mode: '${value.mode}', should be one of: ${Object.values(modeSrc)}`); } if (isNotification) { await entity.commandResponse('ssIasAce', 'armRsp', { armnotification: mode }, {}, value.transaction); // Do not update PanelStatus after confirming transaction. // Instead the server should send an arm_mode command with the necessary state. // e.g. exit_delay as a result of arm_all_zones return; } let panelStatus = mode; if (meta.mapped.model === '3400-D') { panelStatus = mode !== 0 && mode !== 4 ? 0x80 : 0x00; } globalStore.putValue(entity, 'panelStatus', panelStatus); const payload = { panelstatus: panelStatus, secondsremain: 0, audiblenotif: 0, alarmstatus: 0 }; await entity.commandResponse('ssIasAce', 'panelStatusChanged', payload); }, }, battery_percentage_remaining: { key: ['battery'], convertGet: async (entity, key, meta) => { await entity.read('genPowerCfg', ['batteryPercentageRemaining']); }, }, battery_voltage: { key: ['battery', 'voltage'], convertGet: async (entity, key, meta) => { await entity.read('genPowerCfg', ['batteryVoltage']); }, }, power_on_behavior: { key: ['power_on_behavior'], convertSet: async (entity, key, value, meta) => { utils.assertString(value, key); value = value.toLowerCase(); const lookup = { 'off': 0, 'on': 1, 'toggle': 2, 'previous': 255 }; await entity.write('genOnOff', { startUpOnOff: utils.getFromLookup(value, lookup) }, utils.getOptions(meta.mapped, entity)); return { state: { power_on_behavior: value } }; }, convertGet: async (entity, key, meta) => { await entity.read('genOnOff', ['startUpOnOff']); }, }, light_color_mode: { key: ['color_mode'], convertGet: async (entity, key, meta) => { await entity.read('lightingColorCtrl', ['colorMode']); }, }, light_color_options: { key: ['color_options'], convertSet: async (entity, key, value, meta) => { utils.assertObject(value, key); const options = (value.hasOwnProperty('execute_if_off') && value.execute_if_off) ? 1 : 0; await entity.write('lightingColorCtrl', { options }, utils.getOptions(meta.mapped, entity)); return { state: { 'color_options': value } }; }, convertGet: async (entity, key, meta) => { await entity.read('lightingColorCtrl', ['options']); }, }, lock: { key: ['state'], convertSet: async (entity, key, value, meta) => { utils.assertString(value, key); await entity.command('closuresDoorLock', `${value.toLowerCase()}Door`, { 'pincodevalue': '' }, utils.getOptions(meta.mapped, entity)); return { readAfterWriteTime: 200 }; }, convertGet: async (entity, key, meta) => { await entity.read('closuresDoorLock', ['lockState']); }, }, lock_auto_relock_time: { key: ['auto_relock_time'], convertSet: async (entity, key, value, meta) => { await entity.write('closuresDoorLock', { autoRelockTime: value }, utils.getOptions(meta.mapped, entity)); return { state: { auto_relock_time: value } }; }, convertGet: async (entity, key, meta) => { await entity.read('closuresDoorLock', ['autoRelockTime']); }, }, lock_sound_volume: { key: ['sound_volume'], convertSet: async (entity, key, value, meta) => { utils.assertString(value, key); utils.validateValue(value, constants.lockSoundVolume); await entity.write('closuresDoorLock', { soundVolume: constants.lockSoundVolume.indexOf(value) }, utils.getOptions(meta.mapped, entity)); return { state: { sound_volume: value } }; }, convertGet: async (entity, key, meta) => { await entity.read('closuresDoorLock', ['soundVolume']); }, }, pincode_lock: { key: ['pin_code'], convertSet: async (entity, key, value, meta) => { utils.assertObject(value, key); const user = value.user; const userType = value.user_type || 'unrestricted'; const userEnabled = value.hasOwnProperty('user_enabled') ? value.user_enabled : true; const pinCode = value.pin_code; if (isNaN(user)) throw new Error('user must be numbers'); const pinCodeCount = utils.getMetaValue(entity, meta.mapped, 'pinCodeCount'); if (!utils.isInRange(0, pinCodeCount - 1, user)) throw new Error('user must be in range for device'); if (pinCode == null) { await entity.command('closuresDoorLock', 'clearPinCode', { 'userid': user }, utils.getOptions(meta.mapped, entity)); } else { if (isNaN(pinCode)) throw new Error('pinCode must be a number'); const typeLookup = { 'unrestricted': 0, 'year_day_schedule': 1, 'week_day_schedule': 2, 'master': 3, 'non_access': 4 }; const payload = { 'userid': user, 'userstatus': userEnabled ? 1 : 3, 'usertype': utils.getFromLookup(userType, typeLookup), 'pincodevalue': pinCode.toString(), }; await entity.command('closuresDoorLock', 'setPinCode', payload, utils.getOptions(meta.mapped, entity)); } }, convertGet: async (entity, key, meta) => { // @ts-expect-error const user = meta && meta.message && meta.message.pin_code ? meta.message.pin_code.user : undefined; if (user === undefined) { const max = utils.getMetaValue(entity, meta.mapped, 'pinCodeCount'); // Get all const options = utils.getOptions(meta.mapped, entity); for (let i = 0; i < max; i++) { await entity.command('closuresDoorLock', 'getPinCode', { userid: i }, options); } } else { if (isNaN(user)) { throw new Error('user must be numbers'); } const pinCodeCount = utils.getMetaValue(entity, meta.mapped, 'pinCodeCount'); if (!utils.isInRange(0, pinCodeCount - 1, user)) { throw new Error('userId must be in range for device'); } await entity.command('closuresDoorLock', 'getPinCode', { userid: user }, utils.getOptions(meta.mapped, entity)); } }, }, lock_userstatus: { key: ['user_status'], convertSet: async (entity, key, value, meta) => { utils.assertObject(value, key); const user = value.user; if (isNaN(user)) { throw new Error('user must be numbers'); } const pinCodeCount = utils.getMetaValue(entity, meta.mapped, 'pinCodeCount'); if (!utils.isInRange(0, pinCodeCount - 1, user)) { throw new Error('user must be in range for device'); } const status = utils.getKey(constants.lockUserStatus, value.status, undefined, Number); if (status === undefined) { throw new Error(`Unsupported status: '${value.status}', should be one of: ${Object.values(constants.lockUserStatus)}`); } await entity.command('closuresDoorLock', 'setUserStatus', { 'userid': user, 'userstatus': status, }, utils.getOptions(meta.mapped, entity)); }, convertGet: async (entity, key, meta) => { // @ts-expect-error const user = meta && meta.message && meta.message.user_status ? meta.message.user_status.user : undefined; const pinCodeCount = utils.getMetaValue(entity, meta.mapped, 'pinCodeCount'); if (user === undefined) { const max = pinCodeCount; // Get all const options = utils.getOptions(meta.mapped, entity); for (let i = 0; i < max; i++) { await entity.command('closuresDoorLock', 'getUserStatus', { userid: i }, options); } } else { if (isNaN(user)) { throw new Error('user must be numbers'); } if (!utils.isInRange(0, pinCodeCount - 1, user)) { throw new Error('userId must be in range for device'); } await entity.command('closuresDoorLock', 'getUserStatus', { userid: user }, utils.getOptions(meta.mapped, entity)); } }, }, cover_via_brightness: { key: ['position', 'state'], options: [exposes.options.invert_cover()], convertSet: async (entity, key, value, meta) => { if (typeof value !== 'number') { utils.assertString(value, key); value = value.toLowerCase(); if (value === 'stop') { await entity.command('genLevelCtrl', 'stop', {}, utils.getOptions(meta.mapped, entity)); return; } const lookup = { 'open': 100, 'close': 0 }; value = utils.getFromLookup(value, lookup); } const invert = utils.getMetaValue(entity, meta.mapped, 'coverInverted', 'allEqual', false) ? !meta.options.invert_cover : meta.options.invert_cover; utils.assertNumber(value); const position = invert ? 100 - value : value; await entity.command('genLevelCtrl', 'moveToLevelWithOnOff', { level: utils.mapNumberRange(Number(position), 0, 100, 0, 255).toString(), transtime: 0 }, utils.getOptions(meta.mapped, entity)); return { state: { position: value }, readAfterWriteTime: 0 }; }, convertGet: async (entity, key, meta) => { await entity.read('genLevelCtrl', ['currentLevel']); }, }, warning: { key: ['warning'], convertSet: async (entity, key, value, meta) => { const mode = { 'stop': 0, 'burglar': 1, 'fire': 2, 'emergency': 3, 'police_panic': 4, 'fire_panic': 5, 'emergency_panic': 6 }; const level = { 'low': 0, 'medium': 1, 'high': 2, 'very_high': 3 }; const strobeLevel = { 'low': 0, 'medium': 1, 'high': 2, 'very_high': 3 }; const values = { // @ts-expect-error mode: value.mode || 'emergency', // @ts-expect-error level: value.level || 'medium', // @ts-expect-error strobe: value.hasOwnProperty('strobe') ? value.strobe : true, // @ts-expect-error duration: value.hasOwnProperty('duration') ? value.duration : 10, // @ts-expect-error strobeDutyCycle: value.hasOwnProperty('strobe_duty_cycle') ? value.strobe_duty_cycle * 10 : 0, // @ts-expect-error strobeLevel: value.hasOwnProperty('strobe_level') ? utils.getFromLookup(value.strobe_level, strobeLevel) : 1, }; let info; // https://github.com/Koenkk/zigbee2mqtt/issues/8310 some devices require the info to be reversed. if (Array.isArray(meta.mapped)) throw new Error(`Not supported for groups`); if (['SIRZB-110', 'SRAC-23B-ZBSR', 'AV2010/29A', 'AV2010/24A'].includes(meta.mapped.model)) { info = (utils.getFromLookup(values.mode, mode)) + ((values.strobe ? 1 : 0) << 4) + (utils.getFromLookup(values.level, level) << 6); } else { info = (utils.getFromLookup(values.mode, mode) << 4) + ((values.strobe ? 1 : 0) << 2) + (utils.getFromLookup(values.level, level)); } await entity.command('ssIasWd', 'startWarning', { startwarninginfo: info, warningduration: values.duration, strobedutycycle: values.strobeDutyCycle, strobelevel: values.strobeLevel }, utils.getOptions(meta.mapped, entity)); }, }, ias_max_duration: { key: ['max_duration'], convertSet: async (entity, key, value, meta) => { await entity.write('ssIasWd', { 'maxDuration': value }); return { state: { max_duration: value } }; }, convertGet: async (entity, key, meta) => { await entity.read('ssIasWd', ['maxDuration']); }, }, warning_simple: { key: ['alarm'], convertSet: async (entity, key, value, meta) => { const alarmState = (value === 'alarm' || value === 'OFF' ? 0 : 1); let info; // For Develco SMSZB-120 and HESZB-120, introduced change in fw 4.0.5, tested backward with 4.0.4 if (Array.isArray(meta.mapped)) throw new Error(`Not supported for groups`); if (['SMSZB-120', 'HESZB-120'].includes(meta.mapped.model)) { info = ((alarmState) << 7) + ((alarmState) << 6); } else { info = (3 << 6) + ((alarmState) << 2); } await entity.command('ssIasWd', 'startWarning', { startwarninginfo: info, warningduration: 300, strobedutycycle: 0, strobelevel: 0 }, utils.getOptions(meta.mapped, entity)); }, }, squawk: { key: ['squawk'], convertSet: async (entity, key, value, meta) => { utils.assertObject(value, key); const state = { 'system_is_armed': 0, 'system_is_disarmed': 1 }; const level = { 'low': 0, 'medium': 1, 'high': 2, 'very_high': 3 }; const values = { state: value.state, level: value.level || 'very_high', strobe: value.hasOwnProperty('strobe') ? value.strobe : false, }; const info = (utils.getFromLookup(values.state, state)) + ((values.strobe ? 1 : 0) << 4) + (utils.getFromLookup(values.level, level) << 6); await entity.command('ssIasWd', 'squawk', { squawkinfo: info }, utils.getOptions(meta.mapped, entity)); }, }, cover_state: { key: ['state'], convertSet: async (entity, key, value, meta) => { const lookup = { 'open': 'upOpen', 'close': 'downClose', 'stop': 'stop', 'on': 'upOpen', 'off': 'downClose' }; utils.assertString(value, key); value = value.toLowerCase(); await entity.command('closuresWindowCovering', utils.getFromLookup(value, lookup), {}, utils.getOptions(meta.mapped, entity)); }, }, cover_position_tilt: { key: ['position', 'tilt'], options: [exposes.options.invert_cover()], convertSet: async (entity, key, value, meta) => { utils.assertNumber(value, key); const isPosition = (key === 'position'); const invert = !(utils.getMetaValue(entity, meta.mapped, 'coverInverted', 'allEqual', false) ? !meta.options.invert_cover : meta.options.invert_cover); const position = invert ? 100 - value : value; // Zigbee officially expects 'open' to be 0 and 'closed' to be 100 whereas // HomeAssistant etc. work the other way round. // For zigbee-herdsman-converters: open = 100, close = 0 await entity.command('closuresWindowCovering', isPosition ? 'goToLiftPercentage' : 'goToTiltPercentage', isPosition ? { percentageliftvalue: position } : { percentagetiltvalue: position }, utils.getOptions(meta.mapped, entity)); return { state: { [isPosition ? 'position' : 'tilt']: value } }; }, convertGet: async (entity, key, meta) => { const isPosition = (key === 'position'); await entity.read('closuresWindowCovering', [isPosition ? 'currentPositionLiftPercentage' : 'currentPositionTiltPercentage']); }, }, occupancy_timeout: { // Sets delay after motion detector changes from occupied to unoccupied key: ['occupancy_timeout'], convertSet: async (entity, key, value, meta) => { utils.assertNumber(value); value *= 1; await entity.write('msOccupancySensing', { pirOToUDelay: value }, utils.getOptions(meta.mapped, entity)); return { state: { occupancy_timeout: value } }; }, convertGet: async (entity, key, meta) => { await entity.read('msOccupancySensing', ['pirOToUDelay']); }, }, level_config: { key: ['level_config'], convertSet: async (entity, key, value, meta) => { const state = {}; // parse payload to grab the keys if (typeof value === 'string') { try { value = JSON.parse(value); } catch (e) { throw new Error('Payload is not valid JSON'); } } utils.assertObject(value, key); // onOffTransitionTime - range 0x0000 to 0xffff - optional if (value.hasOwnProperty('on_off_transition_time')) { let onOffTransitionTimeValue = Number(value.on_off_transition_time); if (onOffTransitionTimeValue > 65535) onOffTransitionTimeValue = 65535; if (onOffTransitionTimeValue < 0) onOffTransitionTimeValue = 0; await entity.write('genLevelCtrl', { onOffTransitionTime: onOffTransitionTimeValue }, utils.getOptions(meta.mapped, entity)); Object.assign(state, { on_off_transition_time: onOffTransitionTimeValue }); } // onTransitionTime - range 0x0000 to 0xffff - optional // 0xffff = use onOffTransitionTime if (value.hasOwnProperty('on_transition_time')) { let onTransitionTimeValue = value.on_transition_time; if (typeof onTransitionTimeValue === 'string' && onTransitionTimeValue.toLowerCase() == 'disabled') { onTransitionTimeValue = 65535; } else { onTransitionTimeValue = Number(onTransitionTimeValue); } if (onTransitionTimeValue > 65535) onTransitionTimeValue = 65534; if (onTransitionTimeValue < 0) onTransitionTimeValue = 0; await entity.write('genLevelCtrl', { onTransitionTime: onTransitionTimeValue }, utils.getOptions(meta.mapped, entity)); // reverse translate number -> preset if (onTransitionTimeValue == 65535) { onTransitionTimeValue = 'disabled'; } Object.assign(state, { on_transition_time: onTransitionTimeValue }); } // offTransitionTime - range 0x0000 to 0xffff - optional // 0xffff = use onOffTransitionTime if (value.hasOwnProperty('off_transition_time')) { let offTransitionTimeValue = value.off_transition_time; if (typeof offTransitionTimeValue === 'string' && offTransitionTimeValue.toLowerCase() == 'disabled') { offTransitionTimeValue = 65535; } else { offTransitionTimeValue = Number(offTransitionTimeValue); } if (offTransitionTimeValue > 65535) offTransitionTimeValue = 65534; if (offTransitionTimeValue < 0) offTransitionTimeValue = 0; await entity.write('genLevelCtrl', { offTransitionTime: offTransitionTimeValue }, utils.getOptions(meta.mapped, entity)); // reverse translate number -> preset if (offTransitionTimeValue == 65535) { offTransitionTimeValue = 'disabled'; } Object.assign(state, { off_transition_time: offTransitionTimeValue }); } // startUpCurrentLevel - range 0x00 to 0xff - optional // 0x00 = return to minimum supported level // 0xff = return to previous previous if (value.hasOwnProperty('current_level_startup')) { let startUpCurrentLevelValue = value.current_level_startup; if (typeof startUpCurrentLevelValue === 'string' && startUpCurrentLevelValue.toLowerCase() == 'previous') { startUpCurrentLevelValue = 255; } else if (typeof startUpCurrentLevelValue === 'string' && startUpCurrentLevelValue.toLowerCase() == 'minimum') { startUpCurrentLevelValue = 0; } else { startUpCurrentLevelValue = Number(startUpCurrentLevelValue); } if (startUpCurrentLevelValue > 255) startUpCurrentLevelValue = 254; if (startUpCurrentLevelValue < 0) startUpCurrentLevelValue = 1; await entity.write('genLevelCtrl', { startUpCurrentLevel: startUpCurrentLevelValue }, utils.getOptions(meta.mapped, entity)); // reverse translate number -> preset if (startUpCurrentLevelValue == 255) { startUpCurrentLevelValue = 'previous'; } if (startUpCurrentLevelValue == 0) { startUpCurrentLevelValue = 'minimum'; } Object.assign(state, { current_level_startup: startUpCurrentLevelValue }); } // onLevel - range 0x00 to 0xff - optional // Any value outside of MinLevel to MaxLevel, including 0xff and 0x00, is interpreted as "previous". if (value.hasOwnProperty('on_level')) { let onLevel = value.on_level; if (typeof onLevel === 'string' && onLevel.toLowerCase() == 'previous') { onLevel = 255; } else { onLevel = Number(onLevel); } if (onLevel > 255) onLevel = 254; if (onLevel < 1) onLevel = 1; await entity.write('genLevelCtrl', { onLevel }, utils.getOptions(meta.mapped, entity)); Object.assign(state, { on_level: onLevel == 255 ? 'previous' : onLevel }); } // options - 8-bit map // bit 0: ExecuteIfOff - when 0, Move commands are ignored if the device is off; // when 1, CurrentLevel can be changed while the device is off. // bit 1: CoupleColorTempToLevel - when 1, changes to level also change color temperature. // (What this means is not defined, but it's most likely to be "dim to warm".) if (value.hasOwnProperty('execute_if_off')) { const executeIfOffValue = !!value.execute_if_off; await entity.write('genLevelCtrl', { options: executeIfOffValue ? 1 : 0 }, utils.getOptions(meta.mapped, entity)); Object.assign(state, { execute_if_off: executeIfOffValue }); } if (Object.keys(state).length > 0) { return { state: { level_config: state } }; } }, convertGet: async (entity, key, meta) => { for (const attribute of [ 'onOffTransitionTime', 'onTransitionTime', 'offTransitionTime', 'startUpCurrentLevel', 'onLevel', 'options', ]) { try { await entity.read('genLevelCtrl', [attribute]); } catch (ex) { // continue regardless of error, all these are optional in ZCL } } }, }, ballast_config: { key: ['ballast_config', 'ballast_minimum_level', 'ballast_maximum_level', 'ballast_power_on_level'], // zcl attribute names are camel case, but we want to use snake case in the outside communication convertSet: async (entity, key, value, meta) => { if (key === 'ballast_config') { value = utils.toCamelCase(value); for (const [attrName, attrValue] of Object.entries(value)) { const attributes = { [attrName]: attrValue }; await entity.write('lightingBallastCfg', attributes); } } if (key === 'ballast_minimum_level') { await entity.write('lightingBallastCfg', { 'minLevel': value }); } if (key === 'ballast_maximum_level') { await entity.write('lightingBallastCfg', { 'maxLevel': value }); } if (key === 'ballast_power_on_level') { await entity.write('lightingBallastCfg', { 'powerOnLevel': value }); } return { state: { [key]: value } }; }, convertGet: async (entity, key, meta) => { let result = {}; for (const attrName of [ 'ballast_status', 'min_level', 'max_level', 'power_on_level', 'power_on_fade_time', 'intrinsic_ballast_factor', 'ballast_factor_adjustment', 'lamp_quantity', 'lamp_type', 'lamp_manufacturer', 'lamp_rated_hours', 'lamp_burn_hours', 'lamp_alarm_mode', 'lamp_burn_hours_trip_point', ]) { try { // @ts-expect-error result = { ...result, ...(await entity.read('lightingBallastCfg', [utils.toCamelCase(attrName)])) }; } catch (ex) { // continue regardless of error } } if (key === 'ballast_config') { logger_1.logger.debug(`ballast_config attribute results received: ${JSON.stringify(utils.toSnakeCase(result))}`, NS); } }, }, light_brightness_step: { key: ['brightness_step', 'brightness_step_onoff'], options: [exposes.options.transition()], convertSet: async (entity, key, value, meta) => { const onOff = key.endsWith('_onoff'); const command = onOff ? 'stepWithOnOff' : 'step'; value = Number(value); utils.assertNumber(value, key); const mode = value > 0 ? 0 : 1; const transition = utils.getTransition(entity, key, meta).time; const payload = { stepmode: mode, stepsize: Math.abs(value), transtime: transition }; await entity.command('genLevelCtrl', command, payload, utils.getOptions(meta.mapped, entity)); if (meta.state.hasOwnProperty('brightness')) { utils.assertNumber(meta.state.brightness); let brightness = onOff || meta.state.state === 'ON' ? meta.state.brightness + value : meta.state.brightness; if (value === 0) { const entityToRead = utils.getEntityOrFirstGroupMember(entity); if (entityToRead) { brightness = (await entityToRead.read('genLevelCtrl', ['currentLevel'])).currentLevel; } } brightness = Math.min(254, brightness); brightness = Math.max(onOff || meta.state.state === 'OFF' ? 0 : 1, brightness); if (utils.getMetaValue(entity, meta.mapped, 'turnsOffAtBrightness1', 'allEqual', false)) { if (onOff && value < 0 && brightness === 1) { brightness = 0; } else if (onOff && value > 0 && meta.state.brightness === 0) { brightness++; } } return { state: { brightness, state: brightness === 0 ? 'OFF' : 'ON' } }; } }, }, light_brightness_move: { key: ['brightness_move', 'brightness_move_onoff'], convertSet: async (entity, key, value, meta) => { if (value === 'stop' || value === 0) { await entity.command('genLevelCtrl', 'stop', {}, utils.getOptions(meta.mapped, entity)); // As we cannot determine the new brightness state, we read it from the device await utils.sleep(500); const target = utils.getEntityOrFirstGroupMember(entity); const onOff = (await target.read('genOnOff', ['onOff'])).onOff; const brightness = (await target.read('genLevelCtrl', ['currentLevel'])).currentLevel; return { state: { brightness, state: onOff === 1 ? 'ON' : 'OFF' } }; } else { value = Number(value); utils.assertNumber(value, key); const payload = { movemode: value > 0 ? 0 : 1, rate: Math.abs(value) }; const command = key.endsWith('onoff') ? 'moveWithOnOff' : 'move'; await entity.command('genLevelCtrl', command, payload, utils.getOptions(meta.mapped, entity)); } }, }, light_colortemp_step: { key: ['color_temp_step'], options: [exposes.options.transition()], convertSet: async (entity, key, value, meta) => { value = Number(value); utils.assertNumber(value, key); const mode = value > 0 ? 1 : 3; const transition = utils.getTransition(entity, key, meta).time; const payload = { stepmode: mode, stepsize: Math.abs(value), transtime: transition, minimum: 0, maximum: 600 }; await entity.command('lightingColorCtrl', 'stepColorTemp', payload, utils.getOptions(meta.mapped, entity)); // We cannot determine the color temperature from the current state so we read it, because // - We don't know the max/min values // - Color mode could have been switched (x/y or hue/saturation) const entityToRead = utils.getEntityOrFirstGroupMember(entity); if (entityToRead) { await utils.sleep(100 + (transition * 100)); await entityToRead.read('lightingColorCtrl', ['colorTemperature']); } }, }, light_colortemp_move: { key: ['colortemp_move', 'color_temp_move'], convertSet: async (entity, key, value, meta) => { if (key === 'color_temp_move' && (value === 'stop' || utils.isNumber(value))) { value = value === 'stop' ? value : Number(value); const payload = { minimum: 0, maximum: 600 }; if (value === 'stop' || value === 0) { payload.rate = 1; payload.movemode = 0; } else { utils.assertNumber(value, key); payload.rate = Math.abs(value); payload.movemode = value > 0 ? 1 : 3; } await entity.command('lightingColorCtrl', 'moveColorTemp', payload, utils.getOptions(meta.mapped, entity)); // We cannot determine the color temperaturefrom the current state so we read it, because // - Color mode could have been switched (x/y or colortemp) if (value === 'stop' || value === 0) { const entityToRead = utils.getEntityOrFirstGroupMember(entity); if (entityToRead) { await utils.sleep(100); await entityToRead.read('lightingColorCtrl', ['colorTemperature', 'colorMode']); } } } else { // Deprecated const payload = { minimum: 153, maximum: 370, rate: 55 }; const stop = (val) => ['stop', 'release', '0'].some((el) => val.includes(el)); const up = (val) => ['1', 'up'].some((el) => val.includes(el)); const arr = [value.toString()]; const moverate = meta.message.hasOwnProperty('rate') ? Number(meta.message.rate) : 55; payload.rate = moverate; if (arr.filter(stop).length) { payload.movemode = 0; } else { payload.movemode = arr.filter(up).length ? 1 : 3; } await entity.command('lightingColorCtrl', 'moveColorTemp', payload, utils.getOptions(meta.mapped, entity)); } }, }, light_color_and_colortemp_via_color: { key: ['color', 'color_temp', 'color_temp_percent'], options: [exposes.options.color_sync(), exposes.options.transition()], convertSet: async (entity, key, value, meta) => { if (key == 'color') { return await converters1.light_color.convertSet(entity, key, value, meta); } else if (key == 'color_temp' || key == 'color_temp_percent') { utils.assertNumber(value); const xy = libColor.ColorXY.fromMireds(value); const payload = { transtime: utils.getTransition(entity, key, meta).time, colorx: utils.mapNumberRange(xy.x, 0, 1, 0, 65535), colory: utils.mapNumberRange(xy.y, 0, 1, 0, 65535), }; await entity.command('lightingColorCtrl', 'moveToColor', payload, utils.getOptions(meta.mapped, entity)); return { state: libColor.syncColorState({ 'color_mode': constants.colorModeLookup[2], 'color_temp': value }, meta.state, entity, meta.options), readAfterWriteTime: payload.transtime * 100, }; } }, convertGet: async (entity, key, meta) => { await entity.read('lightingColorCtrl', light.readColorAttributes(entity, meta)); }, }, light_hue_saturation_step: { key: ['hue_step', 'saturation_step'], options: [exposes.options.transition()], convertSet: async (entity, key, value, meta) => { value = Number(value); utils.assertNumber(value, key); const command = key === 'hue_step' ? 'stepHue' : 'stepSaturation'; const attribute = key === 'hue_step' ? 'currentHue' : 'currentSaturation'; const mode = value > 0 ? 1 : 3; const transition = utils.getTransition(entity, key, meta).time; const payload = { stepmode: mode, stepsize: Math.abs(value), transtime: transition }; await entity.command('lightingColorCtrl', command, payload, utils.getOptions(meta.mapp