Skip to content

Commit a8d6dd1

Browse files
feat(add): Avatto ME168
fix for window_detection to override access type fixes for max temperature fix the all days schedule description to be more helpful
1 parent f42aa50 commit a8d6dd1

File tree

3 files changed

+186
-10
lines changed

3 files changed

+186
-10
lines changed

src/devices/tuya.ts

+148-2
Original file line numberDiff line numberDiff line change
@@ -4708,7 +4708,6 @@ const definitions: DefinitionWithExtend[] = [
47084708
'_TZE200_jkfbph7l' /* model: 'ME167', vendor: 'AVATTO' */,
47094709
'_TZE200_p3dbf6qs' /* model: 'ME168', vendor: 'AVATTO' */,
47104710
'_TZE200_rxntag7i' /* model: 'ME168', vendor: 'AVATTO' */,
4711-
'_TZE200_ybsqljjg' /* model: 'ME168', vendor: 'AVATTO' */,
47124711
'_TZE200_yqgbrdyo',
47134712
'_TZE284_p3dbf6qs',
47144713
'_TZE200_rxq4iti9',
@@ -4730,7 +4729,7 @@ const definitions: DefinitionWithExtend[] = [
47304729
'_TZE200_9xfjixap',
47314730
'_TZE200_jkfbph7l',
47324731
]),
4733-
tuya.whitelabel('AVATTO', 'ME168', 'Thermostatic radiator valve', ['_TZE200_rxntag7i', '_TZE200_ybsqljjg']),
4732+
tuya.whitelabel('AVATTO', 'ME168', 'Thermostatic radiator valve', ['_TZE200_rxntag7i']),
47344733
tuya.whitelabel('AVATTO', 'TRV06_1', 'Thermostatic radiator valve', ['_TZE200_hvaxb2tc', '_TZE284_o3x45p96']),
47354734
tuya.whitelabel('EARU', 'TRV06', 'Smart thermostat module', ['_TZE200_yqgbrdyo', '_TZE200_rxq4iti9']),
47364735
tuya.whitelabel('AVATTO', 'AVATTO_TRV06', 'Thermostatic radiator valve', ['_TZE284_c6wv4xyo', '_TZE204_o3x45p96']),
@@ -5517,6 +5516,153 @@ const definitions: DefinitionWithExtend[] = [
55175516
await reporting.bind(endpoint, coordinatorEndpoint, ['genOnOff']);
55185517
},
55195518
},
5519+
{
5520+
fingerprint: tuya.fingerprint('TS0601', ['_TZE200_ybsqljjg' /* model: 'ME168', vendor: 'AVATTO' */]),
5521+
model: 'TS0601_thermostat_5',
5522+
vendor: 'Tuya',
5523+
description: 'Thermostatic radiator valve',
5524+
fromZigbee: [tuya.fz.datapoints],
5525+
toZigbee: [tuya.tz.datapoints],
5526+
whiteLabel: [tuya.whitelabel('AVATTO', 'AVATTO_ME168', 'Thermostatic radiator valve', ['_TZE200_ybsqljjg'])],
5527+
onEvent: tuya.onEventSetTime,
5528+
configure: tuya.configureMagicPacket,
5529+
ota: true,
5530+
exposes: [
5531+
e.battery(),
5532+
//! to fix as the exposed format is bitmap
5533+
e.numeric('error', ea.STATE).withDescription('If NTC is damaged, "Er" will be on the TRV display.'),
5534+
e.child_lock().withCategory('config'),
5535+
5536+
e
5537+
.enum('running_mode', ea.STATE, ['auto', 'manual', 'off', 'eco', 'comfort', 'rapid'])
5538+
.withDescription('Actual TRV running mode')
5539+
.withCategory('diagnostic'),
5540+
e
5541+
.climate()
5542+
.withSystemMode(['off', 'heat', 'auto'], ea.STATE_SET, 'Basic modes')
5543+
.withPreset(['eco', 'comfort', 'boost'], 'Additional heat modes')
5544+
.withRunningState(['idle', 'heat'], ea.STATE)
5545+
.withSetpoint('current_heating_setpoint', 4, 35, 1, ea.STATE_SET)
5546+
.withLocalTemperature(ea.STATE)
5547+
.withLocalTemperatureCalibration(-30, 30, 1, ea.STATE_SET),
5548+
5549+
e
5550+
.binary('window_detection', ea.STATE_SET, 'ON', 'OFF')
5551+
.withDescription('Enables/disables window detection on the device')
5552+
.withCategory('config'),
5553+
e.window_open(),
5554+
5555+
e
5556+
.binary('frost_protection', ea.STATE_SET, 'ON', 'OFF')
5557+
.withDescription(
5558+
'When the room temperature is lower than 5 °C, the valve opens; when the temperature rises to 8 °C, the valve closes.',
5559+
)
5560+
.withCategory('config'),
5561+
e
5562+
.binary('scale_protection', ea.STATE_SET, 'ON', 'OFF')
5563+
.withDescription(
5564+
'If the heat sink is not fully opened within ' +
5565+
'two weeks or is not used for a long time, the valve will be blocked due to silting up and the heat sink will not be ' +
5566+
'able to be used. To ensure normal use of the heat sink, the controller will automatically open the valve fully every ' +
5567+
'two weeks. It will run for 30 seconds per time with the screen displaying "Ad", then return to its normal working state ' +
5568+
'again.',
5569+
)
5570+
.withCategory('config'),
5571+
5572+
e
5573+
.numeric('boost_time', ea.STATE_SET)
5574+
.withUnit('min')
5575+
.withDescription('Boost running time')
5576+
.withValueMin(0)
5577+
.withValueMax(255)
5578+
.withCategory('config'),
5579+
e.numeric('boost_timeset_countdown', ea.STATE).withUnit('min').withDescription('Boost time remaining'),
5580+
5581+
e.eco_temperature().withValueMin(5).withValueMax(35).withValueStep(1).withCategory('config'),
5582+
e.comfort_temperature().withValueMin(5).withValueMax(35).withValueStep(1).withCategory('config'),
5583+
5584+
...tuya.exposes
5585+
.scheduleAllDays(ea.STATE_SET, '06:00/21.0 08:00/16.0 12:00/21.0 14:00/16.0 18:00/21.0 22:00/16.0')
5586+
.map((text) => text.withCategory('config')),
5587+
],
5588+
meta: {
5589+
tuyaDatapoints: [
5590+
// mode (RW Enum [0=auto, 1=manual, 2=off, 3=eco, 4=comfort, 5=rapid])
5591+
[2, null, tuya.valueConverter.thermostatME168_systemModeAndPreset(null)],
5592+
[2, 'preset', tuya.valueConverter.thermostatME168_systemModeAndPreset('preset')],
5593+
[2, 'system_mode', tuya.valueConverter.thermostatME168_systemModeAndPreset('system_mode')],
5594+
5595+
// work_state (RO Enum [0=opened, 1=closed])
5596+
[3, 'running_state', tuya.valueConverterBasic.lookup({heat: tuya.enum(0), idle: tuya.enum(1)})],
5597+
5598+
// temp_set (RW Integer, 40-350 C, scale 1 step 10)
5599+
[4, 'current_heating_setpoint', tuya.valueConverter.divideBy10],
5600+
5601+
// temp_current (RO Integer, -0-500 C, scale 1 step 10)
5602+
[5, 'local_temperature', tuya.valueConverter.divideBy10],
5603+
5604+
// battery_percentage (RO, Integer, 0-100 %, scale 0 step 1)
5605+
[6, 'battery', tuya.valueConverter.raw],
5606+
5607+
// child_lock (RW Boolean)
5608+
[7, 'child_lock', tuya.valueConverter.lockUnlock],
5609+
5610+
//! load_status (RW, Enum, range [0=closed, 1=opened]) - Non-functional
5611+
// [13, 'load_status', tuya.valueConverterBasic.lookup({CLOSE: tuya.enum(0), OPEN: tuya.enum(1)})],
5612+
5613+
// window_check (RW Boolean)
5614+
[14, 'window_detection', tuya.valueConverter.onOff],
5615+
5616+
// window_state (RO Enum, range [0=opened, 1=closed])
5617+
[15, 'window_open', tuya.valueConverter.trueFalseEnum0],
5618+
5619+
// week_program_13_(1-7) (RW Raw, maxlen 128, special binary-in-base64 format)
5620+
[28, 'schedule_monday', tuya.valueConverter.thermostatScheduleDayMultiDPWithDayNumber(1, 6)],
5621+
[29, 'schedule_tuesday', tuya.valueConverter.thermostatScheduleDayMultiDPWithDayNumber(2, 6)],
5622+
[30, 'schedule_wednesday', tuya.valueConverter.thermostatScheduleDayMultiDPWithDayNumber(3, 6)],
5623+
[31, 'schedule_thursday', tuya.valueConverter.thermostatScheduleDayMultiDPWithDayNumber(4, 6)],
5624+
[32, 'schedule_friday', tuya.valueConverter.thermostatScheduleDayMultiDPWithDayNumber(5, 6)],
5625+
[33, 'schedule_saturday', tuya.valueConverter.thermostatScheduleDayMultiDPWithDayNumber(6, 6)],
5626+
[34, 'schedule_sunday', tuya.valueConverter.thermostatScheduleDayMultiDPWithDayNumber(7, 6)],
5627+
5628+
//? error (RO Bitmap, maxlen 2, label [0x=low_battery, x0=sensor_fault]?)
5629+
[35, null, tuya.valueConverter.errorOrBatteryLow],
5630+
5631+
// frost (RW Boolean)
5632+
[36, 'frost_protection', tuya.valueConverter.onOff],
5633+
5634+
//! rapid_switch (RW Boolean) - Non-functional
5635+
// [37, 'rapid_switch', tuya.valueConverter.onOff],
5636+
5637+
//! rapid_countdown (RW Integer, 1-12 h, scale 0 step 1) - Non-functional
5638+
// [38, 'rapid_countdown', tuya.valueConverter.raw],
5639+
5640+
// scale_switch (RW Boolean)
5641+
[39, 'scale_protection', tuya.valueConverter.onOff],
5642+
5643+
// temp_correction (RW Integer, -10-10 C, scale 0 step 1)
5644+
[47, 'local_temperature_calibration', tuya.valueConverter.localTempCalibration2],
5645+
5646+
// comfort_temp (RW Integer, 100-250 C, scale 1 step 10)
5647+
[101, 'comfort_temperature', tuya.valueConverter.divideBy10],
5648+
5649+
//! switch (RW Boolean) - Non-functional
5650+
// [102, 'switch', tuya.valueConverter.onOff],
5651+
5652+
// rapid_time_set (RW Integer, 0-180 min, scale 0 step 1)
5653+
[103, 'boost_time', tuya.valueConverter.raw],
5654+
5655+
// heating_countdown (RO Integer, 0-3600 min, scale 0 step 1)
5656+
[104, 'boost_timeset_countdown', tuya.valueConverter.countdown],
5657+
5658+
// eco_temp (RW Integer, 100-200 C, scale 1 step 10)
5659+
[105, 'eco_temperature', tuya.valueConverter.divideBy10],
5660+
5661+
//! eco (RW Boolean) - Non-functional
5662+
// [106, 'eco', tuya.valueConverter.onOff],
5663+
],
5664+
},
5665+
},
55205666
{
55215667
fingerprint: [
55225668
{modelID: 'TS011F', applicationVersion: 160, priority: -1},

src/lib/exposes.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -1299,14 +1299,13 @@ export const presets = {
12991299
.withFeature(new Numeric('strobe_duty_cycle', access.SET).withValueMax(10).withValueMin(0).withDescription('Length of the flash cycle'))
13001300
.withFeature(new Numeric('duration', access.SET).withUnit('s').withDescription('Duration in seconds of the alarm')),
13011301
week: () => new Enum('week', access.STATE_SET, ['5+2', '6+1', '7']).withDescription('Week format user for schedule'),
1302+
/** @deprecated left for compatability, use {@link window_detection_bool} instead */
13021303
window_detection: () =>
13031304
new Switch()
13041305
.withLabel('Window detection')
1305-
.withState('window_detection', true, 'Enables/disables window detection on the device', access.STATE_SET), // left for compatability, do not use
1306-
window_detection_bool: () =>
1307-
new Binary('window_detection', access.ALL, true, false)
1308-
.withDescription('Enables/disables window detection on the device')
1309-
.withCategory('config'),
1306+
.withState('window_detection', true, 'Enables/disables window detection on the device', access.STATE_SET),
1307+
window_detection_bool: (access: number = a.ALL) =>
1308+
new Binary('window_detection', access, true, false).withDescription('Enables/disables window detection on the device').withCategory('config'),
13101309
window_open: () => new Binary('window_open', access.STATE, true, false).withDescription('Indicates if window is open').withCategory('diagnostic'),
13111310
moving: () => new Binary('moving', access.STATE, true, false).withDescription('Indicates if the device is moving'),
13121311
x_axis: () => new Numeric('x_axis', access.STATE).withDescription('Accelerometer X value'),

src/lib/tuya.ts

+34-3
Original file line numberDiff line numberDiff line change
@@ -334,9 +334,9 @@ const tuyaExposes = {
334334
.binary('frost_protection', ea.STATE_SET, 'ON', 'OFF')
335335
.withDescription(`When Anti-Freezing function is activated, the temperature in the house is kept at 8 °C.${extraNote}`),
336336
errorStatus: () => e.numeric('error_status', ea.STATE).withDescription('Error status'),
337-
scheduleAllDays: (access: number, format: string) =>
337+
scheduleAllDays: (access: number, example: string) =>
338338
['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'].map((day) =>
339-
e.text(`schedule_${day}`, access).withDescription(`Schedule for ${day}, format: "${format}"`),
339+
e.text(`schedule_${day}`, access).withDescription(`Schedule for ${day}, example: "${example}"`),
340340
),
341341
temperatureUnit: () => e.enum('temperature_unit', ea.STATE_SET, ['celsius', 'fahrenheit']).withDescription('Temperature unit'),
342342
temperatureCalibration: () =>
@@ -1030,7 +1030,7 @@ export const valueConverter = {
10301030
const hour = parseInt(hourMin[0]);
10311031
const min = parseInt(hourMin[1]);
10321032
const temperature = Math.floor(parseFloat(timeTemp[1]) * 10);
1033-
if (hour < 0 || hour > 24 || min < 0 || min > 60 || temperature < 50 || temperature > 300) {
1033+
if (hour < 0 || hour > 24 || min < 0 || min > 60 || temperature < 50 || temperature > 350) {
10341034
throw new Error('Invalid hour, minute or temperature of: ' + transition);
10351035
}
10361036
payload.push(hour, min, (temperature & 0xff00) >> 8, temperature & 0xff);
@@ -1497,6 +1497,37 @@ export const valueConverter = {
14971497
return data;
14981498
},
14991499
},
1500+
thermostatME168_systemModeAndPreset: (toKey: string) => {
1501+
return {
1502+
from: (v: string) => {
1503+
utils.assertNumber(v, 'system_mode');
1504+
const modeMap: {[mode: number]: {name: string; system_mode: string; preset: string}} = {
1505+
0: {name: 'auto', system_mode: 'auto', preset: 'none'},
1506+
1: {name: 'manual', system_mode: 'heat', preset: 'none'},
1507+
2: {name: 'off', system_mode: 'off', preset: 'none'},
1508+
3: {name: 'eco', system_mode: 'heat', preset: 'eco'},
1509+
4: {name: 'comfort', system_mode: 'heat', preset: 'comfort'},
1510+
5: {name: 'rapid', system_mode: 'heat', preset: 'boost'},
1511+
};
1512+
return {running_mode: modeMap[v].name, preset: modeMap[v].preset, system_mode: modeMap[v].system_mode};
1513+
},
1514+
to: (v: string) => {
1515+
const presetLookup = {
1516+
none: new Enum(1), // manual
1517+
eco: new Enum(3), // eco
1518+
comfort: new Enum(4), // comfort
1519+
boost: new Enum(5), // rapid
1520+
};
1521+
const systemModeLookup = {
1522+
auto: new Enum(0), // auto
1523+
heat: new Enum(1), // manual
1524+
off: new Enum(2), // off
1525+
};
1526+
const lookup = toKey === 'preset' ? presetLookup : systemModeLookup;
1527+
return utils.getFromLookup(v, lookup);
1528+
},
1529+
};
1530+
},
15001531
};
15011532

15021533
const tuyaTz = {

0 commit comments

Comments
 (0)