Skip to content

Commit

Permalink
fix: Improve AVATTO ME168 support (#8651)
Browse files Browse the repository at this point in the history
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
  • Loading branch information
andreypuhovsky and Koenkk authored Jan 27, 2025
1 parent 4ae589e commit a797ca6
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 52 deletions.
155 changes: 154 additions & 1 deletion src/devices/avatto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as tuya from '../lib/tuya';
import {DefinitionWithExtend} from '../lib/types';

const e = exposes.presets;
const ea = exposes.access;

const definitions: DefinitionWithExtend[] = [
{
Expand All @@ -29,7 +30,159 @@ const definitions: DefinitionWithExtend[] = [
],
},
},

{
fingerprint: tuya.fingerprint('TS0601', ['_TZE200_ybsqljjg']),
model: 'ME168',
vendor: 'AVATTO',
description: 'Thermostatic radiator valve',
fromZigbee: [tuya.fz.datapoints],
toZigbee: [tuya.tz.datapoints],
onEvent: tuya.onEventSetTime,
configure: tuya.configureMagicPacket,
ota: true,
exposes: [
e.battery(),
//! to fix as the exposed format is bitmap
e.numeric('error', ea.STATE).withDescription('If NTC is damaged, "Er" will be on the TRV display.'),
e.child_lock().withCategory('config'),
e
.enum('running_mode', ea.STATE, ['auto', 'manual', 'off', 'eco', 'comfort', 'boost'])
.withDescription('Actual TRV running mode')
.withCategory('diagnostic'),
e
.climate()
.withSystemMode(['off', 'heat', 'auto'], ea.STATE_SET, 'Basic modes')
.withPreset(['eco', 'comfort', 'boost'], 'Additional heat modes')
.withRunningState(['idle', 'heat'], ea.STATE)
.withSetpoint('current_heating_setpoint', 4, 35, 1, ea.STATE_SET)
.withLocalTemperature(ea.STATE)
.withLocalTemperatureCalibration(-30, 30, 1, ea.STATE_SET),
e
.binary('window_detection', ea.STATE_SET, 'ON', 'OFF')
.withDescription('Enables/disables window detection on the device')
.withCategory('config'),
e.window_open(),
e
.binary('frost_protection', ea.STATE_SET, 'ON', 'OFF')
.withDescription(
'When the room temperature is lower than 5 °C, the valve opens; when the temperature rises to 8 °C, the valve closes.',
)
.withCategory('config'),
e
.binary('scale_protection', ea.STATE_SET, 'ON', 'OFF')
.withDescription(
'If the heat sink is not fully opened within ' +
'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 ' +
'able to be used. To ensure normal use of the heat sink, the controller will automatically open the valve fully every ' +
'two weeks. It will run for 30 seconds per time with the screen displaying "Ad", then return to its normal working state ' +
'again.',
)
.withCategory('config'),
e
.numeric('boost_time', ea.STATE_SET)
.withUnit('min')
.withDescription('Boost running time')
.withValueMin(0)
.withValueMax(255)
.withCategory('config'),
e.numeric('boost_timeset_countdown', ea.STATE).withUnit('min').withDescription('Boost time remaining'),
e.eco_temperature().withValueMin(5).withValueMax(35).withValueStep(1).withCategory('config'),
e.comfort_temperature().withValueMin(5).withValueMax(35).withValueStep(1).withCategory('config'),
...tuya.exposes
.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')
.map((text) => text.withCategory('config')),
],
meta: {
tuyaDatapoints: [
// mode (RW Enum [0=auto, 1=manual, 2=off, 3=eco, 4=comfort, 5=boost])
[
2,
null,
tuya.valueConverter.thermostatSystemModeAndPresetMap({
fromMap: {
0: {device_mode: 'auto', system_mode: 'auto', preset: 'none'},
1: {device_mode: 'manual', system_mode: 'heat', preset: 'none'},
2: {device_mode: 'off', system_mode: 'off', preset: 'none'},
3: {device_mode: 'eco', system_mode: 'heat', preset: 'eco'},
4: {device_mode: 'comfort', system_mode: 'heat', preset: 'comfort'},
5: {device_mode: 'boost', system_mode: 'heat', preset: 'boost'},
},
}),
],
[
2,
'system_mode',
tuya.valueConverter.thermostatSystemModeAndPresetMap({
toMap: {
auto: new tuya.Enum(0), // auto
heat: new tuya.Enum(1), // manual
off: new tuya.Enum(2), // off
},
}),
],
[
2,
'preset',
tuya.valueConverter.thermostatSystemModeAndPresetMap({
toMap: {
none: new tuya.Enum(1), // manual
eco: new tuya.Enum(3), // eco
comfort: new tuya.Enum(4), // comfort
boost: new tuya.Enum(5), // boost
},
}),
],
// work_state (RO Enum [0=opened, 1=closed])
[3, 'running_state', tuya.valueConverterBasic.lookup({heat: tuya.enum(0), idle: tuya.enum(1)})],
// temp_set (RW Integer, 40-350 C, scale 1 step 10)
[4, 'current_heating_setpoint', tuya.valueConverter.divideBy10],
// temp_current (RO Integer, -0-500 C, scale 1 step 10)
[5, 'local_temperature', tuya.valueConverter.divideBy10],
// battery_percentage (RO, Integer, 0-100 %, scale 0 step 1)
[6, 'battery', tuya.valueConverter.raw],
// child_lock (RW Boolean)
[7, 'child_lock', tuya.valueConverter.lockUnlock],
//! load_status (RW, Enum, range [0=closed, 1=opened]) - Non-functional
// [13, 'load_status', tuya.valueConverterBasic.lookup({CLOSE: tuya.enum(0), OPEN: tuya.enum(1)})],
// window_check (RW Boolean)
[14, 'window_detection', tuya.valueConverter.onOff],
// window_state (RO Enum, range [0=opened, 1=closed])
[15, 'window_open', tuya.valueConverter.trueFalseEnum0],
// week_program_13_(1-7) (RW Raw, maxlen 128, special binary-in-base64 format)
[28, 'schedule_monday', tuya.valueConverter.thermostatScheduleDayMultiDPWithDayNumber(1, 6)],
[29, 'schedule_tuesday', tuya.valueConverter.thermostatScheduleDayMultiDPWithDayNumber(2, 6)],
[30, 'schedule_wednesday', tuya.valueConverter.thermostatScheduleDayMultiDPWithDayNumber(3, 6)],
[31, 'schedule_thursday', tuya.valueConverter.thermostatScheduleDayMultiDPWithDayNumber(4, 6)],
[32, 'schedule_friday', tuya.valueConverter.thermostatScheduleDayMultiDPWithDayNumber(5, 6)],
[33, 'schedule_saturday', tuya.valueConverter.thermostatScheduleDayMultiDPWithDayNumber(6, 6)],
[34, 'schedule_sunday', tuya.valueConverter.thermostatScheduleDayMultiDPWithDayNumber(7, 6)],
//? error (RO Bitmap, maxlen 2, label [0x=low_battery, x0=sensor_fault]?)
[35, null, tuya.valueConverter.errorOrBatteryLow],
// frost (RW Boolean)
[36, 'frost_protection', tuya.valueConverter.onOff],
//! rapid_switch (RW Boolean) - Non-functional
// [37, 'rapid_switch', tuya.valueConverter.onOff],
//! rapid_countdown (RW Integer, 1-12 h, scale 0 step 1) - Non-functional
// [38, 'rapid_countdown', tuya.valueConverter.raw],
// scale_switch (RW Boolean)
[39, 'scale_protection', tuya.valueConverter.onOff],
// temp_correction (RW Integer, -10-10 C, scale 0 step 1)
[47, 'local_temperature_calibration', tuya.valueConverter.localTempCalibration2],
// comfort_temp (RW Integer, 100-250 C, scale 1 step 10)
[101, 'comfort_temperature', tuya.valueConverter.divideBy10],
//! switch (RW Boolean) - Non-functional
// [102, 'switch', tuya.valueConverter.onOff],
// rapid_time_set (RW Integer, 0-180 min, scale 0 step 1)
[103, 'boost_time', tuya.valueConverter.raw],
// heating_countdown (RO Integer, 0-3600 min, scale 0 step 1)
[104, 'boost_timeset_countdown', tuya.valueConverter.countdown],
// eco_temp (RW Integer, 100-200 C, scale 1 step 10)
[105, 'eco_temperature', tuya.valueConverter.divideBy10],
//! eco (RW Boolean) - Non-functional
// [106, 'eco', tuya.valueConverter.onOff],
],
},
},
{
fingerprint: tuya.fingerprint('TS0601', ['_TZE204_goecjd1t']),
model: 'ZWPM16',
Expand Down
45 changes: 40 additions & 5 deletions src/devices/tuya.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4710,7 +4710,6 @@ const definitions: DefinitionWithExtend[] = [
'_TZE200_jkfbph7l' /* model: 'ME167', vendor: 'AVATTO' */,
'_TZE200_p3dbf6qs' /* model: 'ME168', vendor: 'AVATTO' */,
'_TZE200_rxntag7i' /* model: 'ME168', vendor: 'AVATTO' */,
'_TZE200_ybsqljjg' /* model: 'ME168', vendor: 'AVATTO' */,
'_TZE200_yqgbrdyo',
'_TZE284_p3dbf6qs',
'_TZE200_rxq4iti9',
Expand All @@ -4732,7 +4731,7 @@ const definitions: DefinitionWithExtend[] = [
'_TZE200_9xfjixap',
'_TZE200_jkfbph7l',
]),
tuya.whitelabel('AVATTO', 'ME168', 'Thermostatic radiator valve', ['_TZE200_rxntag7i', '_TZE200_ybsqljjg']),
tuya.whitelabel('AVATTO', 'ME168_1', 'Thermostatic radiator valve', ['_TZE200_rxntag7i']),
tuya.whitelabel('AVATTO', 'TRV06_1', 'Thermostatic radiator valve', ['_TZE200_hvaxb2tc', '_TZE284_o3x45p96']),
tuya.whitelabel('EARU', 'TRV06', 'Smart thermostat module', ['_TZE200_yqgbrdyo', '_TZE200_rxq4iti9']),
tuya.whitelabel('AVATTO', 'AVATTO_TRV06', 'Thermostatic radiator valve', ['_TZE284_c6wv4xyo', '_TZE204_o3x45p96']),
Expand Down Expand Up @@ -13239,9 +13238,45 @@ const definitions: DefinitionWithExtend[] = [
],
meta: {
tuyaDatapoints: [
[2, null, tuya.valueConverter.thermostatGtz10SystemModeAndPreset(null)],
[2, 'preset', tuya.valueConverter.thermostatGtz10SystemModeAndPreset('preset')],
[2, 'system_mode', tuya.valueConverter.thermostatGtz10SystemModeAndPreset('system_mode')],
[
2,
null,
tuya.valueConverter.thermostatSystemModeAndPresetMap({
fromMap: {
0: {device_mode: 'manual', system_mode: 'heat', preset: 'manual'},
1: {device_mode: 'auto', system_mode: 'auto', preset: 'auto'},
2: {device_mode: 'holiday', system_mode: 'heat', preset: 'holiday'},
3: {device_mode: 'comfort', system_mode: 'heat', preset: 'comfort'},
4: {device_mode: 'eco', system_mode: 'heat', preset: 'eco'},
5: {device_mode: 'off', system_mode: 'off', preset: 'off'},
},
}),
],
[
2,
'preset',
tuya.valueConverter.thermostatSystemModeAndPresetMap({
toMap: {
manual: new tuya.Enum(0),
auto: new tuya.Enum(1),
holiday: new tuya.Enum(2),
comfort: new tuya.Enum(3),
eco: new tuya.Enum(4),
off: new tuya.Enum(5),
},
}),
],
[
2,
'system_mode',
tuya.valueConverter.thermostatSystemModeAndPresetMap({
toMap: {
heat: new tuya.Enum(0),
auto: new tuya.Enum(1),
off: new tuya.Enum(5),
},
}),
],
[4, 'current_heating_setpoint', tuya.valueConverter.divideBy10],
[5, 'local_temperature', tuya.valueConverter.divideBy10],
[6, 'battery', tuya.valueConverter.raw],
Expand Down
9 changes: 4 additions & 5 deletions src/lib/exposes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1299,14 +1299,13 @@ export const presets = {
.withFeature(new Numeric('strobe_duty_cycle', access.SET).withValueMax(10).withValueMin(0).withDescription('Length of the flash cycle'))
.withFeature(new Numeric('duration', access.SET).withUnit('s').withDescription('Duration in seconds of the alarm')),
week: () => new Enum('week', access.STATE_SET, ['5+2', '6+1', '7']).withDescription('Week format user for schedule'),
/** @deprecated left for compatability, use {@link window_detection_bool} instead */
window_detection: () =>
new Switch()
.withLabel('Window detection')
.withState('window_detection', true, 'Enables/disables window detection on the device', access.STATE_SET), // left for compatability, do not use
window_detection_bool: () =>
new Binary('window_detection', access.ALL, true, false)
.withDescription('Enables/disables window detection on the device')
.withCategory('config'),
.withState('window_detection', true, 'Enables/disables window detection on the device', access.STATE_SET),
window_detection_bool: (access: number = a.ALL) =>
new Binary('window_detection', access, true, false).withDescription('Enables/disables window detection on the device').withCategory('config'),
window_open: () => new Binary('window_open', access.STATE, true, false).withDescription('Indicates if window is open').withCategory('diagnostic'),
moving: () => new Binary('moving', access.STATE, true, false).withDescription('Indicates if the device is moving'),
x_axis: () => new Numeric('x_axis', access.STATE).withDescription('Accelerometer X value'),
Expand Down
63 changes: 22 additions & 41 deletions src/lib/tuya.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,9 +334,9 @@ const tuyaExposes = {
.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: number, format: string) =>
scheduleAllDays: (access: number, example: string) =>
['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'].map((day) =>
e.text(`schedule_${day}`, access).withDescription(`Schedule for ${day}, format: "${format}"`),
e.text(`schedule_${day}`, access).withDescription(`Schedule for ${day}, example: "${example}"`),
),
temperatureUnit: () => e.enum('temperature_unit', ea.STATE_SET, ['celsius', 'fahrenheit']).withDescription('Temperature unit'),
temperatureCalibration: () =>
Expand Down Expand Up @@ -1062,7 +1062,7 @@ export const valueConverter = {
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) {
if (hour < 0 || hour > 24 || min < 0 || min > 60 || temperature < 50 || temperature > 350) {
throw new Error('Invalid hour, minute or temperature of: ' + transition);
}
payload.push(hour, min, (temperature & 0xff00) >> 8, temperature & 0xff);
Expand Down Expand Up @@ -1156,6 +1156,7 @@ export const valueConverter = {
},
};
},
/** @deprecated left for compatibility, use {@link thermostatSystemModeAndPresetMap} */
thermostatSystemModeAndPreset: (toKey: string) => {
return {
from: (v: string) => {
Expand All @@ -1172,44 +1173,6 @@ export const valueConverter = {
},
};
},
thermostatGtz10SystemModeAndPreset: (toKey: string) => {
return {
from: (v: string) => {
utils.assertNumber(v, 'system_mode');
const presetLookup = {
0: 'manual',
1: 'auto',
2: 'holiday',
3: 'comfort',
4: 'eco',
5: 'off',
};
const systemModeLookup = {
0: 'heat',
1: 'auto',
5: 'off',
};
return {preset: presetLookup[v], system_mode: systemModeLookup[v]};
},
to: (v: string) => {
const presetLookup = {
manual: new Enum(0),
auto: new Enum(1),
holiday: new Enum(2),
comfort: new Enum(3),
eco: new Enum(4),
off: new Enum(5),
};
const systemModeLookup = {
heat: new Enum(0),
auto: new Enum(1),
off: new Enum(5),
};
const lookup = toKey === 'preset' ? presetLookup : systemModeLookup;
return utils.getFromLookup(v, lookup);
},
};
},
ZWT198_schedule: {
from: (value: number[], meta: Fz.Meta, options: KeyValue) => {
const programmingMode = [];
Expand Down Expand Up @@ -1529,6 +1492,24 @@ export const valueConverter = {
return data;
},
},
/** @param toMap the key is 'system_mode' or 'preset' related value */
thermostatSystemModeAndPresetMap: ({
fromMap = {},
toMap = {},
}: {
fromMap?: {[modeId: number]: {device_mode: string; system_mode: string; preset: string}};
toMap?: {[key: string]: Enum};
}) => {
return {
from: (v: string) => {
utils.assertNumber(v, 'system_mode');
return {running_mode: fromMap[v].device_mode, system_mode: fromMap[v].system_mode, preset: fromMap[v].preset};
},
to: (v: string) => {
return utils.getFromLookup(v, toMap);
},
};
},
};

const tuyaTz = {
Expand Down

0 comments on commit a797ca6

Please sign in to comment.