CEC is actually a very simple protocol. Each packet is at least two bytes, the first nibble: an integer between 0 and 16 representing the sender, the second an integer between 0 and 16 representing the receiver, followed by a byte-long opcode. Some opcodes allow additional parameters.
All devices talk in “party line” mode, meaning everyone hears every message (there is a maximum of 16 devices, so routing and partitioning is overkill). It also means every device knows what is going on all the time.
The protocol allows you to find out all sorts of interesting information: is a device turned in? What device is currently active?
You can also ask devices to do stuff: turn up the volume, schedule a recording, or switch inputs.
The problem is: no body seems to implement the spec completely. And many manufacturers don’t do it correctly.
For example: my Yamaha receiver (a RX-V347) supports the user control opcode (0x44) and the change AV input opcode (0x69) which is supposed to take another parameter to denote the input you wish to choose. If the input entered is 0, then the next input is selected. This receiver only accepts 0 as a input code, which makes selecting a specific input (without knowledge of what the current input is) impossible.
There was a fun work around for this though; my RaspberryPI is connected to a HDMI input in my receiver, which I know the number of. By working out the distance each input number is from that input, I can select the RaspberryPi as the active input then send a change AV input a number of times until the right one is selected. Do it fast enough and no one would notice.
Why don’t I just interrogate the amp and ask it what input is currently selected? I could, but there is only facilities to find out what HDMI input is active. If the AV1 input is active (which has my Sonos attached) I’m out of luck.
Of course, none of this stuff is documented any where so there is a lot of trial and error going on to work out what opcodes each device handles, and whether they handle it correctly.