Multitech

Creation procedure

Import steps

  1. Payload Management → Sensor Definitions → Import Manufacturer: MClimateSensor Type: XX XX - the specific name of the sensor you are currently importing

  2. Select both files:

    • mclimate-aqi-codec.js

    • mclimate-aqi-definition.json

  3. Click Import, then Save & Apply – the gateway will create read-only Input objects for telemetry and writable Value objects for every "downlink": true property.

Multitech BACnet Codec file

// DataCake
function Decoder(bytes, port){
    var decoded = decodeUplink({ bytes: bytes, fPort: port }).data;
    return decoded;
}

// Milesight
function Decode(port, bytes){
    var decoded = decodeUplink({ bytes: bytes, fPort: port }).data;
    return decoded;
}

// The Things Industries / Main
function decodeUplink(input) {
    try{
        var bytes = input.bytes;
        var data = {};
        function toBool(value){
            return value == '1';
        }
        function calculateTemperature(rawData){return (rawData - 400) / 10};
        function calculateHumidity(rawData){return (rawData * 100) / 256};
        function decbin(number) {
            if (number < 0) {
                number = 0xFFFFFFFF + number + 1
            }
            number = number.toString(2);
            return "00000000".substr(number.length) + number;
        }
        function handleKeepalive(bytes, data){
            var tempRaw = (bytes[1] << 8) | bytes[2];
            var temperature = calculateTemperature(tempRaw);
            var humidity = calculateHumidity(bytes[3]);
            var batteryVoltage = ((bytes[4] << 8) | bytes[5])/1000;

            var targetTemperature, powerSourceStatus, lux, pir;
        if(bytes[0] == 1){
            targetTemperature = bytes[6];
            powerSourceStatus = bytes[7];
            lux = (bytes[8] << 8) | bytes[9];
            pir = toBool(bytes[10]);
        }else{
            targetTemperature = ((parseInt(decbin(bytes[6]), 2)) << 8) | parseInt(decbin(bytes[7]), 2)/10;
            powerSourceStatus = bytes[8];
            lux = (bytes[9] << 8) | bytes[10];
            pir = toBool(bytes[11]);
        }

            data.sensorTemperature = Number(temperature.toFixed(2));
            data.relativeHumidity = Number(humidity.toFixed(2));
            data.batteryVoltage = Number(batteryVoltage.toFixed(2));
            data.targetTemperature = targetTemperature;
            data.powerSourceStatus = powerSourceStatus;
            data.lux = lux;
            data.pir = pir;
            return data;
        }
    
        function handleResponse(bytes, data, keepaliveLength){
        var commands = bytes.map(function(byte){
            return ("0" + byte.toString(16)).substr(-2); 
        });
        commands = commands.slice(0,-keepaliveLength);
        var command_len = 0;
    
        commands.map(function (command, i) {
            switch (command) {
                case '04':
                    {
                        command_len = 2;
                        var hardwareVersion = commands[i + 1];
                        var softwareVersion = commands[i + 2];
                        data.deviceVersions = { hardware: Number(hardwareVersion), software: Number(softwareVersion) };
                    }
                break;
                case '12':
                    {
                        command_len = 1;
                        data.keepAliveTime = parseInt(commands[i + 1], 16);
                    }
                break;
                case '14':
                    {
                        command_len = 1;
                        data.childLock = toBool(parseInt(commands[i + 1], 16)) ;
                    }
                break;
                case '15':
                    {
                        command_len = 2;
                        data.temperatureRangeSettings = { min: parseInt(commands[i + 1], 16), max: parseInt(commands[i + 2], 16) };
                    }
                    break;
                case '19':
                    {
                        command_len = 1;
                        var commandResponse = parseInt(commands[i + 1], 16);
                        var periodInMinutes = commandResponse * 5 / 60;
                        data.joinRetryPeriod =  periodInMinutes;
                    }
                break;
                case '1b':
                    {
                        command_len = 1;
                        data.uplinkType = parseInt(commands[i + 1], 16) ;
                    }
                break;
                case '1d':
                    {
                        command_len = 2;
                        var wdpC = commands[i + 1] == '00' ? false : (parseInt(commands[i + 1], 16))
                        var wdpUc = commands[i + 2] == '00' ? false : parseInt(commands[i + 2], 16);
                        data.watchDogParams= { wdpC: wdpC, wdpUc: wdpUc } ;
                    }
                break;
                case '2f':
                    {
                        command_len = 1;
                        data.targetTemperature = parseInt(commands[i + 1], 16) ;
                    }
                break;
                case '30':
                    {
                        command_len = 1;
                        data.manualTargetTemperatureUpdate = parseInt(commands[i + 1], 16) ;
                    }
                break;
                case '32':
                    {
                        command_len = 1;
                        data.heatingStatus = parseInt(commands[i + 1], 16) ;
                    }
                break;
                case '34':
                    {
                        command_len = 1;
                        data.displayRefreshPeriod = parseInt(commands[i + 1], 16) ;
                    }
                break;
                case '36':
                    {
                        command_len = 1;
                        data.sendTargetTempDelay = parseInt(commands[i + 1], 16) ;
                    }
                break;
                case '38':
                    {
                        command_len = 1;
                        data.automaticHeatingStatus = parseInt(commands[i + 1], 16) ;
                    }
                break;
                case '3a':
                    {
                        command_len = 1;
                        data.sensorMode = parseInt(commands[i + 1], 16) ;
                    }
                break;
                case '3d':
                    {
                        command_len = 1;
                        data.pirSensorStatus = parseInt(commands[i + 1], 16) ;
                    }
                break;
                case '3f':
                    {
                        command_len = 1;
                        data.pirSensorSensitivity = parseInt(commands[i + 1], 16) ;
                    }
                break;
                case '41':
                    {
                        command_len = 1;
                        data.currentTemperatureVisibility = parseInt(commands[i + 1], 16) ;
                    }
                break;
                case '43':
                    {
                        command_len = 1;
                        data.humidityVisibility = parseInt(commands[i + 1], 16) ;
                    }
                break;
                case '45':
                    {
                        command_len = 1;
                        data.lightIntensityVisibility = parseInt(commands[i + 1], 16) ;
                    }
                break;
                case '47':
                    {
                        command_len = 1;
                        data.pirInitPeriod = parseInt(commands[i + 1], 16) ;
                    }
                break;
                case '49':
                    {
                        command_len = 1;
                        data.pirMeasurementPeriod = parseInt(commands[i + 1], 16) ;
                    }
                break;
                case '4b':
                    {
                        command_len = 1;
                        data.pirCheckPeriod = parseInt(commands[i + 1], 16) ;
                    }
                break;
                case '4d':
                    {
                        command_len = 1;
                        data.pirBlindPeriod = parseInt(commands[i + 1], 16) ;
                    }
                break;
                case '4f':
                    {
                        command_len = 1;
                        data.temperatureHysteresis = parseInt(commands[i + 1], 16)/10 ;
                    }
                break;
                case '51':
                    {
                        command_len = 2;
                        data.targetTemperature = ((parseInt(commands[i + 1], 16) << 8) | parseInt(commands[i + 2], 16))/10  ;
                    }
                break;
                case '53':
                    {
                        command_len = 1;
                        data.targetTemperatureStep = parseInt(commands[i + 1], 16) / 10;
                    }
                break;
                case '54':
                    {
                        command_len = 2;
                        data.manualTargetTemperatureUpdate = ((parseInt(commands[i + 1], 16) << 8) | parseInt(commands[i + 2], 16))/10;
                    }
                break;
                case 'a0': {
                    command_len = 4;
                    var fuota_address = (parseInt(commands[i + 1], 16) << 24) | 
                                        (parseInt(commands[i + 2], 16) << 16) | 
                                        (parseInt(commands[i + 3], 16) << 8) | 
                                        parseInt(commands[i + 4], 16);
                    var fuota_address_raw = commands[i + 1] + commands[i + 2] + 
                                            commands[i + 3] + commands[i + 4];
                    
                    data.fuota = { fuota_address: fuota_address, fuota_address_raw: fuota_address_raw };
                    break;
                }
                default:
                    break;
            }
            commands.splice(i,command_len);
        });
        return data;
        }
        if (bytes[0] == 1|| bytes[0] == 129) {
            data = handleKeepalive(bytes, data);
        }else{
            var keepaliveLength = 11;
            var potentialKeepAlive = bytes.slice(-12);
            if(potentialKeepAlive[0] == 129) keepaliveLength = 12;
            data = handleResponse(bytes,data, keepaliveLength);
            bytes = bytes.slice(-keepaliveLength);
            data = handleKeepalive(bytes, data);
        }
        return {data: data};
    } catch (e) {
        console.log(e)
        throw new Error('Unhandled data');
    }
}

