MClimate Vicki thermostat decoder

For who has interest, I have modified the downlink encoder from MClimate and also added all variables in the decoder.

In Node Red I have created a controller to control the valve to get the room to the desired temperature.

Currently I have set or scheduler in Node Red to change the desired temperature on the thermostat. My eventually goal is to integrate the thermostats in Home Assistant and to control the desired temperature from there.

function toBool(value) {
  return value == '1';
}

function roundToOne(num) {    
  return +(Math.round(num + "e+1")  + "e-1");
}

function Decoder(bytes, port) {

  var TYPE_keepAlive        = 0x01; // (9 bytes)
  var TYPE_hswVersion       = 0x04; // (3 bytes)
  var TYPE_keepAlivePeriod  = 0x12; // (2 bytes)
  var TYPE_openWindow       = 0x13; // (5 bytes)
  var TYPE_childLock        = 0x14; // (2 bytes)
  var TYPE_tempRange        = 0x15; // (3 bytes)
  var TYPE_tempAlgoPar      = 0x16; // (4 bytes)
  var TYPE_tempAlgoParTdiff = 0x17; // (3 bytes)
  var TYPE_operationMode    = 0x18; // (2 bytes)
  var TYPE_joinRetryPeriod  = 0x19; // (2 bytes)
  var TYPE_uplinkMsgType    = 0x1b; // (2 bytes)
  var TYPE_radioWatchdog    = 0x1d; // (3 bytes)
  var TYPE_primaryMode      = 0x1f; // (2 bytes)

  var obj = {};

  obj.payloadHex = "";
  for (var i = 0; i < bytes.length; i++) {
    if(bytes[i] < 16) {
      obj.payloadHex += "0" + bytes[i].toString(16);
    } else {
      obj.payloadHex += bytes[i].toString(16);
    }
  }
  
  for (i = 0; i < bytes.length; i++) {
    switch (bytes[i]) {

      // 0x01
      case TYPE_keepAlive: {
        obj.targetTemperature = bytes[i + 1];
        obj.sensorTemperature = roundToOne(((bytes[i + 2] * 165) / 256) - 40);
        obj.relativeHumidity = roundToOne((bytes[i + 3]/ 256) * 100);
        obj.motorPosition = ((bytes[i + 6] >> 4 & 0xF) << 8) | bytes[i + 4];
        obj.motorRange = ((bytes[i + 6] & 0xF) << 8) | bytes[i + 5];
        obj.batteryVoltage = roundToOne(2 + (((bytes[i + 7] >> 4) & 0xF) * 0.1));
        obj.openWindow = toBool((bytes[i + 7] >> 3) & 0x1);
        obj.highMotorConsumption = toBool((bytes[i + 7] >> 2) & 0x1);
        obj.lowMotorConsumption = toBool((bytes[i + 7] >> 1) & 0x1);
        obj.brokenSensor = toBool(bytes[i + 7] & 0x1);
        obj.manualOperated = toBool((bytes[i + 8] >> 7) & 0x1);
        i += 8;
        break;
      }

      // 0x04
      case TYPE_hswVersion: {
        obj.hwVersion = ((bytes[i + 1] >> 4) & 0xF).toString(16) + "." + (bytes[i + 1] & 0xF).toString(16);
        obj.swVersion = ((bytes[i + 2] >> 4) & 0xF).toString(16) + "." + (bytes[i + 2] & 0xF).toString(16);
        i += 2;
        break;
      }

      // 0x12
      case TYPE_keepAlivePeriod: {
        obj.keepAlivePeriod = bytes[i + 1];
        i += 1;
        break;
      }

      // 0x13
      case TYPE_openWindow: {
        obj.openWindowEnabled = toBool(bytes[i + 1] & 0x1);
        obj.openWindowDuration = bytes[i + 2] * 5;
        obj.openWindowMotorPos = ((bytes[i + 4] >> 4 & 0xF) << 8) | bytes[i + 3];
        obj.openWindowTempDiff = bytes[i + 4] & 0xF;
        i += 4;
        break;
      }

      // 0x14
      case TYPE_childLock: {
        obj.childLockEnable = toBool(bytes[i + 1] & 0x1);
        i += 1;
        break;
      }

      // 0x15
      case TYPE_tempRange: {
        obj.tempRangeLower = bytes[i + 1];
        obj.tempRangeUpper = bytes[i + 2];
        i += 2;
        break;
      }

      // 0x16
      case TYPE_tempAlgoPar: {
        obj.tempAlgoParChckPeriod = bytes[i + 1];
        obj.tempAlgoParFirstLast = bytes[i + 2];
        obj.tempAlgoParNext = bytes[i + 3];
        i += 3;
        break;
      }

      // 0x17
      case TYPE_tempAlgoParTdiff: {
        obj.tDiffOpen = bytes[i + 1];
        obj.tDiffClose = bytes[i + 2];
        i += 2;
        break;
      }

      // 0x18
      case TYPE_operationMode: {
        obj.operationMode = bytes[i + 1];
        i += 1;
        break;
      }

      // 0x19
      case TYPE_joinRetryPeriod: {
        obj.joinRetryPeriod = bytes[i + 1] * 5;
        i += 1;
        break;
      }

      // 0x1b
      case TYPE_uplinkMsgType: {
        obj.uplinkMsgType = bytes[i + 1];
        i += 1;
        break;
      }

      // 0x1d
      case TYPE_radioWatchdog: {
        obj.watchdogConf = bytes[i + 1];
        obj.watchdogUnconf = bytes[i + 2];
        i += 2;
        break;
      }

      // 0x1f
      case TYPE_primaryMode: {
        obj.primaryMode = bytes[i + 1];
        i += 1;
        break;
      }

      default: {
        // Unrecognized Parameter ID!
        obj.unRecognizedParameter = bytes[i];
        break;
      }
    }
  }
  return obj;
}
4 Likes

Great, thank You! Would you be so kind and share configuration from NodeRED?

Hi Roberto69,

I don’t know if it still is interesting for you, but I have integrated the Vicki into Home Assistant. There is also the decoder and encoder available.

With kind regards,
Chris

1 Like

Uau, this is absolutely great. It is also usable as template for other similar cases - for example Milesight WT101.

Thank You!

Best regards
Robert

Excellent!

And people complain about necro-threads!

Hi Roberto69,

Indeed, this is just an implementation which could easy be reused for other cases. Nice that you have already another case for it.

With kind regards,
Chris