UNPKG

zigbee-herdsman-converters

Version:

Collection of device converters to be used with zigbee-herdsman

1,076 lines • 150 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; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.boschThermostatExtend = exports.boschSmartPlugExtend = exports.boschSmokeAlarmExtend = exports.boschWaterAlarmExtend = exports.boschBsenExtend = exports.boschDoorWindowContactExtend = exports.boschBsirExtend = exports.boschBmctExtend = exports.boschGeneralSensorDeviceExtend = exports.boschGeneralEnergyDeviceExtend = exports.boschGeneralExtend = exports.manufacturerOptions = void 0; const zigbee_herdsman_1 = require("zigbee-herdsman"); const fz = __importStar(require("../converters/fromZigbee")); const tz = __importStar(require("../converters/toZigbee")); const exposes = __importStar(require("../lib/exposes")); const m = __importStar(require("../lib/modernExtend")); const constants_1 = require("./constants"); const logger_1 = require("./logger"); const reporting_1 = require("./reporting"); const globalStore = __importStar(require("./store")); const utils = __importStar(require("./utils")); const utils_1 = require("./utils"); const e = exposes.presets; const ea = exposes.access; const NS = "zhc:bosch"; exports.manufacturerOptions = { manufacturerCode: zigbee_herdsman_1.Zcl.ManufacturerCode.ROBERT_BOSCH_GMBH, sendPolicy: "immediate", }; //region Generally used Bosch functionality exports.boschGeneralExtend = { /** Some devices now use a different name for some custom clusters than * originally used. This can lead to issues like those described in * https://github.com/Koenkk/zigbee2mqtt/issues/28806. To prevent that * we have to make sure that all attributes of the renamed cluster are * available when using "getEndpoint().getClusterAttributeValue()". */ handleRenamedCustomCluster: (oldClusterName, newClusterName) => { const onEvent = [ async (event) => { if (event.type !== "start") { return; } const device = event.data.device; const renameAlreadyApplied = device.meta.renamedClusters?.includes(oldClusterName); if (!renameAlreadyApplied) { logger_1.logger.debug(`Try to apply cluster rename from ${oldClusterName} to ${newClusterName} for device ${device.ieeeAddr}. Current meta state: ${JSON.stringify(device.meta)}`, NS); const newClusterDefinition = device.customClusters[newClusterName]; const endpointsWithNewCluster = device.endpoints.filter((endpoint) => endpoint.clusters[newClusterName] !== undefined); for (const endpointToRead in endpointsWithNewCluster) { const endpoint = endpointsWithNewCluster[endpointToRead]; logger_1.logger.debug(`Attempt to read all attributes for cluster ${newClusterName} from endpoint ${endpoint.ID}`, NS); for (const attributeToRead in newClusterDefinition.attributes) { const attributeValueExistsInDatabase = endpoint.getClusterAttributeValue(newClusterName, newClusterDefinition.attributes[attributeToRead].ID) !== undefined; if (!attributeValueExistsInDatabase) { logger_1.logger.debug(`Attempt to read undefined attribute ${attributeToRead} in cluster ${newClusterName} from endpoint ${endpoint.ID}`, NS); try { await endpoint.read(newClusterDefinition.ID, [newClusterDefinition.attributes[attributeToRead].ID]); } catch (exception) { logger_1.logger.debug(`Error during read attempt for attribute ${attributeToRead}. Probably unsupported attribute on this device or endpoint. Skipping... ${exception}`, NS); } } else { logger_1.logger.debug(`Value for attribute ${attributeToRead} in cluster ${newClusterName} from endpoint ${endpoint.ID} already in database. Skipping...`, NS); } } } logger_1.logger.debug(`All attributes are read, now calling deviceExposeChanged() for device ${device.ieeeAddr}`, NS); event.data.deviceExposesChanged(); device.meta.renamedClusters = device.meta.renamedClusters === undefined ? [oldClusterName] : [...device.meta.renamedClusters, oldClusterName]; logger_1.logger.debug(`Cluster rename from ${oldClusterName} to ${newClusterName} for device ${device.ieeeAddr} successfully applied. New meta state: ${JSON.stringify(device.meta)}`, NS); } }, ]; return { onEvent, isModernExtend: true, }; }, /** Some Bosch devices ask the coordinator for their ZCL version * during deviceAnnouncement. Without answer, these devices regularly * re-join the network. To avoid that, we have to make sure that a readRequest * for the zclVersion is always being answered. The answered zclVersion is * taken from the Bosch Smart Home Controller II. * * Exception: BTH-RM and BTH-RM230Z ask the coordinator at regular * intervals for their zclVersion (maybe availability check like Z2M does?) * and *not* during interview! To avoid code-duplication, we handle that * case here as well. */ handleZclVersionReadRequest: () => { const onEvent = [ (event) => { if (event.type === "start") { event.data.device.customReadResponse = (frame, endpoint) => { if (frame.isCluster("genBasic") && frame.payload.some((i) => i.attrId === 0x0000)) { // XXX: we're replying to specific attribute, which could be incorrect (not based on the request attrIds) endpoint.readResponse("genBasic", frame.header.transactionSequenceNumber, { zclVersion: 1 }).catch((e) => { logger_1.logger.warning(`Custom zclVersion response failed for '${event.data.device.ieeeAddr}': ${e}`, NS); }); return true; } return false; }; } }, ]; return { onEvent, isModernExtend: true, }; }, batteryWithPercentageAndLowStatus: (args) => m.battery({ percentage: true, percentageReportingConfig: false, lowStatus: true, lowStatusReportingConfig: { min: "MIN", max: "MAX", change: null }, ...args, }), }; exports.boschGeneralEnergyDeviceExtend = { customMeteringCluster: () => m.deviceAddCustomCluster("seMetering", { name: "seMetering", ID: zigbee_herdsman_1.Zcl.Clusters.seMetering.ID, attributes: {}, commands: { resetEnergyMeters: { name: "resetEnergyMeters", ID: 0x80, parameters: [], }, }, commandsResponse: {}, }), resetEnergyMeters: () => { const exposes = [ e .enum("reset_energy_meters", ea.SET, ["reset"]) .withDescription("Triggers the reset of all energy meters on the device to 0 kWh") .withCategory("config"), ]; const toZigbee = [ { key: ["reset_energy_meters"], convertSet: async (entity, key, value, meta) => { await entity.command("seMetering", "resetEnergyMeters", {}, exports.manufacturerOptions); }, }, ]; return { exposes, toZigbee, isModernExtend: true, }; }, autoOff: (args) => { const { endpoint } = args ?? {}; const offOnLookup = { OFF: 0x00, ON: 0x01, }; const expose = (device, options) => { if (utils.isDummyDevice(device)) { return []; } if (device.modelID === "RBSH-MMR-ZB-EU") { const pulsedModeEnabled = device.getEndpoint(1).getClusterAttributeValue("boschEnergyDevice", "pulseLength") !== 0; if (pulsedModeEnabled) { return []; } } const autoOffEnabledExpose = e .binary("auto_off_enabled", ea.ALL, utils.getFromLookupByValue(0x01, offOnLookup), utils.getFromLookupByValue(0x00, offOnLookup)) .withLabel("Enable auto-off") .withDescription("Enable/disable the automatic turn-off feature") .withCategory("config"); const autoOffTimeExpose = e .numeric("auto_off_time", ea.ALL) .withLabel("Auto-off time") .withDescription("Turn off the output after the specified amount of time. Only in action when the automatic turn-off is enabled.") .withUnit("min") .withValueMin(0) .withValueMax(720) .withValueStep(0.5) .withCategory("config"); if (endpoint !== undefined) { autoOffEnabledExpose.withEndpoint(endpoint.toString()); autoOffTimeExpose.withEndpoint(endpoint.toString()); } return [autoOffEnabledExpose, autoOffTimeExpose]; }; const fromZigbee = [ { cluster: "boschEnergyDevice", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { const result = {}; const data = msg.data; if (data.autoOffEnabled !== undefined) { const property = utils.postfixWithEndpointName("auto_off_enabled", msg, model, meta); result[property] = utils.getFromLookupByValue(data.autoOffEnabled, offOnLookup); } if (data.autoOffTime !== undefined) { const property = utils.postfixWithEndpointName("auto_off_time", msg, model, meta); result[property] = data.autoOffTime / 60; } return result; }, }, ]; const toZigbee = [ { key: ["auto_off_enabled", "auto_off_time"], convertSet: async (entity, key, value, meta) => { if (key === "auto_off_enabled") { const selectedState = utils.getFromLookup(value, offOnLookup); await entity.write("boschEnergyDevice", { autoOffEnabled: utils.toNumber(selectedState), }); return { state: { auto_off_enabled: value } }; } if (key === "auto_off_time") { await entity.write("boschEnergyDevice", { autoOffTime: (0, utils_1.toNumber)(value) * 60, }); return { state: { auto_off_time: value } }; } }, convertGet: async (entity, key, meta) => { if (key === "auto_off_enabled") { await entity.read("boschEnergyDevice", ["autoOffEnabled"]); } if (key === "auto_off_time") { await entity.read("boschEnergyDevice", ["autoOffTime"]); } }, }, ]; const configure = [ m.setupConfigureForBinding("boschEnergyDevice", "input", endpoint ? [endpoint.toString()] : null), m.setupConfigureForReading("boschEnergyDevice", ["autoOffEnabled", "autoOffTime"], endpoint ? [endpoint.toString()] : null), ]; return { exposes: [expose], fromZigbee, toZigbee, configure, isModernExtend: true, }; }, }; //endregion //region Generally used Bosch functionality on sensor devices exports.boschGeneralSensorDeviceExtend = { testMode: (args) => { const { testModeDescription, sensitivityLevelToUse, variableTimeoutSupported = false, defaultTimeout = 0, zoneStatusBit = 8 } = args; const testModeLookup = { ON: true, OFF: false, }; const enableTestMode = async (endpoint, sensitivityLevelToUse, timeoutInSeconds) => { await endpoint.command("ssIasZone", "initTestMode", { testModeDuration: timeoutInSeconds, currentZoneSensitivityLevel: sensitivityLevelToUse, }); }; const disableTestMode = async (endpoint) => { await endpoint.command("ssIasZone", "initNormalOpMode", {}); }; const exposes = [ e .binary("test_mode", ea.ALL, utils.getFromLookupByValue(true, testModeLookup), utils.getFromLookupByValue(false, testModeLookup)) .withDescription(testModeDescription) .withCategory("config"), ]; if (variableTimeoutSupported) { exposes.push(e .numeric("test_mode_timeout", ea.ALL) .withDescription(`Determines how long the test mode should be activated. The default length is ${defaultTimeout} seconds.`) .withValueMin(1) .withValueMax(255) .withUnit("seconds") .withCategory("config")); } const fromZigbee = [ { cluster: "ssIasZone", type: ["commandStatusChangeNotification", "attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { const zoneStatus = "zonestatus" in msg.data ? msg.data.zonestatus : msg.data.zoneStatus; if (zoneStatus === undefined) { return; } const result = {}; const testModeEnabled = (zoneStatus & (1 << zoneStatusBit)) > 0; result.test_mode = utils.getFromLookupByValue(testModeEnabled, testModeLookup); return result; }, }, ]; const toZigbee = [ { key: ["test_mode", "test_mode_timeout"], convertSet: async (entity, key, value, meta) => { if (key === "test_mode") { if (value === utils.getFromLookupByValue(true, testModeLookup)) { let timeoutInSeconds; const currentTimeout = meta.state.test_mode_timeout; if (currentTimeout == null) { timeoutInSeconds = defaultTimeout; meta.publish({ test_mode_timeout: timeoutInSeconds }); } else { timeoutInSeconds = utils.toNumber(currentTimeout); } await enableTestMode(entity, sensitivityLevelToUse, timeoutInSeconds); } else { await disableTestMode(entity); } } if (key === "test_mode_timeout") { return { state: { test_mode_timeout: value } }; } }, convertGet: async (entity, key, meta) => { if (key === "test_mode") { await entity.read("ssIasZone", ["zoneStatus"]); } if (key === "test_mode_timeout" && meta.state.test_mode_timeout == null) { meta.publish({ test_mode_timeout: defaultTimeout }); } }, }, ]; const configure = [m.setupConfigureForBinding("ssIasZone", "input"), m.setupConfigureForReading("ssIasZone", ["zoneStatus"])]; return { exposes, fromZigbee, toZigbee, configure, isModernExtend: true, }; }, }; exports.boschBmctExtend = { switchMode: (args) => { const { endpoint, deviceModeLookup, switchModeLookup, switchTypeLookup } = args; const expose = (device, options) => { if (utils.isDummyDevice(device)) { return []; } const switchTypeKey = device.getEndpoint(1).getClusterAttributeValue("boschEnergyDevice", "switchType") ?? 0x00; const selectedSwitchType = utils.getFromLookupByValue(switchTypeKey, switchTypeLookup); if (selectedSwitchType === "none") { return []; } let supportedSwitchModes = Object.keys(switchModeLookup); if (device.modelID === "RBSH-MMS-ZB-EU") { const deviceModeKey = device.getEndpoint(1).getClusterAttributeValue("boschEnergyDevice", "deviceMode"); const deviceMode = utils.getFromLookupByValue(deviceModeKey, deviceModeLookup); if (deviceMode === "light") { if (selectedSwitchType.includes("rocker_switch")) { supportedSwitchModes = supportedSwitchModes.filter((switchMode) => switchMode === "coupled" || switchMode === "decoupled"); } } if (deviceMode === "shutter") { if (selectedSwitchType.includes("button")) { supportedSwitchModes = supportedSwitchModes.filter((switchMode) => switchMode === "coupled" || switchMode === "only_long_press_decoupled"); } else if (selectedSwitchType.includes("rocker_switch")) { supportedSwitchModes = supportedSwitchModes.filter((switchMode) => switchMode === "coupled"); } } } const switchModeExpose = e .enum("switch_mode", ea.ALL, supportedSwitchModes) .withDescription("Decouple the switch from the corresponding output to use it for other purposes. Please keep in mind that the available options may depend on the used switch type.") .withCategory("config"); if (endpoint !== undefined) { switchModeExpose.withEndpoint(endpoint.toString()); } return [switchModeExpose]; }; const fromZigbee = [ { cluster: "boschEnergyDevice", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { const result = {}; const data = msg.data; if (data.switchMode !== undefined) { const property = utils.postfixWithEndpointName("switch_mode", msg, model, meta); result[property] = utils.getFromLookupByValue(data.switchMode, switchModeLookup); } return result; }, }, ]; const toZigbee = [ { key: ["switch_mode"], convertSet: async (entity, key, value, meta) => { if (key === "switch_mode") { const index = utils.getFromLookup(value, switchModeLookup); await entity.write("boschEnergyDevice", { switchMode: index }); return { state: { switch_mode: value } }; } }, convertGet: async (entity, key, meta) => { if (key === "switch_mode") { await entity.read("boschEnergyDevice", ["switchMode"]); } }, }, ]; const configure = [ async (device, coordinatorEndpoint, definition) => { const desiredEndpoint = device.getEndpoint(endpoint ?? 1); await desiredEndpoint.read("boschEnergyDevice", ["switchMode"]); }, ]; return { exposes: [expose], fromZigbee, toZigbee, configure, isModernExtend: true, }; }, childLock: (args) => { const { endpoint } = args ?? {}; const childLockLookup = { UNLOCKED: 0x00, LOCKED: 0x01, }; const expose = (device, options) => { if (utils.isDummyDevice(device)) { return []; } const currentSwitchType = device.getEndpoint(1).getClusterAttributeValue("boschEnergyDevice", "switchType") ?? 0x00; if (currentSwitchType === 0) { return []; } const childLockExpose = e .binary("child_lock", ea.ALL, utils.getFromLookupByValue(0x01, childLockLookup), utils.getFromLookupByValue(0x00, childLockLookup)) .withDescription("Enables/disables physical input on the switch") .withCategory("config"); if (endpoint !== undefined) { childLockExpose.withEndpoint(endpoint.toString()); } return [childLockExpose]; }; const fromZigbee = [ { cluster: "boschEnergyDevice", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { const result = {}; const data = msg.data; if (data.childLock !== undefined) { const property = utils.postfixWithEndpointName("child_lock", msg, model, meta); result[property] = data.childLock; } return result; }, }, ]; const toZigbee = [ { key: ["child_lock"], convertSet: async (entity, key, value, meta) => { if (key === "child_lock") { const selectedMode = utils.getFromLookup(value, childLockLookup); await entity.write("boschEnergyDevice", { childLock: selectedMode }); return { state: { child_lock: value } }; } }, convertGet: async (entity, key, meta) => { if (key === "child_lock") { await entity.read("boschEnergyDevice", ["childLock"]); } }, }, ]; const configure = [ async (device, coordinatorEndpoint, definition) => { const desiredEndpoint = device.getEndpoint(endpoint ?? 1); await desiredEndpoint.read("boschEnergyDevice", ["childLock"]); }, ]; return { exposes: [expose], fromZigbee, toZigbee, configure, isModernExtend: true, }; }, actuatorType: () => m.enumLookup({ name: "actuator_type", cluster: "boschEnergyDevice", attribute: "actuatorType", description: "Select the appropriate actuator type so that the connected device can be controlled correctly.", lookup: { normally_closed: 0x00, normally_open: 0x01, }, entityCategory: "config", }), dimmerType: () => m.enumLookup({ name: "dimmer_type", cluster: "boschEnergyDevice", attribute: "dimmerType", description: "Select the appropriate dimmer type for your lamps. Make sure that you are only using dimmable lamps.", lookup: { leading_edge_phase_cut: 0x00, trailing_edge_phase_cut: 0x01, }, entityCategory: "config", }), brightnessRange: () => { const expose = (device, options) => { if (utils.isDummyDevice(device)) { return []; } const minimumBrightnessExpose = e .numeric("minimum_brightness", ea.ALL) .withLabel("Raise minimum brightness") .withDescription("This raises the minimum brightness level of the connected light") .withValueMin(0) .withValueMax(255) .withCategory("config"); const maximumBrightnessExpose = e .numeric("maximum_brightness", ea.ALL) .withLabel("Lower maximum brightness") .withDescription("This lowers the maximum brightness level of the connected light") .withValueMin(0) .withValueMax(255) .withCategory("config"); return [minimumBrightnessExpose, maximumBrightnessExpose]; }; const fromZigbee = [ { cluster: "boschEnergyDevice", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { const result = {}; const data = msg.data; if (data.minimumBrightness !== undefined) { result.minimum_brightness = data.minimumBrightness; } if (data.maximumBrightness !== undefined) { result.maximum_brightness = data.maximumBrightness; } return result; }, }, ]; const toZigbee = [ { key: ["minimum_brightness", "maximum_brightness"], convertSet: async (entity, key, value, meta) => { if (key === "minimum_brightness") { const newMinimumBrightness = (0, utils_1.toNumber)(value); const currentState = await entity.read("boschEnergyDevice", ["maximumBrightness"]); if (newMinimumBrightness >= currentState.maximumBrightness) { throw new Error("The minimum brightness must be lower than the maximum brightness!"); } await entity.write("boschEnergyDevice", { minimumBrightness: newMinimumBrightness, }); return { state: { minimum_brightness: value } }; } if (key === "maximum_brightness") { const newMaximumBrightness = (0, utils_1.toNumber)(value); const currentState = await entity.read("boschEnergyDevice", ["minimumBrightness"]); if (newMaximumBrightness <= currentState.minimumBrightness) { throw new Error("The maximum brightness must be higher than the minimum brightness!"); } await entity.write("boschEnergyDevice", { maximumBrightness: newMaximumBrightness, }); return { state: { maximum_brightness: value } }; } }, convertGet: async (entity, key, meta) => { if (key === "minimum_brightness") { await entity.read("boschEnergyDevice", ["minimumBrightness"]); } if (key === "maximum_brightness") { await entity.read("boschEnergyDevice", ["maximumBrightness"]); } }, }, ]; const configure = [ async (device, coordinatorEndpoint, definition) => { const endpoint = device.getEndpoint(1); await endpoint.read("boschEnergyDevice", ["minimumBrightness", "maximumBrightness"]); }, ]; return { exposes: [expose], fromZigbee, toZigbee, configure, isModernExtend: true, }; }, rzDeviceModes: (args) => { const { deviceModesLookup } = args; const expose = (device, options) => { if (utils.isDummyDevice(device)) { return []; } const deviceModeExpose = e .enum("device_mode", ea.SET, Object.keys(deviceModesLookup)) .withLabel("Device mode") .withDescription("Set the desired mode of the relay") .withCategory("config"); return [deviceModeExpose]; }; const toZigbee = [ { key: ["device_mode"], convertSet: async (entity, key, value, meta) => { if (key === "device_mode") { const deviceModeChanged = meta.state.device_mode !== value; if (deviceModeChanged) { const newPulseLength = value === utils.getFromLookupByValue(0x00, deviceModesLookup) ? 0 : 10; const endpoint = meta.device.getEndpoint(1); if (newPulseLength > 0) { await endpoint.write("boschEnergyDevice", { switchType: 0x05, switchMode: 0x00, childLock: 0x00, autoOffEnabled: 0x00, autoOffTime: 0, }); await endpoint.read("boschEnergyDevice", [ "switchType", "switchMode", "childLock", "autoOffEnabled", "autoOffTime", ]); } else { await endpoint.write("boschEnergyDevice", { switchType: 0x00, switchMode: 0x00, childLock: 0x00, }); await endpoint.read("boschEnergyDevice", [ "switchType", "switchMode", "childLock", ]); } await endpoint.write("boschEnergyDevice", { pulseLength: newPulseLength, }); await endpoint.read("boschEnergyDevice", ["pulseLength"]); } return { state: { device_mode: value } }; } }, }, ]; return { exposes: [expose], toZigbee, isModernExtend: true, }; }, pulseLength: (args) => { const { updateDeviceMode, deviceModesLookup } = args; const expose = (device, options) => { if (utils.isDummyDevice(device)) { return []; } if (device.getEndpoint(1).getClusterAttributeValue("boschEnergyDevice", "pulseLength") === 0) { return []; } const pulseLengthExpose = e .numeric("pulse_length", ea.ALL) .withLabel("Pulse length") .withDescription("Set the desired pulse length for the relay in seconds.") .withUnit("s") .withValueStep(0.1) .withValueMin(0.5) .withValueMax(20) .withCategory("config"); return [pulseLengthExpose]; }; const fromZigbee = [ { cluster: "boschEnergyDevice", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { const result = {}; const data = msg.data; if (data.pulseLength !== undefined) { const currentPulseLength = data.pulseLength / 10; const oldPulseLength = meta.device.meta.pulseLength ?? 0; const pulsedModeActivated = currentPulseLength > 0 && oldPulseLength === 0; const pulsedModeDeactivated = currentPulseLength === 0 && oldPulseLength > 0; if (pulsedModeActivated || pulsedModeDeactivated) { meta.device.meta.pulseLength = currentPulseLength; meta.deviceExposesChanged(); } if (updateDeviceMode) { result.device_mode = currentPulseLength === 0 ? utils.getFromLookupByValue(0x00, deviceModesLookup) : utils.getFromLookupByValue(0x01, deviceModesLookup); } result.pulse_length = currentPulseLength; } return result; }, }, ]; const toZigbee = [ { key: ["pulse_length"], convertSet: async (entity, key, value, meta) => { if (key === "pulse_length") { const selectedPulseLength = (0, utils_1.toNumber)(value) * 10; await entity.write("boschEnergyDevice", { pulseLength: selectedPulseLength, }); return { state: { pulse_length: value } }; } }, convertGet: async (entity, key, meta) => { if (key === "pulse_length") { await entity.read("boschEnergyDevice", ["pulseLength"]); } }, }, ]; const configure = [ async (device, coordinatorEndpoint, definition) => { const endpoint = device.getEndpoint(1); await endpoint.read("boschEnergyDevice", ["pulseLength"]); }, ]; return { exposes: [expose], fromZigbee, toZigbee, configure, isModernExtend: true, }; }, switchType: (args) => { const { switchTypeLookup } = args; const expose = (device, options) => { if (utils.isDummyDevice(device)) { return []; } let supportedSwitchTypes = Object.keys(switchTypeLookup); if (device.modelID === "RBSH-MMR-ZB-EU") { const pulseModeActive = device.getEndpoint(1).getClusterAttributeValue("boschEnergyDevice", "pulseLength") > 0; if (pulseModeActive) { supportedSwitchTypes = Object.keys(switchTypeLookup).filter((switchType) => switchType !== utils.getFromLookup("rocker_switch", switchTypeLookup)); } } const switchTypeExpose = e .enum("switch_type", ea.ALL, supportedSwitchTypes) .withLabel("Connected switch type") .withDescription("Select which switch type is connected to the module. Please keep in mind that the available options may depend on the selected device mode.") .withCategory("config"); return [switchTypeExpose]; }; const fromZigbee = [ { cluster: "boschEnergyDevice", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { const result = {}; const data = msg.data; if (data.switchType !== undefined) { const switchType = data.switchType; result.switch_type = utils.getFromLookupByValue(switchType, switchTypeLookup); if (switchType !== meta.device.meta.switchType) { meta.device.meta.switchType = switchType; meta.deviceExposesChanged(); } } return result; }, }, ]; const toZigbee = [ { key: ["switch_type"], convertSet: async (entity, key, value, meta) => { if (key === "switch_type") { const selectedSwitchType = utils.getFromLookup(value, switchTypeLookup); if (meta.device.meta.switchType !== selectedSwitchType) { const endpoints = meta.device.endpoints.filter((e) => e.supportsInputCluster("boschEnergyDevice")); for (const endpoint of endpoints) { await endpoint.write("boschEnergyDevice", { switchMode: 0x00, childLock: 0x00, }); await endpoint.read("boschEnergyDevice", ["switchMode", "childLock"]); } } await entity.write("boschEnergyDevice", { switchType: selectedSwitchType, }); await entity.read("boschEnergyDevice", ["switchType"]); return { state: { switch_type: value } }; } }, convertGet: async (entity, key, meta) => { if (key === "switch_type") { await entity.read("boschEnergyDevice", ["switchType"]); } }, }, ]; const configure = [ async (device, coordinatorEndpoint, definition) => { const endpoint = device.getEndpoint(1); await endpoint.read("boschEnergyDevice", ["switchType"]); }, ]; return { exposes: [expose], fromZigbee, toZigbee, configure, isModernExtend: true, }; }, reportSwitchAction: (args) => { const { switchTypeLookup, hasDualSwitchInputs } = args; const expose = (device, options) => { const exposeList = []; if (utils.isDummyDevice(device)) { return exposeList; } const switchTypeKey = device.getEndpoint(1).getClusterAttributeValue("boschEnergyDevice", "switchType") ?? 0x00; const selectedSwitchType = utils.getFromLookupByValue(switchTypeKey, switchTypeLookup); let supportedActionTypes; if (selectedSwitchType.includes("button")) { if (hasDualSwitchInputs) { supportedActionTypes = [ "press_released_left", "press_released_right", "hold_left", "hold_right", "hold_released_left", "hold_released_right", ]; } else { supportedActionTypes = ["press_released", "hold", "hold_released"]; } exposeList.push(e.action(supportedActionTypes), e.action_duration()); } else if (selectedSwitchType.includes("rocker_switch")) { if (hasDualSwitchInputs) { supportedActionTypes = ["opened_left", "opened_right", "closed_left", "closed_right"]; } else { supportedActionTypes = ["opened", "closed"]; } exposeList.push(e.action(supportedActionTypes)); } return exposeList; }; const fromZigbee = [ { cluster: "boschEnergyDevice", type: ["raw"], convert: (model, msg, publish, options, meta) => { const command = msg.data[4]; if (command !== 0x03 && command !== 0x04) { return; } let state; const status = msg.data[5]; const duration = msg.data[6] / 10; switch (status) { case 0: state = "press_released"; break; case 1: state = duration !== 0 ? "hold" : "hold_released"; break; case 2: state = "closed"; break; case 3: state = "opened"; break; } let action; if (hasDualSwitchInputs) { const triggeredSide = command === 0x03 ? "left" : "right"; action = `${state}_${triggeredSide}`; } else { action = state; } return { action: action, action_duration: duration }; }, }, ]; return { exposes: [expose], fromZigbee, isModernExtend: true, }; }, slzExtends: () => { const stateDeviceMode = { light: 0x04, shutter: 0x01, disabled: 0x00, }; const stateMotor = { stopped: 0x00, opening: 0x01, closing: 0x02, unknownOne: 0x03, unknownTwo: 0x04, }; const stateSwitchType = { button: 0x01, button_key_change: 0x02, rocker_switch: 0x03, rocker_switch_key_change: 0x04, none: 0x00, }; const stateSwitchMode = { coupled: 0x00, decoupled: 0x01, only_short_press_decoupled: 0x02, only_long_press_decoupled: 0x03, }; const stateOffOn = { OFF: 0x00, ON: 0x01, }; const fromZigbee = [ fz.on_off_force_multiendpoint, fz.power_on_behavior, fz.cover_position_tilt, { cluster: "boschEnergyDevice", type: ["attributeReport", "readResponse"], convert: (model, msg, publish, options, meta) => { const result = {}; const data = msg.data; if (data.deviceMode !== undefined) { result.device_mode = Object.keys(stateDeviceMode).find((key) => stateDeviceMode[key] === msg.data.deviceMode); const deviceMode = msg.data.deviceMode; if (deviceMode !== meta.device.meta.deviceMode) { meta.device.meta.deviceMode = deviceMode; meta.deviceExposesChanged(); } } if (data.switchType !== undefined) { const switchType = msg.data.switchType; result.switch_type = Object.keys(stateSwitchType).find((key) => stateSwitchType[key] === switchType); if (switchType !== meta.device.meta.switchType) { meta.device.meta.switchType = switchType; meta.deviceExposesChanged(); } } if (data.switchMode !== undefined) { const property = utils.postfixWithEndpointName("switch_mode", msg, model, meta); result[property] = Object.keys(stateSwitchMode).find((key) => stateSwitchMode[key] === msg.data.switchMode); } if (data.calibrationOpeningTime !== undefined) { result.calibration_opening_time = msg.data.calibrationOpeningTime / 10; } if (data.calibrationClosingTime !== undefined) { result.calibration_closing_time = msg.data.calibrationClosingTime / 10; } if (data.calibrationButtonHoldTime !== undefined) { result.calibration_button_hold_time = msg.data.calibrationButtonHoldTime / 10; } if (data.calibrationMotorStartDelay !== undefined) { result.calibration_motor_start_delay = msg.data.calibrationMotorStartDelay / 10; } if (data.childLock !== undefined) { const property = utils.postfixWithEndpointName("child_lock", msg, model, meta); result[property] = msg.data.childLock === 1 ? "ON" : "OFF"; } if (data.motorState !== undefined) { result.motor_state = Object.keys(stateMotor).find((key) => stateMotor[key] === msg.data.motorState); } if (data.autoOffEnabled !== undefined) { const property = utils.postfixWithEndpointName("auto_off_enabled", msg, model, meta); result[property] = msg.data.autoOffEnabled === 1 ? "ON" : "OFF"; } if (data.autoOffTime !== undefined) { const property = utils.postfixWithEndpointName("auto_off_time", msg, model, meta); result[property] = msg.data.autoOffTime / 60; } return result; }, }, ]; const toZigbee = [ tz.power_on_behavior, tz.cover_position_tilt, { key: [ "device_mode", "switch_type", "switch_mode", "child_lock", "state", "on_time", "off_wait_time", "auto_off_enabled", "auto_off_time", ], convertSet: async (entity, key, value, meta) => { if (key === "state") { if ("ID" in entity && entity.ID === 1) { await tz.cover_state.convertSet(entity, key, value, meta); } else { await tz.on_off.convertSet(entity, key, value, meta); } } if (key === "on_time" || key === "on_wait_time") { if ("ID" in entity && entity.ID !== 1) { await tz.on_off.convertSet(entity, key, value, meta); } } if (key === "device_mode") { const index = utils.getFromLookup(value, stateDeviceMode); await entity.write("boschEnergyDevice", { deviceMode: index }); await entity.read("boschEnergyDevice", ["deviceMode"]); return { state: { device_mode: value } }; } if (key === "switch_type") { const applyDefaultForSwitchModeAndChildLock = async (endpoint) => { const switchModeDefault = utils.getFromLookup("coupled", stateSwitchMode); const childLockDefault =