HDMI CEC message translator
Flow to translate HDMI CEC messages received via MQTT. Check out the demo here: https://www.facebook.com/61550246612741/videos/986448103239859/?app=fbl and here: https://youtu.be/hJY-innhytc
[{"id":"d8a9416f72d5d03f","type":"tab","label":"HDMI CEC","disabled":false,"info":"","env":[]},{"id":"e060dae6ccd1eeeb","type":"mqtt in","z":"d8a9416f72d5d03f","name":"","topic":"cec_messages","qos":"2","datatype":"auto","broker":"","nl":false,"rap":false,"inputs":0,"x":120,"y":80,"wires":[["8c3fa7a850836170","26cda5d2fbbf16bc"]]},{"id":"8c3fa7a850836170","type":"function","z":"d8a9416f72d5d03f","name":"HDMI CEC translation","func":"// Translation of device numbers (0-15) to devices\nfunction getDeviceName(nibble) {\n switch (nibble) {\n case \"0\":\n return \"TV\"; // \"TV\"\n case \"1\":\n return \"Recorder 1\"; // \"Recorder 1\"\n case \"2\":\n return \"Recorder 2\"; // \"Recorder 2\"\n case \"3\":\n return \"Tuner 1\"; // \"Tuner 1\"\n case \"4\":\n return \"Playback 1\"; // \"Playback 1\"\n case \"5\":\n return \"Audio system\"; // \"Audio system\"\n case \"6\":\n return \"Tuner 2\"; // \"Tuner 2\"\n case \"7\":\n return \"Tuner 3\"; // \"Tuner 3\"\n case \"8\":\n return \"Playback 2\"; // \"Playback 2\"\n case \"9\":\n return \"Recorder 3\"; // \"Recorder 3\"\n case \"A\":\n return \"Tuner 4\"; // \"Tuner 4\"\n case \"B\":\n return \"Playback 3\"; // \"Playback 3\"\n case \"C\":\n return \"Reserved 1\"; // \"Reserved 1\"\n case \"D\":\n return \"Reserved 2\"; // \"Reserved 2\"\n case \"E\":\n return \"Free Use\"; // \"Free Use\"\n case \"F\":\n return \"Broadcast\"; // \"Broadcast\"\n default:\n return \"Unknown device\"; // \"Unknown device\"\n }\n}\n\n// Translation of the first byte (sender and receiver devices)\nfunction parseDeviceAndMessageType(byte) {\n const senderNibble = byte[0]; // First nibble is the sender\n const receiverNibble = byte[1]; // Second nibble is the receiver\n\n const senderDevice = getDeviceName(senderNibble);\n const receiverDevice = getDeviceName(receiverNibble);\n\n let messageType = '';\n\n // Identify message type based on the receiver nibble\n if (receiverNibble === \"F\") {\n messageType = \"broadcasting:\";\n } else {\n messageType = `to ${receiverDevice}:`;\n }\n\n return `${senderDevice} ${messageType}`;\n}\n\n// Function to format a 4-digit number with periods\nconst formatWithPeriods = number => number.toString().split('').join('.');\n\n// Translation of actions (second and third byte)\nfunction parseAction(bytes) {\n let action = '';\n const byte1 = bytes[0];\n const byte2 = bytes[1] || null;\n const byte3 = bytes[2] || null;\n\n // Format byte2 and byte3\n const formattedByte2And3 = formatWithPeriods(byte2 + byte3);\n \n // Convert byte2 to decimal, subtract 2, and use the updated value\n const volume = parseInt(byte2, 16) - 2;\n \n switch (byte1) {\n case \"0D\":\n action = \"I am the active source\";\n break; \n case \"04\":\n action = \"I am the active source\";\n break; \n case \"A0\":\n action = \"Routing information\";\n break; \n case \"1A\":\n action = \"My state is\";\n if (byte2 === \"02\") {\n action += \" (Off)\";\n } else if (byte2 === \"01\") {\n action += \" (On)\";\n }\n break; \n case \"7A\":\n action = `Audio volume (${volume})`;\n break;\n case \"7D\":\n action = \"Request audio mode status\";\n break;\n case \"7E\":\n action = `Audio mode`;\n if (byte2 === \"00\") {\n action += \" (Off)\";\n } else if (byte2 === \"01\") {\n action += \" (On)\";\n }\n break;\n case \"8C\":\n action = \"Requesting vendor ID\";\n break;\n case \"8E\":\n action = \"CEC routing control command\";\n break;\n case \"8F\":\n action = \"Requesting power state\";\n break;\n case \"9D\":\n action = `Inactive source with physical address (${formattedByte2And3})`;\n break;\n case \"9E\":\n action = 'Reporting CEC version (not translated)';\n break;\n case \"9F\":\n action = 'Requesting CEC version';\n break;\n case \"36\":\n action = \"Standby\";\n break;\n case \"44\":\n action = \"Button pressed\";\n if (byte2 === \"00\") {\n action += \" (Select)\";\n } else if (byte2 === \"01\") {\n action += \" (Up)\";\n } else if (byte2 === \"02\") {\n action += \" (Down)\";\n } else if (byte2 === \"03\") {\n action += \" (Left)\";\n } else if (byte2 === \"04\") {\n action += \" (Right)\";\n } else if (byte2 === \"41\") {\n action += \" (Volume up)\";\n } else if (byte2 === \"42\") {\n action += \" (Volume down)\";\n } else if (byte2 === \"46\") {\n action += \" (Play/Pause)\";\n } else {\n action += ` (Unknown control: ${byte2}${byte3})`;\n }\n break;\n case \"45\":\n action = \"Button released\";\n break;\n case \"47\":\n action = \"Set OSD display name (not translated)\";\n break;\n case \"70\":\n action = `Set active audio source (${formattedByte2And3})`;\n break;\n case \"71\":\n action = \"Requesting system audio mode\";\n break;\n case \"72\":\n action = \"Set system audio mode\";\n if (byte2 === \"00\") {\n action += \" (Off)\";\n } else if (byte2 === \"01\") {\n action += \" (On)\";\n } else {\n action += ` (Unknown value: ${byte2})`;\n }\n break;\n case \"82\":\n action = `Set active source (${formattedByte2And3})`;\n break;\n case \"83\":\n action = \"What is your physical address?\";\n break;\n case \"84\":\n action = `My physical address is (${formattedByte2And3})`;\n break;\n case \"85\":\n action = `Requesting active source`;\n break;\n case \"87\":\n action = `Reporting device vendor ID (not translated)`;\n break;\n case \"89\":\n action = `Vendor command (not translated)`;\n break;\n case \"90\":\n action = \"Reporting power status\";\n if (byte2 === \"00\") {\n action += \" (Power On)\";\n } else if (byte2 === \"01\") {\n action += \" (Standby)\";\n } else if (byte2 === \"02\") {\n action += \" (In transition from Standby to On)\";\n } else if (byte2 === \"03\") {\n action += \" (Power Off)\";\n }\n break;\n default:\n action = `Unknown action (${byte1})`;\n }\n return action;\n}\n\n// Dynamic translation function for HDMI CEC commands\nfunction translateHdmiCecCommand(command) {\n const parts = command.split(\":\").map(part => part.toUpperCase());\n\n if (parts.length < 2) {\n return \"Invalid command\";\n }\n\n const deviceAndMessageType = parseDeviceAndMessageType(parts[0]);\n const action = parseAction(parts.slice(1)); // Send the remaining bytes to parseAction\n\n return `${deviceAndMessageType} ${action}`;\n}\n\n// Receive input from msg.payload\nlet command = msg.payload;\n\n// Call the translation function\nmsg.payload = translateHdmiCecCommand(command);\n\nmsg.raw = command;\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":220,"y":150,"wires":[["7aec172e58613f8c"]]},{"id":"50e372ca8991d0cc","type":"debug","z":"d8a9416f72d5d03f","name":"translated HDMI CEC message","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":710,"y":150,"wires":[]},{"id":"7aec172e58613f8c","type":"change","z":"d8a9416f72d5d03f","name":"Custom device names","rules":[{"t":"change","p":"payload","pt":"msg","from":"Playback 1","fromt":"str","to":"Apple TV","tot":"str"},{"t":"change","p":"payload","pt":"msg","from":"Playback 2","fromt":"str","to":"PlayStation","tot":"str"},{"t":"change","p":"payload","pt":"msg","from":"Audio system","fromt":"str","to":"Denon AVR","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":450,"y":150,"wires":[["50e372ca8991d0cc"]]},{"id":"26cda5d2fbbf16bc","type":"debug","z":"d8a9416f72d5d03f","name":"raw HDMI CEC message","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":370,"y":80,"wires":[]}]