function encodeDownlink(input) {
  var bytes = [];

  for (let key of Object.keys(input.data)) {
    switch (key) {
      case "setKeepAlive": {
        bytes.push(0x02);
        bytes.push(input.data.setKeepAlive);
        break;
      }
      case "getKeepAliveTime": {
        bytes.push(0x12);
        break;
      }
      case "getDeviceVersions": {
        bytes.push(0x04);
        break;
      }
      case "setTargetTemperature": {
        var temp = input.data.setTargetTemperature;
        bytes.push(0x2E);
        bytes.push(temp);
        break;
      }
      case "getTargetTemperature": {
        bytes.push(0x2F);
        break;
      }

      case "setKeysLock": {
        bytes.push(0x07);
        bytes.push(input.data.setKeysLock);
        break;
      }
      case "getKeysLock": {
        bytes.push(0x14);
        break;
      }
      case "setTemperatureRange": {
        bytes.push(0x08);
        bytes.push(input.data.setTemperatureRange.min);
        bytes.push(input.data.setTemperatureRange.max);
        break;
      }
      case "getTemperatureRange": {
        bytes.push(0x15);
        break;
      }
      case "setJoinRetryPeriod": {
        // period should be passed in minutes
        let periodToPass = (input.data.setJoinRetryPeriod * 60) / 5;
        periodToPass = int(periodToPass);
        bytes.push(0x10);
        bytes.push(periodToPass);
        break;
      }
      case "getJoinRetryPeriod": {
        bytes.push(0x19);
        break;
      }
      
      case "setUplinkType": {
        bytes.push(0x11);
        bytes.push(input.data.setUplinkType);
        break;
      }
      case "getUplinkType": {
        bytes.push(0x1B);
        break;
      }
      case "setWatchDogParams": {
          bytes.push(0x1C);
          bytes.push(input.data.SetWatchDogParams.confirmedUplinks);
          bytes.push(input.data.SetWatchDogParams.unconfirmedUplinks);
          break;
        }
        case "getWatchDogParams": {
            bytes.push(0x1D);
            break;
        }
        case "SetHeatingStatus": {
          bytes.push(0x31);
          bytes.push(input.data.SetHeatingStatus);
          break;
        }
        case "GetHeatingStatus": {
          bytes.push(0x32);
          break;
        }
        case "SetDisplayRefreshPeriod": {
          bytes.push(0x33);
          bytes.push(input.data.SetDisplayRefreshPeriod);
          break;
        }
        case "GetDisplayRefreshPeriod": {
          bytes.push(0x34);
          break;
        }
        case "SetTargetSendDelay": {
          bytes.push(0x35);
          bytes.push(input.data.SetTargetSendDelay);
          break;
        }
        case "GetTargetSendDelay": {
          bytes.push(0x36);
          break;
        }
        case "SetAutomaticHeatingStatus": {
          bytes.push(0x37);
          bytes.push(input.data.SetAutomaticHeatingStatus);
          break;
        }
        case "GetAutomaticHeatingStatus": {
          bytes.push(0x38);
          break;
        }
        case "SetSensorMode": {
          bytes.push(0x39);
          bytes.push(input.data.SetSensorMode);
          break;
        }
        case "GetSensorMode": {
          bytes.push(0x3a);
          break;
        }
        case "SetPIRSensorStatus": {
          bytes.push(0x3c);
          bytes.push(input.data.SetPIRSensorStatus);
          break;
        }
        case "GetPIRSensorStatus": {
          bytes.push(0x3d);
          break;
        }
        case "SetPIRSensorSensitivity": {
          bytes.push(0x3e);
          bytes.push(input.data.SetPIRSensorSensitivity);
          break;
        }
        case "GetPIRSensorSensitivity": {
          bytes.push(0x3f);
          break;
        }
        case "SetCurrentTemperatureVisibility": {
          bytes.push(0x40);
          bytes.push(input.data.SetCurrentTemperatureVisibility);
          break;
        }
        case "GetCurrentTemperatureVisibility": {
          bytes.push(0x41);
          break;
        }
        case "SetHumidityVisibility": {
          bytes.push(0x42);
          bytes.push(input.data.SetHumidityVisibility);
          break;
        }
        case "GetHumidityVisibility": {
          bytes.push(0x43);
          break;
        }
        case "SetLightIntensityVisibility": {
          bytes.push(0x44);
          bytes.push(input.data.SetLightIntensityVisibility);
          break;
        }
        case "GetLightIntensityVisibility": {
          bytes.push(0x45);
          break;
        }
        case "SetPIRInitPeriod": {
          bytes.push(0x46);
          bytes.push(input.data.SetPIRInitPeriod);
          break;
        }
        case "GetPIRInitPeriod": {
          bytes.push(0x47);
          break;
        }
        case "SetPIRMeasurementPeriod": {
          bytes.push(0x48);
          bytes.push(input.data.SetPIRMeasurementPeriod);
          break;
        }
        case "GetPIRMeasurementPeriod": {
          bytes.push(0x49);
          break;
        }
        case "SetPIRCheckPeriod": {
          var time = input.data.SetPIRCheckPeriod;
          var timeLowByte = time & 0xFF;  
          var timeHighByte = (time >> 8) & 0xFF; 
          bytes.push(0x4A);
          bytes.push(timeHighByte);
          bytes.push(timeLowByte);
          break;
        }
        case "GetPIRCheckPeriod": {
          bytes.push(0x4B);
          break;
        }
        case "SetPIRBlindPeriod": {
          var time = input.data.SetPIRBlindPeriod;
          var timeLowByte = time & 0xFF;  
          var timeHighByte = (time >> 8) & 0xFF; 
          bytes.push(0x4C);
          bytes.push(timeHighByte);
          bytes.push(timeLowByte);
          break;
        }
        case "GetPIRBlindPeriod": {
          bytes.push(0x4D);
          break;
        }
        case "SetTemperatureHysteresis": {
          bytes.push(0x4E);
          bytes.push(input.data.SetTemperatureHysteresis * 10);
          break;
        }
        case "GetTemperatureHysteresis": {
          bytes.push(0x4F);
          break;
        }
        case "SetTargetTemperaturePrecisely": {
          var targetTemperature = input.data.SetTargetTemperaturePrecisely * 10;
          var targetTemperatureLowByte = targetTemperature & 0xFF;  
          var targetTemperatureHighByte = (targetTemperature >> 8) & 0xFF; 
          bytes.push(0x50);
          bytes.push(targetTemperatureHighByte);
          bytes.push(targetTemperatureLowByte);
          break;
        }
        case "GetTargetTemperaturePrecisely": {
          bytes.push(0x51);
          break;
        }
        case "setTargetTemperatureStep": {
          bytes.push(0x52);
          bytes.push(input.data.setTargetTemperatureStep * 10);
          break;
        }
        case "getTargetTemperatureStep": {
          bytes.push(0x53);
          break;
        }
        case "setTempSensorCompensation": {
          bytes.push(0x55);
          bytes.push(input.data.setTempSensorCompensation.compensation);
          bytes.push(input.data.setTempSensorCompensation.temperature * 10);
          break;
        }
        case "getTempSensorCompensation": {
          bytes.push(0x56);
          break;
        }
        case "restartDevice": {
          bytes.push(0xA5);
          break;
        }
      case "sendCustomHexCommand": {
        let sendCustomHexCommand = input.data.sendCustomHexCommand;
        for (let i = 0; i < sendCustomHexCommand.length; i += 2) {
          const byte = parseInt(sendCustomHexCommand.substr(i, 2), 16);
          bytes.push(byte);
        }
        break;
      }
      default: {
      }
    }
  }

  return {
    bytes: bytes,
    fPort: 1,
    warnings: [],
    errors: [],
  };
}

