zigbee-herdsman-converters
Version:
Collection of device converters to be used with zigbee-herdsman
1,050 lines • 87.8 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.modernExtend = exports.fz = exports.tz = exports.valueConverter = exports.valueConverterBasic = exports.Bitmap = exports.enum = exports.Enum = exports.whitelabel = exports.fingerprint = exports.configureMagicPacket = exports.skip = exports.exposes = exports.sendDataPointStringBuffer = exports.sendDataPointBitmap = exports.sendDataPointRaw = exports.sendDataPointEnum = exports.sendDataPointBool = exports.sendDataPointValue = exports.onEventSetLocalTime = exports.onEventSetTime = exports.onEventMeasurementPoll = exports.onEvent = exports.dataTypes = void 0;
const constants = __importStar(require("./constants"));
const globalStore = __importStar(require("./store"));
const exposes = __importStar(require("./exposes"));
const toZigbee_1 = __importDefault(require("../converters/toZigbee"));
const fromZigbee_1 = __importDefault(require("../converters/fromZigbee"));
const utils = __importStar(require("./utils"));
const modernExtend = __importStar(require("./modernExtend"));
const logger_1 = require("./logger");
// import {Color} from './color';
const NS = 'zhc:tuya';
const e = exposes.presets;
const ea = exposes.access;
exports.dataTypes = {
raw: 0, // [ bytes ]
bool: 1, // [0/1]
number: 2, // [ 4 byte value ]
string: 3, // [ N byte string ]
enum: 4, // [ 0-255 ]
bitmap: 5, // [ 1,2,4 bytes ] as bits
};
function convertBufferToNumber(chunks) {
let value = 0;
for (let i = 0; i < chunks.length; i++) {
value = value << 8;
value += chunks[i];
}
return value;
}
function convertStringToHexArray(value) {
const asciiKeys = [];
for (let i = 0; i < value.length; i++) {
asciiKeys.push(value[i].charCodeAt(0));
}
return asciiKeys;
}
function onEvent(args) {
return async (type, data, device, settings, state) => {
args = { queryOnDeviceAnnounce: false, timeStart: '1970', respondToMcuVersionResponse: true, ...args };
const endpoint = device.endpoints[0];
if (type === 'message' && data.cluster === 'manuSpecificTuya') {
if (args.respondToMcuVersionResponse && data.type === 'commandMcuVersionResponse') {
await endpoint.command('manuSpecificTuya', 'mcuVersionRequest', { 'seq': 0x0002 });
}
else if (data.type === 'commandMcuGatewayConnectionStatus') {
// "payload" can have the following values:
// 0x00: The gateway is not connected to the internet.
// 0x01: The gateway is connected to the internet.
// 0x02: The request timed out after three seconds.
const payload = { payloadSize: 1, payload: 1 };
await endpoint.command('manuSpecificTuya', 'mcuGatewayConnectionStatus', payload, {});
}
}
if (data.type === 'commandMcuSyncTime' && data.cluster === 'manuSpecificTuya') {
try {
const offset = args.timeStart === '2000' ? constants.OneJanuary2000 : 0;
const utcTime = Math.round(((new Date()).getTime() - offset) / 1000);
const localTime = utcTime - (new Date()).getTimezoneOffset() * 60;
const payload = {
payloadSize: 8,
payload: [
...convertDecimalValueTo4ByteHexArray(utcTime),
...convertDecimalValueTo4ByteHexArray(localTime),
],
};
await endpoint.command('manuSpecificTuya', 'mcuSyncTime', payload, {});
}
catch (error) {
/* handle error to prevent crash */
}
}
// Some devices require a dataQuery on deviceAnnounce, otherwise they don't report any data
if (args.queryOnDeviceAnnounce && type === 'deviceAnnounce') {
await endpoint.command('manuSpecificTuya', 'dataQuery', {});
}
if (args.queryIntervalSeconds) {
if (type === 'stop') {
clearTimeout(globalStore.getValue(device, 'query_interval'));
globalStore.clearValue(device, 'query_interval');
}
else if (!globalStore.hasValue(device, 'query_interval')) {
const setTimer = () => {
const timer = setTimeout(async () => {
try {
await endpoint.command('manuSpecificTuya', 'dataQuery', {});
}
catch (error) { /* Do nothing*/ }
setTimer();
}, args.queryIntervalSeconds * 1000);
globalStore.putValue(device, 'query_interval', timer);
};
setTimer();
}
}
};
}
exports.onEvent = onEvent;
function getDataValue(dpValue) {
let dataString = '';
switch (dpValue.datatype) {
case exports.dataTypes.raw:
return dpValue.data;
case exports.dataTypes.bool:
return dpValue.data[0] === 1;
case exports.dataTypes.number:
return convertBufferToNumber(dpValue.data);
case exports.dataTypes.string:
// Don't use .map here, doesn't work: https://github.com/Koenkk/zigbee-herdsman-converters/pull/1799/files#r530377091
for (let i = 0; i < dpValue.data.length; ++i) {
dataString += String.fromCharCode(dpValue.data[i]);
}
return dataString;
case exports.dataTypes.enum:
return dpValue.data[0];
case exports.dataTypes.bitmap:
return convertBufferToNumber(dpValue.data);
}
}
function convertDecimalValueTo4ByteHexArray(value) {
const hexValue = Number(value).toString(16).padStart(8, '0');
const chunk1 = hexValue.substring(0, 2);
const chunk2 = hexValue.substring(2, 4);
const chunk3 = hexValue.substring(4, 6);
const chunk4 = hexValue.substring(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.substring(0, 2);
const chunk2 = hexValue.substring(2);
return [chunk1, chunk2].map((hexVal) => parseInt(hexVal, 16));
}
async function onEventMeasurementPoll(type, data, device, options, electricalMeasurement = true, metering = false) {
const endpoint = device.getEndpoint(1);
if (type === 'stop') {
clearTimeout(globalStore.getValue(device, 'measurement_poll'));
globalStore.clearValue(device, 'measurement_poll');
}
else if (!globalStore.hasValue(device, 'measurement_poll')) {
const seconds = utils.toNumber(options && options.measurement_poll_interval ? options.measurement_poll_interval : 60, 'measurement_poll_interval');
if (seconds === -1)
return;
const setTimer = () => {
const timer = setTimeout(async () => {
try {
if (electricalMeasurement) {
await endpoint.read('haElectricalMeasurement', ['rmsVoltage', 'rmsCurrent', 'activePower']);
}
if (metering) {
await endpoint.read('seMetering', ['currentSummDelivered']);
}
}
catch (error) { /* Do nothing*/ }
setTimer();
}, seconds * 1000);
globalStore.putValue(device, 'measurement_poll', timer);
};
setTimer();
}
}
exports.onEventMeasurementPoll = onEventMeasurementPoll;
async function onEventSetTime(type, data, device) {
// FIXME: Need to join onEventSetTime/onEventSetLocalTime to one command
if (data.type === 'commandMcuSyncTime' && 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', 'mcuSyncTime', 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
}
}
}
exports.onEventSetTime = onEventSetTime;
// 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) {
// FIXME: What actually nextLocalTimeUpdate/forceTimeUpdate do?
// I did not find any timers or something else where it was used.
// Actually, there are two ways to set time on TuYa MCU devices:
// 1. Respond to the `commandMcuSyncTime` event
// 2. Just send `mcuSyncTime` anytime (by 1-hour timer or something else)
const nextLocalTimeUpdate = globalStore.getValue(device, 'nextLocalTimeUpdate');
const forceTimeUpdate = nextLocalTimeUpdate == null || nextLocalTimeUpdate < new Date().getTime();
if ((data.type === 'commandMcuSyncTime' && 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', 'mcuSyncTime', 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
}
}
}
exports.onEventSetLocalTime = onEventSetLocalTime;
// Return `seq` - transaction ID for handling concrete response
async function sendDataPoints(entity, dpValues, cmd = 'dataRequest', seq) {
if (seq === undefined) {
seq = globalStore.getValue(entity, 'sequence', 0);
globalStore.putValue(entity, 'sequence', (seq + 1) % 0xFFFF);
}
await entity.command('manuSpecificTuya', cmd, { seq, dpValues }, { disableDefaultResponse: true });
return seq;
}
function dpValueFromNumberValue(dp, value) {
return { dp, datatype: exports.dataTypes.number, data: convertDecimalValueTo4ByteHexArray(value) };
}
function dpValueFromBool(dp, value) {
return { dp, datatype: exports.dataTypes.bool, data: [value ? 1 : 0] };
}
function dpValueFromEnum(dp, value) {
return { dp, datatype: exports.dataTypes.enum, data: [value] };
}
function dpValueFromString(dp, string) {
return { dp, datatype: exports.dataTypes.string, data: convertStringToHexArray(string) };
}
function dpValueFromRaw(dp, rawBuffer) {
return { dp, datatype: exports.dataTypes.raw, data: rawBuffer };
}
function dpValueFromBitmap(dp, bitmapBuffer) {
return { dp, datatype: exports.dataTypes.bitmap, data: [bitmapBuffer] };
}
async function sendDataPointValue(entity, dp, value, cmd, seq) {
return await sendDataPoints(entity, [dpValueFromNumberValue(dp, value)], cmd, seq);
}
exports.sendDataPointValue = sendDataPointValue;
async function sendDataPointBool(entity, dp, value, cmd, seq) {
return await sendDataPoints(entity, [dpValueFromBool(dp, value)], cmd, seq);
}
exports.sendDataPointBool = sendDataPointBool;
async function sendDataPointEnum(entity, dp, value, cmd, seq) {
return await sendDataPoints(entity, [dpValueFromEnum(dp, value)], cmd, seq);
}
exports.sendDataPointEnum = sendDataPointEnum;
async function sendDataPointRaw(entity, dp, value, cmd, seq) {
return await sendDataPoints(entity, [dpValueFromRaw(dp, value)], cmd, seq);
}
exports.sendDataPointRaw = sendDataPointRaw;
async function sendDataPointBitmap(entity, dp, value, cmd, seq) {
return await sendDataPoints(entity, [dpValueFromBitmap(dp, value)], cmd, seq);
}
exports.sendDataPointBitmap = sendDataPointBitmap;
async function sendDataPointStringBuffer(entity, dp, value, cmd, seq) {
return await sendDataPoints(entity, [dpValueFromString(dp, value)], cmd, seq);
}
exports.sendDataPointStringBuffer = sendDataPointStringBuffer;
const tuyaExposes = {
lightType: () => e.enum('light_type', ea.STATE_SET, ['led', 'incandescent', 'halogen'])
.withDescription('Type of light attached to the device'),
lightBrightnessWithMinMax: () => e.light_brightness().withMinBrightness().withMaxBrightness()
.setAccess('state', ea.STATE_SET)
.setAccess('brightness', ea.STATE_SET)
.setAccess('min_brightness', ea.STATE_SET)
.setAccess('max_brightness', ea.STATE_SET),
lightBrightness: () => e.light_brightness()
.setAccess('state', ea.STATE_SET)
.setAccess('brightness', ea.STATE_SET),
countdown: () => e.numeric('countdown', ea.STATE_SET).withValueMin(0).withValueMax(43200).withValueStep(1).withUnit('s')
.withDescription('Countdown to turn device off after a certain time'),
switch: () => e.switch().setAccess('state', ea.STATE_SET),
selfTest: () => e.binary('self_test', ea.STATE_SET, true, false)
.withDescription('Indicates whether the device is being self-tested'),
selfTestResult: () => e.enum('self_test_result', ea.STATE, ['checking', 'success', 'failure', 'others'])
.withDescription('Result of the self-test'),
faultAlarm: () => e.binary('fault_alarm', ea.STATE, true, false).withDescription('Indicates whether a fault was detected'),
silence: () => e.binary('silence', ea.STATE_SET, true, false).withDescription('Silence the alarm'),
frostProtection: (extraNote = '') => e.binary('frost_protection', ea.STATE_SET, 'ON', 'OFF').withDescription(`When Anti-Freezing function is activated, the temperature in the house is kept at 8 °C.${extraNote}`),
errorStatus: () => e.numeric('error_status', ea.STATE).withDescription('Error status'),
scheduleAllDays: (access, format) => ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']
.map((day) => e.text(`schedule_${day}`, access).withDescription(`Schedule for ${day}, format: "${format}"`)),
temperatureUnit: () => e.enum('temperature_unit', ea.STATE_SET, ['celsius', 'fahrenheit']).withDescription('Temperature unit'),
temperatureCalibration: () => e.numeric('temperature_calibration', ea.STATE_SET).withValueMin(-2.0).withValueMax(2.0)
.withValueStep(0.1).withUnit('°C').withDescription('Temperature calibration'),
humidityCalibration: () => e.numeric('humidity_calibration', ea.STATE_SET).withValueMin(-30).withValueMax(30)
.withValueStep(1).withUnit('%').withDescription('Humidity calibration'),
gasValue: () => e.numeric('gas_value', ea.STATE).withDescription('Measured gas concentration'),
energyWithPhase: (phase) => e.numeric(`energy_${phase}`, ea.STATE).withUnit('kWh')
.withDescription(`Sum of consumed energy (phase ${phase.toUpperCase()})`),
energyProducedWithPhase: (phase) => e.numeric(`energy_produced_${phase}`, ea.STATE).withUnit('kWh')
.withDescription(`Sum of produced energy (phase ${phase.toUpperCase()})`),
energyFlowWithPhase: (phase) => e.enum(`energy_flow_${phase}`, ea.STATE, ['consuming', 'producing'])
.withDescription(`Direction of energy (phase ${phase.toUpperCase()})`),
voltageWithPhase: (phase) => e.numeric(`voltage_${phase}`, ea.STATE).withUnit('V')
.withDescription(`Measured electrical potential value (phase ${phase.toUpperCase()})`),
powerWithPhase: (phase) => e.numeric(`power_${phase}`, ea.STATE).withUnit('W')
.withDescription(`Instantaneous measured power (phase ${phase.toUpperCase()})`),
currentWithPhase: (phase) => e.numeric(`current_${phase}`, ea.STATE).withUnit('A')
.withDescription(`Instantaneous measured electrical current (phase ${phase.toUpperCase()})`),
powerFactorWithPhase: (phase) => e.numeric(`power_factor_${phase}`, ea.STATE).withUnit('%')
.withDescription(`Instantaneous measured power factor (phase ${phase.toUpperCase()})`),
switchType: () => e.enum('switch_type', ea.ALL, ['toggle', 'state', 'momentary']).withDescription('Type of the switch'),
backlightModeLowMediumHigh: () => e.enum('backlight_mode', ea.ALL, ['low', 'medium', 'high'])
.withDescription('Intensity of the backlight'),
backlightModeOffNormalInverted: () => e.enum('backlight_mode', ea.ALL, ['off', 'normal', 'inverted'])
.withDescription('Mode of the backlight'),
backlightModeOffOn: () => e.binary('backlight_mode', ea.ALL, 'ON', 'OFF').withDescription(`Mode of the backlight`),
indicatorMode: () => e.enum('indicator_mode', ea.ALL, ['off', 'off/on', 'on/off', 'on']).withDescription('LED indicator mode'),
indicatorModeNoneRelayPos: () => e.enum('indicator_mode', ea.ALL, ['none', 'relay', 'pos'])
.withDescription('Mode of the indicator light'),
powerOutageMemory: () => e.enum('power_outage_memory', ea.ALL, ['on', 'off', 'restore'])
.withDescription('Recover state after power outage'),
batteryState: () => e.enum('battery_state', ea.STATE, ['low', 'medium', 'high']).withDescription('State of the battery'),
doNotDisturb: () => e.binary('do_not_disturb', ea.STATE_SET, true, false)
.withDescription('Do not disturb mode, when enabled this function will keep the light OFF after a power outage'),
colorPowerOnBehavior: () => e.enum('color_power_on_behavior', ea.STATE_SET, ['initial', 'previous', 'cutomized'])
.withDescription('Power on behavior state'),
switchMode: () => e.enum('switch_mode', ea.STATE_SET, ['switch', 'scene'])
.withDescription('Sets the mode of the switch to act as a switch or as a scene'),
lightMode: () => e.enum('light_mode', ea.STATE_SET, ['normal', 'on', 'off', 'flash'])
.withDescription(`'Sets the indicator mode of l1.
Normal: Orange while off and white while on.
On: Always white. Off: Always orange.
Flash: Flashes white when triggered.
Note: Orange light will turn off after light off delay, white light always stays on. Light mode updates on next state change.'`),
};
exports.exposes = tuyaExposes;
exports.skip = {
// Prevent state from being published when already ON and brightness is also published.
// This prevents 100% -> X% brightness jumps when the switch is already on
// https://github.com/Koenkk/zigbee2mqtt/issues/13800#issuecomment-1263592783
stateOnAndBrightnessPresent: (meta) => {
if (Array.isArray(meta.mapped))
throw new Error('Not supported');
const convertedKey = meta.mapped.meta.multiEndpoint && meta.endpoint_name ? `state_${meta.endpoint_name}` : 'state';
return meta.message.hasOwnProperty('brightness') && meta.state[convertedKey] === meta.message.state;
},
};
const configureMagicPacket = async (device, coordinatorEndpoint) => {
try {
const endpoint = device.endpoints[0];
await endpoint.read('genBasic', ['manufacturerName', 'zclVersion', 'appVersion', 'modelId', 'powerSource', 0xfffe]);
}
catch (e) {
// Fails for some TuYa devices with UNSUPPORTED_ATTRIBUTE, ignore that.
// e.g. https://github.com/Koenkk/zigbee2mqtt/issues/14857
if (e.message.includes('UNSUPPORTED_ATTRIBUTE')) {
logger_1.logger.debug('configureMagicPacket failed, ignoring...', NS);
}
else {
throw e;
}
}
};
exports.configureMagicPacket = configureMagicPacket;
const fingerprint = (modelID, manufacturerNames) => {
return manufacturerNames.map((manufacturerName) => {
return { modelID, manufacturerName };
});
};
exports.fingerprint = fingerprint;
const whitelabel = (vendor, model, description, manufacturerNames) => {
const fingerprint = manufacturerNames.map((manufacturerName) => {
return { manufacturerName };
});
return { vendor, model, description, fingerprint };
};
exports.whitelabel = whitelabel;
class Base {
value;
constructor(value) {
this.value = value;
}
valueOf() {
return this.value;
}
}
class Enum extends Base {
constructor(value) {
super(value);
}
}
exports.Enum = Enum;
const enumConstructor = (value) => new Enum(value);
exports.enum = enumConstructor;
class Bitmap extends Base {
constructor(value) {
super(value);
}
}
exports.Bitmap = Bitmap;
exports.valueConverterBasic = {
lookup: (map, fallbackValue) => {
return {
to: (v, meta) => utils.getFromLookup(v, typeof map === 'function' ? map(meta.options, meta.device) : map),
from: (v, _meta, options) => {
const m = typeof map === 'function' ? map(options, _meta.device) : map;
const value = Object.entries(m).find((i) => i[1].valueOf() === v);
if (!value) {
if (fallbackValue !== undefined)
return fallbackValue;
throw new Error(`Value '${v}' is not allowed, expected one of ${Object.values(m)}`);
}
return value[0];
},
};
},
scale: (min1, max1, min2, max2) => {
return {
to: (v) => utils.mapNumberRange(v, min1, max1, min2, max2),
from: (v) => utils.mapNumberRange(v, min2, max2, min1, max1),
};
},
raw: () => {
return { to: (v) => v, from: (v) => v };
},
divideBy: (value) => {
return { to: (v) => v * value, from: (v) => v / value };
},
divideByFromOnly: (value) => {
return { to: (v) => v, from: (v) => v / value };
},
trueFalse: (valueTrue) => {
return { from: (v) => v === valueTrue.valueOf() };
},
// color1000: () => {
// return {
// // eslint-disable-next-line
// to: (value: any, meta?: Tz.Meta) => {
// const make4sizedString = (v: string) => {
// if (v.length >= 4) {
// return v;
// } else if (v.length === 3) {
// return '0' + v;
// } else if (v.length === 2) {
// return '00' + v;
// } else if (v.length === 1) {
// return '000' + v;
// } else {
// return '0000';
// }
// };
// const fillInHSB = (h: number, s: number, b: number, state: KeyValueAny) => {
// // Define default values. Device expects leading zero in string.
// const hsb = {
// h: '0168', // 360
// s: '03e8', // 1000
// b: '03e8', // 1000
// };
// if (h) {
// // The device expects 0-359
// if (h >= 360) {
// h = 359;
// }
// hsb.h = make4sizedString(h.toString(16));
// } else if (state.color && state.color.hue) {
// hsb.h = make4sizedString(state.color.hue.toString(16));
// }
// // Device expects 0-1000, saturation normally is 0-100 so we expect that from the user
// // The device expects a round number, otherwise everything breaks
// if (s) {
// hsb.s = make4sizedString(utils.mapNumberRange(s, 0, 100, 0, 1000).toString(16));
// } else if (state.color && state.color.saturation) {
// hsb.s = make4sizedString(utils.mapNumberRange(state.color.saturation, 0, 100, 0, 1000).toString(16));
// }
// // Scale 0-255 to 0-1000 what the device expects.
// if (b != null) {
// hsb.b = make4sizedString(utils.mapNumberRange(b, 0, 255, 0, 1000).toString(16));
// } else if (state.brightness != null) {
// hsb.b = make4sizedString(utils.mapNumberRange(state.brightness, 0, 255, 0, 1000).toString(16));
// }
// return hsb;
// };
// const newColor = Color.fromConverterArg(value);
// let hsv;
// if (newColor.isRGB()) {
// hsv = newColor.rgb.toHSV();
// } else {
// if (newColor.isHSV()) {
// hsv = newColor.hsv;
// }
// }
// const hsb = fillInHSB(
// utils.precisionRound(hsv.hue, 0) || null,
// utils.precisionRound(hsv.saturation, 0) || null,
// utils.precisionRound(hsv.brightness, 0) || null,
// meta.state,
// );
// const data: string = hsb.h + hsb.s + hsb.b;
// return data;
// },
// // eslint-disable-next-line
// from: (value: any) => {
// const result: KeyValueAny = {};
// const h = parseInt(value.substring(0, 4), 16);
// const s = parseInt(value.substring(4, 8), 16);
// const b = parseInt(value.substring(8, 12), 16);
// result.color_mode = 'hs';
// result.color = {hue: h, saturation: utils.mapNumberRange(s, 0, 1000, 0, 100)};
// result.brightness = utils.mapNumberRange(b, 0, 1000, 0, 255);
// return result;
// },
// };
// },
};
exports.valueConverter = {
trueFalse0: exports.valueConverterBasic.trueFalse(0),
trueFalse1: exports.valueConverterBasic.trueFalse(1),
trueFalseInvert: {
to: (v) => !v,
from: (v) => !v,
},
trueFalseEnum0: exports.valueConverterBasic.trueFalse(new Enum(0)),
trueFalseEnum1: exports.valueConverterBasic.trueFalse(new Enum(1)),
onOff: exports.valueConverterBasic.lookup({ 'ON': true, 'OFF': false }),
powerOnBehavior: exports.valueConverterBasic.lookup({ 'off': 0, 'on': 1, 'previous': 2 }),
powerOnBehaviorEnum: exports.valueConverterBasic.lookup({ 'off': new Enum(0), 'on': new Enum(1), 'previous': new Enum(2) }),
switchType: exports.valueConverterBasic.lookup({ 'momentary': new Enum(0), 'toggle': new Enum(1), 'state': new Enum(2) }),
switchType2: exports.valueConverterBasic.lookup({ 'toggle': new Enum(0), 'state': new Enum(1), 'momentary': new Enum(2) }),
backlightModeOffNormalInverted: exports.valueConverterBasic.lookup({ 'off': new Enum(0), 'normal': new Enum(1), 'inverted': new Enum(2) }),
backlightModeOffLowMediumHigh: exports.valueConverterBasic.lookup({ 'off': new Enum(0), 'low': new Enum(1), 'medium': new Enum(2), 'high': new Enum(3) }),
lightType: exports.valueConverterBasic.lookup({ 'led': 0, 'incandescent': 1, 'halogen': 2 }),
countdown: exports.valueConverterBasic.raw(),
scale0_254to0_1000: exports.valueConverterBasic.scale(0, 254, 0, 1000),
scale0_1to0_1000: exports.valueConverterBasic.scale(0, 1, 0, 1000),
divideBy100: exports.valueConverterBasic.divideBy(100),
temperatureUnit: exports.valueConverterBasic.lookup({ 'celsius': 0, 'fahrenheit': 1 }),
temperatureUnitEnum: exports.valueConverterBasic.lookup({ 'celsius': new Enum(0), 'fahrenheit': new Enum(1) }),
batteryState: exports.valueConverterBasic.lookup({ 'low': 0, 'medium': 1, 'high': 2 }),
divideBy10: exports.valueConverterBasic.divideBy(10),
divideBy1000: exports.valueConverterBasic.divideBy(1000),
divideBy10FromOnly: exports.valueConverterBasic.divideByFromOnly(10),
switchMode: exports.valueConverterBasic.lookup({ 'switch': new Enum(0), 'scene': new Enum(1) }),
lightMode: exports.valueConverterBasic.lookup({ 'normal': new Enum(0), 'on': new Enum(1), 'off': new Enum(2), 'flash': new Enum(3) }),
raw: exports.valueConverterBasic.raw(),
workingDay: exports.valueConverterBasic.lookup({ 'disabled': new Enum(0), '6-1': new Enum(1), '5-2': new Enum(2), '7': new Enum(3) }),
localTemperatureCalibration: {
from: (value) => value > 4000 ? value - 4096 : value,
to: (value) => value < 0 ? 4096 + value : value,
},
setLimit: {
to: (v) => {
if (!v)
throw new Error('Limit cannot be unset, use factory_reset');
return v;
},
from: (v) => v,
},
coverPosition: {
to: async (v, meta) => {
return meta.options.invert_cover ? 100 - v : v;
},
from: (v, meta, options) => {
return options.invert_cover ? 100 - v : v;
},
},
coverPositionInverted: {
to: async (v, meta) => {
return meta.options.invert_cover ? v : 100 - v;
},
from: (v, meta, options) => {
return options.invert_cover ? v : 100 - v;
},
},
tubularMotorDirection: exports.valueConverterBasic.lookup({ 'normal': new Enum(0), 'reversed': new Enum(1) }),
plus1: {
from: (v) => v + 1,
to: (v) => v - 1,
},
static: (value) => {
return {
from: (v) => {
return value;
},
};
},
phaseVariant1: {
from: (v) => {
const buffer = Buffer.from(v, 'base64');
return { voltage: (buffer[14] | buffer[13] << 8) / 10, current: (buffer[12] | buffer[11] << 8) / 1000 };
},
},
phaseVariant2: {
from: (v) => {
const buf = Buffer.from(v, 'base64');
return { voltage: (buf[1] | buf[0] << 8) / 10, current: (buf[4] | buf[3] << 8) / 1000, power: (buf[7] | buf[6] << 8) };
},
},
phaseVariant2WithPhase: (phase) => {
return {
from: (v) => {
const buf = Buffer.from(v, 'base64');
return {
[`voltage_${phase}`]: (buf[1] | buf[0] << 8) / 10,
[`current_${phase}`]: (buf[4] | buf[3] << 8) / 1000,
[`power_${phase}`]: (buf[7] | buf[6] << 8)
};
},
};
},
phaseVariant3: {
from: (v) => {
const buf = Buffer.from(v, 'base64');
return {
voltage: ((buf[0] << 8) | buf[1]) / 10,
current: ((buf[2] << 16) | (buf[3] << 8) | buf[4]) / 1000,
power: ((buf[5] << 16) | (buf[6] << 8) | buf[7]),
};
},
},
threshold: {
from: (v) => {
const buffer = Buffer.from(v, 'base64');
const stateLookup = { 0: 'not_set', 1: 'over_current_threshold', 3: 'over_voltage_threshold' };
const protectionLookup = { 0: 'OFF', 1: 'ON' };
return {
threshold_1_protection: protectionLookup[buffer[1]],
threshold_1: stateLookup[buffer[0]],
threshold_1_value: (buffer[3] | buffer[2] << 8),
threshold_2_protection: protectionLookup[buffer[5]],
threshold_2: stateLookup[buffer[4]],
threshold_2_value: (buffer[7] | buffer[6] << 8),
};
},
},
selfTestResult: exports.valueConverterBasic.lookup({ 'checking': 0, 'success': 1, 'failure': 2, 'others': 3 }),
lockUnlock: exports.valueConverterBasic.lookup({ 'LOCK': true, 'UNLOCK': false }),
localTempCalibration1: {
from: (v) => {
if (v > 55)
v -= 0x100000000;
return v / 10;
},
to: (v) => {
if (v > 0)
return v * 10;
if (v < 0)
return v * 10 + 0x100000000;
return v;
},
},
localTempCalibration2: {
from: (v) => v,
to: (v) => {
if (v < 0)
return v + 0x100000000;
return v;
},
},
localTempCalibration3: {
from: (v) => {
if (v > 0x7FFFFFFF)
v -= 0x100000000;
return v / 10;
},
to: (v) => {
if (v > 0)
return v * 10;
if (v < 0)
return v * 10 + 0x100000000;
return v;
},
},
thermostatHolidayStartStop: {
from: (v) => {
const start = {
year: v.slice(0, 4), month: v.slice(4, 6), day: v.slice(6, 8),
hours: v.slice(8, 10), minutes: v.slice(10, 12),
};
const end = {
year: v.slice(12, 16), month: v.slice(16, 18), day: v.slice(18, 20),
hours: v.slice(20, 22), minutes: v.slice(22, 24),
};
const startStr = `${start.year}/${start.month}/${start.day} ${start.hours}:${start.minutes}`;
const endStr = `${end.year}/${end.month}/${end.day} ${end.hours}:${end.minutes}`;
return `${startStr} | ${endStr}`;
},
to: (v) => {
const numberPattern = /\d+/g;
// @ts-ignore
return v.match(numberPattern).join([]).toString();
},
},
thermostatScheduleDaySingleDP: {
from: (v) => {
// day split to 10 min segments = total 144 segments
const maxPeriodsInDay = 10;
const periodSize = 3;
const schedule = [];
for (let i = 0; i < maxPeriodsInDay; i++) {
const time = v[i * periodSize];
const totalMinutes = time * 10;
const hours = totalMinutes / 60;
const rHours = Math.floor(hours);
const minutes = (hours - rHours) * 60;
const rMinutes = Math.round(minutes);
const strHours = rHours.toString().padStart(2, '0');
const strMinutes = rMinutes.toString().padStart(2, '0');
const tempHexArray = [v[i * periodSize + 1], v[i * periodSize + 2]];
const tempRaw = Buffer.from(tempHexArray).readUIntBE(0, tempHexArray.length);
const temp = tempRaw / 10;
schedule.push(`${strHours}:${strMinutes}/${temp}`);
if (rHours === 24)
break;
}
return schedule.join(' ');
},
to: (v, meta) => {
const dayByte = {
monday: 1, tuesday: 2, wednesday: 4, thursday: 8,
friday: 16, saturday: 32, sunday: 64,
};
const weekDay = v.week_day;
utils.assertString(weekDay, 'week_day');
if (Object.keys(dayByte).indexOf(weekDay) === -1) {
throw new Error('Invalid "week_day" property value: ' + weekDay);
}
let weekScheduleType;
if (meta.state && meta.state.working_day)
weekScheduleType = meta.state.working_day;
const payload = [];
switch (weekScheduleType) {
case 'mon_sun':
payload.push(127);
break;
case 'mon_fri+sat+sun':
if (['saturday', 'sunday'].indexOf(weekDay) === -1) {
payload.push(31);
break;
}
payload.push(dayByte[weekDay]);
break;
case 'separate':
payload.push(dayByte[weekDay]);
break;
default:
throw new Error('Invalid "working_day" property, need to set it before');
}
// day split to 10 min segments = total 144 segments
const maxPeriodsInDay = 10;
utils.assertString(v.schedule, 'schedule');
const schedule = v.schedule.split(' ');
const schedulePeriods = schedule.length;
if (schedulePeriods > 10)
throw new Error('There cannot be more than 10 periods in the schedule: ' + v);
if (schedulePeriods < 2)
throw new Error('There cannot be less than 2 periods in the schedule: ' + v);
let prevHour;
for (const period of schedule) {
const timeTemp = period.split('/');
const hm = timeTemp[0].split(':', 2);
const h = parseInt(hm[0]);
const m = parseInt(hm[1]);
const temp = parseFloat(timeTemp[1]);
if (h < 0 || h > 24 || m < 0 || m >= 60 || m % 10 !== 0 || temp < 5 || temp > 30 || temp % 0.5 !== 0) {
throw new Error('Invalid hour, minute or temperature of: ' + period);
}
else if (prevHour > h) {
throw new Error(`The hour of the next segment can't be less than the previous one: ${prevHour} > ${h}`);
}
prevHour = h;
const segment = (h * 60 + m) / 10;
const tempHexArray = convertDecimalValueTo2ByteHexArray(temp * 10);
payload.push(segment, ...tempHexArray);
}
// Add "technical" periods to be valid payload
for (let i = 0; i < maxPeriodsInDay - schedulePeriods; i++) {
// by default it sends 9000b2, it's 24 hours and 18 degrees
payload.push(144, 0, 180);
}
return payload;
},
},
thermostatScheduleDayMultiDP: {
from: (v) => {
const schedule = [];
for (let index = 1; index < 17; index = index + 4) {
schedule.push(String(parseInt(v[index + 0])).padStart(2, '0') + ':' +
String(parseInt(v[index + 1])).padStart(2, '0') + '/' +
// @ts-ignore
(parseFloat((v[index + 2] << 8) + v[index + 3]) / 10.0).toFixed(1));
}
return schedule.join(' ');
},
to: (v) => {
const payload = [0];
const transitions = v.split(' ');
if (transitions.length != 4) {
throw new Error('Invalid schedule: there should be 4 transitions');
}
for (const transition of transitions) {
const timeTemp = transition.split('/');
if (timeTemp.length != 2) {
throw new Error('Invalid schedule: wrong transition format: ' + transition);
}
const hourMin = timeTemp[0].split(':');
const hour = parseInt(hourMin[0]);
const min = parseInt(hourMin[1]);
const temperature = Math.floor(parseFloat(timeTemp[1]) * 10);
if (hour < 0 || hour > 24 || min < 0 || min > 60 || temperature < 50 || temperature > 300) {
throw new Error('Invalid hour, minute or temperature of: ' + transition);
}
payload.push(hour, min, (temperature & 0xff00) >> 8, temperature & 0xff);
}
return payload;
},
},
thermostatScheduleDayMultiDPWithDayNumber: (dayNum) => {
return {
from: (v) => exports.valueConverter.thermostatScheduleDayMultiDP.from(v),
to: (v) => {
const data = exports.valueConverter.thermostatScheduleDayMultiDP.to(v);
data[0] = dayNum;
return data;
},
};
},
tv02Preset: () => {
return {
from: (v) => {
if (v === 0)
return 'auto';
else if (v === 1)
return 'manual';
else
return 'holiday'; // 2 and 3 are holiday
},
to: (v, meta) => {
if (v === 'auto')
return new Enum(0);
else if (v === 'manual')
return new Enum(1);
else if (v === 'holiday') {
// https://github.com/Koenkk/zigbee2mqtt/issues/20486
if (meta.device.manufacturerName === '_TZE200_mudxchsu')
return new Enum(2);
else
return new Enum(3);
}
else
throw new Error(`Unsupported preset '${v}'`);
},
};
},
thermostatSystemModeAndPreset: (toKey) => {
return {
from: (v) => {
utils.assertNumber(v, 'system_mode');
const presetLookup = { 0: 'auto', 1: 'manual', 2: 'off', 3: 'on' };
const systemModeLookup = { 0: 'auto', 1: 'auto', 2: 'off', 3: 'heat' };
return { preset: presetLookup[v], system_mode: systemModeLookup[v] };
},
to: (v) => {
const presetLookup = { 'auto': new Enum(0), 'manual': new Enum(1), 'off': new Enum(2), 'on': new Enum(3) };
const systemModeLookup = { 'auto': new Enum(1), 'off': new Enum(2), 'heat': new Enum(3) };
const lookup = toKey === 'preset' ? presetLookup : systemModeLookup;
return utils.getFromLookup(v, lookup);
},
};
},
ZWT198_schedule: {
from: (value, meta, options) => {
const programmingMode = [];
for (let i = 0; i < 8; i++) {
const start = i * 4;
const time = value[start].toString().padStart(2, '0') + ':' + value[start + 1].toString().padStart(2, '0');
const temp = (value[start + 2] * 256 + value[start + 3]) / 10;
const tempStr = temp.toFixed(1) + '°C';
programmingMode.push(time + '/' + tempStr);
}
return {
schedule_weekday: programmingMode.slice(0, 6).join(' '),
schedule_holiday: programmingMode.slice(6, 8).join(' '),
};
},
to: async (v, meta) => {
const dpId = 109;
const payload = [];
let weekdayFormat;
let holidayFormat;
if (meta.message.hasOwnProperty('schedule_weekday')) {
weekdayFormat = v;
holidayFormat = meta.state['schedule_holiday'];
}
else {
weekdayFormat = meta.state['schedule_weekday'];
holidayFormat = v;
}
function scheduleToRaw(key, input, number, payload, meta) {
const items = input.trim().split(/\s+/);
if (items.length != number) {
throw new Error('Wrong number of items for ' + key + ' :' + items.length);
}
else {
for (let i = 0; i < number; i++) {
const timeTemperature = items[i].split('/');
if (timeTemperature.length != 2) {
throw new Error('Invalid schedule: wrong transition format: ' + items[i]);
}
const hourMinute = timeTemperature[0].split(':', 2);
const hour = parseInt(hourMinute[0]);
const minute = parseInt(hourMinute[1]);
const temperature = parseFloat(timeTemperature[1]);
if (!utils.isNumber(hour) || !utils.isNumber(temperature) || !utils.isNumber(minute) ||
hour < 0 || hour >= 24 ||
minute < 0 || minute >= 60 ||
temperature < 5 || temperature >= 35) {
throw new Error('Invalid hour, minute or temperature (5<t<35) in ' + key + ' of: `' +
items[i] + '`; Format is `hh:m/cc.c` or `hh:mm/cc.c°C`');
}
const temperature10 = Math.round(temperature * 10);
payload.push(hour, minute, (temperature10 >> 8) & 0xFF, temperature10 & 0xFF);
}
}
return;
}
scheduleToRaw('schedule_weekday', weekdayFormat, 6, payload, meta);
scheduleToRaw('schedule_holiday', holidayFormat, 2, payload, meta);
const entity = meta.device.endpoints[0];
const sendCommand = utils.getMetaValue(entity, meta.mapped, 'tuyaSendCommand', undefined, 'dataRequest');
await sendDataPointRaw(entity, dpId, payload, sendCommand, 1);
},
},
TV02SystemMode: {
to: async (v, meta) => {
const entity = meta.device.endpoints[0];
if (meta.message.system_mode) {
if (meta.message.system_mode === 'off') {
await sendDataPointBool(entity, 107, true, 'dataRequest', 1);
}
else {
await sendDataPointEnum(entity, 2, 1, 'dataRequest', 1); // manual
}
}
else if (meta.message.heating_stop) {
if (meta.message.heating_stop === 'ON') {
await sendDataPointBool(entity, 107, true, 'dataRequest', 1);
}
else {
await sendDataPointEnum(entity, 2, 1, 'dataRequest', 1); // manual
}
}
},
from: (v) => {
return { system_mode: v === false ? 'heat' : 'off', heating_stop: v === false ? 'OFF' : 'ON' };
},
},
TV02FrostProtection: {
to: async (v, meta) => {
const entity = meta.device.endpoints[0];
if (v === 'ON') {
await sendDataPointBool(entity, 10, true, 'dataRequest', 1);
}
else {
await sendDataPointEnum(entity, 2, 1, 'dataRequest', 1); // manual
}
},
from: (v) => {
return { frost_protection: v === false ? 'OFF' : 'ON' };
},
},
inverse: { to: (v) => !v, from: (v) => !v },
onOffNotStrict: { from: (v) => v ? 'ON' : 'OFF', to: (v) => v === 'ON' },
errorOrBatteryLow: {
from: (v) => {
if (v === 0)
return { 'battery_low': false };
if (v === 1)
return { 'battery_low': true };
return { 'error': v };
},
},
};
const tuyaTz = {
power_on_behavior_1: {
key: ['power_on_behavior', 'power_outage_memory'],
convertSet: async (entity, key, value, meta) => {
// Deprecated: remove power_outage_memory
const moesStartUpOnOff = utils.getFromLookup(value, key === 'power_on_behavior' ?
{ 'off': 0, 'on': 1, 'previous': 2 } : { 'off': 0, 'on': 1, 'restore': 2 });
await entity.write('genOnOff', { moesStartUpOnOff });
return { state: { [key]: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read('genOnOff', ['moesStartUpOnOff']);
},
},
power_on_behavior_2: {
key: ['power_on_behavior'],
convertSet: async (entity, key, value, meta) => {
const powerOnBehavior = utils.getFromLookup(value, { 'off': 0, 'on': 1, 'previous': 2 });
await entity.write('manuSpecificTuya_3', { powerOnBehavior });
return { state: { [key]: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read('manuSpecificTuya_3', ['powerOnBehavior']);
},
},
switch_type: {
key: ['switch_type'],
convertSet: async (entity, key, value, meta) => {
const switchType = utils.getFromLookup(value, { 'toggle': 0, 'state': 1, 'momentary': 2 });
await entity.write('manuSpecificTuya_3', { switchType }, { disableDefaultResponse: true });
return { state: { [key]: value } };
},
convertGet: async (entity, key, meta) => {
await entity.read('manuSpecificTuya_3', ['switchType']);
},
},
backlight_indicat