const exposes = require('zigbee-herdsman-converters/lib/exposes'); const fz = {...require('zigbee-herdsman-converters/converters/fromZigbee'), legacy: require('zigbee-herdsman-converters/lib/legacy').fromZigbee}; const tz = require('zigbee-herdsman-converters/converters/toZigbee'); const constants = require('zigbee-herdsman-converters/lib/constants'); const reporting = require('zigbee-herdsman-converters/lib/reporting'); const globalStore = require('zigbee-herdsman-converters/lib/store'); const utils = require('zigbee-herdsman-converters/lib/utils'); const ota = require('zigbee-herdsman-converters/lib/ota'); const e = exposes.presets; const ea = exposes.access; // develco specific cosntants const manufacturerOptions = {manufacturerCode: 0x1015}; /* MOSZB-1xx - ledControl - bitmap8 - r/w * 0x00 Disable LED when movement is detected. * 0x01 Enables periodic fault flashes. These flashes are used to indicate e.g. low battery level. * 0x02 Enables green application defined LED. This is e.g. used to indicate motion detection. * Default value 0xFF ( seems to be fault + motion) */ const develcoLedControlMap = { 0x00: 'off', 0x01: 'fault_only', 0x02: 'motion_only', 0xFF: 'both', }; // develco specific convertors const develco = { configure: { read_sw_hw_version: async (device, logger) => { for (const ep of device.endpoints) { if (ep.supportsInputCluster('genBasic')) { try { const data = await ep.read('genBasic', ['develcoPrimarySwVersion', 'develcoPrimaryHwVersion'], manufacturerOptions); if (data.hasOwnProperty('develcoPrimarySwVersion')) { device.softwareBuildID = data.develcoPrimarySwVersion.join('.'); } if (data.hasOwnProperty('develcoPrimaryHwVersion')) { device.hardwareVersion = data.develcoPrimaryHwVersion.join('.'); } } catch (error) {/* catch timeouts of sleeping devices */} break; } } }, }, fz: { // Some Develco devices report strange values sometimes // https://github.com/Koenkk/zigbee2mqtt/issues/13329 electrical_measurement: { ...fz.electrical_measurement, convert: (model, msg, publish, options, meta) => { if (msg.data.rmsVoltage !== 0xFFFF && msg.data.rmsCurrent !== 0xFFFF && msg.data.activePower !== -0x8000) { return fz.electrical_measurement.convert(model, msg, publish, options, meta); } }, }, device_temperature: { ...fz.device_temperature, convert: (model, msg, publish, options, meta) => { if (msg.data.currentTemperature !== -0x8000) { return fz.device_temperature.convert(model, msg, publish, options, meta); } }, }, temperature: { ...fz.temperature, convert: (model, msg, publish, options, meta) => { if (msg.data.measuredValue !== -0x8000 && msg.data.measuredValue !== 0xFFFF) { return fz.temperature.convert(model, msg, publish, options, meta); } }, }, metering: { ...fz.metering, convert: (model, msg, publish, options, meta) => { if (msg.data.instantaneousDemand !== -0x800000) { return fz.metering.convert(model, msg, publish, options, meta); } }, }, pulse_configuration: { cluster: 'seMetering', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { const result = {}; if (msg.data.hasOwnProperty('develcoPulseConfiguration')) { result[utils.postfixWithEndpointName('pulse_configuration', msg, model, meta)] = msg.data['develcoPulseConfiguration']; } return result; }, }, interface_mode: { cluster: 'seMetering', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { const result = {}; if (msg.data.hasOwnProperty('develcoInterfaceMode')) { result[utils.postfixWithEndpointName('interface_mode', msg, model, meta)] = constants.develcoInterfaceMode.hasOwnProperty(msg.data['develcoInterfaceMode']) ? constants.develcoInterfaceMode[msg.data['develcoInterfaceMode']] : msg.data['develcoInterfaceMode']; } if (msg.data.hasOwnProperty('status')) { result['battery_low'] = (msg.data.status & 2) > 0; result['check_meter'] = (msg.data.status & 1) > 0; } return result; }, }, fault_status: { cluster: 'genBinaryInput', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { const result = {}; if (msg.data.hasOwnProperty('reliability')) { const lookup = {0: 'no_fault_detected', 7: 'unreliable_other', 8: 'process_error'}; result.reliability = lookup[msg.data['reliability']]; } if (msg.data.hasOwnProperty('statusFlags')) { result.fault = (msg.data['statusFlags']===1); } return result; }, }, voc: { cluster: 'develcoSpecificAirQuality', type: ['attributeReport', 'readResponse'], options: [exposes.options.precision('voc'), exposes.options.calibration('voc')], convert: (model, msg, publish, options, meta) => { // from Sensirion_Gas_Sensors_SGP3x_TVOC_Concept.pdf // "The mean molar mass of this mixture is 110 g/mol and hence, // 1 ppb TVOC corresponds to 4.5 μg/m3." const vocPpb = parseFloat(msg.data['measuredValue']); const voc = vocPpb * 4.5; const vocProperty = utils.postfixWithEndpointName('voc', msg, model, meta); // from aqszb-110-technical-manual-air-quality-sensor-04-08-20.pdf page 6, section 2.2 voc // this contains a ppb to level mapping table. let airQuality; const airQualityProperty = utils.postfixWithEndpointName('air_quality', msg, model, meta); if (vocPpb <= 65) { airQuality = 'excellent'; } else if (vocPpb <= 220) { airQuality = 'good'; } else if (vocPpb <= 660) { airQuality = 'moderate'; } else if (vocPpb <= 2200) { airQuality = 'poor'; } else if (vocPpb <= 5500) { airQuality = 'unhealthy'; } else if (vocPpb > 5500) { airQuality = 'out_of_range'; } else { airQuality = 'unknown'; } return {[vocProperty]: utils.calibrateAndPrecisionRoundOptions(voc, options, 'voc'), [airQualityProperty]: airQuality}; }, }, voc_battery: { cluster: 'genPowerCfg', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { /* * Per the technical documentation for AQSZB-110: * To detect low battery the system can monitor the "BatteryVoltage" by setting up a reporting interval of every 12 hour. * When a voltage of 2.5V is measured the battery should be replaced. * Low batt LED indication–RED LED will blink twice every 60 second. */ const result = fz.battery.convert(model, msg, publish, options, meta); result.battery_low = (result.voltage <= 2500); return result; }, }, led_control: { cluster: 'genBasic', type: ['attributeReport', 'readResponse'], options: [], convert: (model, msg, publish, options, meta) => { const state = {}; if (msg.data.hasOwnProperty('develcoLedControl')) { state['led_control'] = develcoLedControlMap[msg.data['develcoLedControl']]; } return state; }, }, ias_occupancy_timeout: { cluster: 'ssIasZone', type: ['attributeReport', 'readResponse'], options: [], convert: (model, msg, publish, options, meta) => { const state = {}; if (msg.data.hasOwnProperty('develcoAlarmOffDelay')) { state['occupancy_timeout'] = msg.data['develcoAlarmOffDelay']; } return state; }, }, input: { cluster: 'genBinaryInput', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { const result = {}; if (msg.data.hasOwnProperty('presentValue')) { const value = msg.data['presentValue']; result[utils.postfixWithEndpointName('input', msg, model, meta)] = value == 1; } return result; }, }, }, tz: { pulse_configuration: { key: ['pulse_configuration'], convertSet: async (entity, key, value, meta) => { await entity.write('seMetering', {'develcoPulseConfiguration': value}, manufacturerOptions); return {readAfterWriteTime: 200, state: {'pulse_configuration': value}}; }, convertGet: async (entity, key, meta) => { await entity.read('seMetering', ['develcoPulseConfiguration'], manufacturerOptions); }, }, interface_mode: { key: ['interface_mode'], convertSet: async (entity, key, value, meta) => { const payload = {'develcoInterfaceMode': utils.getKey(constants.develcoInterfaceMode, value, undefined, Number)}; await entity.write('seMetering', payload, manufacturerOptions); return {readAfterWriteTime: 200, state: {'interface_mode': value}}; }, convertGet: async (entity, key, meta) => { await entity.read('seMetering', ['develcoInterfaceMode'], manufacturerOptions); }, }, current_summation: { key: ['current_summation'], convertSet: async (entity, key, value, meta) => { await entity.write('seMetering', {'develcoCurrentSummation': value}, manufacturerOptions); return {state: {'current_summation': value}}; }, }, led_control: { key: ['led_control'], convertSet: async (entity, key, value, meta) => { const ledControl = utils.getKey(develcoLedControlMap, value, value, Number); await entity.write('genBasic', {'develcoLedControl': ledControl}, manufacturerOptions); return {state: {led_control: value}}; }, convertGet: async (entity, key, meta) => { await entity.read('genBasic', ['develcoLedControl'], manufacturerOptions); }, }, ias_occupancy_timeout: { key: ['occupancy_timeout'], convertSet: async (entity, key, value, meta) => { let timeoutValue = value; if (timeoutValue < 20) { meta.logger.warn(`Minimum occupancy_timeout is 20, using 20 instead of ${timeoutValue}!`); timeoutValue = 20; } await entity.write('ssIasZone', {'develcoAlarmOffDelay': timeoutValue}, manufacturerOptions); return {state: {occupancy_timeout: timeoutValue}}; }, convertGet: async (entity, key, meta) => { await entity.read('ssIasZone', ['develcoAlarmOffDelay'], manufacturerOptions); }, }, input: { key: ['input'], convertGet: async (entity, key, meta) => { await entity.read('genBinaryInput', ['presentValue']); }, }, }, }; const definition = { zigbeeModel: ['EMIZB-141'], // The model ID from: Device with modelID 'lumi.sens' is not supported. model: 'EMIZB-141', // Vendor model number, look on the device for a model number vendor: 'frient A/S', // Vendor of the device (only used for documentation and startup logging) description: 'frient Powermeter', // Description of the device, copy from vendor site. (only used for documentation and startup logging) fromZigbee: [develco.fz.metering, develco.fz.pulse_configuration, develco.fz.interface_mode, develco.fz.battery], toZigbee: [develco.tz.pulse_configuration, develco.tz.interface_mode, develco.tz.current_summation], endpoint: (device) => { return {'default': 2}; }, configure: async (device, coordinatorEndpoint, logger) => { const endpoint = device.getEndpoint(2); await reporting.bind(endpoint, coordinatorEndpoint, ['seMetering']); await reporting.instantaneousDemand(endpoint); await reporting.readMeteringMultiplierDivisor(endpoint); }, exposes: [ e.power(), e.energy(), e.battery_low(), e.battery(), exposes.numeric('pulse_configuration', ea.ALL).withValueMin(0).withValueMax(65535) .withDescription('Pulses per kwh. Default 1000 imp/kWh. Range 0 to 65535'), exposes.enum('interface_mode', ea.ALL, ['electricity', 'gas', 'water', 'kamstrup-kmp', 'linky', 'IEC62056-21', 'DSMR-2.3', 'DSMR-4.0']) .withDescription('Operating mode/probe'), exposes.numeric('current_summation', ea.SET) .withDescription('Current summation value sent to the display. e.g. 570 = 0,570 kWh').withValueMin(0) .withValueMax(268435455), exposes.binary('check_meter', ea.STATE, true, false) .withDescription('Is true if communication problem with meter is experienced'), ], }; module.exports = definition;