//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}; } const OFFSET_LOOKUP_TABLE = { "-5.0" : 0xf6, "-4.5" : 0xf7, "-4.0" : 0xf8, "-3.5" : 0xf9, "-3.0" : 0xfa, "-2.5" : 0xfb, "-2.0" : 0xfc, "-1.5" : 0xfd, "-1.0" : 0xfe, "-0.5" : 0xff, "0.0" : 0x00, "0.5" : 0x01, "1.0" : 0x02, "1.5" : 0x03, "2.0" : 0x04, "2.5" : 0x05, "3.0" : 0x06, "3.5" : 0x07, "4.0" : 0x08, "4.5" : 0x09, "5.0" : 0x0a, }; function setExternalTemperature(input){ let externalTempData = input.externalTemperatureValue; let output = []; if(externalTempData !== null){ output.push(0xe4); if(externalTempData=='disabled' || externalTempData == 0xff){ output.push(0xff); } else if(externalTempData != 0xff){ if(externalTempData >= 0 && externalTempData <= 50 ){ externalTempData = Math.floor(externalTempData*2); output.push(externalTempData); } else{ output.push(0xff); } } } return output; } function setFlags(input){ let flagByte1 = 0x00; let flagByte2 = 0x00; let output = []; //it is not allowed to set both child lock protection options to the same time if(input.keylock && input.advancedKeylock){ flagByte1 |= 0x01; }else if(input.advancedKeylock){ flagByte1 |= 0x02; }else if(input.keylock){ flagByte1 |= 0x01; } if(input.displayOrientationChanged){ flagByte1 |= 0x04; } flagByte1 |= 0x08; flagByte1 |= 0x10; flagByte2 |= 0x08; output.push(0xf7); output.push(0x80); output.push(flagByte1); output.push(flagByte2); output.push(0x80); output.push(0x80); return output; } function setValvePosition(input){ let currentPosition = input.currentValvePosition; let output = []; if(currentPosition !== null && currentPosition !== undefined && !isNaN(currentPosition)){ output.push(0xff); if(currentPosition > 0 && currentPosition < 256){ output.push(currentPosition.toString(16)); } else{ output.push(0x00); } output.push(0x00); } return output; } function setSetpointLimitation(input){ let lowTempOutput = 0x80; let maxTempOutput = 0x80; let lowTempLimit = Math.floor(input.minimumTemperatureSetpoint * 2); let maxTempLimit = Math.floor(input.maximumTemperatureSetpoint *2); let output = []; if(lowTempLimit !== null && lowTempLimit !== undefined && !isNaN(lowTempLimit)){ if(lowTempLimit < 15 || lowTempLimit > 57 || lowTempLimit > maxTempLimit){ lowTempLimit = 0x80; } }else{ lowTempLimit = 0x80; } lowTempOutput = lowTempLimit; if(maxTempLimit !== null && maxTempLimit !== undefined && !isNaN(maxTempLimit)){ if(maxTempLimit < 15 || maxTempLimit > 57 || lowTempLimit > maxTempLimit){ maxTempLimit = 0x80; } }else{ maxTempLimit = 0x80; } maxTempOutput = maxTempLimit; output.push(0xf9); output.push(lowTempOutput); output.push(maxTempOutput); return output; } function setTemperature(input){ let output = []; output.push(0xf5); output.push(0x80); output.push(0x80); let heatingTemperature = Math.floor(input.heatingTemperature*2); if(heatingTemperature == null || heatingTemperature === undefined || heatingTemperature < 14 || heatingTemperature > 57){ heatingTemperature = 0x80; } output.push(heatingTemperature); let temperatureOffset = OFFSET_LOOKUP_TABLE[input.temperatureOffset]; if(input.temperatureOffset === undefined || input.temperatureOffset === null){ temperatureOffset = 0x80; } output.push(temperatureOffset); let windowOpenDetectionThreshold = Math.floor(input.windowDetectionThreshold); if(windowOpenDetectionThreshold === null || windowOpenDetectionThreshold === undefined || isNaN(windowOpenDetectionThreshold)){ windowOpenDetectionThreshold = 0x80; }else { if(windowOpenDetectionThreshold != 0xff && (windowOpenDetectionThreshold < 4 || windowOpenDetectionThreshold> 12)){ windowOpenDetectionThreshold = 0x80; } } output.push(windowOpenDetectionThreshold); let windowOpenDetectionDuration = input.windowDetectionDuration; if(windowOpenDetectionDuration=== null || windowOpenDetectionDuration === undefined || windowOpenDetectionDuration < 1 || windowOpenDetectionDuration > 30){ windowOpenDetectionDuration = 0x80; } output.push(windowOpenDetectionDuration); output.push(0x80); return output; } function setMaxValvePosition(input){ let output = []; let value = input.maxValvePositionLimitInPercent; output.push(0xE1); if(value >= 0 && value <= 100 && value !== null && !isNaN(value) && value !== undefined){ output.push(value); } else{ output.push(0x80); } return output; } function setWakeupInterval(input){ let intervall = input.wakeupIntervalInSeconds; let output = []; if(intervall >= 60 && intervall <= 7200 && intervall !== null && !isNaN(intervall) && intervall !== undefined){ output.push(0xA3); let lsb = (parseInt(intervall.toString(16),16) & 0xFF); let msb = (parseInt(intervall.toString(16),16) & 0xFF00) >> 8; output.push(lsb); output.push(msb); } return output; } function encodeDownlink(input) { let bytes = []; for(let key of Object.keys(input.data)){ switch(key){ case "getRadioSoftwareVersion": bytes.push(0xa0); break; case "getWakeupInterval": bytes.push(0xa4); break; case "setWakeupInterval": bytes = setWakeupInterval(input.data.setWakeupInterval); break; case "getMaxValvePositionLimit": bytes.push(0xe2); break; case "maxValvePositionLimit": bytes = setMaxValvePosition(input.data.maxValvePositionLimit); break; case "getExternalTemperatureValue": bytes.push(0xe3); break; case "externalTemperatureValue": bytes = setExternalTemperature(input.data); break; case "getSummary": bytes.push(0xe9); break; case "getBaseSoftwareVersion": bytes.push(0xef); break; case "getBatteryValue": bytes.push(0xf3); break; case "getTemperature": bytes.push(0xf4); break; case "temperature": bytes = setTemperature(input.data.temperature); break; case "getFlags": bytes.push(0xf6); break; case "flags": bytes = setFlags(input.data.flags); break; case "getSetpointLimitation": bytes.push(0xf8); break; case "setpointLimitation": bytes = setSetpointLimitation(input.data.setpointLimitation); break; case "getValvePosition": bytes.push(0xfe); break; case "valvePosition": bytes = setValvePosition(input.data.valvePosition); break; default: break; } } return { bytes: bytes, fPort: 2, warnings: [], errors: [] }; }