diff --git a/vendors/mclimate/codecs/ht-sensor.js b/vendors/mclimate/codecs/ht-sensor.js new file mode 100644 index 0000000..2a67961 --- /dev/null +++ b/vendors/mclimate/codecs/ht-sensor.js @@ -0,0 +1,108 @@ +function decodeUplink(input) { + try{ + var bytes = input.bytes; + var data = {}; + var calculateTemperature = function (rawData){return (rawData - 400) / 10}; + var calculateHumidity = function(rawData){return (rawData * 100) / 256}; + function decbin (number) { + if (number < 0) { + number = 0xFFFFFFFF + number + 1; + } + return parseInt(number, 10).toString(2); + } + + function handleKeepalive(bytes, data){ + var tempHex = ("0" + bytes[1].toString(16)).substr(-2) + ("0" + bytes[2].toString(16)).substr(-2); + var tempDec = parseInt(tempHex, 16); + var temperatureValue = calculateTemperature(tempDec); + var humidityValue = calculateHumidity(bytes[3]); + var batteryTmp = ("0" + bytes[4].toString(16)).substr(-2)[0]; + var batteryVoltageCalculated = 2 + parseInt("0x" + batteryTmp, 16) * 0.1; + var temperature = temperatureValue; + var humidity = humidityValue; + var batteryVoltage = batteryVoltageCalculated; + var thermistorProperlyConnected = decbin(bytes[5])[5] == 0; + + var extT1 = ("0" + bytes[5].toString(16)).substr(-2)[1]; + + var extT2 = ("0" + bytes[6].toString(16)).substr(-2); + var extThermistorTemperature = 0; + if(thermistorProperlyConnected){ + extThermistorTemperature = parseInt("0x"+extT1+""+extT2, 16) * 0.1; + } + + data.sensorTemperature = Number(temperature.toFixed(2)); + data.relativeHumidity = Number(humidity.toFixed(2)); + data.batteryVoltage = Number(batteryVoltage.toFixed(2)); + data.thermistorProperlyConnected = thermistorProperlyConnected; + data.extThermistorTemperature = extThermistorTemperature; + return data; + } + + function handleResponse(bytes, data){ + + var commands = bytes.map(function(byte){ + return ("0" + byte.toString(16)).substr(-2); + }); + commands = commands.slice(0,-7); + 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 '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 deviceKeepAlive = 5; + var wdpC = commands[i + 1] == '00' ? false : commands[i + 1] * deviceKeepAlive + 7; + var wdpUc = commands[i + 2] == '00' ? false : parseInt(commands[i + 2], 16); + data.watchDogParams= { wdpC: wdpC, wdpUc: wdpUc } ; + } + break; + default: + break; + } + commands.splice(i,command_len); + }); + return data; + } + if (bytes[0] == 1) { + data = handleKeepalive(bytes, data); + + }else{ + data = handleResponse(bytes,data); + bytes = bytes.slice(-7); + data = handleKeepalive(bytes, data); + } + return {data: data}; + } catch (e) { + throw new Error(e); + } +} \ No newline at end of file diff --git a/vendors/mclimate/codecs/test_decode_ht-sensor.json b/vendors/mclimate/codecs/test_decode_ht-sensor.json new file mode 100644 index 0000000..e91bdce --- /dev/null +++ b/vendors/mclimate/codecs/test_decode_ht-sensor.json @@ -0,0 +1,17 @@ +[ + { + "name": "Test decode", + "input": { + "bytes": [0x01, 0x02, 0x74, 0x73, 0xF1, 0x04, 0x00] + }, + "expected": { + "data": { + "sensorTemperature": 22.8, + "relativeHumidity": 44.92, + "batteryVoltage": 3.5, + "thermistorProperlyConnected": false, + "extThermistorTemperature": 0 + } + } + } +] \ No newline at end of file diff --git a/vendors/mclimate/codecs/test_decode_vicki.json b/vendors/mclimate/codecs/test_decode_vicki.json new file mode 100644 index 0000000..7892e26 --- /dev/null +++ b/vendors/mclimate/codecs/test_decode_vicki.json @@ -0,0 +1,30 @@ +[ + { + "name": "Test decode", + "input": { + "bytes": [0x01, 0x1D, 0x5A, 0x78, 0xFA, 0x2C, 0x01, 0xF0, 0x80] + }, + "expected": { + "data": { + "reason": 1, + "targetTemperature": 29, + "sensorTemperature": 18.01, + "relativeHumidity": 46.88, + "motorRange": 300, + "motorPosition": 250, + "batteryVoltage": 3.5, + "openWindow": false, + "highMotorConsumption": false, + "lowMotorConsumption": false, + "brokenSensor": false, + "childLock": true, + "calibrationFailed": false, + "attachedBackplate": false, + "perceiveAsOnline": false, + "antiFreezeProtection": false, + "valveOpenness": 17, + "targetTemperatureFloat": 29 + } + } + } +] diff --git a/vendors/mclimate/codecs/test_encode_ht-sensor.json b/vendors/mclimate/codecs/test_encode_ht-sensor.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/vendors/mclimate/codecs/test_encode_ht-sensor.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/vendors/mclimate/codecs/test_encode_vicki.json b/vendors/mclimate/codecs/test_encode_vicki.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/vendors/mclimate/codecs/test_encode_vicki.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/vendors/mclimate/codecs/vicki.js b/vendors/mclimate/codecs/vicki.js new file mode 100644 index 0000000..6cc5b60 --- /dev/null +++ b/vendors/mclimate/codecs/vicki.js @@ -0,0 +1,358 @@ +function decodeUplink(input) { + var bytes = input.bytes; + var data = {}; + var resultToPass = {}; + let toBool = function (value) { return value == '1' }; + + function merge_obj(obj1, obj2) { + var obj3 = {}; + for (var attrname in obj1) { obj3[attrname] = obj1[attrname]; } + for (var attrname2 in obj2) { obj3[attrname2] = obj2[attrname2]; } + return obj3; + } + + function handleKeepalive(bytes, data){ + var tmp = ("0" + bytes[6].toString(16)).substr(-2); + var motorRange1 = tmp[1]; + var motorRange2 = ("0" + bytes[5].toString(16)).substr(-2); + var motorRange = parseInt("0x" + motorRange1 + motorRange2, 16); + + var motorPos2 = ("0" + bytes[4].toString(16)).substr(-2); + var motorPos1 = tmp[0]; + var motorPosition = parseInt("0x" + motorPos1 + motorPos2, 16); + + var batteryTmp = ("0" + bytes[7].toString(16)).substr(-2)[0]; + var batteryVoltageCalculated = 2 + parseInt("0x" + batteryTmp, 16) * 0.1; + + let decbin = (number) => { + if (number < 0) { + number = 0xFFFFFFFF + number + 1 + } + number = number.toString(2); + return "00000000".substr(number.length) + number; + } + var byte7Bin = decbin(bytes[7]); + var openWindow = byte7Bin[4]; + var highMotorConsumption = byte7Bin[5]; + var lowMotorConsumption = byte7Bin[6]; + var brokenSensor = byte7Bin[7]; + var byte8Bin = decbin(bytes[8]); + var childLock = byte8Bin[0]; + var calibrationFailed = byte8Bin[1]; + var attachedBackplate = byte8Bin[2]; + var perceiveAsOnline = byte8Bin[3]; + var antiFreezeProtection = byte8Bin[4]; + + var sensorTemp = 0; + if (Number(bytes[0].toString(16)) == 1) { + sensorTemp = (bytes[2] * 165) / 256 - 40; + } + + if (Number(bytes[0].toString(16)) == 81) { + sensorTemp = (bytes[2] - 28.33333) / 5.66666; + } + data.reason = Number(bytes[0].toString(16)); + data.targetTemperature = Number(bytes[1]); + data.sensorTemperature = Number(sensorTemp.toFixed(2)); + data.relativeHumidity = Number(((bytes[3] * 100) / 256).toFixed(2)); + data.motorRange = motorRange; + data.motorPosition = motorPosition; + data.batteryVoltage = Number(batteryVoltageCalculated.toFixed(2)); + data.openWindow = toBool(openWindow); + data.highMotorConsumption = toBool(highMotorConsumption); + data.lowMotorConsumption = toBool(lowMotorConsumption); + data.brokenSensor = toBool(brokenSensor); + data.childLock = toBool(childLock); + data.calibrationFailed = toBool(calibrationFailed); + data.attachedBackplate = toBool(attachedBackplate); + data.perceiveAsOnline = toBool(perceiveAsOnline); + data.antiFreezeProtection = toBool(antiFreezeProtection); + data.valveOpenness = motorRange != 0 ? Math.round((1-(motorPosition/motorRange))*100) : 0; + if(!data.hasOwnProperty('targetTemperatureFloat')){ + data.targetTemperatureFloat = parseFloat(bytes[1]) + } + return data; + } + + function handleResponse(bytes, data){ + var commands = bytes.map(function(byte, i){ + return ("0" + byte.toString(16)).substr(-2); + }); + commands = commands.slice(0,-9); + 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]; + var dataK = { deviceVersions: { hardware: Number(hardwareVersion), software: Number(softwareVersion) } }; + resultToPass = merge_obj(resultToPass, dataK); + } + break; + case '12': + { + command_len = 1; + var dataC = { keepAliveTime: parseInt(commands[i + 1], 16) }; + resultToPass = merge_obj(resultToPass, dataC); + } + break; + case '13': + { + command_len = 4; + var enabled = toBool(parseInt(commands[i + 1], 16)); + var duration = parseInt(commands[i + 2], 16) * 5; + var tmp = ("0" + commands[i + 4].toString(16)).substr(-2); + var motorPos2 = ("0" + commands[i + 3].toString(16)).substr(-2); + var motorPos1 = tmp[0]; + var motorPosition = parseInt('0x' + motorPos1 + motorPos2, 16); + var delta = Number(tmp[1]); + + var dataD = { openWindowParams: { enabled: enabled, duration: duration, motorPosition: motorPosition, delta: delta } }; + resultToPass = merge_obj(resultToPass, dataD); + } + break; + case '14': + { + command_len = 1; + var dataB = { childLock: toBool(parseInt(commands[i + 1], 16)) }; + resultToPass = merge_obj(resultToPass, dataB); + } + break; + case '15': + { + command_len = 2; + var dataA = { temperatureRangeSettings: { min: parseInt(commands[i + 1], 16), max: parseInt(commands[i + 2], 16) } }; + resultToPass = merge_obj(resultToPass, dataA); + } + break; + case '16': + { + command_len = 2; + var data = { internalAlgoParams: { period: parseInt(commands[i + 1], 16), pFirstLast: parseInt(commands[i + 2], 16), pNext: parseInt(commands[i + 3], 16) } }; + resultToPass = merge_obj(resultToPass, data); + } + break; + case '17': + { + command_len = 2; + var dataF = { internalAlgoTdiffParams: { warm: parseInt(commands[i + 1], 16), cold: parseInt(commands[i + 2], 16) } }; + resultToPass = merge_obj(resultToPass, dataF); + } + break; + case '18': + { + command_len = 1; + var dataE = { operationalMode: parseInt(commands[i + 1], 16) }; + resultToPass = merge_obj(resultToPass, dataE); + } + break; + case '19': + { + command_len = 1; + var commandResponse = parseInt(commands[i + 1], 16); + var periodInMinutes = commandResponse * 5 / 60; + var dataH = { joinRetryPeriod: periodInMinutes }; + resultToPass = merge_obj(resultToPass, dataH); + } + break; + case '1b': + { + command_len = 1; + var dataG = { uplinkType: parseInt(commands[i + 1], 16) }; + resultToPass = merge_obj(resultToPass, dataG); + } + break; + case '1d': + { + // get default keepalive if it is not available in data + 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); + var dataJ = { watchDogParams: { wdpC: wdpC, wdpUc: wdpUc } }; + resultToPass = merge_obj(resultToPass, dataJ); + } + break; + case '1f': + { + command_len = 1; + var data = { primaryOperationalMode: commands[i + 1] }; + resultToPass = merge_obj(resultToPass, data); + } + break; + case '21': + { + command_len = 6; + var data = {batteryRangesBoundaries:{ + Boundary1: parseInt(commands[i + 1] + commands[i + 2], 16), + Boundary2: parseInt(commands[i + 3] + commands[i + 4], 16), + Boundary3: parseInt(commands[i + 5] + commands[i + 6], 16), + }}; + resultToPass = merge_obj(resultToPass, data); + } + break; + case '23': + { + command_len = 4; + var data = {batteryRangesOverVoltage:{ + Range1: parseInt(commands[i + 2], 16), + Range2: parseInt(commands[i + 3], 16), + Range3: parseInt(commands[i + 4], 16), + }}; + resultToPass = merge_obj(resultToPass, data); + } + break; + case '27': + { + command_len = 1; + var data = {OVAC: parseInt(commands[i + 1], 16)}; + resultToPass = merge_obj(resultToPass, data); + } + break; + case '28': + { + command_len = 1; + var data = { manualTargetTemperatureUpdate: parseInt(commands[i + 1], 16) }; + resultToPass = merge_obj(resultToPass, data); + + } + break; + case '29': + { + command_len = 2; + var data = { proportionalAlgoParams: { coefficient: parseInt(commands[i + 1], 16), period: parseInt(commands[i + 2], 16) } }; + resultToPass = merge_obj(resultToPass, data); + + } + break; + case '2b': + { + command_len = 1; + var data = { algoType: commands[i + 1] }; + resultToPass = merge_obj(resultToPass, data); + } + break; + case '36': + { + command_len = 3; + var kp = parseInt(`${commands[i + 1]}${commands[i + 2]}${commands[i + 3]}`, 16) / 131072; + var data = { proportionalGain: Number(kp).toFixed(5) }; + resultToPass = merge_obj(resultToPass, data); + } + break; + case '3d': + { + command_len = 3; + var ki = parseInt(`${commands[i + 1]}${commands[i + 2]}${commands[i + 3]}`, 16) / 131072; + var data = { integralGain: Number(ki).toFixed(5) }; + resultToPass = merge_obj(resultToPass, data); + } + break; + case '3f': + { + command_len = 2; + var data = { integralValue : (parseInt(`${commands[i + 1]}${commands[i + 2]}`, 16))/10 }; + resultToPass = merge_obj(resultToPass, data); + } + break; + case '40': + { + command_len = 1; + var data = { piRunPeriod : parseInt(commands[i + 1], 16) }; + resultToPass = merge_obj(resultToPass, data); + } + break; + case '42': + { + command_len = 1; + var data = { tempHysteresis : parseInt(commands[i + 1], 16) }; + resultToPass = merge_obj(resultToPass, data); + } + break; + case '44': + { + command_len = 2; + var data = { extSensorTemperature : (parseInt(`${commands[i + 1]}${commands[i + 2]}`, 16))/10 }; + resultToPass = merge_obj(resultToPass, data); + } + break; + case '46': + { + command_len = 3; + var enabled = toBool(parseInt(commands[i + 1], 16)); + var duration = parseInt(commands[i + 2], 16) * 5; + var delta = parseInt(commands[i + 3], 16) /10; + + var data = { openWindowParams: { enabled: enabled, duration: duration, delta: delta } }; + resultToPass = merge_obj(resultToPass, data); + } + break; + case '48': + { + command_len = 1; + var data = { forceAttach : parseInt(commands[i + 1], 16) }; + resultToPass = merge_obj(resultToPass, data); + } + break; + case '4a': + { + command_len = 3; + var activatedTemperature = parseInt(commands[i + 1], 16)/10; + var deactivatedTemperature = parseInt(commands[i + 2], 16)/10; + var targetTemperature = parseInt(commands[i + 3], 16); + + var data = { antiFreezeParams: { activatedTemperature, deactivatedTemperature, targetTemperature } }; + resultToPass = merge_obj(resultToPass, data); + } + break; + case '4d': + { + command_len = 2; + var data = { piMaxIntegratedError : (parseInt(`${commands[i + 1]}${commands[i + 2]}`, 16))/10 }; + resultToPass = merge_obj(resultToPass, data); + } + break; + case '50': + { + command_len = 2; + var data = { effectiveMotorRange: { minValveOpenness: 100 - parseInt(commands[i + 2], 16), maxValveOpenness: 100 - parseInt(commands[i + 1], 16) } }; + resultToPass = merge_obj(resultToPass, data); + } + break; + case '52': + { + command_len = 2; + var data = { targetTemperatureFloat : (parseInt(`${commands[i + 1]}${commands[i + 2]}`, 16))/10 }; + resultToPass = merge_obj(resultToPass, data); + } + break; + case '54': + { + command_len = 1; + var offset = (parseInt(commands[i + 1], 16) - 28) * 0.176 + var data = { temperatureOffset : offset }; + resultToPass = merge_obj(resultToPass, data); + } + break; + default: + break; + } + commands.splice(i,command_len); + }); + return resultToPass; + } + + if (bytes[0].toString(16) == 1 || bytes[0].toString(16) == 129) { + data = merge_obj(data, handleKeepalive(bytes, data)); + }else{ + data = merge_obj(data, handleResponse(bytes, data)); + bytes = bytes.slice(-9); + data = merge_obj(data, handleKeepalive(bytes, data)); + } + + return { + data: data + }; +} diff --git a/vendors/mclimate/devices/ht-sensor.toml b/vendors/mclimate/devices/ht-sensor.toml new file mode 100644 index 0000000..94ec1cf --- /dev/null +++ b/vendors/mclimate/devices/ht-sensor.toml @@ -0,0 +1,13 @@ +[device] +id = "f241cd5f-5b3b-46a1-bb01-9bd10a90cb09" +name = "Humidity and Temperature Sensor" +description = "The MClimate HT LoRaWAN® Sensor is an indoor temperature and humidity sensor with 10+ years of battery life ideal for monitoring environments in residential and commercial buildings." + +[[device.firmware]] +version = "1.0" +profiles = ["EU868_1_0_3.toml"] +codec = "ht-sensor.js" + +[device.metadata] +product_url = "https://docs.mclimate.eu/mclimate-lorawan-devices/devices/mclimate-ht-sensor-lorawan" +documentation_url = "https://3940008670-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-McDr-jr9h3qA888r1Yp%2Fuploads%2FuWtULrkAnkdqgLeWDsIu%2FMClimate-HT-Sensor-LoRaWAN-Batteries-Datasheet-en.pdf?alt=media&token=efb23d15-d43d-490b-b82a-c205131feb5c" diff --git a/vendors/mclimate/devices/vicki.toml b/vendors/mclimate/devices/vicki.toml new file mode 100644 index 0000000..a4e3333 --- /dev/null +++ b/vendors/mclimate/devices/vicki.toml @@ -0,0 +1,13 @@ +[device] +id = "c521e66f-5745-45a0-b483-00fe1db8eaac" +name = "Vicki - Smart Radiator Thermostat" +description = "The MClimate Vicki is a smart radiator thermostat valve that contains a temperature and humidity sensor allowing for temperature control and monitoring temperature and humidity from distance through a LoRaWAN® network. Manual target temperature selection is possible by rotating the outer ring of the device. The target temperature is displayed on the device." + +[[device.firmware]] +version = "3.1" +profiles = ["EU868_1_0_3.toml"] +codec = "vicki.js" + +[device.metadata] +product_url = "https://docs.mclimate.eu/mclimate-lorawan-devices/devices/mclimate-vicki-lorawan" +documentation_url = "https://3940008670-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-McDr-jr9h3qA888r1Yp%2Fuploads%2FIDndmRaKu2Enz7MpIiW1%2FMClimate-Vicki-LoRaWAN-Datasheet.pdf?alt=media&token=eec857d7-2821-4880-b56a-399293c49d72" diff --git a/vendors/mclimate/profiles/EU868_1_0_3.toml b/vendors/mclimate/profiles/EU868_1_0_3.toml new file mode 100644 index 0000000..462fe81 --- /dev/null +++ b/vendors/mclimate/profiles/EU868_1_0_3.toml @@ -0,0 +1,25 @@ +[profile] +id = "a6ba92b2-41bc-42ff-9ca2-e5e51a0189ef" +vendor_profile_id = 0 +region = "EU868" +mac_version = "1.0.3" +reg_params_revision = "A" +supports_otaa = true +supports_class_b = false +supports_class_c = false +max_eirp = 0 + +[profile.abp] +rx1_delay = 0 +rx1_dr_offset = 0 +rx2_dr = 0 +rx2_freq = 0 + +[profile.class_b] +timeout_secs = 0 +ping_slot_nb_k = 0 +ping_slot_dr = 0 +ping_slot_freq = 0 + +[profile.class_c] +timeout_secs = 0 diff --git a/vendors/mclimate/vendor.toml b/vendors/mclimate/vendor.toml new file mode 100644 index 0000000..89a5976 --- /dev/null +++ b/vendors/mclimate/vendor.toml @@ -0,0 +1,8 @@ +[vendor] +id = "1faa8d1d-2e2e-40db-a578-81d21af4f1ca" +name = "MClimate" +vendor_id = 0 +ouis = [] + +[vendor.metadata] +homepage = "https://mclimate.eu"