zigbee-herdsman-converters
Version:
Collection of device converters to be used with zigbee-herdsman
529 lines (491 loc) • 14.3 kB
JavaScript
;
const constants = require('./constants');
const globalStore = require('./store');
const dataTypes = {
raw: 0, // [ bytes ]
bool: 1, // [0/1]
value: 2, // [ 4 byte value ]
string: 3, // [ N byte string ]
enum: 4, // [ 0-255 ]
bitmap: 5, // [ 1,2,4 bytes ] as bits
};
const convertMultiByteNumberPayloadToSingleDecimalNumber = (chunks) => {
// Destructuring "chunks" is needed because it's a Buffer
// and we need a simple array.
let value = 0;
for (let i = 0; i < chunks.length; i++) {
value = value << 8;
value += chunks[i];
}
return value;
};
function getDataValue(dataType, data) {
switch (dataType) {
case dataTypes.raw:
return data;
case dataTypes.bool:
return data[0] === 1;
case dataTypes.value:
return convertMultiByteNumberPayloadToSingleDecimalNumber(data);
case dataTypes.string:
// eslint-disable-next-line
let dataString = '';
// Don't use .map here, doesn't work: https://github.com/Koenkk/zigbee-herdsman-converters/pull/1799/files#r530377091
for (let i = 0; i < data.length; ++i) {
dataString += String.fromCharCode(data[i]);
}
return dataString;
case dataTypes.enum:
return data[0];
case dataTypes.bitmap:
return convertMultiByteNumberPayloadToSingleDecimalNumber(data);
}
}
function convertDecimalValueTo4ByteHexArray(value) {
const hexValue = Number(value).toString(16).padStart(8, '0');
const chunk1 = hexValue.substr(0, 2);
const chunk2 = hexValue.substr(2, 2);
const chunk3 = hexValue.substr(4, 2);
const chunk4 = hexValue.substr(6);
return [chunk1, chunk2, chunk3, chunk4].map((hexVal) => parseInt(hexVal, 16));
}
function convertDecimalValueTo2ByteHexArray(value) {
const hexValue = Number(value).toString(16).padStart(4, '0');
const chunk1 = hexValue.substr(0, 2);
const chunk2 = hexValue.substr(2);
return [chunk1, chunk2].map((hexVal) => parseInt(hexVal, 16));
}
async function onEventSetTime(type, data, device) {
if (data.type === 'commandSetTimeRequest' && data.cluster === 'manuSpecificTuya') {
try {
const utcTime = Math.round(((new Date()).getTime() - constants.OneJanuary2000) / 1000);
const localTime = utcTime - (new Date()).getTimezoneOffset() * 60;
const endpoint = device.getEndpoint(1);
const payload = {
payloadSize: 8,
payload: [
...convertDecimalValueTo4ByteHexArray(utcTime),
...convertDecimalValueTo4ByteHexArray(localTime),
],
};
await endpoint.command('manuSpecificTuya', 'setTime', payload, {});
} catch (error) {
// endpoint.command can throw an error which needs to
// be caught or the zigbee-herdsman may crash
// Debug message is handled in the zigbee-herdsman
}
}
}
// set UTC and Local Time as total number of seconds from 00: 00: 00 on January 01, 1970
// force to update every device time every hour due to very poor clock
async function onEventSetLocalTime(type, data, device) {
const nextLocalTimeUpdate = globalStore.getValue(device, 'nextLocalTimeUpdate');
const forceTimeUpdate = nextLocalTimeUpdate == null || nextLocalTimeUpdate < new Date().getTime();
if ((data.type === 'commandSetTimeRequest' && data.cluster === 'manuSpecificTuya') || forceTimeUpdate) {
globalStore.putValue(device, 'nextLocalTimeUpdate', new Date().getTime() + 3600 * 1000);
try {
const utcTime = Math.round(((new Date()).getTime()) / 1000);
const localTime = utcTime - (new Date()).getTimezoneOffset() * 60;
const endpoint = device.getEndpoint(1);
const payload = {
payloadSize: 8,
payload: [
...convertDecimalValueTo4ByteHexArray(utcTime),
...convertDecimalValueTo4ByteHexArray(localTime),
],
};
await endpoint.command('manuSpecificTuya', 'setTime', payload, {});
} catch (error) {
// endpoint.command can throw an error which needs to
// be caught or the zigbee-herdsman may crash
// Debug message is handled in the zigbee-herdsman
}
}
}
function convertStringToHexArray(value) {
const asciiKeys = [];
for (let i = 0; i < value.length; i ++) {
asciiKeys.push(value[i].charCodeAt(0));
}
return asciiKeys;
}
// Contains all covers which need their position inverted by default
// Default is 100 = open, 0 = closed; Devices listed here will use 0 = open, 100 = closed instead
// Use manufacturerName to identify device!
// Dont' invert _TZE200_cowvfni3: https://github.com/Koenkk/zigbee2mqtt/issues/6043
const coverPositionInvert = ['_TZE200_wmcdj3aq', '_TZE200_nogaemzt', '_TZE200_xuzcvlku'];
// Gets a boolean indicating whether the cover by this manufacturerName needs reversed positions
function isCoverInverted(manufacturerName) {
// Return true if cover is listed in coverPositionInvert
// Return false by default, not inverted
return coverPositionInvert.includes(manufacturerName);
}
const coverStateOverride = {
// Contains all covers which differentiate from the default enum states
// Use manufacturerName to identify device!
// https://github.com/Koenkk/zigbee2mqtt/issues/5596#issuecomment-759408189
'_TZE200_rddyvrci': {close: 1, open: 2, stop: 0},
'_TZE200_wmcdj3aq': {close: 0, open: 2, stop: 1},
'_TZE200_cowvfni3': {close: 0, open: 2, stop: 1},
'_TYST11_cowvfni3': {close: 0, open: 2, stop: 1},
};
// Gets an array containing which enums have to be used in order for the correct close/open/stop commands to be sent
function getCoverStateEnums(manufacturerName) {
if (manufacturerName in coverStateOverride) {
return coverStateOverride[manufacturerName];
} else {
return {close: 2, open: 0, stop: 1}; // defaults
}
}
const thermostatSystemModes = {
0: 'off',
1: 'auto',
2: 'manual',
3: 'comfort',
4: 'eco',
5: 'boost',
6: 'complex',
};
const thermostatSystemModes2 = {
0: 'auto',
1: 'cool',
2: 'heat',
3: 'dry',
4: 'fan',
};
const thermostatSystemModes3 = {
0: 'auto',
1: 'heat',
2: 'off',
};
const thermostatSystemModes4 = {
0: 'off',
1: 'auto',
2: 'heat',
};
const dataPoints = {
// Common data points
// Below data points are usually shared between devices
state: 1,
heatingSetpoint: 2,
coverPosition: 2,
dimmerLevel: 3,
localTemp: 3,
coverArrived: 3,
occupancy: 3,
mode: 4,
fanMode: 5,
motorDirection: 5,
config: 5,
childLock: 7,
coverChange: 7,
runningState: 14,
valveDetection: 20,
battery: 21,
tempCalibration: 44,
// Data points above 100 are usually custom function data points
waterLeak: 101,
minTemp: 102,
maxTemp: 103,
windowDetection: 104,
boostTime: 105,
coverSpeed: 105,
forceMode: 106,
comfortTemp: 107,
ecoTemp: 108,
valvePos: 109,
batteryLow: 110,
weekFormat: 111,
scheduleWorkday: 112,
scheduleHoliday: 113,
awayTemp: 114,
autoLock: 116,
awayDays: 117,
// Manufacturer specific
// Earda
eardaDimmerLevel: 2,
// Siterwell Thermostat
siterwellWindowDetection: 18,
// Moes Thermostat
moesHold: 2,
moesScheduleEnable: 3,
moesHeatingSetpoint: 16,
moesMaxTempLimit: 18,
moesMaxTemp: 19,
moesDeadZoneTemp: 20,
moesLocalTemp: 24,
moesTempCalibration: 27,
moesValve: 36,
moesChildLock: 40,
moesSensor: 43,
moesSchedule: 101,
etopErrorStatus: 13,
// Neo T&H
neoPowerType: 101,
neoMelody: 102,
neoDuration: 103,
neoAlarm: 104,
neoTemp: 105,
neoHumidity: 106,
neoMinTemp: 107,
neoMaxTemp: 108,
neoMinHumidity: 109,
neoMaxHumidity: 110,
neoUnknown2: 112,
neoTempAlarm: 113,
neoHumidityAlarm: 114,
neoUnknown3: 115,
neoVolume: 116,
// Saswell TRV
saswellWindowDetection: 8,
saswellFrostDetection: 10,
saswellTempCalibration: 27,
saswellChildLock: 40,
saswellState: 101,
saswellLocalTemp: 102,
saswellHeatingSetpoint: 103,
saswellValvePos: 104,
saswellBatteryLow: 105,
saswellAwayMode: 106,
saswellScheduleMode: 107,
saswellScheduleEnable: 108,
saswellScheduleSet: 109,
saswellSetpointHistoryDay: 110,
saswellTimeSync: 111,
saswellSetpointHistoryWeek: 112,
saswellSetpointHistoryMonth: 113,
saswellSetpointHistoryYear: 114,
saswellLocalHistoryDay: 115,
saswellLocalHistoryWeek: 116,
saswellLocalHistoryMonth: 117,
saswellLocalHistoryYear: 118,
saswellMotorHistoryDay: 119,
saswellMotorHistoryWeek: 120,
saswellMotorHistoryMonth: 121,
saswellMotorHistoryYear: 122,
saswellScheduleSunday: 123,
saswellScheduleMonday: 124,
saswellScheduleTuesday: 125,
saswellScheduleWednesday: 126,
saswellScheduleThursday: 127,
saswellScheduleFriday: 128,
saswellScheduleSaturday: 129,
saswellAntiScaling: 130,
// HY thermostat
hyHeating: 102,
hyExternalTemp: 103,
hyAwayDays: 104,
hyAwayTemp: 105,
hyMaxTempProtection: 106,
hyMinTempProtection: 107,
hyTempCalibration: 109,
hyHysteresis: 110,
hyProtectionHysteresis: 111,
hyProtectionMaxTemp: 112,
hyProtectionMinTemp: 113,
hyMaxTemp: 114,
hyMinTemp: 115,
hySensor: 116,
hyPowerOnBehavior: 117,
hyWeekFormat: 118,
hyWorkdaySchedule1: 119,
hyWorkdaySchedule2: 120,
hyHolidaySchedule1: 121,
hyHolidaySchedule2: 122,
hyState: 125,
hyHeatingSetpoint: 126,
hyLocalTemp: 127,
hyMode: 128,
hyChildLock: 129,
hyAlarm: 130,
// Silvercrest
silvercrestChangeMode: 2,
silvercrestSetBrightness: 3,
silvercrestSetColor: 5,
silvercrestSetEffect: 6,
// Fantem
fantemReportingTime: 102,
fantemTempCalibration: 104,
fantemHumidityCalibration: 105,
fantemLuxCalibration: 106,
fantemTemp: 107,
fantemHumidity: 108,
fantemMotionEnable: 109,
fantemBattery: 110,
fantemLedEnable: 111,
fantemReportingEnable: 112,
// Woox
wooxSwitch: 102,
// FrankEver
frankEverTimer: 9,
frankEverTreshold: 101,
// Dinrail power meter switch
dinrailPowerMeterTotalEnergy: 17,
dinrailPowerMeterCurrent: 18,
dinrailPowerMeterPower: 19,
dinrailPowerMeterVoltage: 20,
// tuya smart air box
tuyaSabCO2: 2,
tuyaSabTemp: 18,
tuyaSabHumidity: 19,
tuyaSabVOC: 21,
tuyaSabFormaldehyd: 22,
};
const thermostatWeekFormat = {
0: '5+2',
1: '6+1',
2: '7',
};
const thermostatForceMode = {
0: 'normal',
1: 'open',
2: 'close',
};
const thermostatPresets = {
0: 'away',
1: 'schedule',
2: 'manual',
3: 'comfort',
4: 'eco',
5: 'boost',
6: 'complex',
};
const thermostatScheduleMode = {
1: 'single', // One schedule for all days
2: 'weekday/weekend', // Weekdays(2-5) and Holidays(6-1)
3: 'weekday/sat/sun', // Weekdays(2-6), Saturday(7), Sunday(1)
4: '7day', // 7 day schedule
};
const silvercrestModes = {
white: 0,
color: 1,
effect: 2,
};
const silvercrestEffects = {
steady: '00',
snow: '01',
rainbow: '02',
snake: '03',
twinkle: '04',
firework: '08',
horizontal_flag: '06',
waves: '07',
updown: '08',
vintage: '09',
fading: '0a',
collide: '0b',
strobe: '0c',
sparkles: '0d',
carnaval: '0e',
glow: '0f',
};
const fanModes = {
0: 'low',
1: 'medium',
2: 'high',
3: 'auto',
};
async function sendDataPoint(entity, datatype, dp, data, cmd, transid) {
if (transid === undefined) {
if (sendDataPoint.transId === undefined) {
sendDataPoint.transId = 0;
} else {
sendDataPoint.transId++;
sendDataPoint.transId %= 256;
}
transid = sendDataPoint.transId;
}
await entity.command(
'manuSpecificTuya',
cmd || 'setData',
{
status: 0,
transid,
dp: dp,
datatype: datatype,
length_hi: (data.length >> 8) & 0xFF,
length_lo: data.length & 0xFF,
data: data,
},
{disableDefaultResponse: true},
);
}
async function sendDataPointValue(entity, dp, value, cmd, transid=undefined) {
await sendDataPoint(
entity,
dataTypes.value,
dp,
convertDecimalValueTo4ByteHexArray(value),
cmd,
transid,
);
}
async function sendDataPointBool(entity, dp, value, cmd, transid=undefined) {
await sendDataPoint(
entity,
dataTypes.bool,
dp,
[value ? 1 : 0],
cmd,
transid,
);
}
async function sendDataPointEnum(entity, dp, value, cmd, transid=undefined) {
await sendDataPoint(
entity,
dataTypes.enum,
dp,
[value],
cmd,
transid,
);
}
async function sendDataPointRaw(entity, dp, value, cmd, transid=undefined) {
await sendDataPoint(
entity,
dataTypes.raw,
dp,
value,
cmd,
transid,
);
}
async function sendDataPointBitmap(entity, dp, value, cmd, transid=undefined) {
await sendDataPoint(
entity,
dataTypes.bitmap,
dp,
value,
cmd,
transid,
);
}
module.exports = {
sendDataPoint,
sendDataPointValue,
sendDataPointBool,
sendDataPointEnum,
sendDataPointBitmap,
sendDataPointRaw,
getDataValue,
dataTypes,
dataPoints,
convertDecimalValueTo4ByteHexArray,
convertDecimalValueTo2ByteHexArray,
onEventSetTime,
onEventSetLocalTime,
convertStringToHexArray,
isCoverInverted,
getCoverStateEnums,
thermostatSystemModes4,
thermostatSystemModes3,
thermostatSystemModes2,
thermostatSystemModes,
thermostatWeekFormat,
thermostatForceMode,
thermostatPresets,
thermostatScheduleMode,
silvercrestModes,
silvercrestEffects,
fanModes,
};