UNPKG

zigbee-herdsman-converters

Version:

Collection of device converters to be used with zigbee-herdsman

550 lines (487 loc) 28.9 kB
'use strict'; /* eslint max-len: 0 */ const assert = require('assert'); const constants = require('./constants'); class Base { withEndpoint(endpointName) { this.endpoint = endpointName; if (this.hasOwnProperty('property')) { this.property = `${this.property}_${this.endpoint}`; } if (this.features) { for (const feature of this.features) { if (feature.property) { feature.property = `${feature.property}_${endpointName}`; feature.endpoint = endpointName; } } } return this; } withAccess(a) { assert(this.hasOwnProperty('access'), 'Cannot add access if not defined yet'); this.access = a; return this; } withProperty(property) { this.property = property; return this; } withDescription(description) { this.description = description; return this; } removeFeature(feature) { assert(this.features, 'Does not have any features'); const f = this.features.find((f) => f.name === feature); assert(f, `Does not have feature '${feature}'`); this.features.splice(this.features.indexOf(f), 1); return this; } setAccess(feature, a) { assert(this.features, 'Does not have any features'); const f = this.features.find((f) => f.name === feature); assert(f.access !== a, `Access mode not changed for '${f.name}'`); f.access = a; return this; } } class Switch extends Base { constructor() { super(); this.type = 'switch'; this.features = []; } withState(property, toggle, description, access=a.ALL, value_on='ON', value_off='OFF') { assert(!this.endpoint, 'Cannot add feature after adding endpoint'); const feature = new Binary('state', access, value_on, value_off).withProperty(property).withDescription(description); if (toggle) { feature.withValueToggle('TOGGLE'); } this.features.push(feature); return this; } } class Lock extends Base { constructor() { super(); this.type = 'lock'; this.features = []; } withState(property, valueOn, valueOff, description, access=a.ALL) { assert(!this.endpoint, 'Cannot add feature after adding endpoint'); this.features.push(new Binary('state', access, valueOn, valueOff).withProperty(property).withDescription(description)); return this; } withLockState(property, description) { assert(!this.endpoint, 'Cannot add feature after adding endpoint'); this.features.push(new Enum('lock_state', access.STATE, ['not_fully_locked', 'locked', 'unlocked']).withProperty(property).withDescription(description)); return this; } } class Binary extends Base { constructor(name, access, valueOn, valueOff) { super(); this.type = 'binary'; this.name = name; this.property = name; this.access = access; this.value_on = valueOn; this.value_off = valueOff; } withValueToggle(value) { this.value_toggle = value; return this; } } class Numeric extends Base { constructor(name, access) { super(); this.type = 'numeric'; this.name = name; this.property = name; this.access = access; } withUnit(unit) { this.unit = unit; return this; } withValueMax(value) { this.value_max = value; return this; } withValueMin(value) { this.value_min = value; return this; } withValueStep(value) { this.value_step = value; return this; } withPreset(name, value, description) { if (!this.presets) this.presets = []; this.presets.push({name, value, description}); return this; } } class Enum extends Base { constructor(name, access, values) { super(); this.type = 'enum'; this.name = name; this.property = name; this.access = access; this.values = values; } } class Text extends Base { constructor(name, access) { super(); this.type = 'text'; this.name = name; this.property = name; this.access = access; } } class Composite extends Base { constructor(name, property) { super(); this.type = 'composite'; this.property = property; this.name = name; this.features = []; } withFeature(feature) { assert(!this.endpoint, 'Cannot add feature after adding endpoint'); this.features.push(feature); return this; } } class Light extends Base { constructor() { super(); this.type = 'light'; this.features = []; this.features.push(new Binary('state', access.ALL, 'ON', 'OFF').withValueToggle('TOGGLE').withDescription('On/off state of this light')); } withBrightness() { assert(!this.endpoint, 'Cannot add feature after adding endpoint'); this.features.push(new Numeric('brightness', access.ALL).withValueMin(0).withValueMax(254).withDescription('Brightness of this light')); return this; } withLevelConfig() { assert(!this.endpoint, 'Cannot add feature after adding endpoint'); const levelConfig = new Composite('level_config', 'level_config') .withFeature(new Numeric('on_off_transition_time', access.ALL) .withDescription('Represents the time taken to move to or from the target level when On of Off commands are received by an On/Off cluster'), ) .withFeature(new Numeric('on_transition_time', access.ALL) .withPreset('disabled', 65535, 'Use on_off_transition_time value') .withDescription('Represents the time taken to move the current level from the minimum level to the maximum level when an On command is received'), ) .withFeature(new Numeric('off_transition_time', access.ALL) .withPreset('disabled', 65535, 'Use on_off_transition_time value') .withDescription('Represents the time taken to move the current level from the maximum level to the minimum level when an Off command is received'), ) .withFeature(new Numeric('current_level_startup', access.ALL) .withValueMin(1).withValueMax(254) .withPreset('minimum', 0, 'Use minimum permitted value') .withPreset('previous', 255, 'Use previous value') .withDescription('Defines the desired startup level for a device when it is supplied with power'), ) .withDescription('Configure genLevelCtrl'); this.features.push(levelConfig); return this; } withColorTemp(range) { assert(!this.endpoint, 'Cannot add feature after adding endpoint'); const rangeProvided = range !== undefined; if (range === undefined) { range = [150, 500]; } const feature = new Numeric('color_temp', access.ALL).withUnit('mired').withValueMin(range[0]).withValueMax(range[1]) .withDescription('Color temperature of this light'); if (process.env.ZHC_TEST) { feature._colorTempRangeProvided = rangeProvided; } [ {name: 'coolest', value: range[0], description: 'Coolest temperature supported'}, {name: 'cool', value: 250, description: 'Cool temperature (250 mireds / 4000 Kelvin)'}, {name: 'neutral', value: 370, description: 'Neutral temperature (370 mireds / 2700 Kelvin)'}, {name: 'warm', value: 454, description: 'Warm temperature (454 mireds / 2200 Kelvin)'}, {name: 'warmest', value: range[1], description: 'Warmest temperature supported'}, ].filter((p) => p.value >= range[0] && p.value <= range[1]).forEach((p) => feature.withPreset(p.name, p.value, p.description)); this.features.push(feature); return this; } withColorTempStartup(range) { assert(!this.endpoint, 'Cannot add feature after adding endpoint'); if (range === undefined) { range = [150, 500]; } const feature = new Numeric('color_temp_startup', access.ALL).withUnit('mired').withValueMin(range[0]).withValueMax(range[1]) .withDescription('Color temperature after cold power on of this light'); [ {name: 'coolest', value: range[0], description: 'Coolest temperature supported'}, {name: 'cool', value: 250, description: 'Cool temperature (250 mireds / 4000 Kelvin)'}, {name: 'neutral', value: 370, description: 'Neutral temperature (370 mireds / 2700 Kelvin)'}, {name: 'warm', value: 454, description: 'Warm temperature (454 mireds / 2200 Kelvin)'}, {name: 'warmest', value: range[1], description: 'Warmest temperature supported'}, ].filter((p) => p.value >= range[0] && p.value <= range[1]).forEach((p) => feature.withPreset(p.name, p.value, p.description)); feature.withPreset('previous', 65535, 'Restore previous color_temp on cold power on'); this.features.push(feature); return this; } withColor(types) { assert(!this.endpoint, 'Cannot add feature after adding endpoint'); if (types.includes('xy')) { const colorXY = new Composite('color_xy', 'color') .withFeature(new Numeric('x', access.ALL)) .withFeature(new Numeric('y', access.ALL)) .withDescription('Color of this light in the CIE 1931 color space (x/y)'); this.features.push(colorXY); } if (types.includes('hs')) { const colorHS = new Composite('color_hs', 'color') .withFeature(new Numeric('hue', access.ALL)) .withFeature(new Numeric('saturation', access.ALL)) .withDescription('Color of this light expressed as hue/saturation'); this.features.push(colorHS); } return this; } } class Cover extends Base { constructor() { super(); this.type = 'cover'; this.features = []; this.features.push(new Binary('state', access.STATE_SET, 'OPEN', 'CLOSE')); } withPosition() { assert(!this.endpoint, 'Cannot add feature after adding endpoint'); this.features.push(new Numeric('position', access.ALL).withValueMin(0).withValueMax(100).withDescription('Position of this cover')); return this; } withTilt() { assert(!this.endpoint, 'Cannot add feature after adding endpoint'); this.features.push(new Numeric('tilt', access.ALL).withValueMin(0).withValueMax(100).withDescription('Tilt of this cover')); return this; } } class Fan extends Base { constructor() { super(); this.type = 'fan'; this.features = []; this.features.push(new Binary('state', access.ALL, 'ON', 'OFF').withDescription('On/off state of this fan').withProperty('fan_state')); } withModes(modes, access=a.ALL) { assert(!this.endpoint, 'Cannot add feature after adding endpoint'); this.features.push(new Enum('mode', access, modes).withProperty('fan_mode').withDescription('Mode of this fan')); return this; } } class Climate extends Base { constructor() { super(); this.type = 'climate'; this.features = []; } withSetpoint(property, min, max, step, access=a.ALL) { assert(!this.endpoint, 'Cannot add feature after adding endpoint'); assert(['occupied_heating_setpoint', 'current_heating_setpoint', 'occupied_cooling_setpoint'].includes(property)); this.features.push(new Numeric(property, access) .withValueMin(min).withValueMax(max).withValueStep(step).withUnit('°C').withDescription('Temperature setpoint')); return this; } withLocalTemperature(access=a.STATE_GET) { assert(!this.endpoint, 'Cannot add feature after adding endpoint'); this.features.push(new Numeric('local_temperature', access).withUnit('°C').withDescription('Current temperature measured on the device')); return this; } withLocalTemperatureCalibration(access=a.ALL) { assert(!this.endpoint, 'Cannot add feature after adding endpoint'); this.features.push(new Numeric('local_temperature_calibration', access).withUnit('°C').withDescription('Offset to be used in the local_temperature')); return this; } withSystemMode(modes, access=a.ALL) { assert(!this.endpoint, 'Cannot add feature after adding endpoint'); const allowed = ['off', 'heat', 'cool', 'auto', 'dry', 'fan_only', 'sleep']; modes.forEach((m) => assert(allowed.includes(m))); this.features.push(new Enum('system_mode', access, modes).withDescription('Mode of this device')); return this; } withRunningState(modes, access=a.STATE_GET) { assert(!this.endpoint, 'Cannot add feature after adding endpoint'); const allowed = ['idle', 'heat', 'cool']; modes.forEach((m) => assert(allowed.includes(m))); this.features.push(new Enum('running_state', access, modes).withDescription('The current running state')); return this; } withFanMode(modes) { assert(!this.endpoint, 'Cannot add feature after adding endpoint'); const allowed = ['off', 'low', 'medium', 'high', 'on', 'auto', 'smart']; modes.forEach((m) => assert(allowed.includes(m))); this.features.push(new Enum('fan_mode', access.ALL, modes).withDescription('Mode of the fan')); return this; } withSensor(sensors) { assert(!this.endpoint, 'Cannot add feature after adding endpoint'); this.features.push(new Enum('sensor', access.STATE_SET, sensors).withDescription('Select temperature sensor to use')); return this; } withPreset(modes) { assert(!this.endpoint, 'Cannot add feature after adding endpoint'); this.features.push(new Enum('preset', access.STATE_SET, modes).withDescription('Mode of this device (similar to system_mode)')); return this; } withAwayMode() { assert(!this.endpoint, 'Cannot add feature after adding endpoint'); this.features.push(new Binary('away_mode', access.STATE_SET, 'ON', 'OFF').withDescription('Away mode')); return this; } withPiHeatingDemand(access=a.STATE) { assert(!this.endpoint, 'Cannot add feature after adding endpoint'); this.features.push(new Numeric('pi_heating_demand', access).withValueMin(0).withValueMax(100).withUnit('%').withDescription('Position of the valve (= demanded heat) where 0% is fully closed and 100% is fully open')); return this; } } /** * The access property is a 3-bit bitmask. */ const access = { /** * Bit 0: The property can be found in the published state of this device */ STATE: 0b001, /** * Bit 1: The property can be set with a /set command */ SET: 0b010, /** * Bit 2: The property can be retrieved with a /get command */ GET: 0b100, /** * Bitwise inclusive OR of STATE and SET : 0b001 | 0b010 */ STATE_SET: 0b011, /** * Bitwise inclusive OR of STATE and GET : 0b001 | 0b100 */ STATE_GET: 0b101, /** * Bitwise inclusive OR of STATE and GET and SET : 0b001 | 0b100 | 0b010 */ ALL: 0b111, }; const a = access; module.exports = { access, binary: (name, access, valueOn, valueOff) => new Binary(name, access, valueOn, valueOff), climate: () => new Climate(), composite: (name, property) => new Composite(name, property), cover: () => new Cover(), enum: (name, access, values) => new Enum(name, access, values), light: () => new Light(), numeric: (name, access) => new Numeric(name, access), switch: () => new Switch(), text: (name, access) => new Text(name, access), presets: { action: (values) => new Enum('action', access.STATE, values).withDescription('Triggered action (e.g. a button click)'), angle: (name) => new Numeric(name, access.STATE).withValueMin(-360).withValueMax(360), angle_axis: (name) => new Numeric(name, access.STATE).withValueMin(-90).withValueMax(90), aqi: () => new Numeric('aqi', access.STATE).withDescription('Air quality index'), auto_lock: () => new Switch().withState('auto_lock', false, 'Enable/disable auto lock', access.STATE_SET, 'AUTO', 'MANUAL'), auto_relock_time: () => new Numeric('auto_relock_time', access.ALL).withValueMin(0).withUnit('s').withDescription('The number of seconds to wait after unlocking a lock before it automatically locks again. 0=disabled'), away_mode: () => new Switch().withState('away_mode', false, 'Enable/disable away mode', access.STATE_SET), away_preset_days: () => new Numeric('away_preset_days', access.STATE_SET).withDescription('Away preset days'), away_preset_temperature: () => new Numeric('away_preset_temperature', access.STATE_SET).withUnit('°C').withDescription('Away preset temperature'), battery: () => new Numeric('battery', access.STATE).withUnit('%').withDescription('Remaining battery in %').withValueMin(0).withValueMax(100), battery_low: () => new Binary('battery_low', access.STATE, true, false).withDescription('Indicates if the battery of this device is almost empty'), battery_voltage: () => new Numeric('voltage', access.STATE).withUnit('mV').withDescription('Voltage of the battery in millivolts'), boost_time: () => new Numeric('boost_time', access.STATE_SET).withUnit('s').withDescription('Boost time'), carbon_monoxide: () => new Binary('carbon_monoxide', access.STATE, true, false).withDescription('Indicates if CO (carbon monoxide) is detected'), child_lock: () => new Lock().withState('child_lock', 'LOCK', 'UNLOCK', 'Enables/disables physical input on the device', access.STATE_SET), co2: () => new Numeric('co2', access.STATE).withUnit('ppm').withDescription('The measured CO2 (carbon monoxide) value'), comfort_temperature: () => new Numeric('comfort_temperature', access.STATE_SET).withUnit('°C').withDescription('Comfort temperature'), consumer_connected: () => new Binary('consumer_connected', access.STATE, true, false).withDescription('Indicates whether device is physically attached. Device does not have to pull power or even be connected electrically (switch can be ON even if switch is OFF).'), consumer_overload: () => new Numeric('consumer_overload', access.STATE, true, false).withUnit('W').withDescription('Indicates with how many Watts the maximum possible power consumption is exceeded'), contact: () => new Binary('contact', access.STATE, false, true).withDescription('Indicates if the contact is closed (= true) or open (= false)'), cover_position: () => new Cover().withPosition(), cover_position_tilt: () => new Cover().withPosition().withTilt(), cpu_temperature: () => new Numeric('cpu_temperature', access.STATE).withUnit('°C').withDescription('Temperature of the CPU'), cube_side: (name) => new Numeric(name, access.STATE).withDescription('Side of the cube').withValueMin(0).withValueMax(6).withValueStep(1), current: () => new Numeric('current', access.STATE).withUnit('A').withDescription('Instantaneous measured electrical current'), current_phase_b: () => new Numeric('current_phase_b', access.STATE).withUnit('A').withDescription('Instantaneous measured electrical current on phase B'), current_phase_c: () => new Numeric('current_phase_c', access.STATE).withUnit('A').withDescription('Instantaneous measured electrical current on phase C'), deadzone_temperature: () => new Numeric('deadzone_temperature', access.STATE_SET).withUnit('°C').withDescription('The delta between local_temperature and current_heating_setpoint to trigger Heat. 1-5'), device_temperature: () => new Numeric('device_temperature', access.STATE).withUnit('°C').withDescription('Temperature of the device'), eco2: () => new Numeric('eco2', access.STATE).withUnit('ppm').withDescription('Measured eCO2 value'), eco_temperature: () => new Numeric('eco_temperature', access.STATE_SET).withUnit('°C').withDescription('Eco temperature'), effect: () => new Enum('effect', access.SET, ['blink', 'breathe', 'okay', 'channel_change', 'finish_effect', 'stop_effect']).withDescription('Triggers an effect on the light (e.g. make light blink for a few seconds)'), energy: () => new Numeric('energy', access.STATE).withUnit('kWh').withDescription('Sum of consumed energy'), fan: () => new Fan(), force: () => new Enum('force', access.STATE_SET, ['normal', 'open', 'close']).withDescription('Force the valve position'), formaldehyd: () => new Numeric('formaldehyd', access.STATE).withDescription('The measured formaldehyd value'), gas: () => new Binary('gas', access.STATE, true, false).withDescription('Indicates whether the device detected gas'), hcho: () => new Numeric('hcho', access.STATE).withUnit('mg/m³').withDescription('Measured Hcho value'), humidity: () => new Numeric('humidity', access.STATE).withUnit('%').withDescription('Measured relative humidity'), illuminance: () => new Numeric('illuminance', access.STATE).withDescription('Raw measured illuminance'), illuminance_lux: () => new Numeric('illuminance_lux', access.STATE).withUnit('lx').withDescription('Measured illuminance in lux'), keypad_lockout: () => new Binary('keypad_lockout', access.ALL, 'lock1', 'unlock').withDescription('Enables/disables physical input on the device'), led_disabled_night: () => new Binary('led_disabled_night', access.STATE_SET, true, false).withDescription('Enable/disable the LED at night'), light_brightness: () => new Light().withBrightness(), light_brightness_color: () => new Light().withBrightness().withColor((['xy', 'hs'])), light_brightness_colorhs: () => new Light().withBrightness().withColor(['hs']), light_brightness_colortemp: (colorTempRange) => new Light().withBrightness().withColorTemp(colorTempRange).withColorTempStartup(colorTempRange), light_brightness_colortemp_color: (colorTempRange) => new Light().withBrightness().withColorTemp(colorTempRange).withColorTempStartup(colorTempRange).withColor(['xy', 'hs']), light_brightness_colortemp_colorhs: (colorTempRange) => new Light().withBrightness().withColorTemp(colorTempRange).withColorTempStartup(colorTempRange).withColor(['hs']), light_brightness_colortemp_colorxy: (colorTempRange) => new Light().withBrightness().withColorTemp(colorTempRange).withColorTempStartup(colorTempRange).withColor(['xy']), light_brightness_colorxy: () => new Light().withBrightness().withColor((['xy'])), light_colorhs: () => new Light().withColor(['hs']), linkquality: () => new Numeric('linkquality', access.STATE).withUnit('lqi').withDescription('Link quality (signal strength)').withValueMin(0).withValueMax(255), local_temperature: () => new Numeric('local_temperature', access.STATE_GET).withUnit('°C').withDescription('Current temperature measured on the device'), lock: () => new Lock().withState('state', 'LOCK', 'UNLOCK', 'State of the lock').withLockState('lock_state', 'Actual state of the lock'), max_temperature: () => new Numeric('max_temperature', access.STATE_SET).withUnit('°C').withDescription('Maximum temperature'), max_temperature_limit: () => new Numeric('max_temperature_limit', access.STATE_SET).withUnit('°C').withDescription('Maximum temperature limit'), min_temperature: () => new Numeric('min_temperature', access.STATE_SET).withUnit('°C').withDescription('Minimum temperature'), occupancy: () => new Binary('occupancy', access.STATE, true, false).withDescription('Indicates whether the device detected occupancy'), pm10: () => new Numeric('pm10', access.STATE).withUnit('µg/m³').withDescription('Measured PM10 (particulate matter) concentration'), pm25: () => new Numeric('pm25', access.STATE).withUnit('µg/m³').withDescription('Measured PM2.5 (particulate matter) concentration'), position: () => new Numeric('position', access.STATE).withUnit('%').withDescription('Position'), power: () => new Numeric('power', access.STATE).withUnit('W').withDescription('Instantaneous measured power'), power_on_behavior: () => new Enum('power_on_behavior', access.ALL, ['off', 'previous', 'on']).withDescription('Controls the behavior when the device is powered on'), power_outage_memory: () => new Binary('power_outage_memory', access.ALL, true, false).withDescription('Enable/disable the power outage memory, this recovers the on/off mode after power failure'), presence: () => new Binary('presence', access.STATE, true, false).withDescription('Indicates whether the device detected presence'), pressure: () => new Numeric('pressure', access.STATE).withUnit('hPa').withDescription('The measured atmospheric pressure'), smoke: () => new Binary('smoke', access.STATE, true, false).withDescription('Indicates whether the device detected smoke'), soil_moisture: () => new Numeric('soil_moisture', access.STATE).withUnit('%').withDescription('Measured soil moisture value'), sos: () => new Binary('sos', access.STATE, true, false).withDescription('SOS alarm'), sound_volume: () => new Enum('sound_volume', access.ALL, constants.lockSoundVolume).withDescription('Sound volume of the lock'), switch: () => new Switch().withState('state', true, 'On/off state of the switch'), switch_type: () => new Enum('switch_type', access.ALL, ['toggle', 'momentary']).withDescription('Wall switch type'), tamper: () => new Binary('tamper', access.STATE, true, false).withDescription('Indicates whether the device is tampered'), temperature: () => new Numeric('temperature', access.STATE).withUnit('°C').withDescription('Measured temperature value'), test: () => new Binary('test', access.STATE, true, false).withDescription('Indicates whether the device is being tested'), valve_detection: () => new Switch().withState('valve_detection', true).setAccess('state', access.STATE_SET), vibration: () => new Binary('vibration', access.STATE, true, false).withDescription('Indicates whether the device detected vibration'), voc: () => new Numeric('voc', access.STATE).withUnit('ppb').withDescription('Measured VOC value'), voltage: () => new Numeric('voltage', access.STATE).withUnit('V').withDescription('Measured electrical potential value'), voltage_phase_b: () => new Numeric('voltage_phase_b', access.STATE).withUnit('V').withDescription('Measured electrical potential value on phase B'), voltage_phase_c: () => new Numeric('voltage_phase_c', access.STATE).withUnit('V').withDescription('Measured electrical potential value on phase C'), water_leak: () => new Binary('water_leak', access.STATE, true, false).withDescription('Indicates whether the device detected a water leak'), warning: () => new Composite('warning', 'warning') .withFeature(new Enum('mode', access.SET, ['stop', 'burglar', 'fire', 'emergency', 'police_panic', 'fire_panic', 'emergency_panic']).withDescription('Mode of the warning (sound effect)')) .withFeature(new Enum('level', access.SET, ['low', 'medium', 'high', 'very_high']).withDescription('Sound level')) .withFeature(new Binary('strobe', access.SET, true, false).withDescription('Turn on/off the strobe (light) during warning')) .withFeature(new Numeric('duration', access.SET).withUnit('s').withDescription('Duration in seconds of the alarm')), week: () => new Enum('week', access.STATE_SET, ['5+2', '6+1', '7']).withDescription('Week format user for schedule'), window_detection: () => new Switch().withState('window_detection', true, 'Enables/disables window detection on the device', access.STATE_SET), moving: () => new Binary('moving', access.STATE, true, false).withDescription('Indicates if the device is moving'), x_axis: () => new Numeric('x_axis', access.STATE).withDescription('Accelerometer X value'), y_axis: () => new Numeric('y_axis', access.STATE).withDescription('Accelerometer Y value'), z_axis: () => new Numeric('z_axis', access.STATE).withDescription('Accelerometer Z value'), }, };