Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
SimonSezKossel opened this issue Aug 28, 2022 · 30 comments · Fixed by Koenkk/zigbee-herdsman-converters#5081
Labels
new device support New device support request

Comments

@SimonSezKossel
Copy link

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: #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 SimonSezKossel added the new device support New device support request label Aug 28, 2022
@Koenkk Koenkk closed this as completed Aug 28, 2022
@Koenkk Koenkk reopened this Aug 28, 2022
@Koenkk
Copy link
Owner

Koenkk commented Aug 28, 2022

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

@raketenemo
Copy link

raketenemo commented Sep 5, 2022

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

@Pewidot
Copy link

Pewidot commented Sep 6, 2022

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

@raketenemo
Copy link

raketenemo commented Sep 7, 2022

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

@SimonSezKossel
Copy link
Author

@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

@Stov1k
Copy link

Stov1k commented Sep 12, 2022

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

@wirtsi
Copy link

wirtsi commented Sep 20, 2022

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!

@raketenemo
Copy link

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

@wirtsi
Copy link

wirtsi commented Sep 28, 2022

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

@twhittock
Copy link

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.

@wirtsi
Copy link

wirtsi commented Sep 28, 2022

@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

@twhittock
Copy link

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?

@augard
Copy link

augard commented Oct 5, 2022

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

@twhittock
Copy link

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

@pratsenka
Copy link

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

@pratsenka
Copy link

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

@twhittock
Copy link

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

twhittock added a commit to twhittock/avatto_me167 that referenced this issue Oct 21, 2022
@raketenemo
Copy link

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

@rickx
Copy link

rickx commented Nov 9, 2022

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

@pkoretic
Copy link

pkoretic commented Nov 9, 2022

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

@rickx
Copy link

rickx commented Nov 10, 2022

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

@timderspieler
Copy link

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?

@rickx
Copy link

rickx commented Nov 15, 2022

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?

@hhoang308
Copy link

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.

@rickx
Copy link

rickx commented Nov 28, 2022

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.

@IIChrisII
Copy link

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.

@Evgeka07
Copy link

Evgeka07 commented Dec 9, 2022

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

where is this stated? in which file?

@raketenemo
Copy link

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.

@derpuma
Copy link

derpuma commented Dec 9, 2022

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}

Mephistofeles pushed a commit to Mephistofeles/zigbee-herdsman-converters that referenced this issue Dec 13, 2022
@cgarciaq
Copy link

cgarciaq commented Feb 1, 2023

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
new device support New device support request
Projects
None yet
Development

Successfully merging a pull request may close this issue.