UNPKG

zigbee-herdsman-converters

Version:

Collection of device converters to be used with zigbee-herdsman

945 lines 65.4 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 () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __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 }); exports.definitions = void 0; const node_assert_1 = __importDefault(require("node:assert")); const semver_1 = require("semver"); const zigbee_herdsman_1 = require("zigbee-herdsman"); const fz = __importStar(require("../converters/fromZigbee")); const tz = __importStar(require("../converters/toZigbee")); const constants = __importStar(require("../lib/constants")); const exposes = __importStar(require("../lib/exposes")); const logger_1 = require("../lib/logger"); const m = __importStar(require("../lib/modernExtend")); const reporting = __importStar(require("../lib/reporting")); const ubisys_1 = require("../lib/ubisys"); const utils = __importStar(require("../lib/utils")); 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 ignore ubisysNull: { manufacturerCode: null }, }; const ubisys = { fz: { dimmer_setup: { cluster: "manuSpecificUbisysDimmerSetup", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { if (msg.data.capabilities !== undefined) { 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, capabilities_reverse_phase_control: !!reversePhaseControl, capabilities_reactance_discriminator: !!reactanceDiscriminator, capabilities_configurable_curve: !!configurableCurve, capabilities_overload_detection: !!overloadDetection, }; } if (msg.data.status !== undefined) { 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, status_reverse_phase_control: !!reversePhaseControl, status_overload: !!overload, status_capacitive_load: !!capacitiveLoad, status_inductive_load: !!inductiveLoad, }; } if (msg.data.mode !== undefined) { 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), }; } }, }, // biome-ignore lint/style/useNamingConvention: ignored using `--suppress` dimmer_setup_genLevelCtrl: { cluster: "genLevelCtrl", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { if (msg.data.ubisysMinimumOnLevel !== undefined) { 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.configure_device_setup !== undefined ? 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((el) => 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 await new Promise((resolve) => setTimeout(resolve, s * 1000)); }; const waitUntilStopped = async () => { let operationalStatus = 0; do { await sleepSeconds(2); const response = await entity.read("closuresWindowCovering", [ "operationalStatus", ]); operationalStatus = response.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, 7).toLowerCase() + jsonAttr.substring(7); } if (value[jsonAttr] !== undefined) { let attrValue = value[jsonAttr]; if (converterFunc) { attrValue = converterFunc(attrValue); } const 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.calibrate != null; // cancel any running calibration 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("ubisysWindowCoveringType", undefined, undefined, 2); await writeAttrFromJson("ubisysConfigStatus", undefined, undefined, 2); // @ts-expect-error ignore 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", { ubisysInstalledOpenLimitLiftCm: 0, ubisysInstalledClosedLimitLiftCm: 240, ubisysInstalledOpenLimitTiltDdegree: 0, ubisysInstalledClosedLimitTiltDdegree: 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("ubisysInstalledOpenLimitLiftCm"); await writeAttrFromJson("ubisysInstalledClosedLimitLiftCm"); await writeAttrFromJson("ubisysInstalledOpenLimitTiltDdegree"); await writeAttrFromJson("ubisysInstalledClosedLimitTiltDdegree"); 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."); await 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); } await 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); }, }, // biome-ignore lint/style/useNamingConvention: ignored using `--suppress` 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); } await 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 customCluster = meta.device.customClusters["manuSpecificUbisysDeviceSetup"]; (0, node_assert_1.default)(customCluster); const attributeInputConfigurations = customCluster.attributes.inputConfigurations; const attributeInputActions = customCluster.attributes.inputActions; (0, node_assert_1.default)(attributeInputConfigurations && attributeInputActions); // 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 if (!meta.device.softwareBuildID || !(0, semver_1.valid)(meta.device.softwareBuildID, true) || !(0, semver_1.gte)(meta.device.softwareBuildID, "1.9.0", true)) { logger_1.logger.warning(`ubisys: update firmware of '${meta.options.friendly_name}' before writing configure_device_setup!`, NS); return; } if (value.input_configurations != null) { // example: [0, 0, 0, 0] await devMgmtEp.writeStructured("manuSpecificUbisysDeviceSetup", [ { attrId: attributeInputConfigurations.ID, selector: {}, dataType: zigbee_herdsman_1.Zcl.DataType.ARRAY, elementData: { elementType: zigbee_herdsman_1.Zcl.DataType.DATA8, elements: value.input_configurations, }, }, ], manufacturerOptions.ubisysNull); } if (value.input_actions != null) { // 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]] await devMgmtEp.writeStructured("manuSpecificUbisysDeviceSetup", [ { attrId: attributeInputActions.ID, selector: {}, dataType: zigbee_herdsman_1.Zcl.DataType.ARRAY, elementData: { elementType: zigbee_herdsman_1.Zcl.DataType.OCTET_STR, elements: value.input_actions, }, }, ], manufacturerOptions.ubisysNull); } if (value.input_action_templates != null) { 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; if (Array.isArray(meta.mapped)) { // first client endpoint - depends on actual device 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]; const resultingInputActions = []; for (const template of templates) { 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.input !== undefined) { input = template.input; } if (template.endpoint !== undefined) { endpoint = template.endpoint; } // C4 cover endpoints only start at 5 if ("cover" in templateType && meta.mapped.model === "C4" && endpoint < 5) { endpoint += 4; } let inputActions = []; if ("doubleInputs" in templateType) { // double inputs const inputArr = template.inputs !== undefined ? template.inputs : [input, input + 1]; inputActions = templateType.getInputActions(inputArr, endpoint, template); if (Array.isArray(inputArr)) { input = Math.max(...inputArr); } } else { if ("scene" in templateType) { // scene(s) (always single input) if (template.scene_id === undefined) { throw new Error(`input_action_templates: Need an attribute 'scene_id' for '${template.type}'`); } if (template.group_id !== undefined) { groupId = template.group_id; } inputActions = templateType.getInputActions(input, endpoint, groupId, template.scene_id); if (template.scene_id_2 !== undefined) { if (template.group_id_2 !== undefined) { groupId = template.group_id_2; } inputActions = inputActions.concat(templateType.getInputActions2(input, endpoint, groupId, template.scene_id_2)); } } else { // single input, no scene(s) inputActions = templateType.getInputActions(input, endpoint, template); } } resultingInputActions.push(inputActions); logger_1.logger.warning(`ubisys: Using input ${input} and endpoint ${endpoint} for '${template.type}'.`, NS); input += 1; endpoint += 1; } // XXX: input_action_templates is not validated // it could potentially write values that don't fit OCTET_STR (e.g. value at x index is > 0xff) // write length of octet str array at index 0 await devMgmtEp.writeStructured("manuSpecificUbisysDeviceSetup", [ { attrId: attributeInputActions.ID, selector: { indicatorType: zigbee_herdsman_1.Zcl.StructuredIndicatorType.Whole, indexes: [0] }, dataType: zigbee_herdsman_1.Zcl.DataType.UINT16, elementData: resultingInputActions.flat().length, }, ], manufacturerOptions.ubisysNull); let index = 1; // write in chunks to prevent frame overflow // XXX: depends entirely on the values inside the nested array as far as "not overflowing" // it also is not optimized for "minimum writes", since count is based on nesting for (const inputAction of resultingInputActions) { await devMgmtEp.writeStructured("manuSpecificUbisysDeviceSetup", inputAction.map((a) => ({ attrId: attributeInputActions.ID, selector: { indicatorType: zigbee_herdsman_1.Zcl.StructuredIndicatorType.Whole, indexes: [index++] }, dataType: zigbee_herdsman_1.Zcl.DataType.OCTET_STR, elementData: Buffer.from(a), })), manufacturerOptions.ubisysNull); } } }, }, }, }; exports.definitions = [ { zigbeeModel: ["S1 (5501)"], model: "S1", vendor: "Ubisys", description: "Power switch S1", fromZigbee: [ubisys.fz.configure_device_setup], toZigbee: [ubisys.tz.configure_device_setup], endpoint: (device) => { return { l1: 1, s1: 2 }; }, extend: [ // NOTE: identify is supported but no visual indicator so omitted here m.onOff({ powerOnBehavior: true }), m.electricityMeter({ cluster: "metering", configureReporting: false }), m.commandsOnOff({ endpointNames: ["2"] }), m.commandsLevelCtrl({ endpointNames: ["2"] }), m.commandsColorCtrl({ endpointNames: ["2"] }), ubisys_1.ubisysModernExtend.pollCurrentSummDelivered(3), ubisys_1.ubisysModernExtend.addCustomClusterManuSpecificUbisysDeviceSetup(), ], configure: async (device, coordinatorEndpoint) => { const endpoint = device.getEndpoint(3); await reporting.bind(endpoint, coordinatorEndpoint, ["seMetering"]); await reporting.readMeteringMultiplierDivisor(endpoint); await reporting.instantaneousDemand(endpoint); }, onEvent: (event) => { /* * 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 (event.type === "deviceInterview") { const ep1 = event.data.device.getEndpoint(1); const ep2 = event.data.device.getEndpoint(2); ep2.addBinding("genOnOff", ep1); } }, ota: true, }, { zigbeeModel: ["S1-R (5601)"], model: "S1-R", vendor: "Ubisys", description: "Power switch S1-R", fromZigbee: [ubisys.fz.configure_device_setup], toZigbee: [ubisys.tz.configure_device_setup], endpoint: (device) => { return { l1: 1, s1: 2, s2: 3 }; }, extend: [ m.identify(), m.onOff({ powerOnBehavior: true }), m.electricityMeter({ cluster: "metering", configureReporting: false }), m.commandsOnOff({ endpointNames: ["2", "3"] }), m.commandsLevelCtrl({ endpointNames: ["2", "3"] }), m.commandsColorCtrl({ endpointNames: ["2", "3"] }), ubisys_1.ubisysModernExtend.pollCurrentSummDelivered((device) => (device.hardwareVersion < 16 ? 4 : 1)), ubisys_1.ubisysModernExtend.addCustomClusterManuSpecificUbisysDeviceSetup(), ], configure: async (device, coordinatorEndpoint) => { // Series 2 has metering on endpoint 1, older devices on endpoint 4 // hardwareVersion is 16 const endpoint = device.getEndpoint(device.hardwareVersion < 16 ? 4 : 1); await reporting.bind(endpoint, coordinatorEndpoint, ["seMetering"]); await reporting.readMeteringMultiplierDivisor(endpoint); await reporting.instantaneousDemand(endpoint); }, onEvent: (event) => { /* * 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 (event.type === "deviceInterview") { const ep1 = event.data.device.getEndpoint(1); const ep2 = event.data.device.getEndpoint(2); ep2.addBinding("genOnOff", ep1); } }, ota: true, }, { 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: [ fz.on_off, fz.metering, fz.command_toggle, fz.command_on, fz.command_off, fz.command_recall, fz.command_move, fz.command_stop, fz.power_on_behavior, ubisys.fz.configure_device_setup, ], toZigbee: [tz.on_off, tz.metering_power, ubisys.tz.configure_device_setup, tz.power_on_behavior, tz.currentsummdelivered], endpoint: (device) => { return { l1: 1, l2: 2, s1: 3, s2: 4 }; }, meta: { multiEndpoint: true, multiEndpointSkip: ["power", "energy"] }, extend: [ubisys_1.ubisysModernExtend.addCustomClusterManuSpecificUbisysDeviceSetup(), ubisys_1.ubisysModernExtend.pollCurrentSummDelivered(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: (event) => { /* * 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 (event.type === "deviceInterview") { const ep1 = event.data.device.getEndpoint(1); const ep2 = event.data.device.getEndpoint(2); const ep3 = event.data.device.getEndpoint(3); const ep4 = event.data.device.getEndpoint(4); ep3.addBinding("genOnOff", ep1); ep4.addBinding("genOnOff", ep2); } }, ota: true, }, { zigbeeModel: ["D1 (5503)", "D1-R (5603)"], model: "D1", vendor: "Ubisys", description: "Universal dimmer D1", fromZigbee: [ fz.on_off, fz.brightness, fz.metering, fz.command_toggle, fz.command_on, fz.command_off, fz.command_recall, fz.command_move, fz.command_stop, fz.lighting_ballast_configuration, fz.level_config, ubisys.fz.dimmer_setup, ubisys.fz.dimmer_setup_genLevelCtrl, ubisys.fz.configure_device_setup, ], toZigbee: [ tz.light_onoff_brightness, tz.ballast_config, tz.level_config, ubisys.tz.dimmer_setup, ubisys.tz.dimmer_setup_genLevelCtrl, ubisys.tz.configure_device_setup, tz.ignore_transition, tz.light_brightness_move, tz.light_brightness_step, tz.metering_power, tz.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."), ], extend: [ ubisys_1.ubisysModernExtend.addCustomClusterManuSpecificUbisysDeviceSetup(), ubisys_1.ubisysModernExtend.addCustomClusterManuSpecificUbisysDimmerSetup(), ubisys_1.ubisysModernExtend.addCustomClusterGenLevelCtrl(), ubisys_1.ubisysModernExtend.pollCurrentSummDelivered(4), ], 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"] }, endpoint: (device) => { return { default: 1, s1: 2, s2: 3 }; }, onEvent: (event) => { /* * 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 (event.type === "deviceInterview") { const ep1 = event.data.device.getEndpoint(1); const ep2 = event.data.device.getEndpoint(2); ep2.addBinding("genOnOff", ep1); ep2.addBinding("genLevelCtrl", ep1); } }, ota: true, }, { zigbeeModel: ["J1 (5502)", "J1-R (5602)"], model: "J1", vendor: "Ubisys", description: "Shutter control J1", fromZigbee: [fz.cover_position_tilt, fz.metering, ubisys.fz.configure_device_setup], toZigbee: [ tz.cover_state, tz.cover_position_tilt, tz.metering_power, ubisys.tz.configure_j1, ubisys.tz.configure_device_setup, tz.currentsummdelivered, ], exposes: (device, options) => { const coverExpose = e.cover(); const coverType = !utils.isDummyDevice(device) ? 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 motors exterior, Lift only case 4: // Drapery, Lift only case 5: // Awning, Lift only case 9: // Projector Screen, Lift only coverExpose.withPosition(); break; case 6: // Shutter, Tilt only case 7: // Tilt Blind, Tilt only coverExpose.withTilt(); break; default: coverExpose.withPos