Broan NuTone ERVs have a nice little RS-485 port they used to communicate with their various head units.
The protocol is, of course, wholly undocumented and not any obvious standard, so lets see if we can figure out what’s going on, with the goal of adding this device to Home Assistant in some kind of sane way (eg, not the currently accepted “Buy a switchbot and measure state by how many watts it’s pulling” method.
This is going to be a live document cataloging my effort to reverse engineer this protocol and gain control over it. Packet data is a mix of my own captures and data from whiskthecat’s extremely helpful captures.
Note that this is a work in progress and data here may be incomplete, misleading, or flat out wrong. This page may be updated in the future as I learn more.
# Packet Capture
Initialization
01 01 10 01 05 02 50 69 6e 67 59 04
This is the first thing we read from the data stream, and it contains an ascii string (“Ping”) which is handy, and gives us some clues into how to packetize the next messages:
01 11 10 01 01 04 d9 04
01 10 11 01 01 05 d8 04
These two messages only occur if a controller is connected, so we can presume the first of the two is sent by the controller and the second is the ACK. And indeed if we send this packet on the line we get the second line back from the ERV, though this isn’t enough to get it to send us more data yet.
After a bit of this repeating pattern we get our first new packets, likely again sent from the controller
01 10 11 01 04 40 00 50 00 4a 04
01 11 10 01 03 41 00 50 4a 04
At this point some patterns start to emerge and we can guess a high level parsing structure:
- 1 byte: Always 0x01
- 2 bytes: To/from. The ERV always seems to be 10, and our capture device is 11, though it seems other IDs are allowed up to 32
- 1 byte: Always 01
- 1 byte: Payload length. This looks consistent across messages, and accounts for the remaining message size minus two bytes.
- 1 byte: Checksum. This is the sum of all bytes prior to the checksum (eg, starting from the 0x01), minus one, subtracted from 0. eg, ( 0 - ( ( 01 + 10 + 11 + 01 + 04 + 40 + 0 + 50 + 0 ) - 1 ) -> 0x4a (Mask off the lower 8 bits obviously). Note this relies on overflow, so prepare accordingly.
- 1 byte: Always 0x04
There are still a lot of questions here, but the upshot is we have enough to start parsing messages.
# Messages
Now that we know how to read the data from the packets, lets look at what actual messages we have are. For this section, we’re peeling away the header/footer and only showing the actual data.
** Initialization **
02 P I N G
- Sent by the ERV when no remotes are connected.03 P I N G
- Response from the controller.
Note that this section is special, it only runs at startup, and ignores flow control rules. Each device is given a small window to 03
back, else it’s skipped. Pings continue until at least one device responds. It’s unclear if multiple devices can coexist yet, but the protocol implies up to 32 devices are allowed.
** Flow control **
04
- Flow control offer05
- ACK
this one’s kind of tricky and took a bit to fully work out. Initially I thought this was a heartbeat, but in actually it’s an offer to send data.
Essentially, the ERV sends 04, and the controller has a small window to reply with 05. If it does so, it has flow control, and can send messages. Each message will illicit a response, and this continues until the remote is out of things to send. At this point, it sends it’s own 04 back, and the controller responds with 05 indicating it now has flow control.
So, a typical sequence would look like this:
ERV: 04 - You may send
Con: 05 - I’m in control now
Con: 40 - Request
ERV: 41 - Response
Con: 20 - Request
ERV: 21 - Response
Con: 04 - Ok I’m done!
ERV: 05 - I’m in control now
** Register read **
20 02 20 0c 21 0a 22 ...
21 02 20 01 00 0c 21 04 00 00 43 21 ...
This is the request and response for reading data from the ERV.
In the request, we see 2 byte pairs that mirror what we see in the response, (eg, 02 20
) but the response additionally contains a 2 or 5 bytes after (length + data). This makes it easy to understand what’s going on: the request is a list of registers, and the response is the data for said registers.
** Register write **
40 00 20 01 09
41 00 20
This is the same structure as the read command, but in reverse. It’s not clear which registers are writable, so we’ll need to figure that out. That said, it’s good that we get an ACK so we know if our request was received and processed (Unclear if it will ACK if we try to write a read only register, need to experiment).
# Registers
The big question is: What do these registers do.
Known so far:
14 00
appears to be uptime, in seconds. No idea why this is useful.02 20
appears to be the ERV’s mode, with0A
being MAX,09
being MIN and01
being STB. Additionally,0B
may be used for variable speed mode (used with the0822
and0622
registers)08 22
and06 22
seem to be be set to the same value together, and indicate the speed percentage (Observed in the data dumps). 0% is 32 and 100% is 175, the controller has set increments but it likely accepts arbitrary floats here given a bunch of the middle values are numbers like 53.4500007629395. It would be good to understand the difference between these two registers.
# Project status
I have a github repo with a proof of concept ESPHome component. It currently only supports fan mode and speed, other features will require more captures to determine which registers we need to read or write.
https://github.com/nspitko/broan_erv_uart/
Feel free to open an issue with your findings!