function decodeDownlink(input) {
  return {
    data: {
      bytes: input.bytes,
    },
    warnings: [],
    errors: [],
  };
}

// example downlink commands
// {"setTargetTemperature":20} --> 0x2E14
// {"setTemperatureRange":{"min":15,"max":21}} --> 0x080F15
// {"sendCustomHexCommand":"080F15"} --> 0x080F15

Multitech BACnet Definition file

{
  "description": "MClimate Wireless Thermostat (WT) – BACnet mapping",
  "properties": {

    "sensorTemperature":               { "type": "float",  "units": "celsius" },
    "relativeHumidity":                { "type": "float",  "units": "%"       },
    "batteryVoltage":                  { "type": "float",  "units": "V"       },
    "targetTemperature":               { "type": "float",  "units": "celsius" },
    "powerSourceStatus":               { "type": "uint8"                      },
    "lux":                             { "type": "uint16", "units": "lx"      },
    "pir":                             { "type": "bool"                       },

    "deviceVersions":                  { "type": "object"                     },
    "keepAliveTime":                   { "type": "uint8",  "units": "minutes" },
    "childLock":                       { "type": "bool"                       },
    "temperatureRangeSettings":        { "type": "object"                     },
    "joinRetryPeriod":                 { "type": "float",  "units": "minutes" },
    "uplinkType":                      { "type": "uint8"                      },
    "watchDogParams":                  { "type": "object"                     },
    "manualTargetTemperatureUpdate":   { "type": "float",  "units": "celsius" },
    "heatingStatus":                   { "type": "uint8"                      },
    "displayRefreshPeriod":            { "type": "uint8",  "units": "minutes" },
    "sendTargetTempDelay":             { "type": "uint8",  "units": "seconds" },
    "automaticHeatingStatus":          { "type": "uint8"                      },
    "sensorMode":                      { "type": "uint8"                      },
    "pirSensorStatus":                 { "type": "uint8"                      },
    "pirSensorSensitivity":            { "type": "uint8"                      },
    "currentTemperatureVisibility":    { "type": "uint8"                      },
    "humidityVisibility":              { "type": "uint8"                      },
    "lightIntensityVisibility":        { "type": "uint8"                      },
    "pirInitPeriod":                   { "type": "uint8",  "units": "seconds" },
    "pirMeasurementPeriod":            { "type": "uint8",  "units": "seconds" },
    "pirCheckPeriod":                  { "type": "uint16", "units": "seconds" },
    "pirBlindPeriod":                  { "type": "uint16", "units": "seconds" },
    "temperatureHysteresis":           { "type": "float",  "units": "celsius" },
    "targetTemperatureStep":           { "type": "float",  "units": "celsius" },
    "fuota":                           { "type": "object"                     },

    "setKeepAlive":                    { "type": "uint8",  "units": "minutes", "downlink": true },
    "setTargetTemperature":            { "type": "uint8",  "units": "celsius", "downlink": true },
    "setKeysLock":                     { "type": "bool",                     "downlink": true },
    "setTemperatureRange":             { "type": "object",                   "downlink": true },
    "setJoinRetryPeriod":              { "type": "float",  "units": "minutes", "downlink": true },
    "setUplinkType":                   { "type": "uint8",                    "downlink": true },
    "setWatchDogParams":               { "type": "object",                   "downlink": true },
    "SetHeatingStatus":                { "type": "uint8",                    "downlink": true },
    "SetDisplayRefreshPeriod":         { "type": "uint8",  "units": "minutes", "downlink": true },
    "SetTargetSendDelay":              { "type": "uint8",  "units": "seconds", "downlink": true },
    "SetAutomaticHeatingStatus":       { "type": "uint8",                    "downlink": true },
    "SetSensorMode":                   { "type": "uint8",                    "downlink": true },
    "SetPIRSensorStatus":              { "type": "uint8",                    "downlink": true },
    "SetPIRSensorSensitivity":         { "type": "uint8",                    "downlink": true },
    "SetCurrentTemperatureVisibility": { "type": "uint8",                    "downlink": true },
    "SetHumidityVisibility":           { "type": "uint8",                    "downlink": true },
    "SetLightIntensityVisibility":     { "type": "uint8",                    "downlink": true },
    "SetPIRInitPeriod":                { "type": "uint8",  "units": "seconds", "downlink": true },
    "SetPIRMeasurementPeriod":         { "type": "uint8",  "units": "seconds", "downlink": true },
    "SetPIRCheckPeriod":               { "type": "uint16", "units": "seconds", "downlink": true },
    "SetPIRBlindPeriod":               { "type": "uint16", "units": "seconds", "downlink": true },
    "SetTemperatureHysteresis":        { "type": "float",  "units": "celsius", "downlink": true },
    "SetTargetTemperaturePrecisely":   { "type": "float",  "units": "celsius", "downlink": true },
    "setTargetTemperatureStep":        { "type": "float",  "units": "celsius", "downlink": true },
    "setTempSensorCompensation":       { "type": "object",                   "downlink": true },
    "restartDevice":                   { "type": "bool",                     "downlink": true },
    "sendCustomHexCommand":            { "type": "string",                   "downlink": true }
  },

  "decoder": "mclimate-wt-codec.js",
  "encoder": "mclimate-wt-codec.js"
}

Last updated

Was this helpful?