zigbee2mqtt icon indicating copy to clipboard operation
zigbee2mqtt copied to clipboard

Saswell SEA801-Zigbee/SEA802-Zigbee, Additional manufacturer name[New device support]:

Open SimonSezKossel opened this issue 2 years ago • 25 comments

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

SimonSezKossel avatar Aug 28 '22 09:08 SimonSezKossel

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

Koenkk avatar Aug 28 '22 16:08 Koenkk

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 avatar Sep 05 '22 17:09 raketenemo

@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!

Pewidot avatar Sep 06 '22 10:09 Pewidot

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 avatar Sep 07 '22 21:09 raketenemo

@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

SimonSezKossel avatar Sep 08 '22 20:09 SimonSezKossel

What about battery status? Is battery info available and could it also be added to the mapping? Thanks for the code shared earlier!

Stov1k avatar Sep 12 '22 18:09 Stov1k

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!

wirtsi avatar Sep 20 '22 10:09 wirtsi

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 ;)

raketenemo avatar Sep 27 '22 21:09 raketenemo

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

wirtsi avatar Sep 28 '22 07:09 wirtsi

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 avatar Sep 28 '22 08:09 twhittock

@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

wirtsi avatar Sep 28 '22 08:09 wirtsi

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 avatar Sep 28 '22 08:09 twhittock

@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 avatar Oct 05 '22 12:10 augard

@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.

twhittock avatar Oct 05 '22 12:10 twhittock

I see that anti_scaling and frost_guard cannot be changed neither by web interface nor by direct mqtt /set message

pratsenka avatar Oct 20 '22 09:10 pratsenka

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'); },

pratsenka avatar Oct 21 '22 06:10 pratsenka

Nice that makes sense, i'll update the repository with that.

twhittock avatar Oct 21 '22 10:10 twhittock

Here some screenshots from the "Smart Life" app...

Main menu with low battery error: main menu

Mode selection: mode selection

Time schedule: time schedule

Settings: settings off

settings on

raketenemo avatar Oct 23 '22 14:10 raketenemo

is this going to be integrated? What are the missing steps?

rickx avatar Nov 09 '22 15:11 rickx

@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 :)

pkoretic avatar Nov 09 '22 22:11 pkoretic

ok, will give it a try next week when the hub arrives. Actually schedule support is there?

rickx avatar Nov 10 '22 15:11 rickx

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?

timderspieler avatar Nov 13 '22 14:11 timderspieler

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?

rickx avatar Nov 15 '22 19:11 rickx

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.

hhoang308 avatar Nov 28 '22 03:11 hhoang308

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.

rickx avatar Nov 28 '22 12:11 rickx

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.

IIChrisII avatar Nov 30 '22 15:11 IIChrisII

@raketenemo: "Weekly Schedule: The schedule can be set with <friendly_name>/weekly_schedule/set command with payload:"

where is this stated? in which file?

Evgeka07 avatar Dec 09 '22 13:12 Evgeka07

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.

raketenemo avatar Dec 09 '22 13:12 raketenemo

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? {493FD09C-057A-49D6-9F3E-7D98D52B908A}

derpuma avatar Dec 09 '22 14:12 derpuma

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", ... }

cgarciaq avatar Feb 01 '23 09:02 cgarciaq