zigbee2mqtt
zigbee2mqtt copied to clipboard
Saswell SEA801-Zigbee/SEA802-Zigbee, Additional manufacturer name[New device support]:
Link
https://nl.aliexpress.com/item/1005003952150617.html?spm=a2g0o.order_list.0.0.773979d2HwK8Cy&gatewayAdapt=glo2nld
Database entry
{"id":50,"type":"EndDevice","ieeeAddr":"0x0c4314fffe61c76e","nwkAddr":21015,"manufId":4098,"manufName":"_TZE200_bvu2wnxz","powerSource":"Battery","modelId":"TS0601","epList":[1],"endpoints":{"1":{"profId":260,"epId":1,"devId":81,"inClusterList":[0,4,5,61184],"outClusterList":[25,10],"clusters":{"genBasic":{"attributes":{"65503":"�fp*\u0013\u0000\u0000\u0000\u0000\u0005\u0000\u0000\u0000\u0000\u0005\u0000\u0000\u0000\u0000\f\u0000\u0000\u0000\u0000\u0012\u0000\u0000\u0000\u0000\u00126\u0010�*\u00127\u0010�*\u0012","65506":31,"65508":0,"modelId":"TS0601","manufacturerName":"_TZE200_bvu2wnxz","powerSource":3,"zclVersion":3,"appVersion":72,"stackVersion":0,"hwVersion":1,"dateCode":""}}},"binds":[{"cluster":0,"type":"endpoint","deviceIeeeAddress":"0x00124b0014d9966d","endpointID":1}],"configuredReportings":[],"meta":{}}},"appVersion":72,"stackVersion":0,"hwVersion":1,"dateCode":"","zclVersion":3,"interviewCompleted":true,"meta":{"configured":1},"lastSeen":1661679562548,"defaultSendRequestWhen":"immediate"}
Comments
Previously I tried to do a shortcut with this device hoping it would work: https://github.com/Koenkk/zigbee2mqtt/issues/12315
Conclusion: this device is not compatible with Saswell SEA801. Unfortunately it`s now recognized as such, giving a lot of errors caused by my previous request. No idea how to proceed, but would love to help cleaning up the mess I caused and would like to get this Radiator valve up and running.
External converter
No response
Supported color modes
No response
Color temperature range
No response
Then the data points will be different, meaning it has to be reverse engineered via the TuYa gateway: https://www.zigbee2mqtt.io/advanced/support-new-devices/03_find_tuya_data_points.html#requirements-and-caveats
I will remove the _TZE200_bvu2wnxz
from SEA801-Zigbee/SEA802-Zigbee
Here you have a connector with some basic functionality for the thermostat:
const fz = require('zigbee-herdsman-converters/converters/fromZigbee');
const tz = require('zigbee-herdsman-converters/converters/toZigbee');
const exposes = require('zigbee-herdsman-converters/lib/exposes');
const reporting = require('zigbee-herdsman-converters/lib/reporting');
const extend = require('zigbee-herdsman-converters/lib/extend');
const e = exposes.presets;
const ea = exposes.access;
const tuya = require("zigbee-herdsman-converters/lib/tuya");
const tuyaLocal = {
dataPoints: {
me167Mode: 2,
me167HeatingSetpoint: 4,
me167LocalTemp: 5,
me167ChildLock: 7,
me167Heating: 3,
me167ScheduleMon: 28,
me167ScheduleTue: 29,
me167ScheduleWed: 30,
me167ScheduleThu: 31,
me167ScheduleFri: 32,
me167ScheduleSat: 33,
me167ScheduleSun: 34,
},
};
const fzLocal = {
me167_thermostat: {
cluster: 'manuSpecificTuya',
type: ['commandDataResponse', 'commandDataReport'],
convert: (model, msg, publish, options, meta) => {
const result = {};
// ToDo - currently not sure of the format
// function weeklySchedule(day, value) {
// // byte 0 - Day of Week (0~7 = Mon ~ Sun???)
// // byte 1 - hour ???
// // byte 2 - minute ???
// // byte 3 - second ???
// // byte 4 - Temperature (temp = value / 10)
// const weekDays=['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
// // we get supplied in value only a weekday schedule, so we must add it to
// // the weekly schedule from meta.state, if it exists
// const weeklySchedule= meta.state.hasOwnProperty('weekly_schedule') ? meta.state.weekly_schedule : {};
// meta.logger.info(JSON.stringify({'received day': day, 'received values': value}));
// let daySchedule = []; // result array
// for (let i=1; i<16 && value[i]; ++i) {
// const aHour=value[i];
// ++i;
// const aMinute=value[i];
// ++i;
// const aSecond=value[i];
// ++i;
// const aTemp=value[i];
// daySchedule=[...daySchedule, {
// temperature: Math.floor(aTemp/10),
// hour: aHour,
// minute: aMinute,
// second: aSecond,
// }];
// }
// meta.logger.info(JSON.stringify({'returned weekly schedule: ': daySchedule}));
// return {'weekly-schedule': {...weeklySchedule, [weekDays[day]]: daySchedule}};
//}
for (const dpValue of msg.data.dpValues) {
const value = tuya.getDataValue(dpValue);
if (dpValue>7) {return;} // ToDo...
switch (dpValue.dp) {
case tuyaLocal.dataPoints.me167ChildLock:
result.child_lock = value ? 'LOCK' : 'UNLOCK';
break;
case tuyaLocal.dataPoints.me167HeatingSetpoint:
result.current_heating_setpoint = value/10;
break;
case tuyaLocal.dataPoints.me167LocalTemp:
result.local_temperature = value/10;
break;
case tuyaLocal.dataPoints.me167Heating:
switch(value) {
case 0:
result.heating = "ON"; // valve open
break;
case 1:
result.heating = "OFF"; // valve closed
break;
default:
meta.logger.warn('zigbee-herdsman-converters:me167_thermostat: ' +
`Heating ${value} is not recognized.`);
break;
}
break;
case tuyaLocal.dataPoints.me167Mode:
switch (value) {
case 0: // auto
result.system_mode = 'auto';
break;
case 1: // manu
result.system_mode = 'heat';
break;
case 2: // off
result.system_mode = 'off';
break;
default:
meta.logger.warn('zigbee-herdsman-converters:me167_thermostat: ' +
`Mode ${value} is not recognized.`);
break;
}
break;
// case tuyaLocal.dataPoints.me167ScheduleMon:
// weeklySchedule(0,value);
// break;
// case tuyaLocal.dataPoints.me167ScheduleTue:
// weeklySchedule(1,value);
// break;
// case tuyaLocal.dataPoints.me167ScheduleWed:
// weeklySchedule(2,value);
// break;
// case tuyaLocal.dataPoints.me167ScheduleThu:
// weeklySchedule(3,value);
// break;
// case tuyaLocal.dataPoints.me167ScheduleFri:
// weeklySchedule(4,value);
// break;
// case tuyaLocal.dataPoints.me167ScheduleSat:
// weeklySchedule(5,value);
// break;
// case tuyaLocal.dataPoints.me167ScheduleSun:
// weeklySchedule(6,value);
// break;
default:
meta.logger.warn(`zigbee-herdsman-converters:me167_thermostat: NOT RECOGNIZED ` +
`DP #${dpValue.dp} with data ${JSON.stringify(dpValue)}`);
}
}
return result;
},
},
};
const tzLocal = {
me167_thermostat_current_heating_setpoint: {
key: ['current_heating_setpoint'],
convertSet: async (entity, key, value, meta) => {
const temp = Math.round(value * 10);
await tuya.sendDataPointValue(entity, tuyaLocal.dataPoints.me167HeatingSetpoint, temp);
},
},
me167_thermostat_system_mode: {
key: ['system_mode'],
convertSet: async (entity, key, value, meta) => {
switch (value) {
case 'off':
await tuya.sendDataPointEnum(entity, tuyaLocal.dataPoints.me167Mode, 2 /* off */);
break;
case 'heat':
await tuya.sendDataPointEnum(entity, tuyaLocal.dataPoints.me167Mode, 1 /* manual */);
break;
case 'auto':
await tuya.sendDataPointEnum(entity, tuyaLocal.dataPoints.me167Mode, 0 /* auto */);
break;
}
},
},
me167_thermostat_child_lock: {
key: ['child_lock'],
convertSet: async (entity, key, value, meta) => {
await tuya.sendDataPointBool(entity, tuyaLocal.dataPoints.me167ChildLock, value === 'LOCK');
},
},
// ToDo - currently not sure of the format
// me167_thermostat_schedule: {
// key: ['weekly_schedule'],
// convertSet: async (entity, key, value, meta) => {
// const weekDays=['mon' , 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
// // we overwirte only the received days. The other ones keep stored on the device
// const keys = Object.keys(value);
// for (const dayName of keys) { // for loop in order to delete the empty day schedules
// const output= new Buffer(11); // empty output byte buffer
// const dayNo=weekDays.indexOf(dayName);
// output[0]=dayNo+1;
// const schedule=value[dayName];
// schedule.forEach((el, Index) => {
// if (Index <4) {
// output[1+4*Index]=el.hour;
// output[2+4*Index]=el.minute;
// output[3+4*Index]=el.second;
// output[4+4*Index]=el.temperature*10;
// } else {
// meta.logger.warn('more than 4 schedule points supplied for week-day '+dayName +
// ' additional schedule points will be ignored');
// }
// });
// await tuya.sendDataPointRaw(entity, tuyaLocal.dataPoints.me167ScheduleMon+dayNo, output);
// await new Promise((r) => setTimeout(r, 2000));
// // wait 2 seconds between schedule sends in order not to overload the device
// }
// },
// },
};
const definition = {
// Since a lot of Tuya devices use the same modelID, but use different data points
// it's usually necessary to provide a fingerprint instead of a zigbeeModel
fingerprint: [
{
// The model ID from: Device with modelID 'TS0601' is not supported
// You may need to add \u0000 at the end of the name in some cases
modelID: 'TS0601',
// The manufacturer name from: Device with modelID 'TS0601' is not supported.
manufacturerName: '_TZE200_bvu2wnxz'
},
],
model: 'ME167',
vendor: 'Avatto',
description: 'Thermostatic radiator valve',
fromZigbee: [
fz.ignore_basic_report, // Add this if you are getting no converter for 'genBasic'
//fz.tuya_data_point_dump, // This is a debug converter, it will be described in the next part
fzLocal.me167_thermostat,
],
toZigbee: [
//tz.tuya_data_point_test, // Another debug converter
tzLocal.me167_thermostat_child_lock,
tzLocal.me167_thermostat_current_heating_setpoint,
tzLocal.me167_thermostat_system_mode,
//tzLocal.me167_thermostat_schedule,
],
onEvent: tuya.onEventSetTime, // Add this if you are getting no converter for 'commandMcuSyncTime'
configure: async (device, coordinatorEndpoint, logger) => {
const endpoint = device.getEndpoint(1);
await reporting.bind(endpoint, coordinatorEndpoint, ['genBasic']);
},
exposes: [
e.child_lock(),
exposes.binary('heating', ea.STATE, 'ON', 'OFF').withDescription('Device valve is open or closed (heating or not)'),
exposes.climate().withSetpoint('current_heating_setpoint', 5, 35, 1)
.withLocalTemperature()
.withSystemMode(['auto','heat','off'])
// Here you should put all functionality that your device exposes
],
};
module.exports = definition;
Working functions:
- local temperature
- heating setpoint
- valve position
- mode
- child lock
I'll try to add some functionality, when I get my tuya bridge...
@raketenemo I can confirm your code absolutely works. You have to set all thermostats to heating or off mode. The Time mode overrides all changes to 16 degree after a few minutes. You can also add the battery functionality just by copying the battery code from TS0601_thermostat_1. I had some issues getting the local temperature from 2 of my thermostats but after removing and reconnecting everythign works now!
I was able to map all functions of the thermostat with the help of the tuya brige.
Connector:
const fz = require('zigbee-herdsman-converters/converters/fromZigbee');
const tz = require('zigbee-herdsman-converters/converters/toZigbee');
const exposes = require('zigbee-herdsman-converters/lib/exposes');
const reporting = require('zigbee-herdsman-converters/lib/reporting');
const extend = require('zigbee-herdsman-converters/lib/extend');
const e = exposes.presets;
const ea = exposes.access;
const tuya = require("zigbee-herdsman-converters/lib/tuya");
const tuyaLocal = {
dataPoints: {
me167Mode: 2,
me167HeatingSetpoint: 4,
me167LocalTemp: 5,
me167ChildLock: 7,
me167Heating: 3,
me167Schedule1: 28,
me167Schedule2: 29,
me167Schedule3: 30,
me167Schedule4: 31,
me167Schedule5: 32,
me167Schedule6: 33,
me167Schedule7: 34,
me167ErrorCode: 35,
me167FrostGuard: 36,
me167AntiScaling: 39,
me167TempCalibration: 47,
},
};
const fzLocal = {
me167_thermostat: {
cluster: 'manuSpecificTuya',
type: ['commandDataResponse', 'commandDataReport'],
convert: (model, msg, publish, options, meta) => {
const result = {};
function weeklySchedule(day, value) {
// byte 0 - Day of Week (0~7 = Wed ~ Tue) ???
// byte 1 - hour ???
// byte 2 - minute ???
// byte 3 - Temp (temp = value )
// byte 4 - Temperature (temp = value / 10)
const weekDays=[ 'wed', 'thu', 'fri', 'sat', 'sun','mon', 'tue'];
// we get supplied in value only a weekday schedule, so we must add it to
// the weekly schedule from meta.state, if it exists
const weeklySchedule= meta.state.hasOwnProperty('weekly_schedule') ? meta.state.weekly_schedule : {};
meta.logger.info(JSON.stringify({'received day': day, 'received values': value}));
let daySchedule = []; // result array
for (let i=1; i<16 && value[i]; ++i) {
const aHour=value[i];
++i;
const aMinute=value[i];
++i;
const aTemp2=value[i];
++i;
const aTemp=value[i];
daySchedule=[...daySchedule, {
temperature: Math.floor((aTemp+aTemp2*256)/10),
hour: aHour,
minute: aMinute,
}];
}
meta.logger.info(JSON.stringify({'returned weekly schedule: ': daySchedule}));
return {'weekly-schedule': {...weeklySchedule, [weekDays[day]]: daySchedule}};
}
for (const dpValue of msg.data.dpValues) {
const value = tuya.getDataValue(dpValue);
switch (dpValue.dp) {
case tuyaLocal.dataPoints.me167ChildLock:
result.child_lock = value ? 'LOCK' : 'UNLOCK';
break;
case tuyaLocal.dataPoints.me167HeatingSetpoint:
result.current_heating_setpoint = value/10;
break;
case tuyaLocal.dataPoints.me167LocalTemp:
result.local_temperature = value/10;
break;
case tuyaLocal.dataPoints.me167Heating:
switch(value) {
case 0:
result.heating = "ON"; // valve open
break;
case 1:
result.heating = "OFF"; // valve closed
break;
default:
meta.logger.warn('zigbee-herdsman-converters:me167_thermostat: ' +
`Heating ${value} is not recognized.`);
break;
}
break;
case tuyaLocal.dataPoints.me167Mode:
switch (value) {
case 0: // auto
result.system_mode = 'auto';
break;
case 1: // manu
result.system_mode = 'heat';
break;
case 2: // off
result.system_mode = 'off';
break;
default:
meta.logger.warn('zigbee-herdsman-converters:me167_thermostat: ' +
`Mode ${value} is not recognized.`);
break;
}
break;
case tuyaLocal.dataPoints.me167Schedule1:
weeklySchedule(0,value);
break;
case tuyaLocal.dataPoints.me167Schedule2:
weeklySchedule(1,value);
break;
case tuyaLocal.dataPoints.me167Schedule3:
weeklySchedule(2,value);
break;
case tuyaLocal.dataPoints.me167Schedule4:
weeklySchedule(3,value);
break;
case tuyaLocal.dataPoints.me167Schedule5:
weeklySchedule(4,value);
break;
case tuyaLocal.dataPoints.me167Schedule6:
weeklySchedule(5,value);
break;
case tuyaLocal.dataPoints.me167Schedule7:
weeklySchedule(6,value);
break;
case tuyaLocal.dataPoints.me167TempCalibration:
if (value > 4000000000 ){
result.local_temperature_calibration = (value-4294967295)-1 // negative values
}else{
result.local_temperature_calibration = value
}
break;
case tuyaLocal.dataPoints.me167ErrorCode:
break; // not the faintest idea
case tuyaLocal.dataPoints.me167FrostGuard:
result.frost_guard = value ? 'ON' : 'OFF';
break;
case tuyaLocal.dataPoints.me167AntiScaling:
result.anti_scaling = value ? 'ON' : 'OFF';
break;
default:
meta.logger.warn(`zigbee-herdsman-converters:me167_thermostat: NOT RECOGNIZED ` +
`DP #${dpValue.dp} with data ${JSON.stringify(dpValue)}`);
}
}
return result;
},
},
};
const tzLocal = {
me167_thermostat_current_heating_setpoint: {
key: ['current_heating_setpoint'],
convertSet: async (entity, key, value, meta) => {
const temp = Math.round(value * 10);
await tuya.sendDataPointValue(entity, tuyaLocal.dataPoints.me167HeatingSetpoint, temp);
},
},
me167_thermostat_system_mode: {
key: ['system_mode'],
convertSet: async (entity, key, value, meta) => {
switch (value) {
case 'off':
await tuya.sendDataPointEnum(entity, tuyaLocal.dataPoints.me167Mode, 2 /* off */);
break;
case 'heat':
await tuya.sendDataPointEnum(entity, tuyaLocal.dataPoints.me167Mode, 1 /* manual */);
break;
case 'auto':
await tuya.sendDataPointEnum(entity, tuyaLocal.dataPoints.me167Mode, 0 /* auto */);
break;
}
},
},
me167_thermostat_child_lock: {
key: ['child_lock'],
convertSet: async (entity, key, value, meta) => {
await tuya.sendDataPointBool(entity, tuyaLocal.dataPoints.me167ChildLock, value === 'LOCK');
},
},
me167_thermostat_schedule: {
key: ['weekly_schedule'],
convertSet: async (entity, key, value, meta) => {
const weekDays=['wed', 'thu', 'fri', 'sat', 'sun', 'mon' , 'tue'];
// we overwirte only the received days. The other ones keep stored on the device
const keys = Object.keys(value);
for (const dayName of keys) { // for loop in order to delete the empty day schedules
const output= []; // empty output byte buffer
const dayNo=weekDays.indexOf(dayName);
output[0]=dayNo+1;
const schedule=value[dayName];
schedule.forEach((el, Index) => {
if (Index <4) {
output[1+4*Index]=el.hour;
output[2+4*Index]=el.minute;
output[3+4*Index]=Math.floor((el.temperature*10)/256);
output[4+4*Index]=(el.temperature*10)%256;
} else {
meta.logger.warn('more than 4 schedule points supplied for week-day '+dayName +
' additional schedule points will be ignored');
}
});
meta.logger.info(`zigbee-herdsman-converters:me167_thermostat: Writing Schedule to ` +
`DP #${tuyaLocal.dataPoints.me167Schedule1+dayNo} with data ${JSON.stringify(output)}`);
await tuya.sendDataPointRaw(entity, tuyaLocal.dataPoints.me167Schedule1+dayNo, output);
await new Promise((r) => setTimeout(r, 2000));
// wait 2 seconds between schedule sends in order not to overload the device
}
},
},
me167_thermostat_calibration: {
key: ['local_temperature_calibration'],
convertSet: async (entity, key, value, meta) => {
if (value >= 0) value = value;
if (value < 0) value = value+4294967295+1;
await tuya.sendDataPointValue(entity, tuyaLocal.dataPoints.me167TempCalibration, value);
},
},
me167_thermostat_anti_scaling: {
key: ['anti_scaling'],
convertSet: async (entity, key, value, meta) => {
await tuya.sendDataPointValue(entity, tuyaLocal.dataPoints.me167AntiScaling, value);
},
},
me167_thermostat_frost_guard: {
key: ['frost_guard'],
convertSet: async (entity, key, value, meta) => {
await tuya.sendDataPointValue(entity, tuyaLocal.dataPoints.me167FrostGuard, value);
},
},
};
const definition = {
// Since a lot of Tuya devices use the same modelID, but use different data points
// it's usually necessary to provide a fingerprint instead of a zigbeeModel
fingerprint: [
{
// The model ID from: Device with modelID 'TS0601' is not supported
// You may need to add \u0000 at the end of the name in some cases
modelID: 'TS0601',
// The manufacturer name from: Device with modelID 'TS0601' is not supported.
manufacturerName: '_TZE200_bvu2wnxz'
},
],
model: 'ME167',
vendor: 'Avatto',
description: 'Thermostatic radiator valve',
fromZigbee: [
fz.ignore_basic_report, // Add this if you are getting no converter for 'genBasic'
//fz.tuya_data_point_dump, // This is a debug converter, it will be described in the next part
fzLocal.me167_thermostat,
],
toZigbee: [
//tz.tuya_data_point_test, // Another debug converter
tzLocal.me167_thermostat_child_lock,
tzLocal.me167_thermostat_current_heating_setpoint,
tzLocal.me167_thermostat_system_mode,
tzLocal.me167_thermostat_schedule,
tzLocal.me167_thermostat_calibration,
tzLocal.me167_thermostat_anti_scaling,
tzLocal.me167_thermostat_frost_guard,
],
onEvent: tuya.onEventSetTime, // Add this if you are getting no converter for 'commandMcuSyncTime'
configure: async (device, coordinatorEndpoint, logger) => {
const endpoint = device.getEndpoint(1);
await reporting.bind(endpoint, coordinatorEndpoint, ['genBasic']);
},
exposes: [
e.child_lock(),
exposes.binary('heating', ea.STATE, 'ON', 'OFF').withDescription('Device valve is open or closed (heating or not)'),
exposes.switch().withState('anti_scaling', true).withDescription('Anti Scaling feature is ON or OFF'),
exposes.switch().withState('frost_guard', true).withDescription('Frost Protection feature is ON or OFF'),
exposes.climate().withSetpoint('current_heating_setpoint', 5, 35, 1)
.withLocalTemperature()
.withSystemMode(['auto','heat','off'])
.withLocalTemperatureCalibration(-3, 3, 1, ea.STATE_SET)
],
};
module.exports = definition;
Weekly Schedule:
The schedule can be set with <friendly_name>/weekly_schedule/set
command with payload:
{
"mon":[
{"hour":8,"minute":0,"temperature":10},
{"hour":12,"minute":0,"temperature":11},
{"hour":18,"minute":0,"temperature":12}
],
"tue":[
{"hour":8,"minute":0,"temperature":13},
{"hour":12,"minute":20,"temperature":14},
{"hour":18,"minute":20,"temperature":15}
],
"wed":[
{"hour":8,"minute":0,"temperature":16},
{"hour":12,"minute":0,"temperature":17},
{"hour":18,"minute":0,"temperature":18}
],
"thu":[
{"hour":8,"minute":0,"temperature":19},
{"hour":12,"minute":0,"temperature":20},
{"hour":18,"minute":0,"temperature":21}
],
"fri":[
{"hour":8,"minute":0,"temperature":22},
{"hour":12,"minute":0,"temperature":23},
{"hour":18,"minute":0,"temperature":24}
],
"sat":[
{"hour":8,"minute":0,"temperature":25},
{"hour":12,"minute":0,"temperature":26},
{"hour":18,"minute":0,"temperature":27}
],
"sun":[
{"hour":8,"minute":0,"temperature":28},
{"hour":12,"minute":0,"temperature":29},
{"hour":18,"minute":0,"temperature":30}
]
}
Available Features:
- local temperature
- heating setpoint
- valve position
- mode
- child lock
- weekly schedule
- local temeperature calibration
- anti scaling (untested - should trigger some weekly valve movements)
- frost protection (untested)
I was not able to trigger a status update via Zigbee. Maybe someone with more experience has an idea on how to implement this...
Update: See latest Code below
@raketenemo: Great work!
Tested your converter and it`s looking good. This is what my unit is reporting back:
"anti_scaling": "OFF",
"child_lock": "UNLOCK",
"current_heating_setpoint": 27,
"frost_guard": "OFF",
"heating": "ON",
"linkquality": 112,
"local_temperature": 23,
"local_temperature_calibration": 0,
"system_mode": "heat"
Only I cannot change "frost_guard" and "anti_scaling" " All others settings can be changed
What about battery status? Is battery info available and could it also be added to the mapping? Thanks for the code shared earlier!
I also got one of those valves. Can confirm that temperature adjustments work with the code from @raketenemo , on/off switch (aka heating mode), window_detection and away_mode still seem wonky. Also current temperature and battery is not present, but it's a start! Thanks guys!
const fz = require('zigbee-herdsman-converters/converters/fromZigbee');
const tz = require('zigbee-herdsman-converters/converters/toZigbee');
const exposes = require('zigbee-herdsman-converters/lib/exposes');
const reporting = require('zigbee-herdsman-converters/lib/reporting');
const extend = require('zigbee-herdsman-converters/lib/extend');
const e = exposes.presets;
const ea = exposes.access;
const tuya = require("zigbee-herdsman-converters/lib/tuya");
const tuyaLocal = {
dataPoints: {
me167Mode: 2,
me167HeatingSetpoint: 4,
me167LocalTemp: 5,
me167ChildLock: 7,
me167Heating: 3,
me167Schedule1: 28,
me167Schedule2: 29,
me167Schedule3: 30,
me167Schedule4: 31,
me167Schedule5: 32,
me167Schedule6: 33,
me167Schedule7: 34,
me167ErrorCode: 35,
me167FrostGuard: 36,
me167AntiScaling: 39,
me167TempCalibration: 47,
},
};
const fzLocal = {
me167_thermostat: {
cluster: 'manuSpecificTuya',
type: ['commandDataResponse', 'commandDataReport'],
convert: (model, msg, publish, options, meta) => {
const result = {};
function weeklySchedule(day, value) {
// byte 0 - Day of Week (0~7 = Wed ~ Tue) ???
// byte 1 - hour ???
// byte 2 - minute ???
// byte 3 - Temp (temp = value )
// byte 4 - Temperature (temp = value / 10)
const weekDays=[ 'wed', 'thu', 'fri', 'sat', 'sun','mon', 'tue'];
// we get supplied in value only a weekday schedule, so we must add it to
// the weekly schedule from meta.state, if it exists
const weeklySchedule= meta.state.hasOwnProperty('weekly_schedule') ? meta.state.weekly_schedule : {};
meta.logger.info(JSON.stringify({'received day': day, 'received values': value}));
let daySchedule = []; // result array
for (let i=1; i<16 && value[i]; ++i) {
const aHour=value[i];
++i;
const aMinute=value[i];
++i;
const aTemp2=value[i];
++i;
const aTemp=value[i];
daySchedule=[...daySchedule, {
temperature: Math.floor((aTemp+aTemp2*256)/10),
hour: aHour,
minute: aMinute,
}];
}
meta.logger.info(JSON.stringify({'returned weekly schedule: ': daySchedule}));
return {'weekly-schedule': {...weeklySchedule, [weekDays[day]]: daySchedule}};
}
for (const dpValue of msg.data.dpValues) {
const value = tuya.getDataValue(dpValue);
switch (dpValue.dp) {
case tuyaLocal.dataPoints.me167ChildLock:
result.child_lock = value ? 'LOCK' : 'UNLOCK';
break;
case tuyaLocal.dataPoints.me167HeatingSetpoint:
result.current_heating_setpoint = value/10;
break;
case tuyaLocal.dataPoints.me167LocalTemp:
result.local_temperature = value/10;
break;
case tuyaLocal.dataPoints.me167Heating:
switch(value) {
case 0:
result.heating = "ON"; // valve open
break;
case 1:
result.heating = "OFF"; // valve closed
break;
default:
meta.logger.warn('zigbee-herdsman-converters:me167_thermostat: ' +
`Heating ${value} is not recognized.`);
break;
}
break;
case tuyaLocal.dataPoints.me167Mode:
switch (value) {
case 0: // auto
result.system_mode = 'auto';
break;
case 1: // manu
result.system_mode = 'heat';
break;
case 2: // off
result.system_mode = 'off';
break;
default:
meta.logger.warn('zigbee-herdsman-converters:me167_thermostat: ' +
`Mode ${value} is not recognized.`);
break;
}
break;
case tuyaLocal.dataPoints.me167Schedule1:
weeklySchedule(0,value);
break;
case tuyaLocal.dataPoints.me167Schedule2:
weeklySchedule(1,value);
break;
case tuyaLocal.dataPoints.me167Schedule3:
weeklySchedule(2,value);
break;
case tuyaLocal.dataPoints.me167Schedule4:
weeklySchedule(3,value);
break;
case tuyaLocal.dataPoints.me167Schedule5:
weeklySchedule(4,value);
break;
case tuyaLocal.dataPoints.me167Schedule6:
weeklySchedule(5,value);
break;
case tuyaLocal.dataPoints.me167Schedule7:
weeklySchedule(6,value);
break;
case tuyaLocal.dataPoints.me167TempCalibration:
if (value > 4000000000 ){
result.local_temperature_calibration = (value-4294967295)-1 // negative values
}else{
result.local_temperature_calibration = value
}
break;
case tuyaLocal.dataPoints.me167ErrorCode:
switch (value) {
case 0: // OK
result.battery_low = false;
meta.logger.info(`zigbee-herdsman-converters:me167_thermostat: BattOK - Error Code: ` +
`${JSON.stringify(dpValue)}`);
break;
case 1: // Empty Battery
result.battery_low = true;
meta.logger.info(`zigbee-herdsman-converters:me167_thermostat: BattEmtpy - Error Code: ` +
`${JSON.stringify(dpValue)}`);
break;
default:
meta.logger.info(`zigbee-herdsman-converters:me167_thermostat: Error Code not recognized: ` +
`${JSON.stringify(dpValue)}`);
break;
}
break;
case tuyaLocal.dataPoints.me167FrostGuard:
result.frost_guard = value ? 'ON' : 'OFF';
break;
case tuyaLocal.dataPoints.me167AntiScaling:
result.anti_scaling = value ? 'ON' : 'OFF';
break;
default:
meta.logger.warn(`zigbee-herdsman-converters:me167_thermostat: NOT RECOGNIZED ` +
`DP #${dpValue.dp} with data ${JSON.stringify(dpValue)}`);
}
}
return result;
},
},
};
const tzLocal = {
me167_thermostat_current_heating_setpoint: {
key: ['current_heating_setpoint'],
convertSet: async (entity, key, value, meta) => {
const temp = Math.round(value * 10);
await tuya.sendDataPointValue(entity, tuyaLocal.dataPoints.me167HeatingSetpoint, temp);
},
},
me167_thermostat_system_mode: {
key: ['system_mode'],
convertSet: async (entity, key, value, meta) => {
switch (value) {
case 'off':
await tuya.sendDataPointEnum(entity, tuyaLocal.dataPoints.me167Mode, 2 /* off */);
break;
case 'heat':
await tuya.sendDataPointEnum(entity, tuyaLocal.dataPoints.me167Mode, 1 /* manual */);
break;
case 'auto':
await tuya.sendDataPointEnum(entity, tuyaLocal.dataPoints.me167Mode, 0 /* auto */);
break;
}
},
},
me167_thermostat_child_lock: {
key: ['child_lock'],
convertSet: async (entity, key, value, meta) => {
await tuya.sendDataPointBool(entity, tuyaLocal.dataPoints.me167ChildLock, value === 'LOCK');
},
},
me167_thermostat_schedule: {
key: ['weekly_schedule'],
convertSet: async (entity, key, value, meta) => {
const weekDays=['wed', 'thu', 'fri', 'sat', 'sun', 'mon' , 'tue'];
// we overwirte only the received days. The other ones keep stored on the device
const keys = Object.keys(value);
for (const dayName of keys) { // for loop in order to delete the empty day schedules
const output= []; // empty output byte buffer
const dayNo=weekDays.indexOf(dayName);
output[0]=dayNo+1;
const schedule=value[dayName];
schedule.forEach((el, Index) => {
if (Index <4) {
output[1+4*Index]=el.hour;
output[2+4*Index]=el.minute;
output[3+4*Index]=Math.floor((el.temperature*10)/256);
output[4+4*Index]=(el.temperature*10)%256;
} else {
meta.logger.warn('more than 4 schedule points supplied for week-day '+dayName +
' additional schedule points will be ignored');
}
});
meta.logger.info(`zigbee-herdsman-converters:me167_thermostat: Writing Schedule to ` +
`DP #${tuyaLocal.dataPoints.me167Schedule1+dayNo} with data ${JSON.stringify(output)}`);
await tuya.sendDataPointRaw(entity, tuyaLocal.dataPoints.me167Schedule1+dayNo, output);
await new Promise((r) => setTimeout(r, 2000));
// wait 2 seconds between schedule sends in order not to overload the device
}
},
},
me167_thermostat_calibration: {
key: ['local_temperature_calibration'],
convertSet: async (entity, key, value, meta) => {
if (value >= 0) value = value;
if (value < 0) value = value+4294967295+1;
await tuya.sendDataPointValue(entity, tuyaLocal.dataPoints.me167TempCalibration, value);
},
},
me167_thermostat_anti_scaling: {
key: ['anti_scaling'],
convertSet: async (entity, key, value, meta) => {
await tuya.sendDataPointValue(entity, tuyaLocal.dataPoints.me167AntiScaling, value);
},
},
me167_thermostat_frost_guard: {
key: ['frost_guard'],
convertSet: async (entity, key, value, meta) => {
await tuya.sendDataPointValue(entity, tuyaLocal.dataPoints.me167FrostGuard, value);
},
},
};
const definition = {
// Since a lot of Tuya devices use the same modelID, but use different data points
// it's usually necessary to provide a fingerprint instead of a zigbeeModel
fingerprint: [
{
// The model ID from: Device with modelID 'TS0601' is not supported
// You may need to add \u0000 at the end of the name in some cases
modelID: 'TS0601',
// The manufacturer name from: Device with modelID 'TS0601' is not supported.
manufacturerName: '_TZE200_bvu2wnxz'
},
],
model: 'ME167',
vendor: 'Avatto',
description: 'Thermostatic radiator valve',
fromZigbee: [
fz.ignore_basic_report, // Add this if you are getting no converter for 'genBasic'
//fz.tuya_data_point_dump, // This is a debug converter, it will be described in the next part
fzLocal.me167_thermostat,
],
toZigbee: [
//tz.tuya_data_point_test, // Another debug converter
tzLocal.me167_thermostat_child_lock,
tzLocal.me167_thermostat_current_heating_setpoint,
tzLocal.me167_thermostat_system_mode,
tzLocal.me167_thermostat_schedule,
tzLocal.me167_thermostat_calibration,
tzLocal.me167_thermostat_anti_scaling,
tzLocal.me167_thermostat_frost_guard,
],
onEvent: tuya.onEventSetTime, // Add this if you are getting no converter for 'commandMcuSyncTime'
configure: async (device, coordinatorEndpoint, logger) => {
const endpoint = device.getEndpoint(1);
await reporting.bind(endpoint, coordinatorEndpoint, ['genBasic']);
},
exposes: [
e.child_lock(),
exposes.binary('heating', ea.STATE, 'ON', 'OFF').withDescription('Device valve is open or closed (heating or not)'),
exposes.switch().withState('anti_scaling', true).withDescription('Anti Scaling feature is ON or OFF'),
exposes.switch().withState('frost_guard', true).withDescription('Frost Protection feature is ON or OFF'),
exposes.climate().withSetpoint('current_heating_setpoint', 5, 35, 1)
.withLocalTemperature()
.withSystemMode(['auto','heat','off'])
.withLocalTemperatureCalibration(-3, 3, 1, ea.STATE_SET)
],
};
module.exports = definition;
I added the low battery detection. This makes the thermostat sufficiently usable for my purposes. But please feel welcome to further improve the connector ;)
So I managed to get the code working. I am dropping the code above into node_modules/zigbee-herdsman-converters/devices/
as saswell_buv.js.
To make it work I need to wrap definition in the last line into an array (so [definition]
), I have no idea why that's needed
Thank you @raketenemo - I have made some minor changes and pushed a repo here https://github.com/twhittock/avatto_me167 - I needed the heating state to be a part of the climate entity in home assistant, so I re-exposed heating
as running_state
. I had to delete the state.json entry to make it upgrade properly, though...
@wirtsi the above repo has a very quick rundown on how to install this as an external converter.
HTH.
@twhittock can you elaborate what you mean by config/zigbee2mqtt
... so the installation folder zigbee2mqtt is installed? When I follow the steps in your repo, the web ui only gives me an option to copy paste a new converter. Pasting the me167.js file gives errors about it being in an incompatible format
Sounds like you're using a different version of zigbee2mqtt than me, or not using the HA add-on... https://www.zigbee2mqtt.io/guide/configuration/more-config-options.html#external-converters may be helpful?
@twhittock Battery status will be shown only when it's on lower power? It's possible to see state of anti_scalling or frost_guard. It's reporting just 'null' value.
@augard hi. @raketenemo really did all the work, I just put a repo up to share my small changes and provide a place to get the latest state of the converter.
But as far as I can tell, yes, it's assumed to have 'good' battery until we receive an "error" zigbee message saying the battery is low. I've not run into a low battery situation yet, but in zigbee2mqtt the battery is showing as "OK" in the interface. There don't seem to be any messages which include battery or voltage information, so it's OK until it's not, basically.
As for anti-scaling and frost guard, I've not touched them personally. The code implies they should have the ability to be sent and received, but I've not seen any responses from my device with those message ids. I wonder if they're actually always on, and it's just got a built-in behaviour? @raketenemo put them in, though, so perhaps they know more. Maybe if they turn on, it'd show up as another error code? I'm just guessing, sorry.
I see that anti_scaling and frost_guard cannot be changed neither by web interface nor by direct mqtt /set message
I see that anti_scaling and frost_guard cannot be changed neither by web interface nor by direct mqtt /set message
The right converters for anti_scaling and frost_guard are: me167_thermostat_anti_scaling: { key: ['anti_scaling'], convertSet: async (entity, key, value, meta) => { await tuya.sendDataPointBool(entity, tuyaLocal.dataPoints.me167AntiScaling, value === 'ON'); }, }, me167_thermostat_frost_guard: { key: ['frost_guard'], convertSet: async (entity, key, value, meta) => { await tuya.sendDataPointBool(entity, tuyaLocal.dataPoints.me167FrostGuard, value === 'ON'); },
Nice that makes sense, i'll update the repository with that.
Here some screenshots from the "Smart Life" app...
Main menu with low battery error:
Mode selection:
Time schedule:
Settings:
is this going to be integrated? What are the missing steps?
@rickx it's a least missing schedule support and there are errors on getting values in Z2M (refresh button press) so wouldn't say it's ready yet but we can all try to finish it :)
ok, will give it a try next week when the hub arrives. Actually schedule support is there?
ok, will give it a try next week when the hub arrives. Actually schedule support is there?
I am currently using the scheduler addon to accomplish this. Is there someone out there that knows how the schedule normally can be setup using the trv build in week schedule functionality?
from what I see in my first tests here, raketenemo's first draft was correct, while the latest version is wrong: the commands for the daily schedules (one for each day) start on Monday = 28 and end on Sunday = 34. It might be locale dependent but which locale starts the week on Wednesday?
from what I see in my first tests here, raketenemo's first draft was correct, while the latest version is wrong: the commands for the daily schedules (one for each day) start on Monday = 28 and end on Sunday = 34. It might be locale dependent but which locale starts the week on Wednesday?
can you give me a link to the first draft, because as far as I can see all the versions are the same. Also, do you have a raw byte schedule, please let me know.
this is the change I was referring to:
const weekDays=['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
and which was later changed to start from Wednesday.
this external converter works already pretty fine but in home assistant i don't see the frost_guard and anti_scaling entities. Instead the "main switch" for the device is used for controlling frost_guard and is not working. When i press this switch in home assistant, Zigbee2mqtt is showing "No converter available for 'state' ("ON")". anti_scaling is completly missing in HA. Can somebody confirm?
I use the mosquitto addon as broker inside home assistant and a external zigbee2mqtt server.
@raketenemo: "Weekly Schedule: The schedule can be set with <friendly_name>/weekly_schedule/set command with payload:"
where is this stated? in which file?
Hi @Evgeka07, I have described the schedule mechanism in the code above the comment or in my external converter here in line 208-237.
When using this external converter, you are able to use the <friendly_name>/weekly_schedule/set command to set up the schedule. When sending the command, you have to send the json encoded payload as shown here.
As mentioned before in here, the days might be out of order. The order of the weekdays is set in line 211. My code may be outdated by now.
this is the change I was referring to:
const weekDays=['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
and which was later changed to start from Wednesday.
I think it is more important how you setup your week in HA rather then the sorting of the days there?
Hi, I do not know why but my SEA801-Zigbee thermostat started opening and immediatly closing the valve every 5 minutes, no matter if it is on or off, showing 'AdAP' on the screen, which means anti-scaling is being triggered. It was working well until yesterday. I can see anti-scaling set in the state, but I do not know how to turn it off. Is there a way to do it already?
{ "anti_scaling": "ON", "away_mode": "OFF", ... }