Koenkk / zigbee2mqtt

Zigbee 🐝 to MQTT bridge 🌉, get rid of your proprietary Zigbee bridges 🔨
https://www.zigbee2mqtt.io
GNU General Public License v3.0
11.72k stars 1.64k forks source link

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

Closed SimonSezKossel closed 1 year ago

SimonSezKossel commented 2 years ago

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

Koenkk commented 2 years ago

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 commented 1 year ago

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:

I'll try to add some functionality, when I get my tuya bridge...

Pewidot commented 1 year ago

@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 commented 1 year ago

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:

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 commented 1 year ago

@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 commented 1 year ago

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

wirtsi commented 1 year ago

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 commented 1 year ago
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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

@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 commented 1 year ago

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

pratsenka commented 1 year ago

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 commented 1 year ago

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

raketenemo commented 1 year ago

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 commented 1 year ago

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

pkoretic commented 1 year ago

@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 commented 1 year ago

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

timderspieler commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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

where is this stated? in which file?

raketenemo commented 1 year ago

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 /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 commented 1 year ago

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}

cgarciaq commented 1 year ago

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