UNPKG

zigbee-herdsman-converters

Version:

Collection of device converters to be used with zigbee-herdsman

857 lines 59.9 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; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const exposes = __importStar(require("../lib/exposes")); const fromZigbee_1 = __importDefault(require("../converters/fromZigbee")); const legacy = __importStar(require("../lib/legacy")); const toZigbee_1 = __importDefault(require("../converters/toZigbee")); const ota = __importStar(require("../lib/ota")); const utils = __importStar(require("../lib/utils")); const reporting = __importStar(require("../lib/reporting")); const constants = __importStar(require("../lib/constants")); const zigbee_herdsman_1 = require("zigbee-herdsman"); const ubisys_1 = require("../lib/ubisys"); const semver = __importStar(require("semver")); const logger_1 = require("../lib/logger"); const NS = 'zhc:ubisys'; const e = exposes.presets; const ea = exposes.access; const manufacturerOptions = { /* * Ubisys doesn't accept a manufacturerCode on some commands * This bug has been reported, but it has not been fixed: * https://github.com/Koenkk/zigbee-herdsman/issues/52 */ ubisys: { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.UBISYS_TECHNOLOGIES_GMBH }, // @ts-expect-error ubisysNull: { manufacturerCode: null }, }; const ubisysOnEventReadCurrentSummDelivered = async function (type, data, device) { if (data.type === 'attributeReport' && data.cluster === 'seMetering') { try { await data.endpoint.read('seMetering', ['currentSummDelivered']); } catch (error) { /* Do nothing*/ } } }; const ubisys = { fz: { dimmer_setup: { cluster: 'manuSpecificUbisysDimmerSetup', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { if (msg.data.hasOwnProperty('capabilities')) { const capabilities = msg.data.capabilities; const forwardPhaseControl = capabilities & 1; const reversePhaseControl = (capabilities & 2) >>> 1; const reactanceDiscriminator = (capabilities & 0x20) >>> 5; const configurableCurve = (capabilities & 0x40) >>> 6; const overloadDetection = (capabilities & 0x80) >>> 7; return { capabilities_forward_phase_control: forwardPhaseControl ? true : false, capabilities_reverse_phase_control: reversePhaseControl ? true : false, capabilities_reactance_discriminator: reactanceDiscriminator ? true : false, capabilities_configurable_curve: configurableCurve ? true : false, capabilities_overload_detection: overloadDetection ? true : false, }; } if (msg.data.hasOwnProperty('status')) { const status = msg.data.status; const forwardPhaseControl = status & 1; const reversePhaseControl = (status & 2) >>> 1; const overload = (status & 8) >>> 3; const capacitiveLoad = (status & 0x40) >>> 6; const inductiveLoad = (status & 0x80) >>> 7; return { status_forward_phase_control: forwardPhaseControl ? true : false, status_reverse_phase_control: reversePhaseControl ? true : false, status_overload: overload ? true : false, status_capacitive_load: capacitiveLoad ? true : false, status_inductive_load: inductiveLoad ? true : false, }; } if (msg.data.hasOwnProperty('mode')) { const mode = msg.data.mode; const phaseControl = mode & 3; const phaseControlValues = { 0: 'automatic', 1: 'forward', 2: 'reverse' }; return { mode_phase_control: utils.getFromLookup(phaseControl, phaseControlValues), }; } }, }, dimmer_setup_genLevelCtrl: { cluster: 'genLevelCtrl', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { if (msg.data.hasOwnProperty('ubisysMinimumOnLevel')) { return { minimum_on_level: msg.data.ubisysMinimumOnLevel }; } }, }, configure_device_setup: { cluster: 'manuSpecificUbisysDeviceSetup', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { const result = (meta.state.hasOwnProperty('configure_device_setup') ? meta.state.configure_device_setup : {}); if (msg.data['inputConfigurations'] != null) { result['input_configurations'] = msg.data['inputConfigurations']; } if (msg.data['inputActions'] != null) { result['input_actions'] = msg.data['inputActions'].map(function (el) { return Object.values(el); }); } return { configure_device_setup: result }; }, }, }, tz: { configure_j1: { key: ['configure_j1'], convertSet: async (entity, key, value, meta) => { const log = (message) => { logger_1.logger.warning(`ubisys: ${message}`, NS); }; const sleepSeconds = async (s) => { return new Promise((resolve) => setTimeout(resolve, s * 1000)); }; const waitUntilStopped = async () => { let operationalStatus = 0; do { await sleepSeconds(2); operationalStatus = (await entity.read('closuresWindowCovering', // @ts-expect-error ['operationalStatus'])).operationalStatus; } while (operationalStatus != 0); await sleepSeconds(2); }; const writeAttrFromJson = async (attr, jsonAttr, converterFunc, delaySecondsAfter) => { if (!jsonAttr) jsonAttr = attr; if (jsonAttr.startsWith('ubisys')) { jsonAttr = jsonAttr.substring(6, 1).toLowerCase + jsonAttr.substring(7); } if (value.hasOwnProperty(jsonAttr)) { let attrValue = value[jsonAttr]; if (converterFunc) { attrValue = converterFunc(attrValue); } const attributes = {}; attributes[attr] = attrValue; await entity.write('closuresWindowCovering', attributes, manufacturerOptions.ubisys); if (delaySecondsAfter) { await sleepSeconds(delaySecondsAfter); } } }; const stepsPerSecond = value.steps_per_second || 50; const hasCalibrate = value.hasOwnProperty('calibrate'); // cancel any running calibration // @ts-expect-error let mode = (await entity.read('closuresWindowCovering', ['windowCoveringMode'])).windowCoveringMode; const modeCalibrationBitMask = 0x02; if (mode & modeCalibrationBitMask) { await entity.write('closuresWindowCovering', { windowCoveringMode: mode & ~modeCalibrationBitMask }); await sleepSeconds(2); } // delay a bit if reconfiguring basic configuration attributes await writeAttrFromJson('windowCoveringType', undefined, undefined, 2); await writeAttrFromJson('configStatus', undefined, undefined, 2); // @ts-expect-error if (await writeAttrFromJson('windowCoveringMode', undefined, undefined, 2)) { mode = value['windowCoveringMode']; } if (hasCalibrate) { log('Cover calibration starting...'); // first of all, move to top position to not confuse calibration later log(' Moving cover to top position to get a good starting point...'); await entity.command('closuresWindowCovering', 'upOpen', {}); await waitUntilStopped(); log(' Settings some attributes...'); // reset attributes await entity.write('closuresWindowCovering', { installedOpenLimitLiftCm: 0, installedClosedLimitLiftCm: 240, installedOpenLimitTiltDdegree: 0, installedClosedLimitTiltDdegree: 900, ubisysLiftToTiltTransitionSteps: 0xffff, ubisysTotalSteps: 0xffff, ubisysLiftToTiltTransitionSteps2: 0xffff, ubisysTotalSteps2: 0xffff, }, manufacturerOptions.ubisys); // enable calibration mode await sleepSeconds(2); await entity.write('closuresWindowCovering', { windowCoveringMode: mode | modeCalibrationBitMask }); await sleepSeconds(2); // move down a bit and back up to detect upper limit log(' Moving cover down a bit...'); await entity.command('closuresWindowCovering', 'downClose', {}); await sleepSeconds(5); await entity.command('closuresWindowCovering', 'stop', {}); await sleepSeconds(2); log(' Moving up again to detect upper limit...'); await entity.command('closuresWindowCovering', 'upOpen', {}); await waitUntilStopped(); log(' Moving down to count steps from open to closed...'); await entity.command('closuresWindowCovering', 'downClose', {}); await waitUntilStopped(); log(' Moving up to count steps from closed to open...'); await entity.command('closuresWindowCovering', 'upOpen', {}); await waitUntilStopped(); } // now write any attribute values present in JSON await writeAttrFromJson('installedOpenLimitLiftCm'); await writeAttrFromJson('installedClosedLimitLiftCm'); await writeAttrFromJson('installedOpenLimitTiltDdegree'); await writeAttrFromJson('installedClosedLimitTiltDdegree'); await writeAttrFromJson('ubisysTurnaroundGuardTime'); await writeAttrFromJson('ubisysLiftToTiltTransitionSteps'); await writeAttrFromJson('ubisysTotalSteps'); await writeAttrFromJson('ubisysLiftToTiltTransitionSteps2'); await writeAttrFromJson('ubisysTotalSteps2'); await writeAttrFromJson('ubisysAdditionalSteps'); await writeAttrFromJson('ubisysInactivePowerThreshold'); await writeAttrFromJson('ubisysStartupSteps'); // some convenience functions to not have to calculate await writeAttrFromJson('ubisysTotalSteps', 'open_to_closed_s', (s) => s * stepsPerSecond); await writeAttrFromJson('ubisysTotalSteps2', 'closed_to_open_s', (s) => s * stepsPerSecond); await writeAttrFromJson('ubisysLiftToTiltTransitionSteps', 'lift_to_tilt_transition_ms', (s) => s * stepsPerSecond / 1000); await writeAttrFromJson('ubisysLiftToTiltTransitionSteps2', 'lift_to_tilt_transition_ms', (s) => s * stepsPerSecond / 1000); if (hasCalibrate) { log(' Finalizing calibration...'); // disable calibration mode again await sleepSeconds(2); await entity.write('closuresWindowCovering', { windowCoveringMode: mode & ~modeCalibrationBitMask }); await sleepSeconds(2); // re-read and dump all relevant attributes log(' Done - will now read back the results.'); ubisys.tz.configure_j1.convertGet(entity, key, meta); } }, convertGet: async (entity, key, meta) => { const log = (json) => { logger_1.logger.warning(`ubisys: Cover configuration read: ${JSON.stringify(json)}`, NS); }; log(await entity.read('closuresWindowCovering', [ 'windowCoveringType', 'physicalClosedLimitLiftCm', 'physicalClosedLimitTiltDdegree', 'installedOpenLimitLiftCm', 'installedClosedLimitLiftCm', 'installedOpenLimitTiltDdegree', 'installedClosedLimitTiltDdegree', ])); log(await entity.read('closuresWindowCovering', [ 'configStatus', 'windowCoveringMode', 'currentPositionLiftPercentage', 'currentPositionLiftCm', 'currentPositionTiltPercentage', 'currentPositionTiltDdegree', 'operationalStatus', ])); log(await entity.read('closuresWindowCovering', [ 'ubisysTurnaroundGuardTime', 'ubisysLiftToTiltTransitionSteps', 'ubisysTotalSteps', 'ubisysLiftToTiltTransitionSteps2', 'ubisysTotalSteps2', 'ubisysAdditionalSteps', 'ubisysInactivePowerThreshold', 'ubisysStartupSteps', ], manufacturerOptions.ubisys)); }, }, dimmer_setup: { key: ['capabilities_forward_phase_control', 'capabilities_reverse_phase_control', 'capabilities_reactance_discriminator', 'capabilities_configurable_curve', 'capabilities_overload_detection', 'status_forward_phase_control', 'status_reverse_phase_control', 'status_overload', 'status_capacitive_load', 'status_inductive_load', 'mode_phase_control'], convertSet: async (entity, key, value, meta) => { if (key === 'mode_phase_control') { utils.assertString(value, 'mode_phase_control'); const phaseControl = value.toLowerCase(); const phaseControlValues = { 'automatic': 0, 'forward': 1, 'reverse': 2 }; utils.validateValue(phaseControl, Object.keys(phaseControlValues)); await entity.write('manuSpecificUbisysDimmerSetup', { 'mode': utils.getFromLookup(phaseControl, phaseControlValues) }, manufacturerOptions.ubisysNull); } ubisys.tz.dimmer_setup.convertGet(entity, key, meta); }, convertGet: async (entity, key, meta) => { await entity.read('manuSpecificUbisysDimmerSetup', ['capabilities'], manufacturerOptions.ubisysNull); await entity.read('manuSpecificUbisysDimmerSetup', ['status'], manufacturerOptions.ubisysNull); await entity.read('manuSpecificUbisysDimmerSetup', ['mode'], manufacturerOptions.ubisysNull); }, }, dimmer_setup_genLevelCtrl: { key: ['minimum_on_level'], convertSet: async (entity, key, value, meta) => { if (key === 'minimum_on_level') { await entity.write('genLevelCtrl', { 'ubisysMinimumOnLevel': value }, manufacturerOptions.ubisys); } ubisys.tz.dimmer_setup_genLevelCtrl.convertGet(entity, key, meta); }, convertGet: async (entity, key, meta) => { await entity.read('genLevelCtrl', ['ubisysMinimumOnLevel'], manufacturerOptions.ubisys); }, }, configure_device_setup: { key: ['configure_device_setup'], convertSet: async (entity, key, value, meta) => { const devMgmtEp = meta.device.getEndpoint(232); const cluster = zigbee_herdsman_1.Zcl.Utils.getCluster('manuSpecificUbisysDeviceSetup', null, {}); const attributeInputConfigurations = cluster.getAttribute('inputConfigurations'); const attributeInputActions = cluster.getAttribute('inputActions'); // ubisys switched to writeStructure a while ago, change log only goes back to 1.9.x // and it happened before that but to be safe we will only use writeStrucutre on 1.9.0 and up let useWriteStruct = false; if (meta.device.softwareBuildID != undefined) { useWriteStruct = semver.gte(meta.device.softwareBuildID, '1.9.0', true); } if (useWriteStruct) { logger_1.logger.debug(`ubisys: using writeStructure for '${meta.options.friendly_name}'.`, NS); } if (value.hasOwnProperty('input_configurations')) { // example: [0, 0, 0, 0] if (useWriteStruct) { await devMgmtEp.writeStructured('manuSpecificUbisysDeviceSetup', [{ attrId: attributeInputConfigurations.ID, selector: {}, dataType: zigbee_herdsman_1.Zcl.DataType.array, elementData: { elementType: 'data8', elements: value.input_configurations, }, }], manufacturerOptions.ubisysNull); } else { await devMgmtEp.write('manuSpecificUbisysDeviceSetup', { [attributeInputConfigurations.name]: { elementType: 'data8', elements: value.input_configurations } }, manufacturerOptions.ubisysNull); } } if (value.hasOwnProperty('input_actions')) { // example (default for C4): [[0,13,1,6,0,2], [1,13,2,6,0,2], [2,13,3,6,0,2], [3,13,4,6,0,2]] if (useWriteStruct) { await devMgmtEp.writeStructured('manuSpecificUbisysDeviceSetup', [{ attrId: attributeInputActions.ID, selector: {}, dataType: zigbee_herdsman_1.Zcl.DataType.array, elementData: { elementType: 'octetStr', elements: value.input_actions, }, }], manufacturerOptions.ubisysNull); } else { await devMgmtEp.write('manuSpecificUbisysDeviceSetup', { [attributeInputActions.name]: { elementType: 'octetStr', elements: value.input_actions } }, manufacturerOptions.ubisysNull); } } if (value.hasOwnProperty('input_action_templates')) { const templateTypes = { // source: "ZigBee Device Physical Input Configurations Integrator’s Guide" // (can be obtained directly from ubisys upon request) 'toggle': { getInputActions: (input, endpoint) => [ [input, 0x0D, endpoint, 0x06, 0x00, 0x02], ], }, 'toggle_switch': { getInputActions: (input, endpoint) => [ [input, 0x0D, endpoint, 0x06, 0x00, 0x02], [input, 0x03, endpoint, 0x06, 0x00, 0x02], ], }, 'on_off_switch': { getInputActions: (input, endpoint) => [ [input, 0x0D, endpoint, 0x06, 0x00, 0x01], [input, 0x03, endpoint, 0x06, 0x00, 0x00], ], }, 'on': { getInputActions: (input, endpoint) => [ [input, 0x0D, endpoint, 0x06, 0x00, 0x01], ], }, 'off': { getInputActions: (input, endpoint) => [ [input, 0x0D, endpoint, 0x06, 0x00, 0x00], ], }, 'dimmer_single': { getInputActions: (input, endpoint, template) => { const moveUpCmd = template.no_onoff || template.no_onoff_up ? 0x01 : 0x05; const moveDownCmd = template.no_onoff || template.no_onoff_down ? 0x01 : 0x05; const moveRate = template.rate || 50; return [ [input, 0x07, endpoint, 0x06, 0x00, 0x02], [input, 0x86, endpoint, 0x08, 0x00, moveUpCmd, 0x00, moveRate], [input, 0xC6, endpoint, 0x08, 0x00, moveDownCmd, 0x01, moveRate], [input, 0x0B, endpoint, 0x08, 0x00, 0x03], ]; }, }, 'dimmer_double': { doubleInputs: true, getInputActions: (inputs, endpoint, template) => { const moveUpCmd = template.no_onoff || template.no_onoff_up ? 0x01 : 0x05; const moveDownCmd = template.no_onoff || template.no_onoff_down ? 0x01 : 0x05; const moveRate = template.rate || 50; return [ [inputs[0], 0x07, endpoint, 0x06, 0x00, 0x01], [inputs[0], 0x06, endpoint, 0x08, 0x00, moveUpCmd, 0x00, moveRate], [inputs[0], 0x0B, endpoint, 0x08, 0x00, 0x03], [inputs[1], 0x07, endpoint, 0x06, 0x00, 0x00], [inputs[1], 0x06, endpoint, 0x08, 0x00, moveDownCmd, 0x01, moveRate], [inputs[1], 0x0B, endpoint, 0x08, 0x00, 0x03], ]; }, }, 'cover': { cover: true, doubleInputs: true, getInputActions: (inputs, endpoint) => [ [inputs[0], 0x0D, endpoint, 0x02, 0x01, 0x00], [inputs[0], 0x07, endpoint, 0x02, 0x01, 0x02], [inputs[1], 0x0D, endpoint, 0x02, 0x01, 0x01], [inputs[1], 0x07, endpoint, 0x02, 0x01, 0x02], ], }, 'cover_switch': { cover: true, doubleInputs: true, getInputActions: (inputs, endpoint) => [ [inputs[0], 0x0D, endpoint, 0x02, 0x01, 0x00], [inputs[0], 0x03, endpoint, 0x02, 0x01, 0x02], [inputs[1], 0x0D, endpoint, 0x02, 0x01, 0x01], [inputs[1], 0x03, endpoint, 0x02, 0x01, 0x02], ], }, 'cover_up': { cover: true, getInputActions: (input, endpoint) => [ [input, 0x0D, endpoint, 0x02, 0x01, 0x00], ], }, 'cover_down': { cover: true, getInputActions: (input, endpoint) => [ [input, 0x0D, endpoint, 0x02, 0x01, 0x01], ], }, 'scene': { scene: true, getInputActions: (input, endpoint, groupId, sceneId) => [ [input, 0x07, endpoint, 0x05, 0x00, 0x05, groupId & 0xff, groupId >> 8, sceneId], ], getInputActions2: (input, endpoint, groupId, sceneId) => [ [input, 0x06, endpoint, 0x05, 0x00, 0x05, groupId & 0xff, groupId >> 8, sceneId], ], }, 'scene_switch': { scene: true, getInputActions: (input, endpoint, groupId, sceneId) => [ [input, 0x0D, endpoint, 0x05, 0x00, 0x05, groupId & 0xff, groupId >> 8, sceneId], ], getInputActions2: (input, endpoint, groupId, sceneId) => [ [input, 0x03, endpoint, 0x05, 0x00, 0x05, groupId & 0xff, groupId >> 8, sceneId], ], }, }; // first input let input = 0; // first client endpoint - depends on actual device if (Array.isArray(meta.mapped)) throw new Error(`Not supported for groups`); let endpoint = { 'S1': 2, 'S2': 3, 'D1': 2, 'J1': 2, 'C4': 1 }[meta.mapped.model]; // default group id let groupId = 0; const templates = Array.isArray(value.input_action_templates) ? value.input_action_templates : [value.input_action_templates]; let resultingInputActions = []; for (const template of templates) { // @ts-expect-error const templateType = templateTypes[template.type]; if (!templateType) { throw new Error(`input_action_templates: Template type '${template.type}' is not valid ` + `(valid types: ${Object.keys(templateTypes)})`); } if (template.hasOwnProperty('input')) { input = template.input; } if (template.hasOwnProperty('endpoint')) { endpoint = template.endpoint; } // C4 cover endpoints only start at 5 if (templateType.cover && meta.mapped.model === 'C4' && endpoint < 5) { endpoint += 4; } let inputActions; if (!templateType.doubleInputs) { if (!templateType.scene) { // single input, no scene(s) inputActions = templateType.getInputActions(input, endpoint, template); } else { // scene(s) (always single input) if (!template.hasOwnProperty('scene_id')) { throw new Error(`input_action_templates: Need an attribute 'scene_id' for '${template.type}'`); } if (template.hasOwnProperty('group_id')) { groupId = template.group_id; } inputActions = templateType.getInputActions(input, endpoint, groupId, template.scene_id); if (template.hasOwnProperty('scene_id_2')) { if (template.hasOwnProperty('group_id_2')) { groupId = template.group_id_2; } inputActions = inputActions.concat(templateType.getInputActions2(input, endpoint, groupId, template.scene_id_2)); } } } else { // double inputs input = template.hasOwnProperty('inputs') ? template.inputs : [input, input + 1]; inputActions = templateType.getInputActions(input, endpoint, template); } resultingInputActions = resultingInputActions.concat(inputActions); logger_1.logger.warning(`ubisys: Using input(s) ${input} and endpoint ${endpoint} for '${template.type}'.`, NS); // input might by now be an array (in case of double inputs) input = (Array.isArray(input) ? Math.max(...input) : input) + 1; endpoint += 1; } logger_1.logger.debug(`ubisys: input_actions to be sent to '${meta.options.friendly_name}': ` + JSON.stringify(resultingInputActions), NS); if (useWriteStruct) { await devMgmtEp.writeStructured('manuSpecificUbisysDeviceSetup', [{ attrId: attributeInputActions.ID, selector: {}, dataType: zigbee_herdsman_1.Zcl.DataType.array, elementData: { elementType: 'octetStr', elements: resultingInputActions, }, }], manufacturerOptions.ubisysNull); } else { await devMgmtEp.write('manuSpecificUbisysDeviceSetup', { [attributeInputActions.name]: { elementType: 'octetStr', elements: resultingInputActions } }, manufacturerOptions.ubisysNull); } } // re-read effective settings and dump them to the log await ubisys.tz.configure_device_setup.convertGet(entity, key, meta); }, convertGet: async (entity, key, meta) => { const devMgmtEp = meta.device.getEndpoint(232); await devMgmtEp.read('manuSpecificUbisysDeviceSetup', ['inputConfigurations'], manufacturerOptions.ubisysNull); await devMgmtEp.read('manuSpecificUbisysDeviceSetup', ['inputActions'], manufacturerOptions.ubisysNull); }, }, }, }; const definitions = [ { zigbeeModel: ['S1 (5501)'], model: 'S1', vendor: 'Ubisys', description: 'Power switch S1', exposes: [ e.switch(), e.action([ 'toggle', 'on', 'off', 'recall_*', 'brightness_move_up', 'brightness_move_down', 'brightness_stop', ]), e.power_on_behavior(), e.power().withAccess(ea.STATE_GET), e.energy().withAccess(ea.STATE_GET), ], fromZigbee: [fromZigbee_1.default.on_off, fromZigbee_1.default.metering, fromZigbee_1.default.command_toggle, fromZigbee_1.default.command_on, fromZigbee_1.default.command_off, fromZigbee_1.default.command_recall, fromZigbee_1.default.command_move, fromZigbee_1.default.command_stop, fromZigbee_1.default.power_on_behavior, ubisys.fz.configure_device_setup], toZigbee: [toZigbee_1.default.on_off, toZigbee_1.default.metering_power, toZigbee_1.default.currentsummdelivered, ubisys.tz.configure_device_setup, toZigbee_1.default.power_on_behavior], endpoint: (device) => { return { 'l1': 1, 's1': 2, 'meter': 3 }; }, meta: { multiEndpointEnforce: { 'power': 3, 'energy': 3 } }, configure: async (device, coordinatorEndpoint) => { const endpoint = device.getEndpoint(3); await reporting.bind(endpoint, coordinatorEndpoint, ['seMetering']); await reporting.readMeteringMultiplierDivisor(endpoint); await reporting.instantaneousDemand(endpoint); }, onEvent: async (type, data, device) => { /* * As per technical doc page 18 section 7.3.4 * https://www.ubisys.de/wp-content/uploads/ubisys-s1-technical-reference.pdf * * This cluster uses the binding table for managing command targets. * When factory fresh, this cluster is bound to endpoint #1 to * enable local control. * * We use addBinding to 'record' this default binding. */ if (type === 'deviceInterview') { const ep1 = device.getEndpoint(1); const ep2 = device.getEndpoint(2); ep2.addBinding('genOnOff', ep1); } else { await ubisysOnEventReadCurrentSummDelivered(type, data, device); } }, ota: ota.ubisys, }, { zigbeeModel: ['S1-R (5601)'], model: 'S1-R', vendor: 'Ubisys', description: 'Power switch S1-R', exposes: [ e.switch(), e.action([ 'toggle', 'on', 'off', 'recall_*', 'brightness_move_up', 'brightness_move_down', 'brightness_stop', ]), e.power_on_behavior(), e.power().withAccess(ea.STATE_GET), e.energy().withAccess(ea.STATE_GET), ], fromZigbee: [fromZigbee_1.default.on_off, fromZigbee_1.default.metering, fromZigbee_1.default.command_toggle, fromZigbee_1.default.command_on, fromZigbee_1.default.command_off, fromZigbee_1.default.command_recall, fromZigbee_1.default.command_move, fromZigbee_1.default.command_stop, fromZigbee_1.default.power_on_behavior, ubisys.fz.configure_device_setup], toZigbee: [toZigbee_1.default.on_off, toZigbee_1.default.metering_power, toZigbee_1.default.currentsummdelivered, ubisys.tz.configure_device_setup, toZigbee_1.default.power_on_behavior], meta: { multiEndpointEnforce: { 'power': 4, 'energy': 4 } }, endpoint: (device) => { return { 'l1': 1, 's1': 2, 'meter': 4 }; }, configure: async (device, coordinatorEndpoint) => { const endpoint = device.getEndpoint(4); await reporting.bind(endpoint, coordinatorEndpoint, ['seMetering']); await reporting.readMeteringMultiplierDivisor(endpoint); await reporting.instantaneousDemand(endpoint); }, onEvent: async (type, data, device) => { /* * As per technical doc page 18 section 7.3.4 * https://www.ubisys.de/wp-content/uploads/ubisys-s1-technical-reference.pdf * * This cluster uses the binding table for managing command targets. * When factory fresh, this cluster is bound to endpoint #1 to * enable local control. * * We use addBinding to 'record' this default binding. */ if (type === 'deviceInterview') { const ep1 = device.getEndpoint(1); const ep2 = device.getEndpoint(2); ep2.addBinding('genOnOff', ep1); } else { await ubisysOnEventReadCurrentSummDelivered(type, data, device); } }, ota: ota.ubisys, }, { zigbeeModel: ['S2 (5502)', 'S2-R (5602)'], model: 'S2', vendor: 'Ubisys', description: 'Power switch S2', exposes: [ e.switch().withEndpoint('l1'), e.switch().withEndpoint('l2'), e.action(['toggle_s1', 'toggle_s2', 'on_s1', 'on_s2', 'off_s1', 'off_s2', 'recall_*_s1', 'recal_*_s2', 'brightness_move_up_s1', 'brightness_move_up_s2', 'brightness_move_down_s1', 'brightness_move_down_s2', 'brightness_stop_s1', 'brightness_stop_s2']), e.power_on_behavior().withEndpoint('l1'), e.power_on_behavior().withEndpoint('l2'), e.power().withAccess(ea.STATE_GET), e.energy().withAccess(ea.STATE_GET), ], fromZigbee: [fromZigbee_1.default.on_off, fromZigbee_1.default.metering, fromZigbee_1.default.command_toggle, fromZigbee_1.default.command_on, fromZigbee_1.default.command_off, fromZigbee_1.default.command_recall, fromZigbee_1.default.command_move, fromZigbee_1.default.command_stop, fromZigbee_1.default.power_on_behavior, ubisys.fz.configure_device_setup], toZigbee: [toZigbee_1.default.on_off, toZigbee_1.default.metering_power, ubisys.tz.configure_device_setup, toZigbee_1.default.power_on_behavior, toZigbee_1.default.currentsummdelivered], endpoint: (device) => { return { 'l1': 1, 'l2': 2, 's1': 3, 's2': 4, 'meter': 5 }; }, meta: { multiEndpoint: true, multiEndpointSkip: ['power', 'energy'], multiEndpointEnforce: { 'power': 5, 'energy': 5 } }, configure: async (device, coordinatorEndpoint) => { const endpoint = device.getEndpoint(5); await reporting.bind(endpoint, coordinatorEndpoint, ['seMetering']); await reporting.readMeteringMultiplierDivisor(endpoint); await reporting.instantaneousDemand(endpoint); }, onEvent: async (type, data, device) => { /* * As per technical doc page 20 section 7.4.4 and * page 22 section 7.5.4 * https://www.ubisys.de/wp-content/uploads/ubisys-s2-technical-reference.pdf * * This cluster uses the binding table for managing command targets. * When factory fresh, this cluster is bound to endpoint #1 to * enable local control. * * This cluster uses the binding table for managing command targets. * When factory fresh, this cluster is bound to endpoint #2 to * enable local control * * We use addBinding to 'record' this default binding. */ if (type === 'deviceInterview') { const ep1 = device.getEndpoint(1); const ep2 = device.getEndpoint(2); const ep3 = device.getEndpoint(3); const ep4 = device.getEndpoint(4); ep3.addBinding('genOnOff', ep1); ep4.addBinding('genOnOff', ep2); } else { await ubisysOnEventReadCurrentSummDelivered(type, data, device); } }, ota: ota.ubisys, }, { zigbeeModel: ['D1 (5503)', 'D1-R (5603)'], model: 'D1', vendor: 'Ubisys', description: 'Universal dimmer D1', fromZigbee: [fromZigbee_1.default.on_off, fromZigbee_1.default.brightness, fromZigbee_1.default.metering, fromZigbee_1.default.command_toggle, fromZigbee_1.default.command_on, fromZigbee_1.default.command_off, fromZigbee_1.default.command_recall, fromZigbee_1.default.command_move, fromZigbee_1.default.command_stop, fromZigbee_1.default.lighting_ballast_configuration, fromZigbee_1.default.level_config, ubisys.fz.dimmer_setup, ubisys.fz.dimmer_setup_genLevelCtrl, ubisys.fz.configure_device_setup], toZigbee: [toZigbee_1.default.light_onoff_brightness, toZigbee_1.default.ballast_config, toZigbee_1.default.level_config, ubisys.tz.dimmer_setup, ubisys.tz.dimmer_setup_genLevelCtrl, ubisys.tz.configure_device_setup, toZigbee_1.default.ignore_transition, toZigbee_1.default.light_brightness_move, toZigbee_1.default.light_brightness_step, toZigbee_1.default.metering_power, toZigbee_1.default.currentsummdelivered], exposes: [ e.action(['toggle_s1', 'toggle_s2', 'on_s1', 'on_s2', 'off_s1', 'off_s2', 'recall_*_s1', 'recal_*_s2', 'brightness_move_up_s1', 'brightness_move_up_s2', 'brightness_move_down_s1', 'brightness_move_down_s2', 'brightness_stop_s1', 'brightness_stop_s2']), e.light_brightness(), e.composite('level_config', 'level_config', ea.ALL) .withFeature(e.numeric('on_off_transition_time', ea.ALL) .withDescription('Specifies the amount of time, in units of 0.1 seconds, which will be used during a transition to ' + 'either the on or off state, when an on/off/toggle command of the on/off cluster is used to turn the light on or off')) .withFeature(e.numeric('on_level', ea.ALL) .withValueMin(1).withValueMax(254) .withPreset('previous', 255, 'Use previous value') .withDescription('Specifies the level that shall be applied, when an on/toggle command causes the light to turn on.')) .withFeature(e.binary('execute_if_off', ea.ALL, true, false) .withDescription('Defines if you can send a brightness change without to turn on the light')) .withFeature(e.numeric('current_level_startup', ea.ALL) .withValueMin(1).withValueMax(254) .withPreset('previous', 255, 'Use previous value') .withDescription('Specifies the initial level to be applied after the device is supplied with power')), e.power().withAccess(ea.STATE_GET), e.energy().withAccess(ea.STATE_GET), e.numeric('ballast_minimum_level', ea.ALL).withValueMin(1).withValueMax(254) .withDescription('Specifies the minimum light output of the ballast'), e.numeric('ballast_maximum_level', ea.ALL).withValueMin(1).withValueMax(254) .withDescription('Specifies the maximum light output of the ballast'), e.numeric('minimum_on_level', ea.ALL).withValueMin(0).withValueMax(255) .withDescription('Specifies the minimum level that shall be applied, when an on/toggle command causes the ' + 'light to turn on. When this attribute is set to the invalid value (255) this feature is disabled ' + 'and standard rules apply: The light will either return to the previously active level (before it ' + 'was turned off) if the OnLevel attribute is set to the invalid value (255/previous); or to the specified ' + 'value of the OnLevel attribute if this value is in the range 0…254. Otherwise, if the ' + 'MinimumOnLevel is in the range 0…254, the light will be set to the the previously ' + 'active level (before it was turned off), or the value specified here, whichever is the larger ' + 'value. For example, if the previous level was 30 and the MinimumOnLevel was 40 then ' + 'the light would turn on and move to level 40. Conversely, if the previous level was 50, ' + 'and the MinimumOnLevel was 40, then the light would turn on and move to level 50.'), e.binary('capabilities_forward_phase_control', ea.ALL, true, false) .withDescription('The dimmer supports AC forward phase control.'), e.binary('capabilities_reverse_phase_control', ea.ALL, true, false) .withDescription('The dimmer supports AC reverse phase control.'), e.binary('capabilities_reactance_discriminator', ea.ALL, true, false) .withDescription('The dimmer is capable of measuring the reactanceto distinguish inductive and capacitive loads.'), e.binary('capabilities_configurable_curve', ea.ALL, true, false) .withDescription('The dimmer is capable of replacing the built-in, default dimming curve.'), e.binary('capabilities_overload_detection', ea.ALL, true, false) .withDescription('The dimmer is capable of detecting an output overload and shutting the output off.'), e.binary('status_forward_phase_control', ea.ALL, true, false) .withDescription('The dimmer is currently operating in AC forward phase control mode.'), e.binary('status_reverse_phase_control', ea.ALL, true, false) .withDescription('The dimmer is currently operating in AC reverse phase control mode.'), e.binary('status_overload', ea.ALL, true, false) .withDescription('The output is currently turned off, because the dimmer has detected an overload.'), e.binary('status_capacitive_load', ea.ALL, true, false) .withDescription('The dimmer\'s reactance discriminator had detected a capacitive load.'), e.binary('status_inductive_load', ea.ALL, true, false) .withDescription('The dimmer\'s reactance discriminator had detected an inductive load.'), e.enum('mode_phase_control', ea.ALL, ['automatic', 'forward', 'reverse']) .withDescription('Configures the dimming technique.') ], configure: async (device, coordinatorEndpoint) => { const endpoint = device.getEndpoint(4); await reporting.bind(endpoint, coordinatorEndpoint, ['seMetering']); await reporting.readMeteringMultiplierDivisor(endpoint); await reporting.instantaneousDemand(endpoint); }, meta: { multiEndpoint: true, multiEndpointSkip: ['state', 'brightness', 'power', 'energy'], multiEndpointEnforce: { 'power': 4, 'energy': 4 } }, endpoint: (device) => { return { 'default': 1, 's1': 2, 's2': 3, 'meter': 4 }; }, onEvent: async (type, data, device) => { /* * As per technical doc page 23 section 7.3.4, 7.3.5 * https://www.ubisys.de/wp-content/uploads/ubisys-d1-technical-reference.pdf * * We use addBinding to 'record' this default binding. */ if (type === 'deviceInterview') { const ep1 = device.getEndpoint(1); const ep2 = device.getEndpoint(2); ep2.addBinding('genOnOff', ep1); ep2.addBinding('genLevelCtrl', ep1); } else { await ubisysOnEventReadCurrentSummDelivered(type, data, device); } }, ota: ota.ubisys, }, { zigbeeModel: ['J1 (5502)', 'J1-R (5602)'], model: 'J1', vendor: 'Ubisys', description: 'Shutter control J1', fromZigbee: [fromZigbee_1.default.cover_position_tilt, fromZigbee_1.default.metering, ubisys.fz.configure_device_setup], toZigbee: [toZigbee_1.default.cover_state, toZigbee_1.default.cover_position_tilt, toZigbee_1.default.metering_power, ubisys.tz.configure_j1, ubisys.tz.configure_device_setup, toZigbee_1.default.currentsummdelivered], exposes: (device, options) => { const coverExpose = e.cover(); const coverType = (device?.getEndpoint(1).getClusterAttributeValue('closuresWindowCovering', 'windowCoveringType') ?? undefined); switch (coverType) { // cf. Ubisys J1 Technical Reference Manual, chapter 7.2.5.1 Calibration case 0: // Roller Shade, Lift only case 1: // Roller Shade two motors, Lift only case 2: // Roller Shade exterior, Lift only case 3: // Roller Shade two motor