Skip to content

Commit

Permalink
feat: Support schedule for Avatto ZWT198 and other improvements (#6571)
Browse files Browse the repository at this point in the history
* Added New Avatto ZWT198 updat including scedule, reset and brightness  in devices/tuya.ts;  lib/exposes.ts end lib.tuya.ts

* Final changes, got rid of composite()

* Update exposes.ts

* Update tuya.ts

* Update tuya.ts

---------

Co-authored-by: Koen Kanters <[email protected]>
  • Loading branch information
marcelhoogantink and Koenkk authored Nov 28, 2023
1 parent b7b6788 commit d18f537
Show file tree
Hide file tree
Showing 2 changed files with 150 additions and 30 deletions.
90 changes: 61 additions & 29 deletions src/devices/tuya.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1299,7 +1299,7 @@ const definitions: Definition[] = [
[5, 'max_brightness', tuya.valueConverter.scale0_254to0_1000],
[6, 'countdown', tuya.valueConverter.countdown],
[14, 'power_on_behavior', tuya.valueConverter.powerOnBehavior],
[21, 'backlight_mode', tuya.valueConverter.backlightMode],
[21, 'backlight_mode', tuya.valueConverter.backlightModeOffNormalInverted],
],
},
whiteLabel: [
Expand Down Expand Up @@ -1339,7 +1339,7 @@ const definitions: Definition[] = [
[11, 'max_brightness_l2', tuya.valueConverter.scale0_254to0_1000],
[12, 'countdown_l2', tuya.valueConverter.countdown],
[14, 'power_on_behavior', tuya.valueConverter.powerOnBehaviorEnum],
[21, 'backlight_mode', tuya.valueConverter.backlightMode],
[21, 'backlight_mode', tuya.valueConverter.backlightModeOffNormalInverted],
],
},
endpoint: (device) => {
Expand Down Expand Up @@ -1387,7 +1387,7 @@ const definitions: Definition[] = [
[19, 'max_brightness_l3', tuya.valueConverter.scale0_254to0_1000],
[20, 'countdown_l3', tuya.valueConverter.countdown],
[14, 'power_on_behavior', tuya.valueConverter.powerOnBehaviorEnum],
[21, 'backlight_mode', tuya.valueConverter.backlightMode],
[21, 'backlight_mode', tuya.valueConverter.backlightModeOffNormalInverted],
],
},
endpoint: (device) => {
Expand Down Expand Up @@ -4169,65 +4169,97 @@ const definitions: Definition[] = [
fingerprint: tuya.fingerprint('TS0601', ['_TZE200_viy9ihs7']),
model: 'ZWT198',
vendor: 'TuYa',
description: 'AVATTO battery wall-mount thermostat',
fromZigbee: [tuya.fz.datapoints, fz.ignore_tuya_set_time],
toZigbee: [tuya.tz.datapoints],
description: 'Avatto wall thermostat',
onEvent: tuya.onEvent({timeStart: '1970'}),
fromZigbee: [tuya.fz.datapoints],
toZigbee: [tuya.tz.datapoints],
configure: tuya.configureMagicPacket,
exposes: [
e.binary('factory_reset', ea.STATE_SET, 'ON', 'OFF')
.withDescription('Full factory reset, use with caution!'),
e.child_lock(),
e.climate()
.withSystemMode(['off', 'heat'], ea.STATE_SET)
.withPreset(['auto', 'manual', 'temporary program'])
.withPreset(['auto', 'manual', 'temporary_manual'])
.withSetpoint('current_heating_setpoint', 5, 35, 0.5, ea.STATE_SET)
.withRunningState(['idle', 'heat'], ea.STATE)
.withLocalTemperature(ea.STATE)
.withLocalTemperatureCalibration(-9.9, 9.9, 0.1, ea.STATE_SET),
e.binary('frost_protection', ea.STATE_SET, 'ON', 'OFF')
.withDescription('Antifreeze function'),
e.numeric('upper_temp', ea.STATE_SET).withUnit('°C').withValueMax(95)
.withValueMin(15).withValueStep(1).withPreset('default', 60, 'Default value')
e.max_temperature_limit()
.withUnit('°C')
.withValueMin(15)
.withValueMax(90)
.withValueStep(0.5)
.withPreset('default', 60, 'Default value')
.withDescription('Maximum upper temperature'),
e.numeric('deadzone_temperature', ea.STATE_SET).withUnit('°C').withValueMax(10)
.withValueMin(0.5).withValueStep(0.5).withPreset('default', 1, 'Default value')
.withDescription('The delta between local_temperature and current_heating_setpoint to trigger Heat'),
// Not working yet:
// e.enum('schedule_mode', ea.STATE_SET, ['disabled','weekday/sat+sun','weekday+sat/sun','7day'])
// .withDescription('Schedule mode')
e.numeric('deadzone_temperature', ea.STATE_SET)
.withUnit('°C')
.withValueMax(10)
.withValueMin(0.5)
.withValueStep(0.5)
.withPreset('default', 1, 'Default value')
.withDescription('The delta between local_temperature (5<t<35)and current_heating_setpoint to trigger Heat'),
e.enum('backlight_mode', ea.STATE_SET, ['off', 'low', 'medium', 'high'])
.withDescription('Intensity of the backlight'),
e.enum('working_day', ea.STATE_SET, ['disabled', '6-1', '5-2', '7'])
.withDescription('Workday setting'),
e.text('schedule_weekday', ea.STATE_SET).withDescription('Workdays (6 times `hh:mm/cc.c°C`)'),
e.text('schedule_holiday', ea.STATE_SET).withDescription('Holidays (2 times `hh:mm/cc.c°C)`'),
// ============== exposes for found, but not functional datapoints:
/*
e.min_temperature_limit() // dp 16
.withValueMin(5)
.withValueMax(15)
.withValueStep(0.5)
.withPreset('default', 10, 'Default value')
.withDescription('dp16 is listed in Tuya, but no communication from device'),
e.binary('dp105', ea.STATE_SET, 'ON', 'OFF')
.withDescription('dp105 is not listed in Tuya, but device sends datapoint, binary: true/false'),
e.binary('dp111', ea.STATE_SET, 'ON', 'OFF')
.withDescription('dp111 is not listed in Tuya, but device sends datapoint, binary: true/false'),
*/
],
meta: {
tuyaDatapoints: [
[1, 'system_mode', tuya.valueConverterBasic.lookup({'heat': true, 'off': false})],
[2, 'current_heating_setpoint', tuya.valueConverter.divideBy10],
[3, 'local_temperature', tuya.valueConverter.divideBy10],
[4, 'preset', tuya.valueConverterBasic.lookup({'auto': tuya.enum(0), 'manual': tuya.enum(1), 'temporary program': tuya.enum(2)})],
[4, 'preset', tuya.valueConverterBasic.lookup({'auto': tuya.enum(0), 'manual': tuya.enum(1), 'temporary_manual': tuya.enum(2)})],
[9, 'child_lock', tuya.valueConverter.lockUnlock],
[15, 'upper_temp', tuya.valueConverter.divideBy10],
[19, 'local_temperature_calibration', tuya.valueConverter.divideBy10],
[11, 'faultalarm', tuya.valueConverter.raw],
[15, 'max_temperature_limit', tuya.valueConverter.divideBy10],
[19, 'local_temperature_calibration', tuya.valueConverter.localTempCalibration3],
[101, 'running_state', tuya.valueConverterBasic.lookup({'heat': tuya.enum(1), 'idle': tuya.enum(0)})],
[102, 'frost_protection', tuya.valueConverter.onOff],
[104, 'schedule_mode', tuya.valueConverterBasic.lookup({'disabled': 0, 'weekday/sat+sun': 1, 'weekday+sat/sun': 2, '7day': 3})],
[103, 'factory_reset', tuya.valueConverter.onOff],
[104, 'working_day', tuya.valueConverter.workingDay],
[107, 'deadzone_temperature', tuya.valueConverter.divideBy10],
[109, null, tuya.valueConverter.ZWT198_schedule],
[109, 'schedule_weekday', tuya.valueConverter.ZWT198_schedule],
[109, 'schedule_holiday', tuya.valueConverter.ZWT198_schedule],
[110, 'backlight_mode', tuya.valueConverter.backlightModeOffLowMediumHigh],
// ============== found but not functional datapoints:

// [16, 'min_temperature_limit', tuya.valueConverter.divideBy10], // datapoint listed in Tuya, but no communication from device
// [105, 'dp105', tuya.valueConverter.onOff], // not listed in Tuya, but device sends datapoint
// [111, 'dp111', tuya.valueConverter.onOff], // not listed in Tuya, but device sends datapoint

// These are the schedule values in bytes, 8 periods in total (4 bytes per period).
// For each period:
// 1st byte: hour
// 2nd byte: minute
// 3rd, 4th bytes: temperature multiplied by 10
// Last 2 periods are ignored if schedule_mode is 7day. When schedule_mode is disabled,
// scheduling can't be configured at all.
// On the device last 2 periods are ignored if schedule_mode is 7day. When schedule_mode is disabled,
// scheduling can't be configured at all on the device.
// For example, if schedule_mode is weekday/sat+sun and this byte array is received:
// [6,10,1,144,8,10,0,170,11,40,0,170,12,40,0,170,17,10,0,230,22,10,0,170,8,5,0,200,23,0,0,160]
// Then the schedule is:
// Mon-Fri: 6:10 --> 40C, 8:10 --> 17C, 11:40 --> 17C, 12:40 --> 17C, 17:10 --> 23C, 22:10 --> 17C
// Sat-Sun: 8:05 --> 20C, 23:00 --> 16C
// I wasn't able to find a proper converter or to create one, so i commented it out
// [109, 'dp109', tuya.valueConverter.raw],

// unmapped DPs, still need to figure out what they do
// [103, 'dp103', tuya.valueConverter.trueFalse1],
// [105, 'dp105', tuya.valueConverter.trueFalse1],
// [110, 'dp110', tuya.valueConverter.raw],
// [111, 'dp111', tuya.valueConverter.trueFalse1]
],
},
},
Expand Down
90 changes: 89 additions & 1 deletion src/lib/tuya.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,8 @@ export const valueConverter = {
powerOnBehavior: valueConverterBasic.lookup({'off': 0, 'on': 1, 'previous': 2}),
powerOnBehaviorEnum: valueConverterBasic.lookup({'off': new Enum(0), 'on': new Enum(1), 'previous': new Enum(2)}),
switchType: valueConverterBasic.lookup({'momentary': new Enum(0), 'toggle': new Enum(1), 'state': new Enum(2)}),
backlightMode: valueConverterBasic.lookup({'off': new Enum(0), 'normal': new Enum(1), 'inverted': new Enum(2)}),
backlightModeOffNormalInverted: valueConverterBasic.lookup({'off': new Enum(0), 'normal': new Enum(1), 'inverted': new Enum(2)}),
backlightModeOffLowMediumHigh: valueConverterBasic.lookup({'off': new Enum(0), 'low': new Enum(1), 'medium': new Enum(2), 'high': new Enum(3)}),
lightType: valueConverterBasic.lookup({'led': 0, 'incandescent': 1, 'halogen': 2}),
countdown: valueConverterBasic.raw(),
scale0_254to0_1000: valueConverterBasic.scale(0, 254, 0, 1000),
Expand All @@ -459,6 +460,7 @@ export const valueConverter = {
switchMode: valueConverterBasic.lookup({'switch': new Enum(0), 'scene': new Enum(1)}),
lightMode: valueConverterBasic.lookup({'normal': new Enum(0), 'on': new Enum(1), 'off': new Enum(2), 'flash': new Enum(3)}),
raw: valueConverterBasic.raw(),
workingDay: valueConverterBasic.lookup({'disabled': new Enum(0), '6-1': new Enum(1), '5-2': new Enum(2), '7': new Enum(3)}),
localTemperatureCalibration: {
from: (value: number) => value > 4000 ? value - 4096 : value,
to: (value: number) => value < 0 ? 4096 + value : value,
Expand Down Expand Up @@ -566,6 +568,17 @@ export const valueConverter = {
return v;
},
},
localTempCalibration3: {
from: (v: number) => {
if (v > 0x7FFFFFFF) v -= 0x100000000;
return v / 10;
},
to: (v: number) => {
if (v > 0) return v * 10;
if (v < 0) return v * 10 + 0x100000000;
return v;
},
},
thermostatHolidayStartStop: {
from: (v: string) => {
const start = {
Expand Down Expand Up @@ -729,6 +742,81 @@ export const valueConverter = {
},
};
},
ZWT198_schedule: {
from: (value: number[], meta: Fz.Meta, options: KeyValue) => {
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: string, meta: Tz.Meta) => {
const dpId = 109;
const payload:number[] = [];
let weekdayFormat: string;
let holidayFormat: string;

if (meta.message.hasOwnProperty('schedule_weekday')) {
weekdayFormat = v;
holidayFormat = meta.state['schedule_holiday'] as string;
} else {
weekdayFormat = meta.state['schedule_weekday'] as string;
holidayFormat = v;
}

function scheduleToRaw(key: string, input: string, number: number, payload: number[], meta: Tz.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: number, meta: Tz.Meta) => {
const entity = meta.device.endpoints[0];
Expand Down

0 comments on commit d18f537

Please sign in to comment.