Skip to content

Commit a797ca6

Browse files
fix: Improve AVATTO ME168 support (#8651)
Co-authored-by: Koen Kanters <[email protected]>
1 parent 4ae589e commit a797ca6

File tree

4 files changed

+220
-52
lines changed

4 files changed

+220
-52
lines changed

src/devices/avatto.ts

+154-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as tuya from '../lib/tuya';
33
import {DefinitionWithExtend} from '../lib/types';
44

55
const e = exposes.presets;
6+
const ea = exposes.access;
67

78
const definitions: DefinitionWithExtend[] = [
89
{
@@ -29,7 +30,159 @@ const definitions: DefinitionWithExtend[] = [
2930
],
3031
},
3132
},
32-
33+
{
34+
fingerprint: tuya.fingerprint('TS0601', ['_TZE200_ybsqljjg']),
35+
model: 'ME168',
36+
vendor: 'AVATTO',
37+
description: 'Thermostatic radiator valve',
38+
fromZigbee: [tuya.fz.datapoints],
39+
toZigbee: [tuya.tz.datapoints],
40+
onEvent: tuya.onEventSetTime,
41+
configure: tuya.configureMagicPacket,
42+
ota: true,
43+
exposes: [
44+
e.battery(),
45+
//! to fix as the exposed format is bitmap
46+
e.numeric('error', ea.STATE).withDescription('If NTC is damaged, "Er" will be on the TRV display.'),
47+
e.child_lock().withCategory('config'),
48+
e
49+
.enum('running_mode', ea.STATE, ['auto', 'manual', 'off', 'eco', 'comfort', 'boost'])
50+
.withDescription('Actual TRV running mode')
51+
.withCategory('diagnostic'),
52+
e
53+
.climate()
54+
.withSystemMode(['off', 'heat', 'auto'], ea.STATE_SET, 'Basic modes')
55+
.withPreset(['eco', 'comfort', 'boost'], 'Additional heat modes')
56+
.withRunningState(['idle', 'heat'], ea.STATE)
57+
.withSetpoint('current_heating_setpoint', 4, 35, 1, ea.STATE_SET)
58+
.withLocalTemperature(ea.STATE)
59+
.withLocalTemperatureCalibration(-30, 30, 1, ea.STATE_SET),
60+
e
61+
.binary('window_detection', ea.STATE_SET, 'ON', 'OFF')
62+
.withDescription('Enables/disables window detection on the device')
63+
.withCategory('config'),
64+
e.window_open(),
65+
e
66+
.binary('frost_protection', ea.STATE_SET, 'ON', 'OFF')
67+
.withDescription(
68+
'When the room temperature is lower than 5 °C, the valve opens; when the temperature rises to 8 °C, the valve closes.',
69+
)
70+
.withCategory('config'),
71+
e
72+
.binary('scale_protection', ea.STATE_SET, 'ON', 'OFF')
73+
.withDescription(
74+
'If the heat sink is not fully opened within ' +
75+
'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 ' +
76+
'able to be used. To ensure normal use of the heat sink, the controller will automatically open the valve fully every ' +
77+
'two weeks. It will run for 30 seconds per time with the screen displaying "Ad", then return to its normal working state ' +
78+
'again.',
79+
)
80+
.withCategory('config'),
81+
e
82+
.numeric('boost_time', ea.STATE_SET)
83+
.withUnit('min')
84+
.withDescription('Boost running time')
85+
.withValueMin(0)
86+
.withValueMax(255)
87+
.withCategory('config'),
88+
e.numeric('boost_timeset_countdown', ea.STATE).withUnit('min').withDescription('Boost time remaining'),
89+
e.eco_temperature().withValueMin(5).withValueMax(35).withValueStep(1).withCategory('config'),
90+
e.comfort_temperature().withValueMin(5).withValueMax(35).withValueStep(1).withCategory('config'),
91+
...tuya.exposes
92+
.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')
93+
.map((text) => text.withCategory('config')),
94+
],
95+
meta: {
96+
tuyaDatapoints: [
97+
// mode (RW Enum [0=auto, 1=manual, 2=off, 3=eco, 4=comfort, 5=boost])
98+
[
99+
2,
100+
null,
101+
tuya.valueConverter.thermostatSystemModeAndPresetMap({
102+
fromMap: {
103+
0: {device_mode: 'auto', system_mode: 'auto', preset: 'none'},
104+
1: {device_mode: 'manual', system_mode: 'heat', preset: 'none'},
105+
2: {device_mode: 'off', system_mode: 'off', preset: 'none'},
106+
3: {device_mode: 'eco', system_mode: 'heat', preset: 'eco'},
107+
4: {device_mode: 'comfort', system_mode: 'heat', preset: 'comfort'},
108+
5: {device_mode: 'boost', system_mode: 'heat', preset: 'boost'},
109+
},
110+
}),
111+
],
112+
[
113+
2,
114+
'system_mode',
115+
tuya.valueConverter.thermostatSystemModeAndPresetMap({
116+
toMap: {
117+
auto: new tuya.Enum(0), // auto
118+
heat: new tuya.Enum(1), // manual
119+
off: new tuya.Enum(2), // off
120+
},
121+
}),
122+
],
123+
[
124+
2,
125+
'preset',
126+
tuya.valueConverter.thermostatSystemModeAndPresetMap({
127+
toMap: {
128+
none: new tuya.Enum(1), // manual
129+
eco: new tuya.Enum(3), // eco
130+
comfort: new tuya.Enum(4), // comfort
131+
boost: new tuya.Enum(5), // boost
132+
},
133+
}),
134+
],
135+
// work_state (RO Enum [0=opened, 1=closed])
136+
[3, 'running_state', tuya.valueConverterBasic.lookup({heat: tuya.enum(0), idle: tuya.enum(1)})],
137+
// temp_set (RW Integer, 40-350 C, scale 1 step 10)
138+
[4, 'current_heating_setpoint', tuya.valueConverter.divideBy10],
139+
// temp_current (RO Integer, -0-500 C, scale 1 step 10)
140+
[5, 'local_temperature', tuya.valueConverter.divideBy10],
141+
// battery_percentage (RO, Integer, 0-100 %, scale 0 step 1)
142+
[6, 'battery', tuya.valueConverter.raw],
143+
// child_lock (RW Boolean)
144+
[7, 'child_lock', tuya.valueConverter.lockUnlock],
145+
//! load_status (RW, Enum, range [0=closed, 1=opened]) - Non-functional
146+
// [13, 'load_status', tuya.valueConverterBasic.lookup({CLOSE: tuya.enum(0), OPEN: tuya.enum(1)})],
147+
// window_check (RW Boolean)
148+
[14, 'window_detection', tuya.valueConverter.onOff],
149+
// window_state (RO Enum, range [0=opened, 1=closed])
150+
[15, 'window_open', tuya.valueConverter.trueFalseEnum0],
151+
// week_program_13_(1-7) (RW Raw, maxlen 128, special binary-in-base64 format)
152+
[28, 'schedule_monday', tuya.valueConverter.thermostatScheduleDayMultiDPWithDayNumber(1, 6)],
153+
[29, 'schedule_tuesday', tuya.valueConverter.thermostatScheduleDayMultiDPWithDayNumber(2, 6)],
154+
[30, 'schedule_wednesday', tuya.valueConverter.thermostatScheduleDayMultiDPWithDayNumber(3, 6)],
155+
[31, 'schedule_thursday', tuya.valueConverter.thermostatScheduleDayMultiDPWithDayNumber(4, 6)],
156+
[32, 'schedule_friday', tuya.valueConverter.thermostatScheduleDayMultiDPWithDayNumber(5, 6)],
157+
[33, 'schedule_saturday', tuya.valueConverter.thermostatScheduleDayMultiDPWithDayNumber(6, 6)],
158+
[34, 'schedule_sunday', tuya.valueConverter.thermostatScheduleDayMultiDPWithDayNumber(7, 6)],
159+
//? error (RO Bitmap, maxlen 2, label [0x=low_battery, x0=sensor_fault]?)
160+
[35, null, tuya.valueConverter.errorOrBatteryLow],
161+
// frost (RW Boolean)
162+
[36, 'frost_protection', tuya.valueConverter.onOff],
163+
//! rapid_switch (RW Boolean) - Non-functional
164+
// [37, 'rapid_switch', tuya.valueConverter.onOff],
165+
//! rapid_countdown (RW Integer, 1-12 h, scale 0 step 1) - Non-functional
166+
// [38, 'rapid_countdown', tuya.valueConverter.raw],
167+
// scale_switch (RW Boolean)
168+
[39, 'scale_protection', tuya.valueConverter.onOff],
169+
// temp_correction (RW Integer, -10-10 C, scale 0 step 1)
170+
[47, 'local_temperature_calibration', tuya.valueConverter.localTempCalibration2],
171+
// comfort_temp (RW Integer, 100-250 C, scale 1 step 10)
172+
[101, 'comfort_temperature', tuya.valueConverter.divideBy10],
173+
//! switch (RW Boolean) - Non-functional
174+
// [102, 'switch', tuya.valueConverter.onOff],
175+
// rapid_time_set (RW Integer, 0-180 min, scale 0 step 1)
176+
[103, 'boost_time', tuya.valueConverter.raw],
177+
// heating_countdown (RO Integer, 0-3600 min, scale 0 step 1)
178+
[104, 'boost_timeset_countdown', tuya.valueConverter.countdown],
179+
// eco_temp (RW Integer, 100-200 C, scale 1 step 10)
180+
[105, 'eco_temperature', tuya.valueConverter.divideBy10],
181+
//! eco (RW Boolean) - Non-functional
182+
// [106, 'eco', tuya.valueConverter.onOff],
183+
],
184+
},
185+
},
33186
{
34187
fingerprint: tuya.fingerprint('TS0601', ['_TZE204_goecjd1t']),
35188
model: 'ZWPM16',

src/devices/tuya.ts

+40-5
Original file line numberDiff line numberDiff line change
@@ -4710,7 +4710,6 @@ const definitions: DefinitionWithExtend[] = [
47104710
'_TZE200_jkfbph7l' /* model: 'ME167', vendor: 'AVATTO' */,
47114711
'_TZE200_p3dbf6qs' /* model: 'ME168', vendor: 'AVATTO' */,
47124712
'_TZE200_rxntag7i' /* model: 'ME168', vendor: 'AVATTO' */,
4713-
'_TZE200_ybsqljjg' /* model: 'ME168', vendor: 'AVATTO' */,
47144713
'_TZE200_yqgbrdyo',
47154714
'_TZE284_p3dbf6qs',
47164715
'_TZE200_rxq4iti9',
@@ -4732,7 +4731,7 @@ const definitions: DefinitionWithExtend[] = [
47324731
'_TZE200_9xfjixap',
47334732
'_TZE200_jkfbph7l',
47344733
]),
4735-
tuya.whitelabel('AVATTO', 'ME168', 'Thermostatic radiator valve', ['_TZE200_rxntag7i', '_TZE200_ybsqljjg']),
4734+
tuya.whitelabel('AVATTO', 'ME168_1', 'Thermostatic radiator valve', ['_TZE200_rxntag7i']),
47364735
tuya.whitelabel('AVATTO', 'TRV06_1', 'Thermostatic radiator valve', ['_TZE200_hvaxb2tc', '_TZE284_o3x45p96']),
47374736
tuya.whitelabel('EARU', 'TRV06', 'Smart thermostat module', ['_TZE200_yqgbrdyo', '_TZE200_rxq4iti9']),
47384737
tuya.whitelabel('AVATTO', 'AVATTO_TRV06', 'Thermostatic radiator valve', ['_TZE284_c6wv4xyo', '_TZE204_o3x45p96']),
@@ -13239,9 +13238,45 @@ const definitions: DefinitionWithExtend[] = [
1323913238
],
1324013239
meta: {
1324113240
tuyaDatapoints: [
13242-
[2, null, tuya.valueConverter.thermostatGtz10SystemModeAndPreset(null)],
13243-
[2, 'preset', tuya.valueConverter.thermostatGtz10SystemModeAndPreset('preset')],
13244-
[2, 'system_mode', tuya.valueConverter.thermostatGtz10SystemModeAndPreset('system_mode')],
13241+
[
13242+
2,
13243+
null,
13244+
tuya.valueConverter.thermostatSystemModeAndPresetMap({
13245+
fromMap: {
13246+
0: {device_mode: 'manual', system_mode: 'heat', preset: 'manual'},
13247+
1: {device_mode: 'auto', system_mode: 'auto', preset: 'auto'},
13248+
2: {device_mode: 'holiday', system_mode: 'heat', preset: 'holiday'},
13249+
3: {device_mode: 'comfort', system_mode: 'heat', preset: 'comfort'},
13250+
4: {device_mode: 'eco', system_mode: 'heat', preset: 'eco'},
13251+
5: {device_mode: 'off', system_mode: 'off', preset: 'off'},
13252+
},
13253+
}),
13254+
],
13255+
[
13256+
2,
13257+
'preset',
13258+
tuya.valueConverter.thermostatSystemModeAndPresetMap({
13259+
toMap: {
13260+
manual: new tuya.Enum(0),
13261+
auto: new tuya.Enum(1),
13262+
holiday: new tuya.Enum(2),
13263+
comfort: new tuya.Enum(3),
13264+
eco: new tuya.Enum(4),
13265+
off: new tuya.Enum(5),
13266+
},
13267+
}),
13268+
],
13269+
[
13270+
2,
13271+
'system_mode',
13272+
tuya.valueConverter.thermostatSystemModeAndPresetMap({
13273+
toMap: {
13274+
heat: new tuya.Enum(0),
13275+
auto: new tuya.Enum(1),
13276+
off: new tuya.Enum(5),
13277+
},
13278+
}),
13279+
],
1324513280
[4, 'current_heating_setpoint', tuya.valueConverter.divideBy10],
1324613281
[5, 'local_temperature', tuya.valueConverter.divideBy10],
1324713282
[6, 'battery', tuya.valueConverter.raw],

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

+22-41
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: () =>
@@ -1062,7 +1062,7 @@ export const valueConverter = {
10621062
const hour = parseInt(hourMin[0]);
10631063
const min = parseInt(hourMin[1]);
10641064
const temperature = Math.floor(parseFloat(timeTemp[1]) * 10);
1065-
if (hour < 0 || hour > 24 || min < 0 || min > 60 || temperature < 50 || temperature > 300) {
1065+
if (hour < 0 || hour > 24 || min < 0 || min > 60 || temperature < 50 || temperature > 350) {
10661066
throw new Error('Invalid hour, minute or temperature of: ' + transition);
10671067
}
10681068
payload.push(hour, min, (temperature & 0xff00) >> 8, temperature & 0xff);
@@ -1156,6 +1156,7 @@ export const valueConverter = {
11561156
},
11571157
};
11581158
},
1159+
/** @deprecated left for compatibility, use {@link thermostatSystemModeAndPresetMap} */
11591160
thermostatSystemModeAndPreset: (toKey: string) => {
11601161
return {
11611162
from: (v: string) => {
@@ -1172,44 +1173,6 @@ export const valueConverter = {
11721173
},
11731174
};
11741175
},
1175-
thermostatGtz10SystemModeAndPreset: (toKey: string) => {
1176-
return {
1177-
from: (v: string) => {
1178-
utils.assertNumber(v, 'system_mode');
1179-
const presetLookup = {
1180-
0: 'manual',
1181-
1: 'auto',
1182-
2: 'holiday',
1183-
3: 'comfort',
1184-
4: 'eco',
1185-
5: 'off',
1186-
};
1187-
const systemModeLookup = {
1188-
0: 'heat',
1189-
1: 'auto',
1190-
5: 'off',
1191-
};
1192-
return {preset: presetLookup[v], system_mode: systemModeLookup[v]};
1193-
},
1194-
to: (v: string) => {
1195-
const presetLookup = {
1196-
manual: new Enum(0),
1197-
auto: new Enum(1),
1198-
holiday: new Enum(2),
1199-
comfort: new Enum(3),
1200-
eco: new Enum(4),
1201-
off: new Enum(5),
1202-
};
1203-
const systemModeLookup = {
1204-
heat: new Enum(0),
1205-
auto: new Enum(1),
1206-
off: new Enum(5),
1207-
};
1208-
const lookup = toKey === 'preset' ? presetLookup : systemModeLookup;
1209-
return utils.getFromLookup(v, lookup);
1210-
},
1211-
};
1212-
},
12131176
ZWT198_schedule: {
12141177
from: (value: number[], meta: Fz.Meta, options: KeyValue) => {
12151178
const programmingMode = [];
@@ -1529,6 +1492,24 @@ export const valueConverter = {
15291492
return data;
15301493
},
15311494
},
1495+
/** @param toMap the key is 'system_mode' or 'preset' related value */
1496+
thermostatSystemModeAndPresetMap: ({
1497+
fromMap = {},
1498+
toMap = {},
1499+
}: {
1500+
fromMap?: {[modeId: number]: {device_mode: string; system_mode: string; preset: string}};
1501+
toMap?: {[key: string]: Enum};
1502+
}) => {
1503+
return {
1504+
from: (v: string) => {
1505+
utils.assertNumber(v, 'system_mode');
1506+
return {running_mode: fromMap[v].device_mode, system_mode: fromMap[v].system_mode, preset: fromMap[v].preset};
1507+
},
1508+
to: (v: string) => {
1509+
return utils.getFromLookup(v, toMap);
1510+
},
1511+
};
1512+
},
15321513
};
15331514

15341515
const tuyaTz = {

0 commit comments

Comments
 (0)