Here's my latest flow now including the 0xC3 Set command.
[{"id":"8fdc4e97.ed6b5","type":"inject","z":"18a1c78c.638ef8","name":"Query 0xC0","props":[{"p":"commandCode","v":"0xC0","vt":"num"},{"p":"commandName","v":"Query 0xC0","vt":"str"}],"repeat":"2","crontab":"","once":true,"onceDelay":"2","topic":"","x":150,"y":340,"wires":[["b5298440.55041"]]},{"id":"b5298440.55041","type":"function","z":"18a1c78c.638ef8","name":"Build XYE Query","func":"// CORRECT XYE Protocol with FIXED CRC\n// CRC calculated from bytes 1-13 only (excluding preamble, CRC byte itself, and suffix)\n\nconst DEBUG_ENABLED = false; // Change to true for detailed hex output\n\nconst PREAMBLE = 0xAA;\nconst SUFFIX = 0x55;\nconst COMMAND = msg.commandCode || 0xC0;\n\nconst DEVICE_ID = 0x00; // Device ID (0x00 for first unit)\nconst MASTER_ID = 0x80; // Master ID\n\nconst frame = [\n PREAMBLE, // Byte 0: 0xAA (NOT in CRC)\n COMMAND, // Byte 1: Command (IN CRC)\n DEVICE_ID, // Byte 2: Destination (IN CRC)\n MASTER_ID, // Byte 3: Source (IN CRC)\n 0x80, // Byte 4: From master flag (IN CRC)\n MASTER_ID, // Byte 5: Source repeated (IN CRC)\n // Payload - 7 bytes, all zeros for query/lock/unlock (IN CRC)\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n 0, // Byte 13: Command check (IN CRC)\n 0, // Byte 14: CRC placeholder (NOT in CRC)\n SUFFIX // Byte 15: 0x55 (NOT in CRC)\n];\n\n// Calculate command check: 255 - command code\nframe[13] = (255 - COMMAND) & 0xFF;\n\n// Calculate CRC: ONLY bytes 1-13 (excluding preamble, CRC itself, and suffix)\n// Formula: 255 - sum(bytes 1-13) % 256 + 1\nlet sum = 0;\nfor (let i = 1; i <= 13; i++) { // Only bytes 1-13\n sum += frame[i];\n}\nframe[14] = (255 - (sum % 256) + 1) & 0xFF;\n\nmsg.payload = Buffer.from(frame);\n\n// Format output like Parse function\nconst labels = {\n 0: \"preamble\",\n 1: \"command (C0:Query, C3:Set, CC:Lock, CD:Unlock)\",\n 2: \"destination, device id\",\n 3: \"source, master id\",\n 4: \"from master flag\",\n 5: \"source repeated, master id\",\n 6: \"payload byte 0, mode for Set\",\n 7: \"payload byte 1, fan for Set\",\n 8: \"payload byte 2, setTemp for Set\",\n 9: \"payload byte 3, flags for Set\",\n 10: \"payload byte 4, timer start for Set\",\n 11: \"payload byte 5, timer stop for Set\",\n 12: \"payload byte 6, reserved\",\n 13: \"command check, 255 - cmd\",\n 14: \"CRC\",\n 15: \"suffix\"\n};\n\nconst formattedHex = Array.from(frame).map((b, i) => {\n const hex = '0x' + b.toString(16).padStart(2, '0');\n const label = labels[i] || `byte_${i}`;\n const paddedI = i.toString().padStart(2);\n return `${paddedI} ${hex} (${label})`;\n}).join('\\n');\n\nif (DEBUG_ENABLED) node.warn(`${msg.commandName} - 16-BYTE QUERY:\\n${formattedHex}`);\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":250,"y":400,"wires":[["863b7fe1.d6b34"]]},{"id":"863b7fe1.d6b34","type":"tcp request","z":"18a1c78c.638ef8","server":"192.168.12.89","port":"1024","out":"time","splitc":" ","name":"Hexin XYE","x":370,"y":340,"wires":[["ca2c2dee.712d88"]]},{"id":"ca2c2dee.712d88","type":"function","z":"18a1c78c.638ef8","name":"Parse XYE Response","func":"// Parse XYE Response - 40MUAA Specific Decoding\n// Updated with discovered protocol mappings\n\n// ============================================================\n// DEBUG CONTROL - Set to false to suppress detailed output\n// ============================================================\nconst DEBUG_ENABLED = false; // Change to true for detailed hex output\n\nif (!msg.payload || msg.payload.length !== 32) {\n node.error(`Invalid response length: ${msg.payload ? msg.payload.length : 0}`);\n return null;\n}\n\nconst buffer = msg.payload;\n\n// Verify CRC - ONLY bytes 1-29 (excluding preamble, CRC itself, and suffix)\nlet sum = 0;\nfor (let i = 1; i <= 29; i++) {\n sum += buffer[i];\n}\nconst calculatedCRC = (255 - (sum % 256) + 1) & 0xFF;\nconst receivedCRC = buffer[30];\nconst crcValid = (receivedCRC === calculatedCRC);\n\n// ALWAYS show CRC errors (critical alert)\nif (!crcValid) {\n node.error(`⚠️ CRC INVALID: received=0x${receivedCRC.toString(16)} calculated=0x${calculatedCRC.toString(16)}`);\n}\n\n// ============================================================\n// DECODING FUNCTIONS - 40MUAA Specific\n// ============================================================\n\n// Operating Mode (Byte 8) - 40MUAA uses different encoding than standard XYE\nfunction getOperMode(byte8) {\n const modes = {\n 0x00: 'off',\n 0x80: 'aux', // Auxiliary/Emergency heat only\n 0x81: 'fan_only',\n 0x82: 'dry',\n 0x84: 'heat',\n 0x88: 'cool',\n 0x91: 'auto'\n };\n return modes[byte8] || `unknown_0x${byte8.toString(16)}`;\n}\n\n// Fan Speed (Byte 9) - MODE DEPENDENT!\nfunction getFanSpeed(byte9, operMode) {\n // OFF mode - fan is off\n if (operMode === 'off') {\n return 'off';\n }\n \n // HEAT/AUX modes\n if (operMode === 'heat' || operMode === 'heat_aux') {\n if (byte9 === 0x80) return 'auto'; // Idle/starting\n if (byte9 === 0x81) return 'auto'; // Active/high demand\n if (byte9 === 0x82) return 'auto'; // Steady state/cruising\n if (byte9 === 0x00) return 'high';\n if (byte9 === 0x01) return 'high';\n if (byte9 === 0x02) return 'mid';\n if (byte9 === 0x04) return 'low';\n }\n \n // COOL/DRY/FAN/AUTO modes\n if (byte9 === 0x84) return 'auto';\n if (byte9 === 0x81) return 'auto';\n if (byte9 === 0x82) return 'auto';\n if (byte9 === 0x01) return 'high';\n if (byte9 === 0x02) return 'mid';\n if (byte9 === 0x04) return 'low';\n \n return `unknown_0x${byte9.toString(16)}`;\n}\n\n// Set Temperature (Byte 10) - FAHRENHEIT on 40MUAA\nfunction getSetTemp(byte10) {\n return {\n fahrenheit: byte10,\n celsius: Math.round(((byte10 - 32) * 5 / 9) * 10) / 10\n };\n}\n\n// Sensor Temperatures (Bytes 11-14) - XYE formula\nfunction getSensorTemp(byteValue) {\n if (byteValue === 0xFF) {\n return null; // No data\n }\n return Math.round(((byteValue - 0x30) / 2.0) * 10) / 10;\n}\n\n// Aux Heat Active (Byte 20, Bit 1)\nfunction hasAuxHeat(byte20) {\n return (byte20 & 0x02) !== 0;\n}\n\n// Water Pump (Byte 21, Bit 2)\nfunction hasWaterPump(byte21) {\n return (byte21 & 0x04) !== 0;\n}\n\n// Unit Locked (Byte 21, Bit 7)\nfunction isLocked(byte21) {\n return (byte21 & 0x80) !== 0;\n}\n\n// ============================================================\n// PARSE RESPONSE\n// ============================================================\n\nconst operMode = getOperMode(buffer[8]);\nconst fanSpeed = getFanSpeed(buffer[9], operMode);\nconst setTemp = getSetTemp(buffer[10]);\n\nconst data = {\n // Frame info\n commandSent: msg.commandName || 'Query',\n crcValid: crcValid,\n responseCode: buffer[1],\n deviceId: buffer[4],\n \n // Capabilities (constant for this unit)\n capabilities1: buffer[6],\n capabilities2: buffer[7],\n \n // Operating state\n operMode: operMode,\n operModeRaw: buffer[8],\n fanSpeed: fanSpeed,\n fanSpeedRaw: buffer[9],\n \n // Temperatures\n setTempF: setTemp.fahrenheit,\n setTempC: setTemp.celsius,\n t1Temp: getSensorTemp(buffer[11]), // Outdoor (or duplicate)\n t1TempRaw: buffer[11], // Raw byte value\n t2aTemp: getSensorTemp(buffer[12]), // Indoor/Return Air ⭐ ACTUAL INDOOR\n t2aTempRaw: buffer[12], // Raw byte value\n t2bTemp: getSensorTemp(buffer[13]), // Coil temp (hot end/discharge)\n t2bTempRaw: buffer[13], // Raw byte value\n t3Temp: getSensorTemp(buffer[14]), // Outdoor (primary)\n t3TempRaw: buffer[14], // Raw byte value\n \n // Power\n current: buffer[15] === 0xFF ? null : buffer[15],\n unknown16: buffer[16],\n \n // Timers\n timerStart: buffer[17],\n timerStop: buffer[18],\n running: buffer[19],\n \n // Status flags\n auxHeatActive: hasAuxHeat(buffer[20]),\n modeFlags: buffer[20],\n waterPump: hasWaterPump(buffer[21]),\n locked: isLocked(buffer[21]),\n operFlags: buffer[21],\n \n // Errors/Protection\n error1: buffer[22],\n error2: buffer[23],\n protect1: buffer[24],\n protect2: buffer[25],\n ccmCommError: buffer[26],\n \n // Raw bytes for debugging\n rawOperMode: `0x${buffer[8].toString(16)}`,\n rawFanSpeed: `0x${buffer[9].toString(16)}`,\n rawModeFlags: `0x${buffer[20].toString(16)}`,\n rawOperFlags: `0x${buffer[21].toString(16)}`\n};\n\n// ============================================================\n// DEBUG OUTPUT - DETAILED HEX (only if DEBUG_ENABLED)\n// ============================================================\n\nif (DEBUG_ENABLED) {\n // Map labels based on XYE/Midea protocol documentation\n const labels = {\n 0: \"preamble\",\n 1: \"response code\",\n 2: \"to master\",\n 3: \"dest (master id)\",\n 4: \"source (device id)\",\n 5: \"dest repeat\",\n 6: \"cap1\",\n 7: \"cap2\",\n 8: \"OPER MODE\",\n 9: \"FAN SPEED\",\n 10: \"SET TEMP (°F)\",\n 11: \"T1 (outdoor/dup)\",\n 12: \"T2A (INDOOR ⭐)\",\n 13: \"T2B (coil hot)\",\n 14: \"T3 (outdoor)\",\n 15: \"CURRENT\",\n 16: \"unk16\",\n 17: \"timer_start\",\n 18: \"timer_stop\",\n 19: \"running\",\n 20: \"MODE FLAGS\",\n 21: \"OPER FLAGS\",\n 22: \"err1\",\n 23: \"err2\",\n 24: \"prot1\",\n 25: \"prot2\",\n 26: \"ccm_err\",\n 27: \"unk27\",\n 28: \"unk28\",\n 29: \"unk29\",\n 30: \"CRC\",\n 31: \"suffix\"\n };\n\n const detailedHex = Array.from(buffer).map((b, i) => {\n const hex = '0x' + b.toString(16).padStart(2, '0');\n const label = labels[i];\n const paddedI = i.toString().padStart(2);\n // Highlight key bytes\n const important = [8, 9, 10, 15, 20, 21].includes(i) ? \"***\" : \" \";\n return `${paddedI} ${hex} ${important} ${label}`;\n }).join('\\n');\n\n node.warn(`RAW 32-BYTE RESPONSE:\\n${detailedHex}`);\n\n // Summary line\n node.warn(`MODE: ${data.operMode} | FAN: ${data.fanSpeed} | SETPOINT: ${data.setTempF}°F | AUX: ${data.auxHeatActive ? 'ON' : 'OFF'}`);\n\n if (data.t1Temp !== null) {\n node.warn(`TEMPS: Indoor=${data.t2aTemp}°C Outdoor=${data.t3Temp}°C Coil=${data.t2bTemp}°C`);\n }\n\n if (data.current !== null) {\n node.warn(`CURRENT: ${data.current}A`);\n }\n}\n\n// ALWAYS show communication errors (alerts)\nif (data.ccmCommError > 0) {\n node.error(`⚠️ CCM Communication Error: ${data.ccmCommError}`);\n}\n\n// ALWAYS show system errors (alerts)\nif (data.error1 !== 0 || data.error2 !== 0) {\n node.error(`⚠️ System Error: E1=0x${data.error1.toString(16)} E2=0x${data.error2.toString(16)}`);\n}\n\n// ALWAYS show protection codes (alerts)\n/*if (data.protect1 !== 0 || data.protect2 !== 0) {\n node.error(`⚠️ Protection Active: P1=0x${data.protect1.toString(16)} P2=0x${data.protect2.toString(16)}`);\n}*/\n\n// ============================================================\n// OUTPUT\n// ============================================================\n\nmsg.xye = data;\nmsg.payload = data;\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":500,"y":400,"wires":[["91449075.d62c6"]]},{"id":"91449075.d62c6","type":"function","z":"18a1c78c.638ef8","name":"Publish to MQTT","func":"// Publish to MQTT - 40MUAA Specific Topics\n// Includes ALL bytes 6-26 for comprehensive monitoring\n// Removed: current (not reported), outdoor temp (not available), t1 (duplicate/unavailable)\n\nconst data = msg.xye;\nconst baseTopic = 'farm/house/hvac/upstairs/hp';\nconst messages = [];\n\n// Only publish if we have valid data\nif (!data || !data.crcValid) {\n node.warn('Skipping MQTT publish - invalid or missing data');\n return null;\n}\n\n// Calculate effective mode\nlet effectiveMode = data.operMode;\nif (data.operMode === 'heat' && data.auxHeatActive) {\n effectiveMode = 'heat_with_aux'; // or 'aux_heat' or whatever you prefer\n}\n\n// Set aux_heat_mode true when it's in Aux only mode,\n// for some reason it only flags aux mode when it's \"Aux & Heat\"\nlet auxHeatMode = data.auxHeatActive;\nif (effectiveMode === 'aux') {\n auxHeatMode = 'true';\n}\n\n// Create individual MQTT messages\nconst topics = {\n // Operating state\n 'state/mode': effectiveMode, //data.operMode,\n 'state/fan': data.fanSpeed,\n 'state/setpoint_f': data.setTempF,\n 'state/setpoint_c': data.setTempC,\n \n // Temperature sensors (all sensors with scaled values)\n 'sensor/t1_temp': data.t1Temp !== null ? data.t1Temp : 'unavailable', // T1 - outdoor duplicate\n 'sensor/t2a_temp': data.t2aTemp !== null ? data.t2aTemp : 'unavailable', // T2A - Indoor/Return Air\n 'sensor/t2b_temp': data.t2bTemp !== null ? data.t2bTemp : 'unavailable', // T2B - Coil hot end\n 'sensor/t3_temp': data.t3Temp !== null ? data.t3Temp : 'unavailable', // T3 - Outdoor primary\n \n // Status flags\n 'state/aux_heat_active': auxHeatMode,\n 'state/water_pump': data.waterPump,\n 'state/locked': data.locked,\n \n // ALL RAW BYTES 6-26 (for comprehensive monitoring)\n 'raw/byte_06_cap1': data.capabilities1,\n 'raw/byte_07_cap2': data.capabilities2,\n 'raw/byte_08_oper_mode': data.operModeRaw,\n 'raw/byte_09_fan_speed': data.fanSpeedRaw,\n 'raw/byte_10_set_temp': data.setTempF,\n 'raw/byte_11_t1': data.t1TempRaw || 0,\n 'raw/byte_12_t2a': data.t2aTempRaw || 0,\n 'raw/byte_13_t2b': data.t2bTempRaw || 0,\n 'raw/byte_14_t3': data.t3TempRaw || 0,\n 'raw/byte_15_current': data.current !== null ? data.current : 255,\n 'raw/byte_16_unknown': data.unknown16 || 0,\n 'raw/byte_17_timer_start': data.timerStart,\n 'raw/byte_18_timer_stop': data.timerStop,\n 'raw/byte_19_running': data.running || 0,\n 'raw/byte_20_mode_flags': data.modeFlags,\n 'raw/byte_21_oper_flags': data.operFlags,\n 'raw/byte_22_error1': data.error1,\n 'raw/byte_23_error2': data.error2,\n 'raw/byte_24_protect1': data.protect1,\n 'raw/byte_25_protect2': data.protect2,\n 'raw/byte_26_ccm_error': data.ccmCommError,\n \n // Diagnostics\n 'debug/crc_valid': data.crcValid,\n 'debug/device_id': data.deviceId\n};\n\n// Create message for each topic\nfor (const [topic, value] of Object.entries(topics)) {\n messages.push({\n topic: `${baseTopic}/${topic}`,\n payload: value.toString(),\n retain: true\n });\n}\n\n// Add a JSON summary topic for easy Home Assistant integration\nmessages.push({\n topic: `${baseTopic}/state`,\n payload: JSON.stringify({\n mode: data.operMode,\n fan: data.fanSpeed,\n setpoint_f: data.setTempF,\n setpoint_c: data.setTempC,\n aux_heat: data.auxHeatActive,\n t1_temp: data.t1Temp, // Outdoor duplicate\n t2a_temp: data.t2aTemp, // Indoor/Return Air\n t2b_temp: data.t2bTemp, // Coil hot end\n t3_temp: data.t3Temp, // Outdoor primary\n available: true\n }),\n retain: true\n});\n\n// Add complete raw bytes as single JSON topic\nmessages.push({\n topic: `${baseTopic}/raw/all_bytes`,\n payload: JSON.stringify({\n byte_06: data.capabilities1,\n byte_07: data.capabilities2,\n byte_08: data.operModeRaw,\n byte_09: data.fanSpeedRaw,\n byte_10: data.setTempF,\n byte_11: data.t1TempRaw || 0,\n byte_12: data.t2aTempRaw || 0,\n byte_13: data.t2bTempRaw || 0,\n byte_14: data.t3TempRaw || 0,\n byte_15: data.current !== null ? data.current : 255,\n byte_16: data.unknown16 || 0,\n byte_17: data.timerStart,\n byte_18: data.timerStop,\n byte_19: data.running || 0,\n byte_20: data.modeFlags,\n byte_21: data.operFlags,\n byte_22: data.error1,\n byte_23: data.error2,\n byte_24: data.protect1,\n byte_25: data.protect2,\n byte_26: data.ccmCommError\n }),\n retain: true\n});\n\nnode.status({\n fill: data.operMode === 'off' ? 'grey' : 'green',\n shape: 'dot',\n text: `${data.operMode} ${data.setTempF}°F`\n});\n\n// Store current state in flow context (nested under common name)\nflow.set('upstairs_heat_pump', {\n mode: effectiveMode,\n fan: data.fanSpeed,\n setpoint_f: data.setTempF,\n setpoint_c: data.setTempC,\n aux_heat_active: auxHeatMode\n});\n\nreturn [messages];","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":630,"y":340,"wires":[["baa17f3d.f16888"]]},{"id":"baa17f3d.f16888","type":"mqtt out","z":"18a1c78c.638ef8","name":"MQTT Broker","topic":"","qos":"0","retain":"true","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"ad7b7ee9.73295","x":760,"y":400,"wires":[]},{"id":"31fd2279.416e06","type":"comment","z":"18a1c78c.638ef8","name":"XYE Interface with Indoor AHU","info":"","x":170,"y":160,"wires":[]},{"id":"17041e93.6ce761","type":"mqtt in","z":"18a1c78c.638ef8","name":"CMD Mode","topic":"farm/house/hvac/upstairs/hp/cmd/mode","qos":"1","datatype":"utf8","broker":"ad7b7ee9.73295","nl":false,"rap":true,"rh":0,"x":130,"y":200,"wires":[["35ff8cd0.4b92ac"]]},{"id":"9d62dbf3.31ca8","type":"mqtt in","z":"18a1c78c.638ef8","name":"CMD Fan","topic":"farm/house/hvac/upstairs/hp/cmd/fan","qos":"1","datatype":"utf8","broker":"ad7b7ee9.73295","nl":false,"rap":true,"rh":0,"x":120,"y":240,"wires":[["35ff8cd0.4b92ac"]]},{"id":"3ca3128e.4b2b06","type":"mqtt in","z":"18a1c78c.638ef8","name":"CMD Setpoint °C","topic":"farm/house/hvac/upstairs/hp/cmd/setpoint_c","qos":"1","datatype":"auto","broker":"ad7b7ee9.73295","nl":false,"rap":true,"rh":0,"x":140,"y":280,"wires":[["35ff8cd0.4b92ac"]]},{"id":"35ff8cd0.4b92ac","type":"function","z":"18a1c78c.638ef8","name":"Merge & Validate Command","func":"// Get current state\nconst currentState = flow.get('upstairs_heat_pump') || {\n mode: 'off',\n fan: 'auto',\n setpoint_f: 73,\n setpoint_c: 22.8\n};\n\n// Parse incoming command based on topic\nconst topic = msg.topic;\nconst payload = msg.payload;\n\nlet desiredState = Object.assign({}, currentState);\n\nif (topic.includes('/cmd/mode')) {\n // Validate mode\n const validModes = ['off', 'heat', 'heat_with_aux', 'aux', 'cool', 'fan_only', 'dry', 'auto'];\n if (!validModes.includes(payload)) {\n node.error('Invalid mode: ' + payload);\n return null;\n }\n desiredState.mode = payload;\n \n} else if (topic.includes('/cmd/fan')) {\n // Validate fan\n const validFans = ['auto', 'low', 'mid', 'high'];\n if (!validFans.includes(payload)) {\n node.error('Invalid fan: ' + payload);\n return null;\n }\n desiredState.fan = payload;\n \n} else if (topic.includes('/cmd/setpoint_c')) {\n // Convert Celsius to Fahrenheit (round to nearest degree F)\n const tempC = parseFloat(payload);\n if (isNaN(tempC) || tempC < 10 || tempC > 35) {\n node.error('Invalid temperature: ' + payload + '°C');\n return null;\n }\n const tempF = Math.round(tempC * 9 / 5 + 32);\n desiredState.setpoint_c = tempC;\n desiredState.setpoint_f = tempF;\n}\n\nmsg.desiredState = desiredState;\nmsg.commandName = 'Set 0xC3';\n\nnode.status({\n fill: 'blue',\n shape: 'dot',\n text: desiredState.mode + ' ' + desiredState.fan + ' ' + desiredState.setpoint_f + '°F'\n});\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":360,"y":220,"wires":[["cba6275b.a3aae8"]]},{"id":"cba6275b.a3aae8","type":"function","z":"18a1c78c.638ef8","name":"Build Set 0xC3 Command","func":"// Build XYE Set Command (0xC3)\nconst desired = msg.desiredState;\n\nconst DEBUG_ENABLED = false;\n\nconst PREAMBLE = 0xAA;\nconst SUFFIX = 0x55;\nconst COMMAND = 0xC3;\nconst DEVICE_ID = 0x00;\nconst MASTER_ID = 0x80;\n\n// ===== ENCODE MODE (Byte 6) =====\nfunction encodeMode(mode) {\n const modes = {\n 'off': 0x00,\n 'aux': 0x80,\n 'heat_with_aux': 0x80,\n 'fan_only': 0x81,\n 'dry': 0x82,\n 'heat': 0x84,\n 'cool': 0x88,\n 'auto': 0x91\n };\n return modes[mode] || 0x00;\n}\n\n// ===== ENCODE FAN (Byte 7) - MODE DEPENDENT! =====\nfunction encodeFan(fan, mode) {\n // For heat/aux modes\n if (mode === 'heat' || mode === 'heat_with_aux' || mode === 'aux') {\n if (fan === 'auto') return 0x80;\n if (fan === 'low') return 0x04;\n if (fan === 'mid') return 0x02;\n if (fan === 'high') return 0x01;\n }\n \n // For cool/dry/fan/auto modes\n if (fan === 'auto') return 0x84;\n if (fan === 'low') return 0x04;\n if (fan === 'mid') return 0x02;\n if (fan === 'high') return 0x01;\n \n return 0x84;\n}\n\nconst modeByte = encodeMode(desired.mode);\nconst fanByte = encodeFan(desired.fan, desired.mode);\nconst tempByte = desired.setpoint_f;\n\nconst frame = [\n PREAMBLE,\n COMMAND,\n DEVICE_ID,\n MASTER_ID,\n 0x80,\n MASTER_ID,\n modeByte,\n fanByte,\n tempByte,\n 0x00,\n 0x00,\n 0x00,\n 0x00,\n 0,\n 0,\n SUFFIX\n];\n\n// Calculate command check: 255 - command code\nframe[13] = (255 - COMMAND) & 0xFF;\n\n// Calculate CRC: bytes 1-13 only\nlet sum = 0;\nfor (let i = 1; i <= 13; i++) {\n sum += frame[i];\n}\nframe[14] = (255 - (sum % 256) + 1) & 0xFF;\n\nmsg.payload = Buffer.from(frame);\n\nif (DEBUG_ENABLED) {\n const hex = [];\n for (let i = 0; i < frame.length; i++) {\n hex.push('0x' + frame[i].toString(16).padStart(2, '0'));\n }\n node.warn('SET CMD: Mode=' + desired.mode + '(0x' + modeByte.toString(16) + ') Fan=' + desired.fan + '(0x' + fanByte.toString(16) + ') Temp=' + tempByte + '°F');\n node.warn('FRAME: ' + hex.join(' '));\n}\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":410,"y":280,"wires":[["863b7fe1.d6b34"]]},{"id":"ad7b7ee9.73295","type":"mqtt-broker","name":"local mosquitto","broker":"192.168.12.253","port":"1883","clientid":"node-red-local","usetls":false,"compatmode":true,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""}]
I'm not sure I completely "believe" the reported T1, T2A, T2B & T3 temperatures. I wonder if the raw conversion is wrong as none of the reported temps match any other sensors I have installed in the air return, plenum and outdoors.