mirror of
https://github.com/formtapez/ZigUP.git
synced 2025-02-23 09:34:50 +01:00
3384 lines
123 KiB
JavaScript
3384 lines
123 KiB
JavaScript
'use strict';
|
|
|
|
const debounce = require('debounce');
|
|
const common = require('./common');
|
|
|
|
const clickLookup = {
|
|
1: 'single',
|
|
2: 'double',
|
|
3: 'triple',
|
|
4: 'quadruple',
|
|
};
|
|
|
|
const occupancyTimeout = 90; // In seconds
|
|
|
|
const defaultPrecision = {
|
|
temperature: 2,
|
|
humidity: 2,
|
|
pressure: 1,
|
|
};
|
|
|
|
const precisionRoundOptions = (number, options, type) => {
|
|
const key = `${type}_precision`;
|
|
const defaultValue = defaultPrecision[type];
|
|
const precision = options && options.hasOwnProperty(key) ? options[key] : defaultValue;
|
|
return precisionRound(number, precision);
|
|
};
|
|
|
|
const precisionRound = (number, precision) => {
|
|
const factor = Math.pow(10, precision);
|
|
return Math.round(number * factor) / factor;
|
|
};
|
|
|
|
const calibrateOptions = (number, options, type) => {
|
|
const key = `${type}_calibration`;
|
|
let calibrationOffset = options && options.hasOwnProperty(key) ? options[key] : 0;
|
|
if (type == 'illuminance') {
|
|
// linear calibration because measured value is zero based
|
|
// +/- percent
|
|
calibrationOffset = Math.round(number * calibrationOffset / 100);
|
|
}
|
|
return number + calibrationOffset;
|
|
};
|
|
|
|
const toPercentage = (value, min, max) => {
|
|
if (value > max) {
|
|
value = max;
|
|
} else if (value < min) {
|
|
value = min;
|
|
}
|
|
|
|
const normalised = (value - min) / (max - min);
|
|
return (normalised * 100).toFixed(2);
|
|
};
|
|
|
|
const toPercentageCR2032 = (voltage) => {
|
|
let percentage = null;
|
|
|
|
if (voltage < 2100) {
|
|
percentage = 0;
|
|
} else if (voltage < 2440) {
|
|
percentage = 6 - ((2440 - voltage) * 6) / 340;
|
|
} else if (voltage < 2740) {
|
|
percentage = 18 - ((2740 - voltage) * 12) / 300;
|
|
} else if (voltage < 2900) {
|
|
percentage = 42 - ((2900 - voltage) * 24) / 160;
|
|
} else if (voltage < 3000) {
|
|
percentage = 100 - ((3000 - voltage) * 58) / 100;
|
|
} else if (voltage >= 3000) {
|
|
percentage = 100;
|
|
}
|
|
|
|
return Math.round(percentage);
|
|
};
|
|
|
|
const numberWithinRange = (number, min, max) => {
|
|
if (number > max) {
|
|
return max;
|
|
} else if (number < min) {
|
|
return min;
|
|
} else {
|
|
return number;
|
|
}
|
|
};
|
|
|
|
// get object property name (key) by it's value
|
|
const getKey = (object, value) => {
|
|
for (const key in object) {
|
|
if (object[key]==value) return key;
|
|
}
|
|
};
|
|
|
|
// Global variable store that can be used by devices.
|
|
const store = {};
|
|
|
|
const ictcg1 = (model, msg, publish, options, action) => {
|
|
const deviceID = msg.endpoints[0].device.ieeeAddr;
|
|
const payload = {};
|
|
|
|
if (!store[deviceID]) {
|
|
const _publish = debounce((msg) => publish(msg), 250);
|
|
store[deviceID] = {since: false, direction: false, value: 255, publish: _publish};
|
|
}
|
|
|
|
const s = store[deviceID];
|
|
if (s.since && s.direction) {
|
|
// Update value
|
|
const duration = Date.now() - s.since;
|
|
const delta = Math.round((duration / 10) * (s.direction === 'left' ? -1 : 1));
|
|
const newValue = s.value + delta;
|
|
if (newValue >= 0 && newValue <= 255) {
|
|
s.value = newValue;
|
|
}
|
|
}
|
|
|
|
if (action === 'move') {
|
|
s.since = Date.now();
|
|
const direction = msg.data.data.movemode === 1 ? 'left' : 'right';
|
|
s.direction = direction;
|
|
payload.action = `rotate_${direction}`;
|
|
} else if (action === 'stop' || action === 'level') {
|
|
if (action === 'level') {
|
|
s.value = msg.data.data.level;
|
|
const direction = s.value === 0 ? 'left' : 'right';
|
|
payload.action = `rotate_${direction}_quick`;
|
|
} else {
|
|
payload.action = 'rotate_stop';
|
|
}
|
|
|
|
s.since = false;
|
|
s.direction = false;
|
|
}
|
|
|
|
payload.brightness = s.value;
|
|
s.publish(payload);
|
|
};
|
|
|
|
const holdUpdateBrightness324131092621 = (deviceID) => {
|
|
if (store[deviceID] && store[deviceID].brightnessSince && store[deviceID].brightnessDirection) {
|
|
const duration = Date.now() - store[deviceID].brightnessSince;
|
|
const delta = (duration / 10) * (store[deviceID].brightnessDirection === 'up' ? 1 : -1);
|
|
const newValue = store[deviceID].brightnessValue + delta;
|
|
store[deviceID].brightnessValue = numberWithinRange(newValue, 0, 255);
|
|
}
|
|
};
|
|
|
|
|
|
const converters = {
|
|
HS2SK_power: {
|
|
cid: 'haElectricalMeasurement',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
return {
|
|
power: msg.data.data['activePower'] / 10,
|
|
current: msg.data.data['rmsCurrent'] / 100,
|
|
voltage: msg.data.data['rmsVoltage'] / 100,
|
|
};
|
|
},
|
|
},
|
|
generic_lock: {
|
|
cid: 'closuresDoorLock',
|
|
type: ['attReport', 'readRsp', 'devChange'],
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data.hasOwnProperty('lockState')) {
|
|
return {state: msg.data.data.lockState == 2 ? 'UNLOCK' : 'LOCK'};
|
|
}
|
|
},
|
|
},
|
|
generic_lock_operation_event: {
|
|
cid: 'closuresDoorLock',
|
|
type: 'cmdOperationEventNotification',
|
|
convert: (model, msg, publish, options) => {
|
|
return {
|
|
state: msg.data.data['opereventcode'] == 2 ? 'UNLOCK' : 'LOCK',
|
|
user: msg.data.data['userid'],
|
|
source: msg.data.data['opereventsrc'],
|
|
};
|
|
},
|
|
},
|
|
genOnOff_cmdOn: {
|
|
cid: 'genOnOff',
|
|
type: 'cmdOn',
|
|
convert: (model, msg, publish, options) => {
|
|
return {click: 'on'};
|
|
},
|
|
},
|
|
genOnOff_cmdOff: {
|
|
cid: 'genOnOff',
|
|
type: 'cmdOff',
|
|
convert: (model, msg, publish, options) => {
|
|
return {click: 'off'};
|
|
},
|
|
},
|
|
E1743_brightness_up: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdMove',
|
|
convert: (model, msg, publish, options) => {
|
|
return {click: 'brightness_down'};
|
|
},
|
|
},
|
|
E1743_brightness_down: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdMoveWithOnOff',
|
|
convert: (model, msg, publish, options) => {
|
|
return {click: 'brightness_up'};
|
|
},
|
|
},
|
|
E1743_brightness_stop: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdStopWithOnOff',
|
|
convert: (model, msg, publish, options) => {
|
|
return {click: 'brightness_stop'};
|
|
},
|
|
},
|
|
AC0251100NJ_long_middle: {
|
|
cid: 'lightingColorCtrl',
|
|
type: 'cmdMoveHue',
|
|
convert: (model, msg, publish, options) => {
|
|
return {click: 'long_middle'};
|
|
},
|
|
},
|
|
AV2010_34_click: {
|
|
cid: 'genScenes',
|
|
type: 'cmdRecall',
|
|
convert: (model, msg, publish, options) => {
|
|
return {click: msg.data.data.groupid};
|
|
},
|
|
},
|
|
bitron_power: {
|
|
cid: 'seMetering',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
return {power: parseFloat(msg.data.data['instantaneousDemand']) / 10.0};
|
|
},
|
|
},
|
|
bitron_occupancy: {
|
|
cid: 'ssIasZone',
|
|
type: 'statusChange',
|
|
convert: (model, msg, publish, options) => {
|
|
// The occupancy sensor only sends a message when motion detected.
|
|
// Therefore we need to publish the no_motion detected by ourselves.
|
|
const useOptionsTimeout = options && options.hasOwnProperty('occupancy_timeout');
|
|
const timeout = useOptionsTimeout ? options.occupancy_timeout : occupancyTimeout;
|
|
const deviceID = msg.endpoints[0].device.ieeeAddr;
|
|
|
|
// Stop existing timer because motion is detected and set a new one.
|
|
if (store[deviceID]) {
|
|
clearTimeout(store[deviceID]);
|
|
store[deviceID] = null;
|
|
}
|
|
|
|
if (timeout !== 0) {
|
|
store[deviceID] = setTimeout(() => {
|
|
publish({occupancy: false});
|
|
store[deviceID] = null;
|
|
}, timeout * 1000);
|
|
}
|
|
|
|
return {occupancy: true};
|
|
},
|
|
},
|
|
bitron_battery_att_report: {
|
|
cid: 'genPowerCfg',
|
|
type: 'attReport',
|
|
convert: (model, msg, publish, options) => {
|
|
const result = {};
|
|
if (typeof msg.data.data['batteryVoltage'] == 'number') {
|
|
const battery = {max: 3200, min: 2500};
|
|
const voltage = msg.data.data['batteryVoltage'] * 100;
|
|
result.battery = toPercentage(voltage, battery.min, battery.max);
|
|
result.voltage = voltage;
|
|
}
|
|
if (typeof msg.data.data['batteryAlarmState'] == 'number') {
|
|
result.battery_alarm_state = msg.data.data['batteryAlarmState'];
|
|
}
|
|
return result;
|
|
},
|
|
},
|
|
bitron_battery_dev_change: {
|
|
cid: 'genPowerCfg',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const result = {};
|
|
if (typeof msg.data.data['batteryVoltage'] == 'number') {
|
|
const battery = {max: 3200, min: 2500};
|
|
const voltage = msg.data.data['batteryVoltage'] * 100;
|
|
result.battery = toPercentage(voltage, battery.min, battery.max);
|
|
result.voltage = voltage;
|
|
}
|
|
if (typeof msg.data.data['batteryAlarmState'] == 'number') {
|
|
result.battery_alarm_state = msg.data.data['batteryAlarmState'];
|
|
}
|
|
return result;
|
|
},
|
|
},
|
|
bitron_thermostat_att_report: {
|
|
cid: 'hvacThermostat',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const result = {};
|
|
if (typeof msg.data.data['localTemp'] == 'number') {
|
|
result.local_temperature = precisionRound(msg.data.data['localTemp'], 2) / 100;
|
|
}
|
|
if (typeof msg.data.data['localTemperatureCalibration'] == 'number') {
|
|
result.local_temperature_calibration =
|
|
precisionRound(msg.data.data['localTemperatureCalibration'], 2) / 10;
|
|
}
|
|
if (typeof msg.data.data['occupiedHeatingSetpoint'] == 'number') {
|
|
result.occupied_heating_setpoint =
|
|
precisionRound(msg.data.data['occupiedHeatingSetpoint'], 2) / 100;
|
|
}
|
|
if (typeof msg.data.data['runningState'] == 'number') {
|
|
result.running_state = msg.data.data['runningState'];
|
|
}
|
|
if (typeof msg.data.data['batteryAlarmState'] == 'number') {
|
|
result.battery_alarm_state = msg.data.data['batteryAlarmState'];
|
|
}
|
|
return result;
|
|
},
|
|
},
|
|
bitron_thermostat_dev_change: {
|
|
cid: 'hvacThermostat',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const result = {};
|
|
if (typeof msg.data.data['localTemp'] == 'number') {
|
|
result.local_temperature = precisionRound(msg.data.data['localTemp'], 2) / 100;
|
|
}
|
|
if (typeof msg.data.data['localTemperatureCalibration'] == 'number') {
|
|
result.local_temperature_calibration =
|
|
precisionRound(msg.data.data['localTemperatureCalibration'], 2) / 10;
|
|
}
|
|
if (typeof msg.data.data['occupiedHeatingSetpoint'] == 'number') {
|
|
result.occupied_heating_setpoint =
|
|
precisionRound(msg.data.data['occupiedHeatingSetpoint'], 2) / 100;
|
|
}
|
|
if (typeof msg.data.data['runningState'] == 'number') {
|
|
result.running_state = msg.data.data['runningState'];
|
|
}
|
|
if (typeof msg.data.data['batteryAlarmState'] == 'number') {
|
|
result.battery_alarm_state = msg.data.data['batteryAlarmState'];
|
|
}
|
|
return result;
|
|
},
|
|
},
|
|
nue_click: {
|
|
cid: 'genScenes',
|
|
type: 'cmdRecall',
|
|
convert: (model, msg, publish, options) => {
|
|
return {click: msg.data.data.sceneid};
|
|
},
|
|
},
|
|
smartthings_contact: {
|
|
cid: 'ssIasZone',
|
|
type: 'statusChange',
|
|
convert: (model, msg, publish, options) => {
|
|
return {contact: msg.data.zoneStatus === 48};
|
|
},
|
|
},
|
|
xiaomi_battery_3v: {
|
|
cid: 'genBasic',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
let voltage = null;
|
|
|
|
if (msg.data.data['65281']) {
|
|
voltage = msg.data.data['65281']['1'];
|
|
} else if (msg.data.data['65282']) {
|
|
voltage = msg.data.data['65282']['1'].elmVal;
|
|
}
|
|
|
|
if (voltage) {
|
|
return {
|
|
battery: parseFloat(toPercentageCR2032(voltage)),
|
|
voltage: voltage,
|
|
};
|
|
}
|
|
},
|
|
},
|
|
RTCGQ11LM_interval: {
|
|
cid: 'genBasic',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data['65281']) {
|
|
return {illuminance: msg.data.data['65281']['11']};
|
|
}
|
|
},
|
|
},
|
|
WSDCGQ01LM_WSDCGQ11LM_interval: {
|
|
cid: 'genBasic',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data['65281']) {
|
|
const temperature = parseFloat(msg.data.data['65281']['100']) / 100.0;
|
|
const humidity = parseFloat(msg.data.data['65281']['101']) / 100.0;
|
|
const result = {};
|
|
|
|
// https://github.com/Koenkk/zigbee2mqtt/issues/798
|
|
// Sometimes the sensor publishes non-realistic vales, as the sensor only works from
|
|
// -20 till +60, don't produce messages beyond these values.
|
|
if (temperature > -25 && temperature < 65) {
|
|
result.temperature = precisionRoundOptions(temperature, options, 'temperature');
|
|
}
|
|
|
|
// in the 0 - 100 range, don't produce messages beyond these values.
|
|
if (humidity >= 0 && humidity <= 100) {
|
|
result.humidity = precisionRoundOptions(humidity, options, 'humidity');
|
|
}
|
|
|
|
// Check if contains pressure (WSDCGQ11LM only)
|
|
if (msg.data.data['65281'].hasOwnProperty('102')) {
|
|
const pressure = parseFloat(msg.data.data['65281']['102']) / 100.0;
|
|
result.pressure = precisionRoundOptions(pressure, options, 'pressure');
|
|
}
|
|
|
|
return result;
|
|
}
|
|
},
|
|
},
|
|
WXKG01LM_click: {
|
|
cid: 'genOnOff',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const deviceID = msg.endpoints[0].device.ieeeAddr;
|
|
const state = msg.data.data['onOff'];
|
|
|
|
if (!store[deviceID]) {
|
|
store[deviceID] = {};
|
|
}
|
|
|
|
// 0 = click down, 1 = click up, else = multiple clicks
|
|
if (state === 0) {
|
|
store[deviceID].timer = setTimeout(() => {
|
|
publish({click: 'long'});
|
|
store[deviceID].timer = null;
|
|
store[deviceID].long = Date.now();
|
|
}, options.long_timeout || 1000); // After 1000 milliseconds of not releasing we assume long click.
|
|
} else if (state === 1) {
|
|
if (store[deviceID].long) {
|
|
const duration = Date.now() - store[deviceID].long;
|
|
publish({click: 'long_release', duration: duration});
|
|
store[deviceID].long = false;
|
|
}
|
|
|
|
if (store[deviceID].timer) {
|
|
clearTimeout(store[deviceID].timer);
|
|
store[deviceID].timer = null;
|
|
publish({click: 'single'});
|
|
}
|
|
} else {
|
|
const clicks = msg.data.data['32768'];
|
|
const payload = clickLookup[clicks] ? clickLookup[clicks] : 'many';
|
|
publish({click: payload});
|
|
}
|
|
},
|
|
},
|
|
generic_temperature: {
|
|
cid: 'msTemperatureMeasurement',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const temperature = parseFloat(msg.data.data['measuredValue']) / 100.0;
|
|
const calTemperature = calibrateOptions(temperature, options, 'temperature');
|
|
return {temperature: precisionRoundOptions(calTemperature, options, 'temperature')};
|
|
},
|
|
},
|
|
generic_temperature_change: {
|
|
cid: 'msTemperatureMeasurement',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const temperature = parseFloat(msg.data.data['measuredValue']) / 100.0;
|
|
const calTemperature = calibrateOptions(temperature, options, 'temperature');
|
|
return {temperature: precisionRoundOptions(calTemperature, options, 'temperature')};
|
|
},
|
|
},
|
|
xiaomi_temperature: {
|
|
cid: 'msTemperatureMeasurement',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const temperature = parseFloat(msg.data.data['measuredValue']) / 100.0;
|
|
|
|
// https://github.com/Koenkk/zigbee2mqtt/issues/798
|
|
// Sometimes the sensor publishes non-realistic vales, as the sensor only works from
|
|
// -20 till +60, don't produce messages beyond these values.
|
|
if (temperature > -25 && temperature < 65) {
|
|
return {temperature: precisionRoundOptions(temperature, options, 'temperature')};
|
|
}
|
|
},
|
|
},
|
|
MFKZQ01LM_action_multistate: {
|
|
cid: 'genMultistateInput',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
/*
|
|
Source: https://github.com/kirovilya/ioBroker.zigbee
|
|
+---+
|
|
| 2 |
|
|
+---+---+---+
|
|
| 4 | 0 | 1 |
|
|
+---+---+---+
|
|
|M5I|
|
|
+---+
|
|
| 3 |
|
|
+---+
|
|
Side 5 is with the MI logo, side 3 contains the battery door.
|
|
presentValue = 0 = shake
|
|
presentValue = 2 = wakeup
|
|
presentValue = 3 = fly/fall
|
|
presentValue = y + x * 8 + 64 = 90º Flip from side x on top to side y on top
|
|
presentValue = x + 128 = 180º flip to side x on top
|
|
presentValue = x + 256 = push/slide cube while side x is on top
|
|
presentValue = x + 512 = double tap while side x is on top
|
|
*/
|
|
const value = msg.data.data['presentValue'];
|
|
let action = null;
|
|
|
|
if (value === 0) action = {'action': 'shake'};
|
|
else if (value === 2) action = {'action': 'wakeup'};
|
|
else if (value === 3) action = {'action': 'fall'};
|
|
else if (value >= 512) action = {'action': 'tap', 'side': value-512};
|
|
else if (value >= 256) action = {'action': 'slide', 'side': value-256};
|
|
else if (value >= 128) action = {'action': 'flip180', 'side': value-128};
|
|
else if (value >= 64) {
|
|
action = {'action': 'flip90', 'from_side': Math.floor((value-64) / 8), 'to_side': value % 8};
|
|
}
|
|
|
|
return action ? action : null;
|
|
},
|
|
},
|
|
MFKZQ01LM_action_analog: {
|
|
cid: 'genAnalogInput',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
/*
|
|
Source: https://github.com/kirovilya/ioBroker.zigbee
|
|
presentValue = rotation angle left < 0, right > 0
|
|
*/
|
|
const value = msg.data.data['presentValue'];
|
|
return {
|
|
action: value < 0 ? 'rotate_left' : 'rotate_right',
|
|
angle: Math.floor(value * 100) / 100,
|
|
};
|
|
},
|
|
},
|
|
WXKG12LM_action_click_multistate: {
|
|
cid: 'genMultistateInput',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const value = msg.data.data['presentValue'];
|
|
const lookup = {
|
|
1: {click: 'single'}, // single click
|
|
2: {click: 'double'}, // double click
|
|
16: {action: 'hold'}, // hold for more than 400ms
|
|
17: {action: 'release'}, // release after hold for more than 400ms
|
|
18: {action: 'shake'}, // shake
|
|
};
|
|
|
|
return lookup[value] ? lookup[value] : null;
|
|
},
|
|
},
|
|
xiaomi_action_click_multistate: {
|
|
cid: 'genMultistateInput',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const value = msg.data.data['presentValue'];
|
|
const lookup = {
|
|
1: {click: 'single'}, // single click
|
|
2: {click: 'double'}, // double click
|
|
0: {action: 'hold'}, // hold for more than 400ms
|
|
255: {action: 'release'}, // release after hold for more than 400ms
|
|
};
|
|
|
|
return lookup[value] ? lookup[value] : null;
|
|
},
|
|
},
|
|
generic_humidity: {
|
|
cid: 'msRelativeHumidity',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const humidity = parseFloat(msg.data.data['measuredValue']) / 100.0;
|
|
|
|
// https://github.com/Koenkk/zigbee2mqtt/issues/798
|
|
// Sometimes the sensor publishes non-realistic vales, it should only publish message
|
|
// in the 0 - 100 range, don't produce messages beyond these values.
|
|
if (humidity >= 0 && humidity <= 100) {
|
|
return {humidity: precisionRoundOptions(humidity, options, 'humidity')};
|
|
}
|
|
},
|
|
},
|
|
generic_occupancy: {
|
|
// This is for occupancy sensor that send motion start AND stop messages
|
|
// Note: options.occupancy_timeout not available yet, to implement it will be
|
|
// needed to update device report intervall as well, see devices.js
|
|
cid: 'msOccupancySensing',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data.occupancy === 0) {
|
|
return {occupancy: false};
|
|
} else if (msg.data.data.occupancy === 1) {
|
|
return {occupancy: true};
|
|
}
|
|
},
|
|
},
|
|
E1525_occupancy: {
|
|
cid: 'genOnOff',
|
|
type: 'cmdOnWithTimedOff',
|
|
convert: (model, msg, publish, options) => {
|
|
const timeout = msg.data.data.ontime / 10;
|
|
const deviceID = msg.endpoints[0].device.ieeeAddr;
|
|
|
|
// Stop existing timer because motion is detected and set a new one.
|
|
if (store[deviceID]) {
|
|
clearTimeout(store[deviceID]);
|
|
store[deviceID] = null;
|
|
}
|
|
|
|
if (timeout !== 0) {
|
|
store[deviceID] = setTimeout(() => {
|
|
publish({occupancy: false});
|
|
store[deviceID] = null;
|
|
}, timeout * 1000);
|
|
}
|
|
|
|
return {occupancy: true};
|
|
},
|
|
},
|
|
generic_occupancy_no_off_msg: {
|
|
// This is for occupancy sensor that only send a message when motion detected,
|
|
// but do not send a motion stop.
|
|
// Therefore we need to publish the no_motion detected by ourselves.
|
|
cid: 'msOccupancySensing',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data.occupancy !== 1) {
|
|
// In case of 0 no occupancy is reported.
|
|
// https://github.com/Koenkk/zigbee2mqtt/issues/467
|
|
return;
|
|
}
|
|
|
|
// The occupancy sensor only sends a message when motion detected.
|
|
// Therefore we need to publish the no_motion detected by ourselves.
|
|
const useOptionsTimeout = options && options.hasOwnProperty('occupancy_timeout');
|
|
const timeout = useOptionsTimeout ? options.occupancy_timeout : occupancyTimeout;
|
|
const deviceID = msg.endpoints[0].device.ieeeAddr;
|
|
|
|
// Stop existing timers because motion is detected and set a new one.
|
|
if (store[deviceID]) {
|
|
store[deviceID].forEach((t) => clearTimeout(t));
|
|
}
|
|
|
|
store[deviceID] = [];
|
|
|
|
if (timeout !== 0) {
|
|
const timer = setTimeout(() => {
|
|
publish({occupancy: false});
|
|
}, timeout * 1000);
|
|
|
|
store[deviceID].push(timer);
|
|
}
|
|
|
|
// No occupancy since
|
|
if (options && options.no_occupancy_since) {
|
|
options.no_occupancy_since.forEach((since) => {
|
|
const timer = setTimeout(() => {
|
|
publish({no_occupancy_since: since});
|
|
}, since * 1000);
|
|
store[deviceID].push(timer);
|
|
});
|
|
}
|
|
|
|
if (options && options.no_occupancy_since) {
|
|
return {occupancy: true, no_occupancy_since: 0};
|
|
} else {
|
|
return {occupancy: true};
|
|
}
|
|
},
|
|
},
|
|
xiaomi_contact: {
|
|
cid: 'genOnOff',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
return {contact: msg.data.data['onOff'] === 0};
|
|
},
|
|
},
|
|
xiaomi_contact_interval: {
|
|
cid: 'genBasic',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data.hasOwnProperty('65281') && msg.data.data['65281'].hasOwnProperty('100')) {
|
|
return {contact: msg.data.data['65281']['100'] === 0};
|
|
}
|
|
},
|
|
},
|
|
brightness: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data.hasOwnProperty('currentLevel')) {
|
|
return {brightness: msg.data.data['currentLevel']};
|
|
}
|
|
},
|
|
},
|
|
color_colortemp: {
|
|
cid: 'lightingColorCtrl',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const result = {};
|
|
|
|
if (msg.data.data['colorTemperature']) {
|
|
result.color_temp = msg.data.data['colorTemperature'];
|
|
}
|
|
|
|
if (msg.data.data['colorMode']) {
|
|
result.color_mode = msg.data.data['colorMode'];
|
|
}
|
|
|
|
if (
|
|
msg.data.data['currentX']
|
|
|| msg.data.data['currentY']
|
|
|| msg.data.data['currentSaturation']
|
|
|| msg.data.data['enhancedCurrentHue']
|
|
) {
|
|
result.color = {};
|
|
|
|
if (msg.data.data['currentX']) {
|
|
result.color.x = precisionRound(msg.data.data['currentX'] / 65535, 4);
|
|
}
|
|
|
|
if (msg.data.data['currentY']) {
|
|
result.color.y = precisionRound(msg.data.data['currentY'] / 65535, 4);
|
|
}
|
|
|
|
if (msg.data.data['currentSaturation']) {
|
|
result.color.saturation = precisionRound(msg.data.data['currentSaturation'] / 2.54, 1);
|
|
}
|
|
|
|
if (msg.data.data['enhancedCurrentHue']) {
|
|
result.color.hue = precisionRound(msg.data.data['enhancedCurrentHue'] / (65535 / 360), 1);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
},
|
|
},
|
|
color_colortemp_report: {
|
|
cid: 'lightingColorCtrl',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const result = {};
|
|
|
|
if (msg.data.data['colorTemperature']) {
|
|
result.color_temp = msg.data.data['colorTemperature'];
|
|
}
|
|
|
|
if (msg.data.data['colorMode']) {
|
|
result.color_mode = msg.data.data['colorMode'];
|
|
}
|
|
|
|
if (
|
|
msg.data.data['currentX']
|
|
|| msg.data.data['currentY']
|
|
|| msg.data.data['currentSaturation']
|
|
|| msg.data.data['enhancedCurrentHue']
|
|
) {
|
|
result.color = {};
|
|
|
|
if (msg.data.data['currentX']) {
|
|
result.color.x = precisionRound(msg.data.data['currentX'] / 65535, 4);
|
|
}
|
|
|
|
if (msg.data.data['currentY']) {
|
|
result.color.y = precisionRound(msg.data.data['currentY'] / 65535, 4);
|
|
}
|
|
|
|
if (msg.data.data['currentSaturation']) {
|
|
result.color.saturation = precisionRound(msg.data.data['currentSaturation'] / 2.54, 1);
|
|
}
|
|
|
|
if (msg.data.data['enhancedCurrentHue']) {
|
|
result.color.hue = precisionRound(msg.data.data['enhancedCurrentHue'] / (65535 / 360), 1);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
},
|
|
},
|
|
WXKG11LM_click: {
|
|
cid: 'genOnOff',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const data = msg.data.data;
|
|
let clicks;
|
|
|
|
if (data.onOff) {
|
|
clicks = 1;
|
|
} else if (data['32768']) {
|
|
clicks = data['32768'];
|
|
}
|
|
|
|
if (clickLookup[clicks]) {
|
|
return {click: clickLookup[clicks]};
|
|
}
|
|
},
|
|
},
|
|
generic_illuminance: {
|
|
cid: 'msIlluminanceMeasurement',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const illuminance = msg.data.data['measuredValue'];
|
|
const calIlluminance = calibrateOptions(illuminance, options, 'illuminance');
|
|
// calibration value in +/- percent!
|
|
return {illuminance: calIlluminance};
|
|
},
|
|
},
|
|
generic_pressure: {
|
|
cid: 'msPressureMeasurement',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const pressure = parseFloat(msg.data.data['measuredValue']);
|
|
const calPressure = calibrateOptions(pressure, options, 'pressure');
|
|
return {pressure: precisionRoundOptions(calPressure, options, 'pressure')};
|
|
},
|
|
},
|
|
WXKG02LM_click: {
|
|
cid: 'genOnOff',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const ep = msg.endpoints[0];
|
|
return {click: getKey(model.ep(ep.device), ep.epId)};
|
|
},
|
|
},
|
|
WXKG02LM_click_multistate: {
|
|
cid: 'genMultistateInput',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const ep = msg.endpoints[0];
|
|
const button = getKey(model.ep(ep.device), ep.epId);
|
|
const value = msg.data.data['presentValue'];
|
|
|
|
const actionLookup = {
|
|
0: 'long',
|
|
1: null,
|
|
2: 'double',
|
|
};
|
|
|
|
const action = actionLookup[value];
|
|
|
|
if (button) {
|
|
return {click: button + (action ? `_${action}` : '')};
|
|
}
|
|
},
|
|
},
|
|
WXKG03LM_click: {
|
|
cid: 'genOnOff',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
return {click: 'single'};
|
|
},
|
|
},
|
|
KEF1PA_arm: {
|
|
cid: 'ssIasAce',
|
|
type: 'cmdArm',
|
|
convert: (model, msg, publish, options) => {
|
|
const action = msg.data.data['armmode'];
|
|
delete msg.data.data['armmode'];
|
|
const modeLookup = {
|
|
0: 'home',
|
|
2: 'sleep',
|
|
3: 'away',
|
|
};
|
|
return {action: modeLookup[action]};
|
|
},
|
|
},
|
|
KEF1PA_panic: {
|
|
cid: 'ssIasAce',
|
|
type: 'cmdPanic',
|
|
convert: (model, msg, publish, options) => {
|
|
delete msg.data.data['armmode'];
|
|
return {action: 'panic'};
|
|
},
|
|
},
|
|
SJCGQ11LM_water_leak_iaszone: {
|
|
cid: 'ssIasZone',
|
|
type: 'statusChange',
|
|
convert: (model, msg, publish, options) => {
|
|
return {water_leak: msg.data.zoneStatus === 1};
|
|
},
|
|
},
|
|
SJCGQ11LM_water_leak_interval: {
|
|
cid: 'genBasic',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data.hasOwnProperty('65281') && msg.data.data['65281'].hasOwnProperty('100')) {
|
|
return {water_leak: msg.data.data['65281']['100'] === 1};
|
|
}
|
|
},
|
|
},
|
|
state: {
|
|
cid: 'genOnOff',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data.hasOwnProperty('onOff')) {
|
|
return {state: msg.data.data['onOff'] === 1 ? 'ON' : 'OFF'};
|
|
}
|
|
},
|
|
},
|
|
state_report: {
|
|
cid: 'genOnOff',
|
|
type: 'attReport',
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data.hasOwnProperty('onOff')) {
|
|
return {state: msg.data.data['onOff'] === 1 ? 'ON' : 'OFF'};
|
|
}
|
|
},
|
|
},
|
|
state_change: {
|
|
cid: 'genOnOff',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data.hasOwnProperty('onOff')) {
|
|
return {state: msg.data.data['onOff'] === 1 ? 'ON' : 'OFF'};
|
|
}
|
|
},
|
|
},
|
|
xiaomi_power: {
|
|
cid: 'genAnalogInput',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
return {power: precisionRound(msg.data.data['presentValue'], 2)};
|
|
},
|
|
},
|
|
xiaomi_plug_state: {
|
|
cid: 'genBasic',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data['65281']) {
|
|
const data = msg.data.data['65281'];
|
|
return {
|
|
state: data['100'] === 1 ? 'ON' : 'OFF',
|
|
power: precisionRound(data['152'], 2),
|
|
voltage: precisionRound(data['150'] * 0.1, 1),
|
|
consumption: precisionRound(data['149'], 2),
|
|
temperature: precisionRoundOptions(data['3'], options, 'temperature'),
|
|
};
|
|
}
|
|
},
|
|
},
|
|
xiaomi_bulb_interval: {
|
|
cid: 'genBasic',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data['65281']) {
|
|
const data = msg.data.data['65281'];
|
|
return {
|
|
state: data['100'] === 1 ? 'ON' : 'OFF',
|
|
brightness: data['101'],
|
|
color_temp: data['102'],
|
|
};
|
|
}
|
|
},
|
|
},
|
|
QBKG11LM_power: {
|
|
cid: 'genBasic',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data['65281']) {
|
|
const data = msg.data.data['65281'];
|
|
return {
|
|
power: precisionRound(data['152'], 2),
|
|
consumption: precisionRound(data['149'], 2),
|
|
temperature: precisionRoundOptions(data['3'], options, 'temperature'),
|
|
};
|
|
}
|
|
},
|
|
},
|
|
QBKG12LM_LLKZMK11LM_power: {
|
|
cid: 'genBasic',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data['65281']) {
|
|
const data = msg.data.data['65281'];
|
|
return {
|
|
power: precisionRound(data['152'], 2),
|
|
consumption: precisionRound(data['149'], 2),
|
|
temperature: precisionRoundOptions(data['3'], options, 'temperature'),
|
|
};
|
|
}
|
|
},
|
|
},
|
|
QBKG04LM_QBKG11LM_state: {
|
|
cid: 'genOnOff',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data['61440']) {
|
|
return {state: msg.data.data['onOff'] === 1 ? 'ON' : 'OFF'};
|
|
} else {
|
|
return {click: 'single'};
|
|
}
|
|
},
|
|
},
|
|
QBKG04LM_buttons: {
|
|
cid: 'genOnOff',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.endpoints[0].epId == 4) {
|
|
return {action: msg.data.data['onOff'] === 1 ? 'release' : 'hold'};
|
|
}
|
|
},
|
|
},
|
|
QBKG04LM_QBKG11LM_operation_mode: {
|
|
cid: 'genBasic',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const mappingMode = {
|
|
0x12: 'control_relay',
|
|
0xFE: 'decoupled',
|
|
};
|
|
const key = '65314';
|
|
if (msg.data.data.hasOwnProperty(key)) {
|
|
const mode = mappingMode[msg.data.data[key]];
|
|
return {operation_mode: mode};
|
|
}
|
|
},
|
|
},
|
|
QBKG03LM_QBKG12LM_LLKZMK11LM_state: {
|
|
cid: 'genOnOff',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data['61440']) {
|
|
const ep = msg.endpoints[0];
|
|
const key = `state_${getKey(model.ep(ep.device), ep.epId)}`;
|
|
const payload = {};
|
|
payload[key] = msg.data.data['onOff'] === 1 ? 'ON' : 'OFF';
|
|
return payload;
|
|
} else {
|
|
const mapping = {4: 'left', 5: 'right', 6: 'both'};
|
|
const button = mapping[msg.endpoints[0].epId];
|
|
return {click: button};
|
|
}
|
|
},
|
|
},
|
|
QBKG11LM_click: {
|
|
cid: 'genMultistateInput',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
if ([1, 2].includes(msg.data.data.presentValue)) {
|
|
const times = {1: 'single', 2: 'double'};
|
|
return {click: times[msg.data.data.presentValue]};
|
|
}
|
|
},
|
|
},
|
|
QBKG12LM_click: {
|
|
cid: 'genMultistateInput',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
if ([1, 2].includes(msg.data.data.presentValue)) {
|
|
const mapping = {5: 'left', 6: 'right', 7: 'both'};
|
|
const times = {1: 'single', 2: 'double'};
|
|
const button = mapping[msg.endpoints[0].epId];
|
|
return {click: `${button}_${times[msg.data.data.presentValue]}`};
|
|
}
|
|
},
|
|
},
|
|
QBKG03LM_buttons: {
|
|
cid: 'genOnOff',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const mapping = {4: 'left', 5: 'right'};
|
|
const button = mapping[msg.endpoints[0].epId];
|
|
if (button) {
|
|
const payload = {};
|
|
payload[`button_${button}`] = msg.data.data['onOff'] === 1 ? 'release' : 'hold';
|
|
return payload;
|
|
}
|
|
},
|
|
},
|
|
QBKG03LM_QBKG12LM_operation_mode: {
|
|
cid: 'genBasic',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const mappingButton = {
|
|
'65314': 'left',
|
|
'65315': 'right',
|
|
};
|
|
const mappingMode = {
|
|
0x12: 'control_left_relay',
|
|
0x22: 'control_right_relay',
|
|
0xFE: 'decoupled',
|
|
};
|
|
for (const key in mappingButton) {
|
|
if (msg.data.data.hasOwnProperty(key)) {
|
|
const payload = {};
|
|
const mode = mappingMode[msg.data.data[key]];
|
|
payload[`operation_mode_${mappingButton[key]}`] = mode;
|
|
return payload;
|
|
}
|
|
}
|
|
},
|
|
},
|
|
xiaomi_lock_report: {
|
|
cid: 'genBasic',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data['65328']) {
|
|
const data = msg.data.data['65328'];
|
|
const state = data.substr(2, 2);
|
|
const action = data.substr(4, 2);
|
|
const keynum = data.substr(6, 2);
|
|
if (state == 11) {
|
|
if (action == 1) {
|
|
// unknown key
|
|
return {keyerror: true, inserted: 'unknown'};
|
|
}
|
|
if (action == 3) {
|
|
// explicitly disabled key (i.e. reported lost)
|
|
return {keyerror: true, inserted: keynum};
|
|
}
|
|
if (action == 7) {
|
|
// strange object introduced into the cylinder (e.g. a lock pick)
|
|
return {keyerror: true, inserted: 'strange'};
|
|
}
|
|
}
|
|
if (state == 12) {
|
|
if (action == 1) {
|
|
return {inserted: keynum};
|
|
}
|
|
if (action == 11) {
|
|
return {forgotten: keynum};
|
|
}
|
|
}
|
|
}
|
|
},
|
|
},
|
|
ZNCLDJ11LM_curtain_genAnalogOutput_change: {
|
|
cid: 'genAnalogOutput',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => {
|
|
let running = false;
|
|
|
|
if (msg.data.data['61440']) {
|
|
running = msg.data.data['61440'] !== 0;
|
|
}
|
|
|
|
const position = precisionRound(msg.data.data['presentValue'], 2);
|
|
return {position: position, running: running};
|
|
},
|
|
},
|
|
ZNCLDJ11LM_curtain_genAnalogOutput_report: {
|
|
cid: 'genAnalogOutput',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
let running = false;
|
|
|
|
if (msg.data.data['61440']) {
|
|
running = msg.data.data['61440'] !== 0;
|
|
}
|
|
|
|
const position = precisionRound(msg.data.data['presentValue'], 2);
|
|
return {position: position, running: running};
|
|
},
|
|
},
|
|
JTYJGD01LMBW_smoke: {
|
|
cid: 'ssIasZone',
|
|
type: 'statusChange',
|
|
convert: (model, msg, publish, options) => {
|
|
return {smoke: msg.data.zoneStatus === 1};
|
|
},
|
|
},
|
|
heiman_pir: {
|
|
cid: 'ssIasZone',
|
|
type: 'statusChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const zoneStatus = msg.data.zoneStatus;
|
|
return {
|
|
occupancy: (zoneStatus & 1) > 0, // Bit 1 = Alarm: Motion detection
|
|
tamper: (zoneStatus & 1<<2) > 0, // Bit 3 = Tamper status
|
|
battery_low: (zoneStatus & 1<<3) > 0, // Bit 4 = Battery LOW indicator
|
|
};
|
|
},
|
|
},
|
|
heiman_smoke: {
|
|
cid: 'ssIasZone',
|
|
type: 'statusChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const zoneStatus = msg.data.zoneStatus;
|
|
return {
|
|
smoke: (zoneStatus & 1) > 0, // Bit 1 = Alarm: Smoke
|
|
battery_low: (zoneStatus & 1<<3) > 0, // Bit 4 = Battery LOW indicator
|
|
};
|
|
},
|
|
},
|
|
heiman_smart_controller_armmode: {
|
|
cid: 'ssIasAce',
|
|
type: 'cmdArm',
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data.armmode != null) {
|
|
const lookup = {
|
|
0: 'disarm',
|
|
1: 'arm_partial_zones',
|
|
3: 'arm_all_zones',
|
|
};
|
|
|
|
const value = msg.data.data.armmode;
|
|
return {action: lookup[value] || `armmode_${value}`};
|
|
}
|
|
},
|
|
},
|
|
heiman_smart_controller_emergency: {
|
|
cid: 'ssIasAce',
|
|
type: 'cmdEmergency',
|
|
convert: (model, msg, publish, options) => {
|
|
return {action: 'emergency'};
|
|
},
|
|
},
|
|
battery_200: {
|
|
cid: 'genPowerCfg',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const batt = msg.data.data.batteryPercentageRemaining;
|
|
const battLow = msg.data.data.batteryAlarmState;
|
|
const voltage = msg.data.data.batteryVoltage;
|
|
const results = {};
|
|
if (batt != null) {
|
|
const value = Math.round(batt/200.0*10000)/100; // Out of 200
|
|
results['battery'] = value;
|
|
}
|
|
if (battLow != null) {
|
|
if (battLow) {
|
|
results['battery_low'] = true;
|
|
} else {
|
|
results['battery_low'] = false;
|
|
}
|
|
}
|
|
if (voltage != null) {
|
|
results['voltage'] = voltage * 100;
|
|
}
|
|
return results;
|
|
},
|
|
},
|
|
heiman_smoke_enrolled: {
|
|
cid: 'ssIasZone',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const zoneId = msg.data.data.zoneId;
|
|
const zoneState = msg.data.data.zoneState;
|
|
const results = {};
|
|
if (zoneState) {
|
|
results['enrolled'] = true;
|
|
} else {
|
|
results['enrolled'] = false;
|
|
}
|
|
results['zone_id'] = zoneId;
|
|
return results;
|
|
},
|
|
},
|
|
heiman_gas: {
|
|
cid: 'ssIasZone',
|
|
type: 'statusChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const zoneStatus = msg.data.zoneStatus;
|
|
return {
|
|
gas: (zoneStatus & 1) > 0, // Bit 1 = Alarm: Gas
|
|
battery_low: (zoneStatus & 1<<3) > 0, // Bit 4 = Battery LOW indicator
|
|
};
|
|
},
|
|
},
|
|
heiman_water_leak: {
|
|
cid: 'ssIasZone',
|
|
type: 'statusChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const zoneStatus = msg.data.zoneStatus;
|
|
return {
|
|
water_leak: (zoneStatus & 1) > 0, // Bit 1 = Alarm: Water leak
|
|
tamper: (zoneStatus & 1<<2) > 0, // Bit 3 = Tamper status
|
|
battery_low: (zoneStatus & 1<<3) > 0, // Bit 4 = Battery LOW indicator
|
|
};
|
|
},
|
|
},
|
|
heiman_contact: {
|
|
cid: 'ssIasZone',
|
|
type: 'statusChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const zoneStatus = msg.data.zoneStatus;
|
|
return {
|
|
contact: (zoneStatus & 1) > 0, // Bit 1 = Alarm: Contact detection
|
|
tamper: (zoneStatus & 1<<2) > 0, // Bit 3 = Tamper status
|
|
battery_low: (zoneStatus & 1<<3) > 0, // Bit 4 = Battery LOW indicator
|
|
};
|
|
},
|
|
},
|
|
heiman_carbon_monoxide: {
|
|
cid: 'ssIasZone',
|
|
type: 'statusChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const zoneStatus = msg.data.zoneStatus;
|
|
return {
|
|
carbon_monoxide: (zoneStatus & 1) > 0, // Bit 1 = Alarm: Carbon monoxide
|
|
battery_low: (zoneStatus & 1<<3) > 0, // Bit 4 = Battery LOW indicator
|
|
};
|
|
},
|
|
},
|
|
JTQJBF01LMBW_gas: {
|
|
cid: 'ssIasZone',
|
|
type: 'statusChange',
|
|
convert: (model, msg, publish, options) => {
|
|
return {gas: msg.data.zoneStatus === 1};
|
|
},
|
|
},
|
|
JTQJBF01LMBW_gas_density: {
|
|
cid: 'genBasic',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const data = msg.data.data;
|
|
if (data && data['65281']) {
|
|
const basicAttrs = data['65281'];
|
|
if (basicAttrs.hasOwnProperty('100')) {
|
|
return {gas_density: basicAttrs['100']};
|
|
}
|
|
}
|
|
},
|
|
},
|
|
JTQJBF01LMBW_sensitivity: {
|
|
cid: 'ssIasZone',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const data = msg.data.data;
|
|
const lookup = {
|
|
'1': 'low',
|
|
'2': 'medium',
|
|
'3': 'high',
|
|
};
|
|
|
|
if (data && data.hasOwnProperty('65520')) {
|
|
const value = data['65520'];
|
|
if (value && value.startsWith('0x020')) {
|
|
return {
|
|
sensitivity: lookup[value.charAt(5)],
|
|
};
|
|
}
|
|
}
|
|
},
|
|
},
|
|
DJT11LM_vibration: {
|
|
cid: 'closuresDoorLock',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const result = {};
|
|
const vibrationLookup = {
|
|
1: 'vibration',
|
|
2: 'tilt',
|
|
3: 'drop',
|
|
};
|
|
|
|
if (msg.data.data['85']) {
|
|
const data = msg.data.data['85'];
|
|
result.action = vibrationLookup[data];
|
|
}
|
|
if (msg.data.data['1283']) {
|
|
const data = msg.data.data['1283'];
|
|
result.angle = data;
|
|
}
|
|
|
|
if (msg.data.data['1288']) {
|
|
const data = msg.data.data['1288'];
|
|
|
|
// array interpretation:
|
|
// 12 bit two's complement sign extended integer
|
|
// data[1][bit0..bit15] : x
|
|
// data[1][bit16..bit31]: y
|
|
// data[0][bit0..bit15] : z
|
|
// left shift first to preserve sign extension for 'x'
|
|
const x = ((data['1'] << 16) >> 16);
|
|
const y = (data['1'] >> 16);
|
|
// left shift first to preserve sign extension for 'z'
|
|
const z = ((data['0'] << 16) >> 16);
|
|
|
|
// calculate angle
|
|
result.angle_x = Math.round(Math.atan(x/Math.sqrt(y*y+z*z)) * 180 / Math.PI);
|
|
result.angle_y = Math.round(Math.atan(y/Math.sqrt(x*x+z*z)) * 180 / Math.PI);
|
|
result.angle_z = Math.round(Math.atan(z/Math.sqrt(x*x+y*y)) * 180 / Math.PI);
|
|
|
|
// calculate absolulte angle
|
|
const R = Math.sqrt(x * x + y * y + z * z);
|
|
result.angle_x_absolute = Math.round((Math.acos(x / R)) * 180 / Math.PI);
|
|
result.angle_y_absolute = Math.round((Math.acos(y / R)) * 180 / Math.PI);
|
|
}
|
|
|
|
return result;
|
|
},
|
|
},
|
|
generic_power: {
|
|
cid: 'seMetering',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const result = {};
|
|
const endpoint = msg.endpoints[0];
|
|
let factor = null;
|
|
|
|
if (endpoint.clusters.has('seMetering')) {
|
|
const attrs = endpoint.clusters['seMetering'].attrs;
|
|
|
|
if (attrs.multiplier && attrs.divisor) {
|
|
factor = attrs.multiplier / attrs.divisor;
|
|
}
|
|
}
|
|
|
|
if (msg.data.data.hasOwnProperty('instantaneousDemand')) {
|
|
let power = msg.data.data['instantaneousDemand'];
|
|
if (factor != null) {
|
|
power = (power * factor) * 1000; // kWh to Watt
|
|
}
|
|
result.power = precisionRound(power, 2);
|
|
}
|
|
|
|
if (factor != null && (msg.data.data.hasOwnProperty('currentSummDelivered') ||
|
|
msg.data.data.hasOwnProperty('currentSummReceived'))) {
|
|
result.energy = 0;
|
|
if (msg.data.data.hasOwnProperty('currentSummDelivered')) {
|
|
const data = msg.data.data['currentSummDelivered'];
|
|
const value = (parseInt(data[0]) << 32) + parseInt(data[1]);
|
|
result.energy += value * factor;
|
|
}
|
|
if (msg.data.data.hasOwnProperty('currentSummReceived')) {
|
|
const data = msg.data.data['currentSummReceived'];
|
|
const value = (parseInt(data[0]) << 32) + parseInt(data[1]);
|
|
result.energy -= value * factor;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
},
|
|
},
|
|
CC2530ROUTER_state: {
|
|
cid: 'genOnOff',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
return {state: true, led_state: msg.data.data['onOff'] === 1};
|
|
},
|
|
},
|
|
CC2530ROUTER_meta: {
|
|
cid: 'genBinaryValue',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const data = msg.data.data;
|
|
return {
|
|
description: data['description'],
|
|
type: data['inactiveText'],
|
|
rssi: data['presentValue'],
|
|
};
|
|
},
|
|
},
|
|
DNCKAT_S00X_state: {
|
|
cid: 'genOnOff',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const ep = msg.endpoints[0];
|
|
const key = `state_${getKey(model.ep(ep.device), ep.epId)}`;
|
|
const payload = {};
|
|
payload[key] = msg.data.data['onOff'] === 1 ? 'ON' : 'OFF';
|
|
return payload;
|
|
},
|
|
},
|
|
DNCKAT_S00X_buttons: {
|
|
cid: 'genOnOff',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const ep = msg.endpoints[0];
|
|
const key = `button_${getKey(model.ep(ep.device), ep.epId)}`;
|
|
const payload = {};
|
|
payload[key] = msg.data.data['onOff'] === 1 ? 'release' : 'hold';
|
|
return payload;
|
|
},
|
|
},
|
|
ZigUP_parse: {
|
|
cid: 'genOnOff',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const lookup = {
|
|
'0': 'timer',
|
|
'1': 'key',
|
|
'2': 'dig-in',
|
|
};
|
|
|
|
let ds18b20Id = null;
|
|
let ds18b20Value = null;
|
|
if (msg.data.data['41368']) {
|
|
ds18b20Id = msg.data.data['41368'].split(':')[0];
|
|
ds18b20Value = precisionRound(msg.data.data['41368'].split(':')[1], 2);
|
|
}
|
|
|
|
return {
|
|
state: msg.data.data['onOff'] === 1 ? 'ON' : 'OFF',
|
|
cpu_temperature: precisionRound(msg.data.data['41361'], 2),
|
|
external_temperature: precisionRound(msg.data.data['41362'], 1),
|
|
external_humidity: precisionRound(msg.data.data['41363'], 1),
|
|
s0_counts: msg.data.data['41364'],
|
|
adc_volt: precisionRound(msg.data.data['41365'], 3),
|
|
dig_input: msg.data.data['41366'],
|
|
reason: lookup[msg.data.data['41367']],
|
|
[`${ds18b20Id}`]: ds18b20Value,
|
|
};
|
|
},
|
|
},
|
|
Z809A_power: {
|
|
cid: 'haElectricalMeasurement',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
return {
|
|
power: msg.data.data['activePower'],
|
|
current: msg.data.data['rmsCurrent'],
|
|
voltage: msg.data.data['rmsVoltage'],
|
|
power_factor: msg.data.data['powerFactor'],
|
|
};
|
|
},
|
|
},
|
|
SP120_power: {
|
|
cid: 'haElectricalMeasurement',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const result = {};
|
|
|
|
if (msg.data.data.hasOwnProperty('activePower')) {
|
|
result.power = msg.data.data['activePower'];
|
|
}
|
|
|
|
if (msg.data.data.hasOwnProperty('rmsCurrent')) {
|
|
result.current = msg.data.data['rmsCurrent'] / 1000;
|
|
}
|
|
|
|
if (msg.data.data.hasOwnProperty('rmsVoltage')) {
|
|
result.voltage = msg.data.data['rmsVoltage'];
|
|
}
|
|
|
|
return result;
|
|
},
|
|
},
|
|
peanut_electrical: {
|
|
cid: 'haElectricalMeasurement',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const result = {};
|
|
const deviceID = msg.endpoints[0].device.ieeeAddr;
|
|
|
|
// initialize stored defaults with observed values
|
|
if (!store[deviceID]) {
|
|
store[deviceID] = {
|
|
acVoltageMultiplier: 180, acVoltageDivisor: 39321, acCurrentMultiplier: 72,
|
|
acCurrentDivisor: 39321, acPowerMultiplier: 10255, acPowerDivisor: 39321,
|
|
};
|
|
}
|
|
|
|
// if new multipliers/divisors come in, replace prior values or defaults
|
|
Object.keys(store[deviceID]).forEach((key) => {
|
|
if (msg.data.data.hasOwnProperty(key)) {
|
|
store[deviceID][key] = msg.data.data[key];
|
|
}
|
|
});
|
|
|
|
// if raw measurement comes in, apply stored/default multiplier and divisor
|
|
if (msg.data.data.hasOwnProperty('rmsVoltage')) {
|
|
result.voltage = (msg.data.data['rmsVoltage']
|
|
* store[deviceID].acVoltageMultiplier
|
|
/ store[deviceID].acVoltageDivisor).toFixed(2);
|
|
}
|
|
|
|
if (msg.data.data.hasOwnProperty('rmsCurrent')) {
|
|
result.current = (msg.data.data['rmsCurrent']
|
|
* store[deviceID].acCurrentMultiplier
|
|
/ store[deviceID].acCurrentDivisor).toFixed(2);
|
|
}
|
|
|
|
if (msg.data.data.hasOwnProperty('activePower')) {
|
|
result.power = (msg.data.data['activePower']
|
|
* store[deviceID].acPowerMultiplier
|
|
/ store[deviceID].acPowerDivisor).toFixed(2);
|
|
}
|
|
|
|
return result;
|
|
},
|
|
},
|
|
STS_PRS_251_presence: {
|
|
cid: 'genBinaryInput',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const useOptionsTimeout = options && options.hasOwnProperty('presence_timeout');
|
|
const timeout = useOptionsTimeout ? options.presence_timeout : 100; // 100 seconds by default
|
|
const deviceID = msg.endpoints[0].device.ieeeAddr;
|
|
|
|
// Stop existing timer because presence is detected and set a new one.
|
|
if (store.hasOwnProperty(deviceID)) {
|
|
clearTimeout(store[deviceID]);
|
|
store[deviceID] = null;
|
|
}
|
|
|
|
store[deviceID] = setTimeout(() => {
|
|
publish({presence: false});
|
|
store[deviceID] = null;
|
|
}, timeout * 1000);
|
|
|
|
return {presence: true};
|
|
},
|
|
},
|
|
generic_batteryvoltage_3000_2500: {
|
|
cid: 'genPowerCfg',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const battery = {max: 3000, min: 2500};
|
|
const voltage = msg.data.data['batteryVoltage'] * 100;
|
|
return {
|
|
battery: toPercentage(voltage, battery.min, battery.max),
|
|
voltage: voltage,
|
|
};
|
|
},
|
|
},
|
|
STS_PRS_251_beeping: {
|
|
cid: 'genIdentify',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => {
|
|
return {action: 'beeping'};
|
|
},
|
|
},
|
|
_324131092621_notification: {
|
|
cid: 'manuSpecificPhilips',
|
|
type: 'cmdHueNotification',
|
|
convert: (model, msg, publish, options) => {
|
|
const multiplePressTimeout = options && options.hasOwnProperty('multiple_press_timeout') ?
|
|
options.multiple_press_timeout : 0.25;
|
|
|
|
const getPayload = function(button, pressType, pressDuration, pressCounter,
|
|
brightnessSend, brightnessValue) {
|
|
const payLoad = {};
|
|
payLoad['action'] = `${button}-${pressType}`;
|
|
payLoad['duration'] = pressDuration / 1000;
|
|
if (pressCounter) {
|
|
payLoad['counter'] = pressCounter;
|
|
}
|
|
if (brightnessSend) {
|
|
payLoad['brightness'] = store[deviceID].brightnessValue;
|
|
}
|
|
return payLoad;
|
|
};
|
|
|
|
const deviceID = msg.endpoints[0].device.ieeeAddr;
|
|
let button = null;
|
|
switch (msg.data.data['button']) {
|
|
case 1:
|
|
button = 'on';
|
|
break;
|
|
case 2:
|
|
button = 'up';
|
|
break;
|
|
case 3:
|
|
button = 'down';
|
|
break;
|
|
case 4:
|
|
button = 'off';
|
|
break;
|
|
}
|
|
let type = null;
|
|
switch (msg.data.data['type']) {
|
|
case 0:
|
|
type = 'press';
|
|
break;
|
|
case 1:
|
|
type = 'hold';
|
|
break;
|
|
case 2:
|
|
case 3:
|
|
type = 'release';
|
|
break;
|
|
}
|
|
|
|
const brightnessEnabled = options && options.hasOwnProperty('send_brightess') ?
|
|
options.send_brightess : true;
|
|
const brightnessSend = brightnessEnabled && button && (button == 'up' || button == 'down');
|
|
|
|
// Initialize store
|
|
if (!store[deviceID]) {
|
|
store[deviceID] = {pressStart: null, pressType: null,
|
|
delayedButton: null, delayedBrightnessSend: null, delayedType: null,
|
|
delayedCounter: 0, delayedTimerStart: null, delayedTimer: null};
|
|
if (brightnessEnabled) {
|
|
store[deviceID].brightnessValue = 255;
|
|
store[deviceID].brightnessSince = null;
|
|
store[deviceID].brightnessDirection = null;
|
|
}
|
|
}
|
|
|
|
if (button && type) {
|
|
if (type == 'press') {
|
|
store[deviceID].pressStart = Date.now();
|
|
store[deviceID].pressType = 'press';
|
|
if (brightnessSend) {
|
|
const newValue = store[deviceID].brightnessValue + (button === 'up' ? 50 : -50);
|
|
store[deviceID].brightnessValue = numberWithinRange(newValue, 0, 255);
|
|
}
|
|
} else if (type == 'hold') {
|
|
store[deviceID].pressType = 'hold';
|
|
if (brightnessSend) {
|
|
holdUpdateBrightness324131092621(deviceID);
|
|
store[deviceID].brightnessSince = Date.now();
|
|
store[deviceID].brightnessDirection = button;
|
|
}
|
|
} else if (type == 'release') {
|
|
if (brightnessSend) {
|
|
store[deviceID].brightnessSince = null;
|
|
store[deviceID].brightnessDirection = null;
|
|
}
|
|
if (store[deviceID].pressType == 'hold') {
|
|
store[deviceID].pressType += '-release';
|
|
}
|
|
}
|
|
if (type == 'press') {
|
|
// pressed different button
|
|
if (store[deviceID].delayedTimer && (store[deviceID].delayedButton != button)) {
|
|
clearTimeout(store[deviceID].delayedTimer);
|
|
store[deviceID].delayedTimer = null;
|
|
publish(getPayload(store[deviceID].delayedButton,
|
|
store[deviceID].delayedType, 0, store[deviceID].delayedCounter,
|
|
store[deviceID].delayedBrightnessSend,
|
|
store[deviceID].brightnessValue));
|
|
}
|
|
} else {
|
|
// released after press: start timer
|
|
if (store[deviceID].pressType == 'press') {
|
|
if (store[deviceID].delayedTimer) {
|
|
clearTimeout(store[deviceID].delayedTimer);
|
|
store[deviceID].delayedTimer = null;
|
|
} else {
|
|
store[deviceID].delayedCounter = 0;
|
|
}
|
|
store[deviceID].delayedButton = button;
|
|
store[deviceID].delayedBrightnessSend = brightnessSend;
|
|
store[deviceID].delayedType = store[deviceID].pressType;
|
|
store[deviceID].delayedCounter++;
|
|
store[deviceID].delayedTimerStart = Date.now();
|
|
store[deviceID].delayedTimer = setTimeout(() => {
|
|
publish(getPayload(store[deviceID].delayedButton,
|
|
store[deviceID].delayedType, 0, store[deviceID].delayedCounter,
|
|
store[deviceID].delayedBrightnessSend,
|
|
store[deviceID].brightnessValue));
|
|
store[deviceID].delayedTimer = null;
|
|
}, multiplePressTimeout * 1000);
|
|
} else {
|
|
const pressDuration =
|
|
(store[deviceID].pressType == 'hold' || store[deviceID].pressType == 'hold-release') ?
|
|
Date.now() - store[deviceID].pressStart : 0;
|
|
return getPayload(button,
|
|
store[deviceID].pressType, pressDuration, null, brightnessSend,
|
|
store[deviceID].brightnessValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
return {};
|
|
},
|
|
},
|
|
generic_battery: {
|
|
cid: 'genPowerCfg',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data.hasOwnProperty('batteryPercentageRemaining')) {
|
|
return {battery: msg.data.data['batteryPercentageRemaining']};
|
|
}
|
|
},
|
|
},
|
|
generic_battery_change: {
|
|
cid: 'genPowerCfg',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data.hasOwnProperty('batteryPercentageRemaining')) {
|
|
return {battery: msg.data.data['batteryPercentageRemaining']};
|
|
}
|
|
},
|
|
},
|
|
generic_battery_remaining: {
|
|
cid: 'genPowerCfg',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data.hasOwnProperty('batteryPercentageRemaining')) {
|
|
return {battery: precisionRound(msg.data.data['batteryPercentageRemaining'] / 2, 2)};
|
|
}
|
|
},
|
|
},
|
|
generic_battery_voltage: {
|
|
cid: 'genPowerCfg',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data.hasOwnProperty('batteryVoltage')) {
|
|
return {voltage: msg.data.data['batteryVoltage'] / 100};
|
|
}
|
|
},
|
|
},
|
|
cmd_move: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdMove',
|
|
convert: (model, msg, publish, options) => {
|
|
ictcg1(model, msg, publish, options, 'move');
|
|
const direction = msg.data.data.movemode === 1 ? 'left' : 'right';
|
|
return {action: `rotate_${direction}`, rate: msg.data.data.rate};
|
|
},
|
|
},
|
|
cmd_move_with_onoff: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdMoveWithOnOff',
|
|
convert: (model, msg, publish, options) => {
|
|
ictcg1(model, msg, publish, options, 'move');
|
|
const direction = msg.data.data.movemode === 1 ? 'left' : 'right';
|
|
return {action: `rotate_${direction}`, rate: msg.data.data.rate};
|
|
},
|
|
},
|
|
cmd_stop: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdStop',
|
|
convert: (model, msg, publish, options) => {
|
|
ictcg1(model, msg, publish, options, 'stop');
|
|
return {action: `rotate_stop`};
|
|
},
|
|
},
|
|
cmd_stop_with_onoff: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdStopWithOnOff',
|
|
convert: (model, msg, publish, options) => {
|
|
ictcg1(model, msg, publish, options, 'stop');
|
|
return {action: `rotate_stop`};
|
|
},
|
|
},
|
|
cmd_move_to_level_with_onoff: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdMoveToLevelWithOnOff',
|
|
convert: (model, msg, publish, options) => {
|
|
ictcg1(model, msg, publish, options, 'level');
|
|
const direction = msg.data.data.level === 0 ? 'left' : 'right';
|
|
return {action: `rotate_${direction}_quick`, level: msg.data.data.level};
|
|
},
|
|
},
|
|
iris_3210L_power: {
|
|
cid: 'haElectricalMeasurement',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
return {power: msg.data.data['activePower'] / 10.0};
|
|
},
|
|
},
|
|
iris_3320L_contact: {
|
|
cid: 'ssIasZone',
|
|
type: 'statusChange',
|
|
convert: (model, msg, publish, options) => {
|
|
return {contact: msg.data.zoneStatus === 36};
|
|
},
|
|
},
|
|
nue_power_state: {
|
|
cid: 'genOnOff',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const ep = msg.endpoints[0];
|
|
const button = getKey(model.ep(ep.device), ep.epId);
|
|
if (button) {
|
|
const payload = {};
|
|
payload[`state_${button}`] = msg.data.data['onOff'] === 1 ? 'ON' : 'OFF';
|
|
return payload;
|
|
}
|
|
},
|
|
},
|
|
generic_state_multi_ep: {
|
|
cid: 'genOnOff',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const ep = msg.endpoints[0];
|
|
const key = `state_${getKey(model.ep(ep.device), ep.epId)}`;
|
|
const payload = {};
|
|
payload[key] = msg.data.data['onOff'] === 1 ? 'ON' : 'OFF';
|
|
return payload;
|
|
},
|
|
},
|
|
RZHAC_4256251_power: {
|
|
cid: 'haElectricalMeasurement',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
return {
|
|
power: msg.data.data['activePower'],
|
|
current: msg.data.data['rmsCurrent'],
|
|
voltage: msg.data.data['rmsVoltage'],
|
|
};
|
|
},
|
|
},
|
|
ias_zone_motion_dev_change: {
|
|
cid: 'ssIasZone',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data.zoneType === 0x000D) { // type 0x000D = motion sensor
|
|
const zoneStatus = msg.data.data.zoneStatus;
|
|
return {
|
|
occupancy: (zoneStatus & 1<<1) > 0, // Bit 1 = Alarm 2: Presence Indication
|
|
tamper: (zoneStatus & 1<<2) > 0, // Bit 2 = Tamper status
|
|
battery_low: (zoneStatus & 1<<3) > 0, // Bit 3 = Battery LOW indicator (trips around 2.4V)
|
|
};
|
|
}
|
|
},
|
|
},
|
|
ias_zone_motion_status_change: {
|
|
cid: 'ssIasZone',
|
|
type: 'statusChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const zoneStatus = msg.data.zoneStatus;
|
|
return {
|
|
occupancy: (zoneStatus & 1<<1) > 0, // Bit 1 = Alarm 2: Presence Indication
|
|
tamper: (zoneStatus & 1<<2) > 0, // Bit 2 = Tamper status
|
|
battery_low: (zoneStatus & 1<<3) > 0, // Bit 3 = Battery LOW indicator (trips around 2.4V)
|
|
};
|
|
},
|
|
},
|
|
generic_ias_zone_occupancy_status_change: {
|
|
cid: 'ssIasZone',
|
|
type: 'statusChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const zoneStatus = msg.data.zoneStatus;
|
|
return {
|
|
occupancy: (zoneStatus & 1) > 0, // Bit 0 = Alarm 1: Presence Indication
|
|
tamper: (zoneStatus & 1<<2) > 0, // Bit 2 = Tamper status
|
|
battery_low: (zoneStatus & 1<<3) > 0, // Bit 3 = Battery LOW indicator (trips around 2.4V)
|
|
};
|
|
},
|
|
},
|
|
generic_ias_zone_occupancy_status_change_no_off_msg: {
|
|
cid: 'ssIasZone',
|
|
type: 'statusChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const zoneStatus = msg.data.zoneStatus;
|
|
const useOptionsTimeout = options && options.hasOwnProperty('occupancy_timeout');
|
|
const timeout = useOptionsTimeout ? options.occupancy_timeout : occupancyTimeout;
|
|
const deviceID = msg.endpoints[0].device.ieeeAddr;
|
|
|
|
if (store[deviceID]) {
|
|
clearTimeout(store[deviceID]);
|
|
store[deviceID] = null;
|
|
}
|
|
|
|
if (timeout !== 0) {
|
|
store[deviceID] = setTimeout(() => {
|
|
publish({occupancy: false});
|
|
store[deviceID] = null;
|
|
}, timeout * 1000);
|
|
}
|
|
|
|
return {
|
|
occupancy: (zoneStatus & 1) > 0, // Bit 0 = Alarm 1: Presence Indication
|
|
tamper: (zoneStatus & 1<<2) > 0, // Bit 2 = Tamper status
|
|
battery_low: (zoneStatus & 1<<3) > 0, // Bit 3 = Battery LOW indicator (trips around 2.4V)
|
|
};
|
|
},
|
|
},
|
|
generic_ias_zone_motion_dev_change: {
|
|
cid: 'ssIasZone',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const zoneStatus = msg.data.zoneStatus;
|
|
return {
|
|
occupancy: (zoneStatus & 1) > 0, // Bit 0 = Alarm 1: Presence Indication
|
|
tamper: (zoneStatus & 1<<2) > 0, // Bit 2 = Tamper status
|
|
battery_low: (zoneStatus & 1<<3) > 0, // Bit 3 = Battery LOW indicator (trips around 2.4V)
|
|
};
|
|
},
|
|
},
|
|
ias_contact_dev_change: {
|
|
cid: 'ssIasZone',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const zoneStatus = msg.data.zoneStatus;
|
|
return {
|
|
contact: !((zoneStatus & 1) > 0),
|
|
};
|
|
},
|
|
},
|
|
ias_contact_status_change: {
|
|
cid: 'ssIasZone',
|
|
type: 'statusChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const zoneStatus = msg.data.zoneStatus;
|
|
return {
|
|
contact: !((zoneStatus & 1) > 0),
|
|
};
|
|
},
|
|
},
|
|
brightness_report: {
|
|
cid: 'genLevelCtrl',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data.hasOwnProperty('currentLevel')) {
|
|
return {brightness: msg.data.data['currentLevel']};
|
|
}
|
|
},
|
|
},
|
|
smartsense_multi: {
|
|
cid: 'ssIasZone',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const zoneStatus = msg.data.data.zoneStatus;
|
|
return {
|
|
contact: !(zoneStatus & 1), // Bit 1 = Contact
|
|
// Bit 5 = Currently always set?
|
|
};
|
|
},
|
|
},
|
|
st_leak: {
|
|
cid: 'ssIasZone',
|
|
type: 'attReport',
|
|
convert: (model, msg, publish, options) => {
|
|
const zoneStatus = msg.data.data.zoneStatus;
|
|
return {
|
|
water_leak: (zoneStatus & 1) > 0, // Bit 1 = wet
|
|
};
|
|
},
|
|
},
|
|
st_leak_change: {
|
|
cid: 'ssIasZone',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const zoneStatus = msg.data.data.zoneStatus;
|
|
return {
|
|
water_leak: (zoneStatus & 1) > 0, // Bit 1 = wet
|
|
};
|
|
},
|
|
},
|
|
st_contact_status_change: {
|
|
cid: 'ssIasZone',
|
|
type: 'statusChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const zoneStatus = msg.data.zoneStatus;
|
|
return {
|
|
contact: !((zoneStatus & 1) > 0), // Bit 0 = Alarm: Contact detection
|
|
battery_low: (zoneStatus & 1<<3) > 0, // Bit 3 = Battery LOW indicator
|
|
};
|
|
},
|
|
},
|
|
st_button_state: {
|
|
cid: 'ssIasZone',
|
|
type: 'statusChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const buttonStates = {
|
|
0: 'off',
|
|
1: 'single',
|
|
2: 'double',
|
|
3: 'hold',
|
|
};
|
|
|
|
if (msg.data.hasOwnProperty('data')) {
|
|
const zoneStatus = msg.data.data.zoneStatus;
|
|
return {click: buttonStates[zoneStatus]};
|
|
} else {
|
|
const zoneStatus = msg.data.zoneStatus;
|
|
return {click: buttonStates[zoneStatus]};
|
|
}
|
|
},
|
|
},
|
|
CTR_U_brightness_updown_click: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdStep',
|
|
convert: (model, msg, publish, options) => {
|
|
const deviceID = msg.endpoints[0].device.ieeeAddr;
|
|
const direction = msg.data.data.stepmode === 1 ? 'down' : 'up';
|
|
|
|
// Save last direction for release event
|
|
if (!store[deviceID]) {
|
|
store[deviceID] = {};
|
|
}
|
|
store[deviceID].direction = direction;
|
|
|
|
return {
|
|
action: `brightness_${direction}_click`,
|
|
step_size: msg.data.data.stepsize,
|
|
transition_time: msg.data.data.transtime,
|
|
};
|
|
},
|
|
},
|
|
CTR_U_brightness_updown_hold: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdMove',
|
|
convert: (model, msg, publish, options) => {
|
|
const deviceID = msg.endpoints[0].device.ieeeAddr;
|
|
const direction = msg.data.data.movemode === 1 ? 'down' : 'up';
|
|
|
|
// Save last direction for release event
|
|
if (!store[deviceID]) {
|
|
store[deviceID] = {};
|
|
}
|
|
store[deviceID].direction = direction;
|
|
|
|
return {
|
|
action: `brightness_${direction}_hold`,
|
|
rate: msg.data.data.rate,
|
|
};
|
|
},
|
|
},
|
|
CTR_U_brightness_updown_release: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdStop',
|
|
convert: (model, msg, publish, options) => {
|
|
const deviceID = msg.endpoints[0].device.ieeeAddr;
|
|
if (!store[deviceID]) {
|
|
return null;
|
|
}
|
|
|
|
const direction = store[deviceID].direction;
|
|
return {
|
|
action: `brightness_${direction}_release`,
|
|
};
|
|
},
|
|
},
|
|
CTR_U_scene: {
|
|
cid: 'genScenes',
|
|
type: 'cmdRecall',
|
|
convert: (model, msg, publish, options) => {
|
|
return {click: `scene_${msg.data.data.groupid}_${msg.data.data.sceneid}`};
|
|
},
|
|
},
|
|
thermostat_dev_change: {
|
|
cid: 'hvacThermostat',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const result = {};
|
|
if (typeof msg.data.data['localTemp'] == 'number') {
|
|
result.local_temperature = precisionRound(msg.data.data['localTemp'], 2) / 100;
|
|
}
|
|
if (typeof msg.data.data['localTemperatureCalibration'] == 'number') {
|
|
result.local_temperature_calibration =
|
|
precisionRound(msg.data.data['localTemperatureCalibration'], 2) / 10;
|
|
}
|
|
if (typeof msg.data.data['occupancy'] == 'number') {
|
|
result.occupancy = msg.data.data['occupancy'];
|
|
}
|
|
if (typeof msg.data.data['occupiedHeatingSetpoint'] == 'number') {
|
|
result.occupied_heating_setpoint =
|
|
precisionRound(msg.data.data['occupiedHeatingSetpoint'], 2) / 100;
|
|
}
|
|
if (typeof msg.data.data['unoccupiedHeatingSetpoint'] == 'number') {
|
|
result.unoccupied_heating_setpoint =
|
|
precisionRound(msg.data.data['unoccupiedHeatingSetpoint'], 2) / 100;
|
|
}
|
|
if (typeof msg.data.data['occupiedCoolingSetpoint'] == 'number') {
|
|
result.occupied_cooling_setpoint =
|
|
precisionRound(msg.data.data['occupiedCoolingSetpoint'], 2) / 100;
|
|
}
|
|
if (typeof msg.data.data['weeklySchedule'] == 'number') {
|
|
result.weekly_schedule = msg.data.data['weeklySchedule'];
|
|
}
|
|
if (typeof msg.data.data['setpointChangeAmount'] == 'number') {
|
|
result.setpoint_change_amount = msg.data.data['setpointChangeAmount'] / 100;
|
|
}
|
|
if (typeof msg.data.data['setpointChangeSource'] == 'number') {
|
|
result.setpoint_change_source = msg.data.data['setpointChangeSource'];
|
|
}
|
|
if (typeof msg.data.data['setpointChangeSourceTimeStamp'] == 'number') {
|
|
result.setpoint_change_source_timestamp = msg.data.data['setpointChangeSourceTimeStamp'];
|
|
}
|
|
if (typeof msg.data.data['remoteSensing'] == 'number') {
|
|
result.remote_sensing = msg.data.data['remoteSensing'];
|
|
}
|
|
const ctrl = msg.data.data['ctrlSeqeOfOper'];
|
|
if (typeof ctrl == 'number' && common.thermostatControlSequenceOfOperations.hasOwnProperty(ctrl)) {
|
|
result.control_sequence_of_operation = common.thermostatControlSequenceOfOperations[ctrl];
|
|
}
|
|
const smode = msg.data.data['systemMode'];
|
|
if (typeof smode == 'number' && common.thermostatSystemModes.hasOwnProperty(smode)) {
|
|
result.system_mode = common.thermostatSystemModes[smode];
|
|
}
|
|
const rmode = msg.data.data['runningMode'];
|
|
if (typeof rmode == 'number' && common.thermostatSystemModes.hasOwnProperty(rmode)) {
|
|
result.running_mode = common.thermostatSystemModes[rmode];
|
|
}
|
|
const state = msg.data.data['runningState'];
|
|
if (typeof state == 'number' && common.thermostatRunningStates.hasOwnProperty(state)) {
|
|
result.running_state = common.thermostatRunningStates[state];
|
|
}
|
|
if (typeof msg.data.data['pIHeatingDemand'] == 'number') {
|
|
result.pi_heating_demand = msg.data.data['pIHeatingDemand'];
|
|
}
|
|
return result;
|
|
},
|
|
},
|
|
thermostat_att_report: {
|
|
cid: 'hvacThermostat',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const result = {};
|
|
if (typeof msg.data.data['localTemp'] == 'number') {
|
|
result.local_temperature = precisionRound(msg.data.data['localTemp'], 2) / 100;
|
|
}
|
|
if (typeof msg.data.data['localTemperatureCalibration'] == 'number') {
|
|
result.local_temperature_calibration =
|
|
precisionRound(msg.data.data['localTemperatureCalibration'], 2) / 10;
|
|
}
|
|
if (typeof msg.data.data['occupancy'] == 'number') {
|
|
result.occupancy = msg.data.data['occupancy'];
|
|
}
|
|
if (typeof msg.data.data['occupiedHeatingSetpoint'] == 'number') {
|
|
result.occupied_heating_setpoint =
|
|
precisionRound(msg.data.data['occupiedHeatingSetpoint'], 2) / 100;
|
|
}
|
|
if (typeof msg.data.data['unoccupiedHeatingSetpoint'] == 'number') {
|
|
result.unoccupied_heating_setpoint =
|
|
precisionRound(msg.data.data['unoccupiedHeatingSetpoint'], 2) / 100;
|
|
}
|
|
if (typeof msg.data.data['occupiedCoolingSetpoint'] == 'number') {
|
|
result.occupied_cooling_setpoint =
|
|
precisionRound(msg.data.data['occupiedCoolingSetpoint'], 2) / 100;
|
|
}
|
|
if (typeof msg.data.data['weeklySchedule'] == 'number') {
|
|
result.weekly_schedule = msg.data.data['weeklySchedule'];
|
|
}
|
|
if (typeof msg.data.data['setpointChangeAmount'] == 'number') {
|
|
result.setpoint_change_amount = msg.data.data['setpointChangeAmount'] / 100;
|
|
}
|
|
if (typeof msg.data.data['setpointChangeSource'] == 'number') {
|
|
result.setpoint_change_source = msg.data.data['setpointChangeSource'];
|
|
}
|
|
if (typeof msg.data.data['setpointChangeSourceTimeStamp'] == 'number') {
|
|
result.setpoint_change_source_timestamp = msg.data.data['setpointChangeSourceTimeStamp'];
|
|
}
|
|
if (typeof msg.data.data['remoteSensing'] == 'number') {
|
|
result.remote_sensing = msg.data.data['remoteSensing'];
|
|
}
|
|
const ctrl = msg.data.data['ctrlSeqeOfOper'];
|
|
if (typeof ctrl == 'number' && common.thermostatControlSequenceOfOperations.hasOwnProperty(ctrl)) {
|
|
result.control_sequence_of_operation = common.thermostatControlSequenceOfOperations[ctrl];
|
|
}
|
|
const smode = msg.data.data['systemMode'];
|
|
if (typeof smode == 'number' && common.thermostatSystemModes.hasOwnProperty(smode)) {
|
|
result.system_mode = common.thermostatSystemModes[smode];
|
|
}
|
|
const rmode = msg.data.data['runningMode'];
|
|
if (typeof rmode == 'number' && common.thermostatSystemModes.hasOwnProperty(rmode)) {
|
|
result.running_mode = common.thermostatSystemModes[rmode];
|
|
}
|
|
const state = msg.data.data['runningState'];
|
|
if (typeof state == 'number' && common.thermostatRunningStates.hasOwnProperty(state)) {
|
|
result.running_state = common.thermostatRunningStates[state];
|
|
}
|
|
if (typeof msg.data.data['pIHeatingDemand'] == 'number') {
|
|
result.pi_heating_demand = msg.data.data['pIHeatingDemand'];
|
|
}
|
|
return result;
|
|
},
|
|
},
|
|
eurotronic_thermostat_dev_change: {
|
|
cid: 'hvacThermostat',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const result = {};
|
|
if (typeof msg.data.data[0x4003] == 'number') {
|
|
result.current_heating_setpoint =
|
|
precisionRound(msg.data.data[0x4003], 2) / 100;
|
|
}
|
|
if (typeof msg.data.data[0x4008] == 'number') {
|
|
result.eurotronic_system_mode = msg.data.data[0x4008];
|
|
}
|
|
if (typeof msg.data.data[0x4002] == 'number') {
|
|
result.eurotronic_error_status = msg.data.data[0x4002];
|
|
}
|
|
if (typeof msg.data.data[0x4000] == 'number') {
|
|
result.eurotronic_trv_mode = msg.data.data[0x4000];
|
|
}
|
|
if (typeof msg.data.data[0x4001] == 'number') {
|
|
result.eurotronic_valve_position = msg.data.data[0x4001];
|
|
}
|
|
return result;
|
|
},
|
|
},
|
|
tint404011_on: {
|
|
cid: 'genOnOff',
|
|
type: 'cmdOn',
|
|
convert: (model, msg, publish, options) => {
|
|
return {action: 'on'};
|
|
},
|
|
},
|
|
tint404011_off: {
|
|
cid: 'genOnOff',
|
|
type: 'cmdOff',
|
|
convert: (model, msg, publish, options) => {
|
|
return {action: 'off'};
|
|
},
|
|
},
|
|
tint404011_brightness_updown_click: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdStep',
|
|
convert: (model, msg, publish, options) => {
|
|
const direction = msg.data.data.stepmode === 1 ? 'down' : 'up';
|
|
return {
|
|
action: `brightness_${direction}_click`,
|
|
step_size: msg.data.data.stepsize,
|
|
transition_time: msg.data.data.transtime,
|
|
};
|
|
},
|
|
},
|
|
tint404011_brightness_updown_hold: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdMove',
|
|
convert: (model, msg, publish, options) => {
|
|
const deviceID = msg.endpoints[0].device.ieeeAddr;
|
|
const direction = msg.data.data.movemode === 1 ? 'down' : 'up';
|
|
|
|
// Save last direction for release event
|
|
if (!store[deviceID]) {
|
|
store[deviceID] = {};
|
|
}
|
|
store[deviceID].movemode = direction;
|
|
|
|
return {
|
|
action: `brightness_${direction}_hold`,
|
|
rate: msg.data.data.rate,
|
|
};
|
|
},
|
|
},
|
|
tint404011_brightness_updown_release: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdStop',
|
|
convert: (model, msg, publish, options) => {
|
|
const deviceID = msg.endpoints[0].device.ieeeAddr;
|
|
if (!store[deviceID]) {
|
|
return null;
|
|
}
|
|
|
|
const direction = store[deviceID].movemode;
|
|
return {
|
|
action: `brightness_${direction}_release`,
|
|
};
|
|
},
|
|
},
|
|
tint404011_scene: {
|
|
cid: 'genBasic',
|
|
type: 'cmdWrite',
|
|
convert: (model, msg, publish, options) => {
|
|
return {action: `scene${msg.data.data[0].attrData}`};
|
|
},
|
|
},
|
|
tint404011_move_to_color_temp: {
|
|
cid: 'lightingColorCtrl',
|
|
type: 'cmdMoveToColorTemp',
|
|
convert: (model, msg, publish, options) => {
|
|
return {
|
|
action: `color_temp`,
|
|
action_color_temperature: msg.data.data.colortemp,
|
|
transition_time: msg.data.data.transtime,
|
|
};
|
|
},
|
|
},
|
|
tint404011_move_to_color: {
|
|
cid: 'lightingColorCtrl',
|
|
type: 'cmdMoveToColor',
|
|
convert: (model, msg, publish, options) => {
|
|
return {
|
|
action_color: {
|
|
x: precisionRound(msg.data.data.colorx / 65535, 3),
|
|
y: precisionRound(msg.data.data.colory / 65535, 3),
|
|
},
|
|
action: 'color_wheel',
|
|
transition_time: msg.data.data.transtime,
|
|
};
|
|
},
|
|
},
|
|
cmdToggle: {
|
|
cid: 'genOnOff',
|
|
type: 'cmdToggle',
|
|
convert: (model, msg, publish, options) => {
|
|
return {action: 'toggle'};
|
|
},
|
|
},
|
|
E1524_hold: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdMoveToLevelWithOnOff',
|
|
convert: (model, msg, publish, options) => {
|
|
return {action: 'toggle_hold'};
|
|
},
|
|
},
|
|
E1524_arrow_click: {
|
|
cid: 'genScenes',
|
|
type: 'cmdTradfriArrowSingle',
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data.value === 2) {
|
|
// This is send on toggle hold, ignore it as a toggle_hold is already handled above.
|
|
return;
|
|
}
|
|
|
|
const direction = msg.data.data.value === 257 ? 'left' : 'right';
|
|
return {action: `arrow_${direction}_click`};
|
|
},
|
|
},
|
|
E1524_arrow_hold: {
|
|
cid: 'genScenes',
|
|
type: 'cmdTradfriArrowHold',
|
|
convert: (model, msg, publish, options) => {
|
|
const direction = msg.data.data.value === 3329 ? 'left' : 'right';
|
|
store[msg.endpoints[0].device.ieeeAddr] = direction;
|
|
return {action: `arrow_${direction}_hold`};
|
|
},
|
|
},
|
|
E1524_arrow_release: {
|
|
cid: 'genScenes',
|
|
type: 'cmdTradfriArrowRelease',
|
|
convert: (model, msg, publish, options) => {
|
|
const direction = store[msg.endpoints[0].device.ieeeAddr];
|
|
if (direction) {
|
|
delete store[msg.endpoints[0].device.ieeeAddr];
|
|
return {action: `arrow_${direction}_release`, duration: msg.data.data.value / 1000};
|
|
}
|
|
},
|
|
},
|
|
E1524_brightness_up_click: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdStepWithOnOff',
|
|
convert: (model, msg, publish, options) => {
|
|
return {action: `brightness_up_click`};
|
|
},
|
|
},
|
|
E1524_brightness_down_click: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdStep',
|
|
convert: (model, msg, publish, options) => {
|
|
return {action: `brightness_down_click`};
|
|
},
|
|
},
|
|
E1524_brightness_up_hold: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdMoveWithOnOff',
|
|
convert: (model, msg, publish, options) => {
|
|
return {action: `brightness_up_hold`};
|
|
},
|
|
},
|
|
E1524_brightness_up_release: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdStopWithOnOff',
|
|
convert: (model, msg, publish, options) => {
|
|
return {action: `brightness_up_release`};
|
|
},
|
|
},
|
|
E1524_brightness_down_hold: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdMove',
|
|
convert: (model, msg, publish, options) => {
|
|
return {action: `brightness_down_hold`};
|
|
},
|
|
},
|
|
E1524_brightness_down_release: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdStop',
|
|
convert: (model, msg, publish, options) => {
|
|
return {action: `brightness_down_release`};
|
|
},
|
|
},
|
|
livolo_switch_dev_change: {
|
|
cid: 'genOnOff',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const status = msg.data.data.onOff;
|
|
const payload = {};
|
|
payload['state_left'] = status & 1 ? 'ON' : 'OFF';
|
|
payload['state_right'] = status & 2 ? 'ON' : 'OFF';
|
|
if (msg.endpoints[0].hasOwnProperty('linkquality')) {
|
|
payload['linkquality'] = msg.endpoints[0].linkquality;
|
|
}
|
|
return payload;
|
|
},
|
|
},
|
|
eria_81825_on: {
|
|
cid: 'genOnOff',
|
|
type: 'cmdOn',
|
|
convert: (model, msg, publish, options) => {
|
|
return {action: 'on'};
|
|
},
|
|
},
|
|
eria_81825_off: {
|
|
cid: 'genOnOff',
|
|
type: 'cmdOff',
|
|
convert: (model, msg, publish, options) => {
|
|
return {action: 'off'};
|
|
},
|
|
},
|
|
eria_81825_updown: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdStep',
|
|
convert: (model, msg, publish, options) => {
|
|
const direction = msg.data.data.stepmode === 0 ? 'up' : 'down';
|
|
return {action: `${direction}`};
|
|
},
|
|
},
|
|
ZYCT202_on: {
|
|
cid: 'genOnOff',
|
|
type: 'cmdOn',
|
|
convert: (model, msg, publish, options) => {
|
|
return {action: 'on', action_group: msg.groupid};
|
|
},
|
|
},
|
|
ZYCT202_off: {
|
|
cid: 'genOnOff',
|
|
type: 'cmdOffWithEffect',
|
|
convert: (model, msg, publish, options) => {
|
|
return {action: 'off', action_group: msg.groupid};
|
|
},
|
|
},
|
|
ZYCT202_stop: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdStop',
|
|
convert: (model, msg, publish, options) => {
|
|
return {action: 'stop', action_group: msg.groupid};
|
|
},
|
|
},
|
|
ZYCT202_up_down: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdMove',
|
|
convert: (model, msg, publish, options) => {
|
|
const value = msg.data.data['movemode'];
|
|
let action = null;
|
|
if (value === 0) action = {'action': 'up-press', 'action_group': msg.groupid};
|
|
else if (value === 1) action = {'action': 'down-press', 'action_group': msg.groupid};
|
|
return action ? action : null;
|
|
},
|
|
},
|
|
cover_position: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const currentLevel = msg.data.data['currentLevel'];
|
|
const position = Math.round(Number(currentLevel) / 2.55).toString();
|
|
const state = position > 0 ? 'OPEN' : 'CLOSE';
|
|
return {state: state, position: position};
|
|
},
|
|
},
|
|
cover_position_report: {
|
|
cid: 'genLevelCtrl',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const currentLevel = msg.data.data['currentLevel'];
|
|
const position = Math.round(Number(currentLevel) / 2.55).toString();
|
|
const state = position > 0 ? 'OPEN' : 'CLOSE';
|
|
return {state: state, position: position};
|
|
},
|
|
},
|
|
cover_state_report: {
|
|
cid: 'genOnOff',
|
|
type: 'attReport',
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data.hasOwnProperty('onOff')) {
|
|
return {state: msg.data.data['onOff'] === 1 ? 'OPEN' : 'CLOSE'};
|
|
}
|
|
},
|
|
},
|
|
cover_state_change: {
|
|
cid: 'genOnOff',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data.hasOwnProperty('onOff')) {
|
|
return {state: msg.data.data['onOff'] === 1 ? 'OPEN' : 'CLOSE'};
|
|
}
|
|
},
|
|
},
|
|
keen_home_smart_vent_pressure: {
|
|
cid: 'msPressureMeasurement',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => {
|
|
// '{"cid":"msPressureMeasurement","data":{"32":990494}}'
|
|
const pressure = parseFloat(msg.data.data['32']) / 1000.0;
|
|
return {pressure: precisionRoundOptions(pressure, options, 'pressure')};
|
|
},
|
|
},
|
|
keen_home_smart_vent_pressure_report: {
|
|
cid: 'msPressureMeasurement',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
// '{"cid":"msPressureMeasurement","data":{"32":990494}}'
|
|
const pressure = parseFloat(msg.data.data['32']) / 1000.0;
|
|
return {pressure: precisionRoundOptions(pressure, options, 'pressure')};
|
|
},
|
|
},
|
|
AC0251100NJ_cmdOn: {
|
|
cid: 'genOnOff',
|
|
type: 'cmdOn',
|
|
convert: (model, msg, publish, options) => {
|
|
return {action: 'up'};
|
|
},
|
|
},
|
|
AC0251100NJ_cmdOff: {
|
|
cid: 'genOnOff',
|
|
type: 'cmdOff',
|
|
convert: (model, msg, publish, options) => {
|
|
return {action: 'down'};
|
|
},
|
|
},
|
|
AC0251100NJ_cmdMoveWithOnOff: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdMoveWithOnOff',
|
|
convert: (model, msg, publish, options) => {
|
|
return {action: 'up_hold'};
|
|
},
|
|
},
|
|
AC0251100NJ_cmdStop: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdStop',
|
|
convert: (model, msg, publish, options) => {
|
|
const map = {
|
|
1: 'up_release',
|
|
2: 'down_release',
|
|
};
|
|
|
|
return {action: map[msg.endpoints[0].epId]};
|
|
},
|
|
},
|
|
AC0251100NJ_cmdMove: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdMove',
|
|
convert: (model, msg, publish, options) => {
|
|
return {action: 'down_hold'};
|
|
},
|
|
},
|
|
AC0251100NJ_cmdMoveHue: {
|
|
cid: 'lightingColorCtrl',
|
|
type: 'cmdMoveHue',
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data.movemode === 0) {
|
|
return {action: 'circle_release'};
|
|
}
|
|
},
|
|
},
|
|
AC0251100NJ_cmdMoveToSaturation: {
|
|
cid: 'lightingColorCtrl',
|
|
type: 'cmdMoveToSaturation',
|
|
convert: (model, msg, publish, options) => {
|
|
return {action: 'circle_hold'};
|
|
},
|
|
},
|
|
AC0251100NJ_cmdMoveToLevelWithOnOff: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdMoveToLevelWithOnOff',
|
|
convert: (model, msg, publish, options) => {
|
|
return {action: 'circle_click'};
|
|
},
|
|
},
|
|
AC0251100NJ_cmdMoveToColorTemp: {
|
|
cid: 'lightingColorCtrl',
|
|
type: 'cmdMoveToColorTemp',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
visonic_contact: {
|
|
cid: 'ssIasZone',
|
|
type: 'statusChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const zoneStatus = msg.data.zoneStatus;
|
|
return {
|
|
contact: !((zoneStatus & 1) > 0), // Bit 1 = Alarm: Contact detection
|
|
tamper: (zoneStatus & 1<<2) > 0, // Bit 3 = Tamper status
|
|
battery_low: (zoneStatus & 1<<3) > 0, // Bit 4 = Battery LOW indicator
|
|
};
|
|
},
|
|
},
|
|
OJBCR701YZ_statuschange: {
|
|
cid: 'ssIasZone',
|
|
type: 'statusChange',
|
|
convert: (model, msg, publish, options) => {
|
|
const {zoneStatus} = msg.data;
|
|
return {
|
|
carbon_monoxide: (zoneStatus & 1) > 0, // Bit 0 = Alarm 1: Carbon Monoxide (CO)
|
|
gas: (zoneStatus & 1 << 1) > 0, // Bit 1 = Alarm 2: Gas (CH4)
|
|
tamper: (zoneStatus & 1 << 2) > 0, // Bit 2 = Tamper
|
|
battery_low: (zoneStatus & 1 << 3) > 0, // Bit 3 = Low battery alarm
|
|
trouble: (zoneStatus & 1 << 6) > 0, // Bit 6 = Trouble/Failure
|
|
ac_connected: !((zoneStatus & 1 << 7) > 0), // Bit 7 = AC Connected
|
|
test: (zoneStatus & 1 << 8) > 0, // Bit 8 = Self test
|
|
battery_defect: (zoneStatus & 1 << 9) > 0, // Bit 9 = Battery Defect
|
|
};
|
|
},
|
|
},
|
|
closuresWindowCovering_report: {
|
|
cid: 'closuresWindowCovering',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
return {position: msg.data.data.currentPositionLiftPercentage};
|
|
},
|
|
},
|
|
closuresWindowCovering_report_pos_and_tilt: {
|
|
cid: 'closuresWindowCovering',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const result = {};
|
|
// ZigBee officially expects "open" to be 0 and "closed" to be 100 whereas
|
|
// HomeAssistant etc. work the other way round.
|
|
// ubisys J1 will report 255 if lift or tilt positions are not known.
|
|
if (msg.data.data.hasOwnProperty('currentPositionLiftPercentage')) {
|
|
const liftPercentage = msg.data.data['currentPositionLiftPercentage'];
|
|
result.position = liftPercentage <= 100 ? (100 - liftPercentage) : null;
|
|
}
|
|
if (msg.data.data.hasOwnProperty('currentPositionTiltPercentage')) {
|
|
const tiltPercentage = msg.data.data['currentPositionTiltPercentage'];
|
|
result.tilt = tiltPercentage <= 100 ? (100 - tiltPercentage) : null;
|
|
}
|
|
return result;
|
|
},
|
|
},
|
|
generic_fan_mode: {
|
|
cid: 'hvacFanCtrl',
|
|
type: 'attReport',
|
|
convert: (model, msg, publish, options) => {
|
|
const key = getKey(common.fanMode, msg.data.data.fanMode);
|
|
return {fan_mode: key, fan_state: key === 'off' ? 'OFF' : 'ON'};
|
|
},
|
|
},
|
|
GIRA2430_scene_click: {
|
|
cid: 'genScenes',
|
|
type: 'cmdRecall',
|
|
convert: (model, msg, publish, options) => {
|
|
return {
|
|
action: `select_${msg.data.data.sceneid}`,
|
|
};
|
|
},
|
|
},
|
|
GIRA2430_on_click: {
|
|
cid: 'genOnOff',
|
|
type: 'cmdOn',
|
|
convert: (model, msg, publish, options) => {
|
|
return {action: 'on'};
|
|
},
|
|
},
|
|
GIRA2430_off_click: {
|
|
cid: 'genOnOff',
|
|
type: 'cmdOffWithEffect',
|
|
convert: (model, msg, publish, options) => {
|
|
return {action: 'off'};
|
|
},
|
|
},
|
|
GIRA2430_down_hold: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdStep',
|
|
convert: (model, msg, publish, options) => {
|
|
return {
|
|
action: 'down',
|
|
step_mode: msg.data.data.stepmode,
|
|
step_size: msg.data.data.stepsize,
|
|
transition_time: msg.data.data.transtime,
|
|
};
|
|
},
|
|
},
|
|
GIRA2430_up_hold: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdStepWithOnOff',
|
|
convert: (model, msg, publish, options) => {
|
|
return {
|
|
action: 'up',
|
|
step_mode: msg.data.data.stepmode,
|
|
step_size: msg.data.data.stepsize,
|
|
transition_time: msg.data.data.transtime,
|
|
};
|
|
},
|
|
},
|
|
GIRA2430_stop: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdStop',
|
|
convert: (model, msg, publish, options) => {
|
|
return {
|
|
action: 'stop',
|
|
};
|
|
},
|
|
},
|
|
ZGRC013_cmdOn: {
|
|
cid: 'genOnOff',
|
|
type: 'cmdOn',
|
|
convert: (model, msg, publish, options) => {
|
|
const button = msg.endpoints[0].epId;
|
|
if (button) {
|
|
return {click: `${button}_on`};
|
|
}
|
|
},
|
|
},
|
|
ZGRC013_cmdOff: {
|
|
cid: 'genOnOff',
|
|
type: 'cmdOff',
|
|
convert: (model, msg, publish, options) => {
|
|
const button = msg.endpoints[0].epId;
|
|
if (button) {
|
|
return {click: `${button}_off`};
|
|
}
|
|
},
|
|
},
|
|
ZGRC013_brightness: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdMove',
|
|
convert: (model, msg, publish, options) => {
|
|
const button = msg.endpoints[0].epId;
|
|
const direction = msg.data.data.movemode == 0 ? 'up' : 'down';
|
|
if (button) {
|
|
return {click: `${button}_${direction}`};
|
|
}
|
|
},
|
|
},
|
|
ZGRC013_brightness_onoff: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdMoveWithOnOff',
|
|
convert: (model, msg, publish, options) => {
|
|
const button = msg.endpoints[0].epId;
|
|
const direction = msg.data.data.movemode == 0 ? 'up' : 'down';
|
|
if (button) {
|
|
return {click: `${button}_${direction}`};
|
|
}
|
|
},
|
|
},
|
|
ZGRC013_brightness_stop: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdStopWithOnOff',
|
|
convert: (model, msg, publish, options) => {
|
|
const button = msg.endpoints[0].epId;
|
|
if (button) {
|
|
return {click: `${button}_stop`};
|
|
}
|
|
},
|
|
},
|
|
ZGRC013_scene: {
|
|
cid: 'genScenes',
|
|
type: 'cmdRecall',
|
|
convert: (model, msg, publish, options) => {
|
|
return {click: `scene_${msg.data.data.groupid}_${msg.data.data.sceneid}`};
|
|
},
|
|
},
|
|
SZ_ESW01_AU_power: {
|
|
cid: 'seMetering',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data.hasOwnProperty('instantaneousDemand')) {
|
|
return {power: precisionRound(msg.data.data['instantaneousDemand'] / 1000, 2)};
|
|
}
|
|
},
|
|
},
|
|
meazon_meter: {
|
|
cid: 'seMetering',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const result = {};
|
|
// typo on property name to stick with zcl definition
|
|
if (msg.data.data.hasOwnProperty('inletTempreature')) {
|
|
result.inletTemperature = precisionRound(msg.data.data['inletTempreature'], 2);
|
|
}
|
|
|
|
if (msg.data.data.hasOwnProperty('status')) {
|
|
result.status = precisionRound(msg.data.data['status'], 2);
|
|
}
|
|
|
|
if (msg.data.data.hasOwnProperty('8192')) {
|
|
result.linefrequency = precisionRound((parseFloat(msg.data.data['8192'])) / 100.0, 2);
|
|
}
|
|
|
|
if (msg.data.data.hasOwnProperty('8193')) {
|
|
result.power = precisionRound(msg.data.data['8193'], 2);
|
|
}
|
|
|
|
if (msg.data.data.hasOwnProperty('8196')) {
|
|
result.voltage = precisionRound(msg.data.data['8196'], 2);
|
|
}
|
|
|
|
if (msg.data.data.hasOwnProperty('8213')) {
|
|
result.voltage = precisionRound(msg.data.data['8213'], 2);
|
|
}
|
|
|
|
if (msg.data.data.hasOwnProperty('8199')) {
|
|
result.current = precisionRound(msg.data.data['8199'], 2);
|
|
}
|
|
|
|
if (msg.data.data.hasOwnProperty('8216')) {
|
|
result.current = precisionRound(msg.data.data['8216'], 2);
|
|
}
|
|
|
|
if (msg.data.data.hasOwnProperty('8202')) {
|
|
result.reactivepower = precisionRound(msg.data.data['8202'], 2);
|
|
}
|
|
|
|
if (msg.data.data.hasOwnProperty('12288')) {
|
|
result.energyconsumed = precisionRound(msg.data.data['12288'], 2);
|
|
}
|
|
|
|
if (msg.data.data.hasOwnProperty('12291')) {
|
|
result.energyproduced = precisionRound(msg.data.data['12291'], 2);
|
|
}
|
|
|
|
if (msg.data.data.hasOwnProperty('12294')) {
|
|
result.reactivesummation = precisionRound(msg.data.data['12294'], 2);
|
|
}
|
|
|
|
if (msg.data.data.hasOwnProperty('16408')) {
|
|
result.measureserial = precisionRound(msg.data.data['16408'], 2);
|
|
}
|
|
|
|
return result;
|
|
},
|
|
},
|
|
ZNMS12LM_ZNMS13LM_closuresDoorLock_report: {
|
|
cid: 'closuresDoorLock',
|
|
type: 'attReport',
|
|
convert: (model, msg, publish, options) => {
|
|
const result = {};
|
|
const lockStatusLookup = {
|
|
1: 'finger_not_match',
|
|
2: 'password_not_match',
|
|
3: 'reverse_lock', // disable open from outside
|
|
4: 'reverse_lock_cancel', // enable open from outside
|
|
5: 'locked',
|
|
6: 'lock_opened',
|
|
7: 'finger_add',
|
|
8: 'finger_delete',
|
|
9: 'password_add',
|
|
10: 'password_delete',
|
|
11: 'lock_opened_inside', // Open form inside reverse lock enbable
|
|
12: 'lock_opened_outside', // Open form outside reverse lock disable
|
|
13: 'ring_bell',
|
|
14: 'change_language_to',
|
|
15: 'finger_open',
|
|
16: 'password_open',
|
|
17: 'door_closed',
|
|
};
|
|
result.user = null;
|
|
result.repeat = null;
|
|
if (msg.data.data['65526']) { // lock final status
|
|
// Convert data back to hex to decode
|
|
const data = Buffer.from(msg.data.data['65526'], 'ascii').toString('hex');
|
|
const command = data.substr(6, 4);
|
|
if (
|
|
command === '0301' // ZNMS12LM
|
|
|| command === '0341' // ZNMS13LM
|
|
) {
|
|
result.action = lockStatusLookup[4];
|
|
result.state = 'UNLOCK';
|
|
result.reverse = 'UNLOCK';
|
|
} else if (
|
|
command === '0311' // ZNMS12LM
|
|
|| command === '0351' // ZNMS13LM
|
|
) {
|
|
result.action = lockStatusLookup[4];
|
|
result.state = 'LOCK';
|
|
result.reverse = 'UNLOCK';
|
|
} else if (
|
|
command === '0205' // ZNMS12LM
|
|
|| command === '0245' // ZNMS13LM
|
|
) {
|
|
result.action = lockStatusLookup[3];
|
|
result.state = 'UNLOCK';
|
|
result.reverse = 'LOCK';
|
|
} else if (
|
|
command === '0215' // ZNMS12LM
|
|
|| command === '0255' // ZNMS13LM
|
|
|| command === '1355' // ZNMS13LM
|
|
) {
|
|
result.action = lockStatusLookup[3];
|
|
result.state = 'LOCK';
|
|
result.reverse = 'LOCK';
|
|
} else if (
|
|
command === '0111' // ZNMS12LM
|
|
|| command === '1351' // ZNMS13LM locked from inside
|
|
|| command === '1451' // ZNMS13LM locked from outside
|
|
) {
|
|
result.action = lockStatusLookup[5];
|
|
result.state = 'LOCK';
|
|
result.reverse = 'UNLOCK';
|
|
} else if (
|
|
command === '0b00' // ZNMS12LM
|
|
|| command === '0640' // ZNMS13LM
|
|
||command === '0600' // ZNMS13LM
|
|
|
|
) {
|
|
result.action = lockStatusLookup[12];
|
|
result.state = 'UNLOCK';
|
|
result.reverse = 'UNLOCK';
|
|
} else if (
|
|
command === '0c00' // ZNMS12LM
|
|
|| command === '2300' // ZNMS13LM
|
|
|| command === '0540' // ZNMS13LM
|
|
|| command === '0440' // ZNMS13LM
|
|
) {
|
|
result.action = lockStatusLookup[11];
|
|
result.state = 'UNLOCK';
|
|
result.reverse = 'UNLOCK';
|
|
} else if (
|
|
command === '2400' // ZNMS13LM door closed from insed
|
|
|| command === '2401' // ZNMS13LM door closed from outside
|
|
) {
|
|
result.action = lockStatusLookup[17];
|
|
result.state = 'UNLOCK';
|
|
result.reverse = 'UNLOCK';
|
|
}
|
|
} else if (msg.data.data['65296']) { // finger/password success
|
|
const data = Buffer.from(msg.data.data['65296'], 'ascii').toString('hex');
|
|
const command = data.substr(6, 2); // 1 finger open, 2 password open
|
|
const userId = data.substr(12, 2);
|
|
const userType = data.substr(8, 1); // 1 admin, 2 user
|
|
result.action = (lockStatusLookup[14+parseInt(command, 16)]
|
|
+ (userType === '1' ? '_admin' : '_user') + '_id' + parseInt(userId, 16).toString());
|
|
result.user = parseInt(userId, 16);
|
|
} else if (msg.data.data['65297']) { // finger, password failed or bell
|
|
const data = Buffer.from(msg.data.data['65297'], 'ascii').toString('hex');
|
|
const times = data.substr(6, 2);
|
|
const type = data.substr(12, 2); // 00 bell, 02 password, 40 error finger
|
|
if (type === '40') {
|
|
result.action = lockStatusLookup[1];
|
|
result.repeat = parseInt(times, 16);
|
|
} else if (type === '00') {
|
|
result.action = lockStatusLookup[13];
|
|
result.repeat = null;
|
|
} else if (type === '02') {
|
|
result.action = lockStatusLookup[2];
|
|
result.repeat = parseInt(times, 16);
|
|
}
|
|
} else if (msg.data.data['65281']) { // password added/delete
|
|
const data = Buffer.from(msg.data.data['65281'], 'ascii').toString('hex');
|
|
const command = data.substr(18, 2); // 1 add, 2 delete
|
|
const userId = data.substr(12, 2);
|
|
result.action = lockStatusLookup[6+parseInt(command, 16)];
|
|
result.user = parseInt(userId, 16);
|
|
result.repeat = null;
|
|
} else if (msg.data.data['65522']) { // set languge
|
|
const data = Buffer.from(msg.data.data['65522'], 'ascii').toString('hex');
|
|
const langId = data.substr(6, 2); // 1 chinese, 2: english
|
|
result.action = (lockStatusLookup[14])+ (langId==='2'?'_english':'_chinese');
|
|
result.user = null;
|
|
result.repeat = null;
|
|
}
|
|
return result;
|
|
},
|
|
},
|
|
DTB190502A1_parse: {
|
|
cid: 'genOnOff',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const lookupKEY = {
|
|
'0': 'KEY_SYS',
|
|
'1': 'KEY_UP',
|
|
'2': 'KEY_DOWN',
|
|
'3': 'KEY_NONE',
|
|
};
|
|
const lookupLED = {
|
|
'0': 'OFF',
|
|
'1': 'ON',
|
|
};
|
|
return {
|
|
cpu_temperature: precisionRound(msg.data.data['41361'], 2),
|
|
key_state: lookupKEY[msg.data.data['41362']],
|
|
led_state: lookupLED[msg.data.data['41363']],
|
|
};
|
|
},
|
|
},
|
|
konke_click: {
|
|
cid: 'genOnOff',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
const value = msg.data.data['onOff'];
|
|
const lookup = {
|
|
128: {click: 'single'}, // single click
|
|
129: {click: 'double'}, // double and many click
|
|
130: {click: 'long'}, // hold
|
|
};
|
|
|
|
return lookup[value] ? lookup[value] : null;
|
|
},
|
|
},
|
|
E1746_linkquality: {
|
|
cid: 'genBasic',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => {
|
|
return {linkquality: msg.linkquality};
|
|
},
|
|
},
|
|
generic_change_batteryvoltage_3000_2500: {
|
|
cid: 'genPowerCfg',
|
|
type: ['devChange'],
|
|
convert: (model, msg, publish, options) => {
|
|
const battery = {max: 3000, min: 2500};
|
|
const voltage = msg.data.data['batteryVoltage'] * 100;
|
|
return {
|
|
battery: toPercentage(voltage, battery.min, battery.max),
|
|
voltage: voltage,
|
|
};
|
|
},
|
|
},
|
|
generic_device_temperature: {
|
|
cid: 'genDeviceTempCfg',
|
|
type: ['devChange'],
|
|
convert: (model, msg, publish, options) => {
|
|
if (msg.data.data.hasOwnProperty('currentTemperature')) {
|
|
return {temperature: msg.data.data.currentTemperature};
|
|
}
|
|
},
|
|
},
|
|
ptvo_switch_state: {
|
|
cid: 'genOnOff',
|
|
type: 'attReport',
|
|
convert: (model, msg, publish, options) => {
|
|
const ep = msg.endpoints[0];
|
|
const key = `state_${getKey(model.ep(ep.device), ep.epId)}`;
|
|
const payload = {};
|
|
payload[key] = msg.data.data['onOff'] === 1 ? 'ON' : 'OFF';
|
|
return payload;
|
|
},
|
|
},
|
|
ptvo_switch_buttons: {
|
|
cid: 'genMultistateInput',
|
|
type: 'attReport',
|
|
convert: (model, msg, publish, options) => {
|
|
const ep = msg.endpoints[0];
|
|
const button = getKey(model.ep(ep.device), ep.epId);
|
|
const value = msg.data.data['presentValue'];
|
|
|
|
const actionLookup = {
|
|
1: 'single',
|
|
2: 'double',
|
|
3: 'tripple',
|
|
4: 'hold',
|
|
};
|
|
|
|
const action = actionLookup[value];
|
|
|
|
if (button) {
|
|
return {click: button + (action ? `_${action}` : '')};
|
|
}
|
|
},
|
|
},
|
|
keypad20states: {
|
|
cid: 'genOnOff',
|
|
type: ['devChange', 'attReport'],
|
|
convert: (model, msg, publish, options) => {
|
|
const ep = msg.endpoints[0];
|
|
const button = getKey(model.ep(ep.device), ep.epId);
|
|
const state = msg.data.data['onOff'] === 1 ? true : false;
|
|
if (button) {
|
|
return {[button]: state};
|
|
}
|
|
},
|
|
},
|
|
keypad20_battery: {
|
|
cid: 'genPowerCfg',
|
|
type: ['devChange', 'attReport'],
|
|
convert: (model, msg, publish, options) => {
|
|
const battery = {max: 3000, min: 2100};
|
|
const voltage = msg.data.data['mainsVoltage'] /10;
|
|
return {
|
|
battery: toPercentage(voltage, battery.min, battery.max),
|
|
voltage: voltage,
|
|
};
|
|
},
|
|
},
|
|
|
|
// Ignore converters (these message dont need parsing).
|
|
ignore_fan_change: {
|
|
cid: 'hvacFanCtrl',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_light_brightness_change: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_doorlock_change: {
|
|
cid: 'closuresDoorLock',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_onoff_change: {
|
|
cid: 'genOnOff',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_onoff_report: {
|
|
cid: 'genOnOff',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_basic_change: {
|
|
cid: 'genBasic',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_basic_report: {
|
|
cid: 'genBasic',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_illuminance_change: {
|
|
cid: 'msIlluminanceMeasurement',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_occupancy_change: {
|
|
cid: 'msOccupancySensing',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_illuminance_report: {
|
|
cid: 'msIlluminanceMeasurement',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_occupancy_report: {
|
|
cid: 'msOccupancySensing',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_temperature_change: {
|
|
cid: 'msTemperatureMeasurement',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_temperature_report: {
|
|
cid: 'msTemperatureMeasurement',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_humidity_change: {
|
|
cid: 'msRelativeHumidity',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_humidity_report: {
|
|
cid: 'msRelativeHumidity',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_pressure_change: {
|
|
cid: 'msPressureMeasurement',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_pressure_report: {
|
|
cid: 'msPressureMeasurement',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_analog_change: {
|
|
cid: 'genAnalogInput',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_analog_report: {
|
|
cid: 'genAnalogInput',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_multistate_report: {
|
|
cid: 'genMultistateInput',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_multistate_change: {
|
|
cid: 'genMultistateInput',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_power_change: {
|
|
cid: 'genPowerCfg',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_power_report: {
|
|
cid: 'genPowerCfg',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_metering_change: {
|
|
cid: 'seMetering',
|
|
type: ['devChange', 'attReport'],
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_electrical_change: {
|
|
cid: 'haElectricalMeasurement',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_light_brightness_report: {
|
|
cid: 'genLevelCtrl',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_light_color_colortemp_report: {
|
|
cid: 'lightingColorCtrl',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_closuresWindowCovering_change: {
|
|
cid: 'closuresWindowCovering',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_closuresWindowCovering_report: {
|
|
cid: 'closuresWindowCovering',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_thermostat_change: {
|
|
cid: 'hvacThermostat',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_thermostat_report: {
|
|
cid: 'hvacThermostat',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_genGroups_devChange: {
|
|
cid: 'genGroups',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_iaszone_attreport: {
|
|
cid: 'ssIasZone',
|
|
type: 'attReport',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_iaszone_change: {
|
|
cid: 'ssIasZone',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_iaszone_statuschange: {
|
|
cid: 'ssIasZone',
|
|
type: 'statusChange',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_iaszone_report: {
|
|
cid: 'ssIasZone',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_genIdentify: {
|
|
cid: 'genIdentify',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
_324131092621_ignore_on: {
|
|
cid: 'genOnOff',
|
|
type: 'cmdOn',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
_324131092621_ignore_off: {
|
|
cid: 'genOnOff',
|
|
type: 'cmdOffWithEffect',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
_324131092621_ignore_step: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdStep',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
_324131092621_ignore_stop: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'cmdStop',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_poll_ctrl: {
|
|
cid: 'genPollCtrl',
|
|
type: ['attReport', 'readRsp'],
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_poll_ctrl_change: {
|
|
cid: 'genPollCtrl',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_genIdentify_change: {
|
|
cid: 'genIdentify',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_diagnostic_change: {
|
|
cid: 'haDiagnostic',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_genScenes_change: {
|
|
cid: 'genScenes',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_lightLink_change: {
|
|
cid: 'lightLink',
|
|
type: 'devChange',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
ignore_genLevelCtrl_report: {
|
|
cid: 'genLevelCtrl',
|
|
type: 'attReport',
|
|
convert: (model, msg, publish, options) => null,
|
|
},
|
|
};
|
|
|
|
module.exports = converters;
|