//Parser version 1.3.2, 02-06-2025 /* Changelog: 1.0.2 add new command to e2 1.0.3 - e3 parser error, if e3 value is 0xff fixed 1.1 - general corrections 1.2 Add command to show radio software version 1.3 - Add Command to determine base software version - Wakeup interval handling (new function with 1.43+0.4.20) - Format OFFSET_LOOKUP table changed. 1.3.1 - Wording correction - reduced complexity 1.3.2 - change wording: child protectio = keylock - change wording: childProtectionPlus = advanced keylock - change wording: windowOpenDetectio => windowDetection /* Parser can used for Stella R devices Supported Software Versions: Base Software 1.43 (and older) Radio software 0.4.20 (and older) */ const OFFSET_LOOKUP = { 0xf6: "-5.0", 0xf7: "-4.5", 0xf8: "-4.0", 0xf9: "-3.5", 0xfa: "-3.0", 0xfb: "-2.5", 0xfc: "-2.0", 0xfd: "-1.5", 0xfe: "-1.0", 0xff: "-0.5", 0x00: "0.0", 0x01: "0.5", 0x02: "1.0", 0x03: "1.5", 0x04: "2.0", 0x05: "2.5", 0x06: "3.0", 0x07: "3.5", 0x08: "4.0", 0x09: "4.5", 0x0a: "5.0", }; function buildJsonFile(jsonFile, newData) { let output = {}; for (let valueJsonFile in jsonFile) { output[valueJsonFile] = jsonFile[valueJsonFile]; } for (let valueNewData in newData) { output[valueNewData] = newData[valueNewData]; } return output; } function boolCon(bit) { return bit == 1; } function calculateWindowOpenDetection(threshhold) { let output = 'deactivated'; threshhold = parseInt(threshhold, 16); if (threshhold >= 4 && threshhold <= 12) { output = threshhold; } return output; } function changeWakeupInterval(lsb, msb){ let value = msb+lsb; return(parseInt(value, 16)); } function decbin(number) { if (number < 0) { number = 0xFFFFFFFF + number + 1; } number = number.toString(2); return "00000000".substring(number.length) + number; } function flagCalculation(flagByte0, flagByte1, flagByte2) { let flagByte0Bin = decbin(parseInt(flagByte0, 16)); let flagByte1Bin = decbin(parseInt(flagByte1, 16)); let flagByte2Bin = decbin(parseInt(flagByte2, 16)); return { heatingTemperatureInUse: boolCon(flagByte0Bin[6]), positionModeEnabled: boolCon(flagByte0Bin[3]), windowOpenDetectionTimerActive: boolCon(flagByte0Bin[2]), keylock: boolCon(flagByte1Bin[7]), advancedKeylock: boolCon(flagByte1Bin[6]), displayOrientationChanged: boolCon(flagByte1Bin[5]), dailyBatteryReporting: boolCon(flagByte2Bin[4]), batteryLessThan15Percent: boolCon(flagByte2Bin[3]), batteryLessThan25Percent: boolCon(flagByte2Bin[2]), deviceErrorState: flagErrorState(flagByte2Bin), }; } function flagErrorState(flagByte2) { let errorState = (flagByte2[5] + flagByte2[6] + flagByte2[7]).toString(); let mountErrorEOne = false; let mountErrorEThree = false; let busyState = false; let mountPosition = false; let valveMountPosition = false; switch (errorState) { case '001': mountErrorEOne = true; break; case '011': mountErrorEThree = true; break; case '101': busyState = true; break; case '110': mountPosition = true; break; case '111': valveMountPosition = true; break; default: break; } return { deviceError: { e1ErrorState: mountErrorEOne, e2ErrorState: mountErrorEThree, deviceIsBusy: busyState, deviceInMountingPosition: mountPosition, deviceInValveMountingPosition: valveMountPosition, } }; } //Calculate temperature values function temperature(temp) { let parsedTemp = parseInt(temp, 16); if (parsedTemp => 15 || parsedTemp <= 57) { parsedTemp = parsedTemp / 2; } else { //21° is default value parsedTemp = 21; } return parsedTemp; } function decodeUplink(input){ let jsonFile = {}; let commands = input.bytes.map(function (byte) { return (byte.toString(16)); }); let payloadSize = 0; commands.map(function(command, i){ switch(command){ case "a0": //radio software version payloadSize = 3; let radioSoftwareVersion = {radioSoftwareVersion:`${parseInt(commands[i+1],16)}.${parseInt(commands[i+2],16)}.${parseInt(commands[i+3],16)}`}; jsonFile = buildJsonFile(jsonFile, radioSoftwareVersion); break; case "a4": //wakeup interval payloadSize = 2; let wakeupInterval = {wakeupIntervalInSeconds : changeWakeupInterval(commands[i+1], commands[i+2])}; jsonFile = buildJsonFile(jsonFile, wakeupInterval); break; case "e2": //valve max opening limit payloadSize = 1; let maxValveOpeningLimit = {maxValvePositionLimit : {maxValvePositionLimitInPercent : parseInt(commands[i+1],16)}}; jsonFile = buildJsonFile(jsonFile, maxValveOpeningLimit); break; case "e3": //get external temperature value payloadSize = 1; let externalTempData = ""; if (parseInt(commands[i + 1], 16) != 0xff) { //the thermostat can work with external temperature values between 0.0 and 50.0 °C if (parseInt(command[i + 1], 16) >= 0 || parseInt(command[i + 1], 16) <= 0x32) { externalTempData = temperature(commands[i + 1]); } } else { externalTempData = "disabled"; } let extTempValue = { externalTemperatureValue: externalTempData }; jsonFile = buildJsonFile(jsonFile, extTempValue); break; case "e9": //summary payloadSize = 6; let summaryFlag0 = commands[i + 1]; let summaryFlag1 = commands[i + 2]; let summaryFlag2 = commands[i + 3]; let actaulTemp = commands[i + 5]; let valvePosSummary = parseInt(commands[i + 6], 16); let summary = { deviceSummary: { flags: flagCalculation(summaryFlag0, summaryFlag1, summaryFlag2), measuredTemperature: temperature(actaulTemp), valvePosition: parseInt(valvePosSummary), } }; jsonFile = buildJsonFile(jsonFile, summary); break; case "ef": //base software version payloadSize = 8; let swVersion = ""; for (let i = 1; i <= payloadSize; i++) { var hex = commands[i].toString(); swVersion += String.fromCharCode(parseInt(hex, 16)); } let baseSwVersion = { baseSoftwareVersion: swVersion }; jsonFile = buildJsonFile(jsonFile, baseSwVersion); break; case "f3": //battery value payloadSize = 1; let batteryValue = parseInt(commands[i + 1], 16); if (batteryValue > 100) { batteryValue = 100; } let batteryData = { batteryValueInPercent: batteryValue }; jsonFile = buildJsonFile(jsonFile, batteryData); break; case "f4": //temperature payloadSize = 7; let temperaturSettings = { temperature: { measuredTemperature: temperature(commands[i + 1]), heatingTemperature: temperature(commands[i + 3]), temperatureOffset: OFFSET_LOOKUP[parseInt(commands[i + 4], 16)], windowDetectionThreshold: calculateWindowOpenDetection(commands[i + 5]), windowDetectionDuration: parseInt(commands[i + 6], 16) } } jsonFile = buildJsonFile(jsonFile, temperaturSettings); break; case "f6": //flags payloadSize = 5; let flagByte0 = commands[i + 1]; let flagByte1 = commands[i + 2]; let flagByte2 = commands[i + 3]; let flags = flagCalculation(flagByte0, flagByte1, flagByte2); jsonFile = buildJsonFile(jsonFile, { flags: flags }); break; case "f8": //setpoint limitation payloadSize = 2; let minRegulationTemperature = commands[i + 1]; let maxTempRegulation = commands[i + 2]; let setpointLimit = { setpointLimitation: { minimumTemperatureSetpoint: temperature(minRegulationTemperature), maximumTemperatureSetpoint: temperature(maxTempRegulation), } }; jsonFile = buildJsonFile(jsonFile, setpointLimit); break; case "fe": //valve position payloadSize = 2; let valveControl = { currentValvePosition: parseInt(commands[i + 1], 16), }; jsonFile = buildJsonFile(jsonFile, valveControl); break; default: break; } commands.splice(i,payloadSize); }); return{data: jsonFile}; }