Navien, ESP32 Navilink interface

@mbcomer Yes, I guess this should be considered the new home for the the Navien native connectivity. If you have experience writing ESPHome components that’s awesome if you are willing to tackle that. If you choose to go that route, I might suggest checking out the ESPHome-Econet on GitHub. It seems like they have worked out quite a bit of this for the econet devices. I thought about trying to fork that, and copy/paste my way along, but it was a bit over my head. As for this protocol - I think it’s pretty close, but there are quite a few bytes that aren’t being used. After looking at the econet information, it made me wonder if there was additional information in those bytes that we weren’t using/accessing. Maybe understanding what is in the Navilink device for information that we haven’t already captured may help sort out anything that we are missing. Is that something you can help with?

As for the bytes that we have decoded so far, I’m pretty confident in the “structure”, but maybe not my calculations on the BTU, Therms, and kCal entities :wink:. Each of them change in an order of magnitude and behavior that I’d expect, but not sure I have calculated them right.

I have been successful in turning the unit on/off using my templated switch, however I have yet to get the setpoint to change, so I have currently removed that from my most recent config.

See screenshots:

As for the connector. I took some measurements using an inch based ruler it’s in 16th’s of an inch (sorry, it’s all I have :smile:) for the connector, which led me to ordering a “kit” of JST-XH connectors that should be here Saturday. Maybe this, or the measurements below could help keep @aruffell out of his warm attic! Anyway - I’m not positive, but I think this may be the connector that would work. It looks like the only difference is that it does not have the locking mechanism like on the original Navien cable. I’d have to think that wouldn’t be too much of an issue, as there’s really limited/no vibration in the unit when it’s running.

Measurements on connector:

image

image

Most Recent Config:

  substitutions:
  comment: "redacted"
  friendly_name: Navien Water Heater
  logger_level: WARN


# Include basic info in all ESP Home Configs
# Basic Info Includes
# logger:
# wifi:
# ap:
# web_server:
# captive_portal:
# button: to restart the device

<<: !include .base1.yaml

binary_sensor:
  - platform: status
    name: Status
    internal: True

esphome:
  name: navien
  friendly_name: Navien Water Heater

esp32:
  board: esp32dev
  framework:
    type: arduino

# Enable logging
logger:
  level: ${logger_level}
  baud_rate: 0 #disable logging over uart


# Enable Home Assistant API
api:
  encryption:
    key: "redacted"

ota:
  password: "redacted"

globals:
   - id: init_set_temp
     type: int
     restore_value: yes
     initial_value: '140'


#################################
#### UART SETUP/DECODE BYTES ####
#################################

uart:
  id: uart_bus
  tx_pin: 1
  rx_pin: 3
  baud_rate: 19200
  data_bits: 8
  stop_bits: 1
  parity: NONE
  debug:
    direction: BOTH
    dummy_receiver: True
    after:
      delimiter: "\n"
    sequence:
      - lambda: |-
          //
          // SPECIAL THANKS TO @suva on the HomeAssistant forum for Decoding these.
          // https://community.home-assistant.io/t/navien-hot-water-heater-navilink/330044/92
          //  
          UARTDebug::log_string(direction, bytes);
          if (bytes[0] == 247 && bytes[1] == 5 && bytes[2] == 80 && bytes[3] == 80 && bytes[4] == 144 && bytes[5] == 34) {
          // Water information
          id(uart_water_byte0).publish_state(bytes[0]); //Common To all Packets
          id(uart_water_byte1).publish_state(bytes[1]); //Common To all Packets
          id(uart_water_byte2).publish_state(bytes[2]); //Packet id Byte 0
          id(uart_water_byte3).publish_state(bytes[3]); //Packet id Byte 1
          id(uart_water_byte4).publish_state(bytes[4]); //Packet id Byte 2
          id(uart_water_byte5).publish_state(bytes[5]); //Data Length
          id(uart_water_byte6).publish_state(bytes[6]); 
          id(uart_water_byte7).publish_state(bytes[7]);
          id(uart_water_byte8).publish_state(bytes[8]);
          id(uart_water_byte9).publish_state(bytes[9]); // System Power: high nibble: Unknown (values 0 and 0x20 observed); low nibble = 0=off 0x5=on
          id(uart_water_byte10).publish_state(bytes[10]);
          id(set_temp_read).publish_state((bytes[11]/2)); //Set temp - measured in 0.5 degrees C.
          id(outlet_temp).publish_state((bytes[12]/2));  //Outlet temp - measured in 0.5 degrees C.
          id(inlet_temp).publish_state((bytes[13]/2)); //Inlet temp - measured in 0.5 degrees C.
          id(uart_water_byte14).publish_state(bytes[14]);
          id(uart_water_byte15).publish_state(bytes[15]);
          id(uart_water_byte16).publish_state(bytes[16]);
          id(uart_water_byte17).publish_state(bytes[17]);
          id(water_flow_lpm).publish_state(bytes[18] / 10); //Flow rate - measured in 0.1 liters per minute (divide by 10 to get LPM)
          id(uart_water_byte19).publish_state(bytes[19]);
          id(uart_water_byte20).publish_state(bytes[20]);
          id(uart_water_byte21).publish_state(bytes[21]);
          id(uart_water_byte22).publish_state(bytes[22]);
          id(uart_water_byte23).publish_state(bytes[23]);
          id(uart_water_byte24).publish_state(bytes[24]); //System status. Probably a bitwise field. Partially decoded: display units: 0x08 position: 1=metric 0=imperial. 0x02 position: 1=weekly 0=hotbutton
          id(uart_water_byte25).publish_state(bytes[25]);
          id(uart_water_byte26).publish_state(bytes[26]);
          id(uart_water_byte27).publish_state(bytes[27]);
          id(uart_water_byte28).publish_state(bytes[28]);
          id(uart_water_byte29).publish_state(bytes[29]);
          id(uart_water_byte30).publish_state(bytes[30]);
          id(uart_water_byte31).publish_state(bytes[31]);
          id(uart_water_byte32).publish_state(bytes[32]);
          id(uart_water_byte33).publish_state(bytes[33]);
          id(uart_water_byte34).publish_state(bytes[34]);
          id(uart_water_byte35).publish_state(bytes[35]);
          id(uart_water_byte36).publish_state(bytes[36]);
          id(uart_water_byte37).publish_state(bytes[37]);
          id(uart_water_byte38).publish_state(bytes[38]);
          id(uart_water_byte39).publish_state(bytes[39]);
          id(uart_water_byte40).publish_state(bytes[40]);          
          }
          else if (bytes[0] == 247 && bytes[1] == 5 && bytes[2] == 80 && bytes[3] == 15 && bytes[4] == 144 && bytes[5] == 42) {
          // Gas information
          id(uart_gas_byte0).publish_state(bytes[0]); //Common to All Packets
          id(uart_gas_byte1).publish_state(bytes[1]); //Common to All Packets
          id(uart_gas_byte2).publish_state(bytes[2]); //Packet id Byte 0
          id(uart_gas_byte3).publish_state(bytes[3]); //Packet id Byte 1
          id(uart_gas_byte4).publish_state(bytes[4]); //Packet id Byte 2
          id(uart_gas_byte5).publish_state(bytes[5]); //Data Length
          id(uart_gas_byte6).publish_state(bytes[6]);
          id(uart_gas_byte7).publish_state(bytes[7]);
          id(uart_gas_byte8).publish_state(bytes[8]);
          id(uart_gas_byte9).publish_state(bytes[9]);
          id(uart_gas_byte10).publish_state(bytes[10]);
          id(uart_gas_byte11).publish_state(bytes[11]);
          id(uart_gas_byte12).publish_state(bytes[12]);
          id(uart_gas_byte13).publish_state(bytes[13]);                    
          id(uart_gas_byte14).publish_state(bytes[14]);
          id(uart_gas_byte15).publish_state(bytes[15]);
          id(uart_gas_byte16).publish_state(bytes[16]);
          id(uart_gas_byte17).publish_state(bytes[17]);
          id(uart_gas_byte18).publish_state(bytes[18]);
          id(uart_gas_byte19).publish_state(bytes[19]);
          id(uart_gas_byte20).publish_state(bytes[20]);
          id(uart_gas_byte21).publish_state(bytes[21]);
          id(low_byte_kcal).publish_state(bytes[22]); //Low byte current gas usage in kcal
          id(high_byte_kcal).publish_state(bytes[23]); //High byte current gas usage in kcal
          id(total_gas_use_m3).publish_state(bytes[24] / 10); //Total gas usage in 0.1m^3 (divide by 10 to get m^3)
          id(uart_gas_byte25).publish_state(bytes[25]);
          id(uart_gas_byte26).publish_state(bytes[26]);
          id(uart_gas_byte27).publish_state(bytes[27]);
          id(uart_gas_byte28).publish_state(bytes[28]);
          id(uart_gas_byte29).publish_state(bytes[29]);
          id(uart_gas_byte30).publish_state(bytes[30]);
          id(uart_gas_byte31).publish_state(bytes[31]);
          id(uart_gas_byte32).publish_state(bytes[32]);
          id(uart_gas_byte33).publish_state(bytes[33]);
          id(uart_gas_byte34).publish_state(bytes[34]);
          id(uart_gas_byte35).publish_state(bytes[35]);
          id(uart_gas_byte36).publish_state(bytes[36]);
          id(uart_gas_byte37).publish_state(bytes[37]);
          id(uart_gas_byte38).publish_state(bytes[38]);
          id(uart_gas_byte39).publish_state(bytes[39]);
          id(uart_gas_byte40).publish_state(bytes[40]);          
          } else {
          // Handle other cases
          }
      

###############################
#### ENABLE/DISABLE HEATER ####
###############################

switch:
  - platform: template
    name: "HW Heater On/Off"
    id: navien_switch
    restore_mode: ALWAYS_ON
    icon: mdi:water-boiler
    lambda: |-
      if ((id(uart_water_byte9).state) == 5) {
        return true;
        id(navien_switch).publish_state(true);
      } else {
        return false;
        id(navien_switch).publish_state(false);
      }
    turn_on_action:
      - uart.write: !lambda 
          return {0xf7, 0x05, 0x0f, 0x50, 0x10, 0x0c, 0x4f, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xce};
          id(navien_switch).publish_state(true);
    turn_off_action:
      - uart.write: !lambda 
          return {0xf7, 0x05, 0x0f, 0x50, 0x10, 0x0c, 0x4f, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa};
          id(navien_switch).publish_state(false);
  
  - platform: template
    name: "Fake Thermostat Switch"
    id: fake_switch
    restore_mode: ALWAYS_ON
    optimistic: True
    internal: True


####################################################################
#### SETPOINT ENTRY TEMPLATE  - STILL WORKING ON THIS 4/29/2024 ####
####################################################################

climate:
  - platform: thermostat
    name: "Climate Controller"
    visual:
      min_temperature: 120 °F
      max_temperature: 140 °F
      temperature_step: 1.0 °F
    sensor: outlet_temp
    min_heating_off_time: 300s
    min_heating_run_time: 300s
    min_idle_time: 30s
    heat_action:
      - switch.turn_on: fake_switch
    idle_action:
      - switch.turn_off: fake_switch

################################
#### WATER TEMPLATE SENSORS ####
################################
sensor:
  - platform: template
    name: "WByte_0"
    id: uart_water_byte0
    internal: True
  - platform: template
    name: "WByte_01"
    id: uart_water_byte1
    internal: True
  - platform: template
    name: "WByte_02"
    id: uart_water_byte2
    internal: True
  - platform: template
    name: "WByte_03"
    id: uart_water_byte3
    internal: True
  - platform: template
    name: "WByte_04"
    id: uart_water_byte4
    internal: True
  - platform: template
    name: "WByte_05"
    id: uart_water_byte5
    internal: True
  - platform: template
    name: "WByte_06"
    id: uart_water_byte6
    internal: True
  - platform: template
    name: "WByte_07"
    id: uart_water_byte7
    internal: True
  - platform: template
    name: "WByte_08"
    id: uart_water_byte8
    internal: True
  - platform: template
    name: "WByte_09"
    id: uart_water_byte9
    internal: true
  - platform: template
    name: "WByte_10"
    id: uart_water_byte10
    internal: True
  - platform: template
    name: "Temp Set - Read"
    id: set_temp_read
    icon: mdi:thermometer-water
    unit_of_measurement: °C
    accuracy_decimals: 1
    device_class: "temperature"
    state_class: "measurement"
  - platform: template
    name: "Temp Outlet"
    id: outlet_temp
    icon: mdi:thermometer-water
    unit_of_measurement: °C
    accuracy_decimals: 1
    device_class: "temperature"
    state_class: "measurement"
  - platform: template
    name: "Temp Inlet"
    id: inlet_temp
    icon: mdi:thermometer-water
    unit_of_measurement: °C
    accuracy_decimals: 1
    device_class: "temperature"
    state_class: "measurement"
  - platform: template
    name: "WByte_14"
    id: uart_water_byte14
    internal: True
  - platform: template
    name: "WByte_15"
    id: uart_water_byte15
    internal: True
  - platform: template
    name: "WByte_16"
    id: uart_water_byte16
    internal: True
  - platform: template
    name: "WByte_17"
    id: uart_water_byte17
    internal: True
  - platform: template
    name: "Water Flow LPM"
    id: water_flow_lpm #multiply by 0.2642 to get GPM
    icon: mdi:water-percent
    unit_of_measurement: l/min
  - platform: template
    name: "Water Flow GPM"
    id: water_flow_gpm #multiply by 0.2642 to get GPM
    icon: mdi:water-percent
    unit_of_measurement: GPM
    lambda: |-
      return (id(water_flow_lpm).state * 0.2642);
  - platform: template
    name: "WByte_19 Unknown Flow"
    id: uart_water_byte19
    # internal: True
  - platform: template
    name: "WByte_20"
    id: uart_water_byte20
    internal: True
  - platform: template
    name: "WByte_21"
    id: uart_water_byte21
    internal: True
  - platform: template
    name: "WByte_22"
    id: uart_water_byte22
    internal: True
  - platform: template
    name: "WByte_23"
    id: uart_water_byte23
    internal: True
  - platform: template
    name: "WByte_24 Sys Status"
    id: uart_water_byte24
    # internal: True
  - platform: template
    name: "WByte_25"
    id: uart_water_byte25
    internal: True
  - platform: template
    name: "WByte_26"
    id: uart_water_byte26
    internal: True
  - platform: template
    name: "WByte_27"
    id: uart_water_byte27
    internal: True
  - platform: template
    name: "WByte_28"
    id: uart_water_byte28
    internal: True
  - platform: template
    name: "WByte_29"
    id: uart_water_byte29
    internal: True
  - platform: template
    name: "WByte_30"
    id: uart_water_byte30
    internal: True
  - platform: template
    name: "WByte_31"
    id: uart_water_byte31
    internal: True
  - platform: template
    name: "WByte_32"
    id: uart_water_byte32
    internal: True
  - platform: template
    name: "WByte_33"
    id: uart_water_byte33
    internal: True
  - platform: template
    name: "WByte_34"
    id: uart_water_byte34
    internal: True
  - platform: template
    name: "WByte_35"
    id: uart_water_byte35
    internal: True
  - platform: template
    name: "WByte_36"
    id: uart_water_byte36
    internal: True
  - platform: template
    name: "WByte_37"
    id: uart_water_byte37
    internal: True
  - platform: template
    name: "WByte_38"
    id: uart_water_byte38
    internal: True
  - platform: template
    name: "WByte_39"
    id: uart_water_byte39
    internal: True
  - platform: template
    name: "WByte_40"
    id: uart_water_byte40
    internal: True
##############################
#### GAS TEMPLATE SENSORS ####
##############################
  - platform: template
    name: "GByte_0"
    id: uart_gas_byte0
    internal: True
  - platform: template
    name: "GByte_01"
    id: uart_gas_byte1
    internal: True
  - platform: template
    name: "GByte_02"
    id: uart_gas_byte2
    internal: True
  - platform: template
    name: "GByte_03"
    id: uart_gas_byte3
    internal: True
  - platform: template
    name: "GByte_04"
    id: uart_gas_byte4
    internal: True
  - platform: template
    name: "GByte_05"
    id: uart_gas_byte5
    internal: True
  - platform: template
    name: "GByte_06"
    id: uart_gas_byte6
    internal: True
  - platform: template
    name: "GByte_07"
    id: uart_gas_byte7
    internal: True
  - platform: template
    name: "GByte_08"
    id: uart_gas_byte8
    internal: True
  - platform: template
    name: "GByte_09"
    id: uart_gas_byte9
    internal: true
  - platform: template
    name: "GByte_10"
    id: uart_gas_byte10
    internal: True
  - platform: template
    name: "GByte_11"
    id: uart_gas_byte11
    internal: True
  - platform: template
    name: "GByte_12"
    id: uart_gas_byte12
    internal: True
  - platform: template
    name: "GByte_13"
    id: uart_gas_byte13
    internal: True
  - platform: template
    name: "GByte_14"
    id: uart_gas_byte14
    internal: True
  - platform: template
    name: "GByte_15"
    id: uart_gas_byte15
    internal: True
  - platform: template
    name: "GByte_16"
    id: uart_gas_byte16
    internal: True
  - platform: template
    name: "GByte_17"
    id: uart_gas_byte17
    internal: True
  - platform: template
    name: "GByte_18"
    id: uart_gas_byte18
    internal: True
  - platform: template
    name: "GByte_19"
    id: uart_gas_byte19
    internal: True
  - platform: template
    name: "GByte_20"
    id: uart_gas_byte20
    internal: True
  - platform: template
    name: "GByte_21"
    id: uart_gas_byte21
    internal: True
  - platform: template
    name: "GByte_22"
    id: uart_gas_byte22
    internal: True
  - platform: template
    name: "GByte_23"
    id: uart_gas_byte23
    internal: True
  - platform: template
    name: "GByte_24"
    id: uart_gas_byte24
    internal: True
  - platform: template
    name: "GByte_25"
    id: uart_gas_byte25
    internal: True
  - platform: template
    name: "GByte_26"
    id: uart_gas_byte26
    internal: True
  - platform: template
    name: "GByte_27"
    id: uart_gas_byte27
    internal: True
  - platform: template
    name: "GByte_28"
    id: uart_gas_byte28
    internal: True
  - platform: template
    name: "GByte_29"
    id: uart_gas_byte29
    internal: True
  - platform: template
    name: "GByte_30"
    id: uart_gas_byte30
    internal: True
  - platform: template
    name: "GByte_31"
    id: uart_gas_byte31
    internal: True
  - platform: template
    name: "GByte_32"
    id: uart_gas_byte32
    internal: True
  - platform: template
    name: "GByte_33"
    id: uart_gas_byte33
    internal: True
  - platform: template
    name: "GByte_34"
    id: uart_gas_byte34
    internal: True
  - platform: template
    name: "GByte_35"
    id: uart_gas_byte35
    internal: True
  - platform: template
    name: "GByte_36"
    id: uart_gas_byte36
    internal: True
  - platform: template
    name: "GByte_37"
    id: uart_gas_byte37
    internal: True
  - platform: template
    name: "GByte_38"
    id: uart_gas_byte38
    internal: True
  - platform: template
    name: "GByte_39"
    id: uart_gas_byte39
    internal: True
  - platform: template
    name: "GByte_40"
    id: uart_gas_byte40
    internal: True
  - platform: template
    name: "Total Gas Use Cubic Meters"
    id: total_gas_use_m3 # multiply by 0.3508 to get therms
    icon: mdi:gas-cylinder
    unit_of_measurement: m³
    accuracy_decimals: 1
  - platform: template
    name: "Total Gas Use Therms"
    id: total_gas_use_therms # multiply by 0.3508 to get therms
    icon: mdi:gas-cylinder
    unit_of_measurement: therms
    accuracy_decimals: 1
    lambda: |-
      return (id(total_gas_use_m3).state * 0.358);
  - platform: template
    name: "kCal Low Byte"
    id: low_byte_kcal
    icon: mdi:gas-burner
    unit_of_measurement: kCal
  - platform: template
    name: "kCal High Byte"
    id: high_byte_kcal
    icon: mdi:gas-burner
    unit_of_measurement: kCal
  - platform: template
    name: "Instant BTUs"
    id: instant_btus
    icon: mdi:gas-burner
    unit_of_measurement: "kbtu/hr"
    accuracy_decimals: 3
    state_class: "measurement"
    lambda: |-
      return max((id(outlet_temp).state - id(inlet_temp).state) * id(water_flow_gpm).state * 8.334 * 60 / 0.92 / 1000, 0.0);
  # calculated as shown here https://www.plctalk.net/threads/low-byte-high-byte.78315/
  - platform: template
    name: "Total kCal"
    id: total_kcal
    icon: mdi:gas-burner
    unit_of_measurement: kCal
    accuracy_decimals: 2
    state_class: "measurement"
    lambda: |-
      return (id(high_byte_kcal).state*256) + (id(low_byte_kcal).state);

Let me know if I can supply any further information.

Cheers!

1 Like

As far as I can recall, the 485 bus allows for multiple devices on the same bus so I wonder whether you can add a sniffer in between the navilink and the navien to see what traffic flows between them. I guess this would be mostly useful in discovering the commands which you appear to have already done, but I wonder whether it might help with figuring out whether the navien sends any acknowledgements to commands. But… that you can likely already do with what you have set up so maybe I am making a mute point. Leaving the comment just in case it sparks an idea…

Edit:
According to Google “RS485 is a standard for serial data transmission. Data exchanged through RS485 interfacing primarily uses the MODBUS protocol.”

Not sure if this helps at all… but look at this manual. At the end of the manual there is Appendix C.1 NFB-C Single Boiler Modbus RTU Mappings to BACnet/IP and BACnet MS/TP. Does it help at all?

@aruffell You are correct RS-485 does allow for that, and at the present - I am running the initial Python code I wrote in parallel with my ESP32. That way, I can “see” if the values are similar/same as I was seeing in the initial prototype. I haven’t “sniffed” the rest of the packets much since the initial inception when I used my logic analyzer to track down the communication parameters. After that @suva took care of decoding the important points noted in the other Navien post. (Thanks again if I had not already said that.) If I remember from the decoding, yes - it seems to mimic the command sent 6 times to acknowledge the commands. So, you are certainly on the right track there! I believe there was some thought about a checksum, but not sure if @suva was continuing with the decoding that or not.

Regarding the reference to MODBUS - In this case, where I have connected to the device, and the Navilink connects - is not MODBUS protocol, but rather a bunch of hex characters being spit out on the RS485 bus which we can decode. The device you show essentially takes that same RS485 information, decodes it and creates an output for one, or more of those other Protocols. What’s interesting, and GREAT find that you pointed out is the points list in the back of that manual. Thank You!!

Now, where to start with that? :thinking:

You may note that in my ESPHome config, I have a bunch of internal entities. Essentially, I was using those as place holders for some of the future entities that we may discover - we may need to add even more when we get the other points sorted out.

I’m excited to continue this and help others as it seems there is quite a bit of struggle around the Navilink cloud service.

Cheers!

2 Likes

Awesome! Since I still have my NaviLink connected, I may start back at the ground floor and connect my LA to see if I can fill in any of the blanks in the wire protocol decoding which we still have. After that I’ll connect up an ESP32 (just ordered a 485 interface for one) and will start a custom component for this.

Thanks!

1 Like

Received atom lite and rs485 expansion board for it over the weekend as well as XHB pigtail. Connected everything and works just fine. XH connector works. B signifies “buckle” for locking, but I guess it is not required as connector sits tight by itself.

1 Like

@brystmar It looks like @kkopachev has confirmed that the XH, or XHB connector will work for this. I have also confirmed the XH connector fits snugly when plugged into my device, though I didn’t order the “B” type with the buckle. I know you were looking to order one just thought I’d tag you in case you didn’t see this post.

@aruffell I finally had a chance to look through the pdf you linked above, and I realized after looking through it a bit, that appears to be for the Navien Boiler product line - not the Navien Hot Water Heater product line. Unless of course I missed something in my review - which is always possible. That said - I’m not sure how much of that cross references to the domestic hot water side. I did a quick search to see if there was a document like this for the NPE series, but I didn’t find one. Is there somewhere special you found this or was it just through a web search - that landed you on Navien’s page?

@mbcomer This is awesome news that you are heading towards a custom component for this! I’m certain that your code will be far more efficient at decoding, and reviewing the received information than mine! Anyway, just in case you haven’t looked at it, the information in this other post, may help too? Please let me know if I can be of assistance in anyway on that. My programming skills are limited in creating these, but I’m willing to try!

I appreciate the work of the group here! Thanks for everyone’s input and information. It is what makes this community great!


here is a picture of my install. Using Atomic RS485 base and atom lite controller. XHB pigtails from aliexpress

1 Like

I spent a little while trying to figure out what the checksum algorithm is with no luck (it doesn’t seem to be an ordinary CRC code - I tried several of the messages both including the header and without it).

It does seem like it’s something more complicated than (e.g.) adding up the bytes or XORing them together, because single bit changes will change anywhere between 1 and 3 bits in the checksum, e.g. in this example from https://community.home-assistant.io/t/navien-hot-water-heater-navilink/330044/93:

  f7 05 0f 50 10 0c 4f 00 0b 00 00 00 00 00 00 00 00 00 0a : off
  f7 05 0f 50 10 0c 4f 00 0a 00 00 00 00 00 00 00 00 00 ce : on

  f7 05 0f 50 10 0c 4f 00 00 00 00 01 00 00 00 00 00 00 6a : hotbutton press
  f7 05 0f 50 10 0c 4f 00 00 00 00 00 00 00 00 00 00 00 2a : hotbutton release

I have an NPE-210S, and bought the 5 pin to RJ-45 Navien cable. Then - using an RJ45 breakout connector. I measured with my DVM each one to ground, and found that there were 2 lines that were around 2.5-3VDC, and one line that was 12VDC. So, I think that the 5 pin connector (which only has 4 wires attached) actually is 2 RS-485 wires, 1 - Power, and 1 Ground wire. Then, I took a bit of time over the last few days to analyze the signal using my logic analyzer. I think I finally got it to read the signals without any framing errors. Additionally - I think it’s right as I see quite a bit more data when the unit is running and producing hot water than I do when it is idle.

It looks like the comm specs are 19200, 8, NONE,1 It seems that RS485-A is MSB-first, and Inverted. and RS485-B is MSB-first, and NOT Inverted.

My end goal here would be to utilize ESPHOME to interface with this unit and be able to read the data that is being transmitted on these communication lines. Maybe even using the 12VDC from the unit to power the ESP?

Now for the part that might be a bit over my head – using the decoder to interpret the data. Thoughts on where/How to start?

2 Likes

RS-485 is really just standard serial / UART at the protocol level. It’s a little different at the physical level in that it is a differential set of wires to help reject noise and make it more reliable in harsh noisy environments. (So, instead of say 3 wires - gnd, tx, rx there will be five wires - gnd, rx-, rx+, tx-, tx+) (this is assuming separate rx/tx). RS-485 can also operate at higher differential voltages, again to support better high-noise communication (higher signal-to-noise-ratio).

In any case - I just got the Lite unit hooked up to my water heater today. I’ll get to some data sniffing in the next few days hopefully!

1 Like

@mbcomer - Awesome News that someone else is going down this road too! Glad to hear you have a Lite unit all hooked up and something to sniff the data lines from. I have been trying different things here - and struggling a bit to understand your statement above regarding the 5 wire’s vs what I thought I knew about RS-485 (I thought the RS422 was 5 wire?). As I noted above, the cable only has 4 wires of the 8 in the RJ-45 connector. This was verified and continuity tested. If my DVM was correct, it seems that I have 12VDC, GND, TX+, and TX- for the communications. I’m curious, does the Navilink Lite get power from the RJ-45 or does it need it’s own power connector? If so, that likely explains the 12VDC. Based on this video, it seems I have a different connector. My connector only has 4 wires. I went and put jumpers on all 5 pins in that connector (directly on the board) - the 5th place in that connector does not appear to have any signal/voltage on it. Anyway, here is a snip of what I see from the logic analyzer on just the raw data being transmitted from the unit. At the time of the screenshot I had disconnected the 12VDC - as it didn’t seem necessary to monitor any longer. Also, it appears that either have quite a bit of noise on one of the lines. I have read 120ohm resistor could help that? You can see it to the left of the RS-485B signal line near the beginning of the screenshot.

Then, I setup the decoding. Either I don’t understand what it is telling me or I have something setup wrong on the decoding front. Thoughts here are welcomed! I do see that I get some framing errors at first (within the first few bits) but they go away after that. I have read in a few other places - where RS-485 requires some “math” to get the right data as it is a differential protocol. It does seem that the wave forms from the first capture are just that - inverted from each other.

Appreciate any further direction from this information, and insight into where to turn next.

1 Like

If they were similar in price, is it better to get Navi Link or Navi Link Lite?

depends how many Navien water heaters you have. If you have one, then get the Navi-Link Lite.

Thanks, I ordered the Lite version. I’m so looking forward to make water heating smarter :smile: Getting stats is the first step.

Ultimately I want to make the hot button recirc intelligent , like described here:

I built an RS485 sniffer similar to this one: RS485 sniffer

I used the UART settings @tsquared provided (19200, 8N1) (thanks!), and connected the sniffer to a tap on the cat5 cable provided with a Navilink lite. Using this cable, the blue and blue-white wires appear to be the RS485 signals. I connected blue to RS485-B and blue-white to RS485-A.

With no navilink attached, the water heater continuously emits two alternating packets (with a short interval between each). I believe each of them contains continuous status information. Let’s call them A and B. With a navilink connected, the navilink sends an announcement packet (let’s call it C) after each packet from the heater. The result is a stream that looks like: A C B C A C B C…

There is a fourth type of packet when the navilink sends a command. It appears the navilink simply repeats the command several times (typically 6), with no acknowledgement from the heater.

I observed the packets while performing various operations, and I believe I have decoded most of the data and commands available in the navilink app. Here are the packet captures and what I have identified so far.

Common Fields

The first 6 bytes appear to be a packet header. The first two bytes are common to all packets. The next three uniquely identify each of our three packet types. Byte index 5 is the length of the data, not including the header (6 bytes) or the final byte, which I believe is a checksum. In other words, byte index 5 is the length of the packet minus 7 bytes.

Index Value Notes
0 f7 Common to all packets
1 05 Common to all packets
2 Packet id byte 0
3 Packet id byte 1
4 Packet id byte 2
5 Data length
Data
N Checksum

Packet A (from heater)

This packet seems to have information related to water.

Here is a full example packet:

f7 05 50 50 90 22 42 00 00 05 14 72 37 2e 00 00 00 00 00 00 f8 8e 00 00 02 00 00 00 05 00 07 00 00 02 00 00 00 00 00 00 67

And the fields decoded so far:

Index Value Notes
0 f7 Common to all packets
1 05 Common to all packets
2 50 Packet id byte 0
3 50 Packet id byte 1
4 90 Packet id byte 2
5 22 Data length
09 05 System power: high nibble: unknown (values 0 and 0x20 observed); low nibble: 0=off 0x5=on
11 72 Set temp - measured in 0.5 degrees C. 57C in this case
12 37 Outlet temp - measured in 0.5 degrees C. 27.5C in this case
13 2e Unconfirmed, but possibly inlet temperature? This seems very close to the inlet temperature, but was usually off by 0.5-1.5C. My inlet temperature did not vary enough to analyze more
18 00 Unknown, but appeared proportional to water flow rate
19 00 Flow rate - measured in 0.1 liters per minute (divide by 10 to get LPM)
24 02 System status. Probably a bitwise field. Partially decoded: display units: 0x08 position: 1=metric 0=imperial. 0x02 position: 1=weekly 0=hotbutton

Packet B (from heater)

This packet seems to have information related to gas.

Here is a full example packet:

f7 05 50 0f 90 2a 45 00 0b 01 0c 03 17 00 72 6d 23 00 00 00 00 00 00 00 61 00 00 00 0b 00 1d 00 f6 34 00 00 05 00 00 00 00 00 aa 48 00 00 01 00 b3

And the fields decoded so far:

Index Value Notes
0 f7 Common to all packets
1 05 Common to all packets
2 50 Packet id byte 0
3 0f Packet id byte 1
4 90 Packet id byte 2
5 2a Data length
22 00 Low byte current gas usage in kcal
23 00 High byte current gas usage in kcal
24 61 Total gas usage in 0.1m^3 (divide by 10 to get m^3; 9.7 cubic meters in this case)

Packet C (from navilink)

This appears to be an announcement packet. It tells the heater there is a navilink attached. The heater uses this to disable local setting of the weekly schedule and instead require setting the schedule from the app.

Here is a full example packet:

f7 05 0f 50 10 03 4a 00 01 55

Packet D (from navilink)

This is the command packet. I tested each of the following commands: power off, power on, set temp, hotbutton. Each type of command uses a different field. I only ever saw a single field used since I tested it using the navilink app, but the format suggests that more than one command could be combined into a single packet. The navilink repeats the on/off/set commands 6 times. For hotbutton presses, it sends the on command twice followed by the off command once.

Here is a full example:

f7 05 0f 50 10 0c 4f 00 0b 00 00 00 00 00 00 00 00 00 0a

Index Value Notes
0 f7 Common to all packets
1 05 Common to all packets
2 0f Packet id byte 0
3 50 Packet id byte 1
4 10 Packet id byte 2
5 0c Data length
8 0b Power: 0x0a=on 0x0b=off
9 00 Set temperature (units of 0.5C: 0x74 is 58C)
11 00 Hotbutton: 0x01=press 0x00=release

Here are all the commands I have seen as full packets (so you can see the checksums):

  f7 05 0f 50 10 0c 4f 00 0b 00 00 00 00 00 00 00 00 00 0a : off
  f7 05 0f 50 10 0c 4f 00 0a 00 00 00 00 00 00 00 00 00 ce : on
  f7 05 0f 50 10 0c 4f 00 00 74 00 00 00 00 00 00 00 00 ea : set to 58
  f7 05 0f 50 10 0c 4f 00 00 72 00 00 00 00 00 00 00 00 c4 : set to 57
  f7 05 0f 50 10 0c 4f 00 00 00 00 01 00 00 00 00 00 00 6a : hotbutton press
  f7 05 0f 50 10 0c 4f 00 00 00 00 00 00 00 00 00 00 00 2a : hotbutton release

Checksum

I believe the final byte is a checksum, but I have not identified the algorithm yet. It would be useful to do so for the commands (especially the temperature set commands).

4 Likes

the easiest option for me was having the bathroom light switch trigger the hot button.

So far this project has been very interesting. I am learning at every turn! This is great information! Thank You @suva! I took what you had, and tried with my limited python skills to pull it into python directly using a RS485/USB adapter. Also, built an ESPHome UART Sniffer as you describe above. In both cases - essentially, It reads the information you show above!! I am now trying to figure out how to manipulate the data to get the respective bits/bytes/strings out of this information.

This brings me to my next question - although you note, and I can see the f7 05 common data for all, the data streamed from the COM port starts long before I can see those values.

For example: The raw stream looks like this - (I **'d the f7 05 for clarity - it shows up 2x in this example.)

b'\xd8\xbc\x1a\x00\xb0\x02\x00\x00\x00\x00\xa6I\x00\x00\x00\x00\xb9\**xf7\x05PP**\x90"B\x00\x00\x05\x14x0*\x00\x00\x00\x00\x00\x00\x00\xaf\x00\x18\x00\x00\x00\x00\xb0\x02\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\xbb**\xf7\x05P**\x0f\x90*E\x00\x01\x01\x14\x07"\x00x0\x1b\x00\x00\x00\x00\x01\x00\x00\x9f\x1f\x00\x00\xd1\x03Z\n'

Interesting enough - that data stream also shows some weird values like the P or PP next to the 05 among other oddities. I have been able to convert it directly to a hex stream and received the following results which appear more accurate:

0xd8bc1a00b00200000000a64900000000b9f70550509022420000051478302a00000000000000af001800000000b00200000002000000000000bbf705500f902a450001011407220078301b000000000100009f1f0000d1035a0a

It seems like I should be able to slice the hex data stream better than the raw one as it appears more accurate? I am grateful for all the help!

Anyone on Next thoughts on python libraries, or ESPHome coding that helps break this down into a useable format?

I did find this post, but it doesn’t seem “solved”. It links to this post, which talks about custom devices in ESPHome, which currently appears past my abilities. I guess I’m back to the reading and learning phase!

Ok - I worked my way through some really crude python and I have it displaying some information regarding the water, and printing to the REPL. Now that I can actually read it for what data it is supposed to be showing, It appears that I may have some differences from what @suva shows.

As it relates to index 18 & 19 (flow) on the water side. It seems that my water flow is actually index 18, as index 19 never changes for me. Index 19 is always zero. I confirmed that the front panel shows the value I am calculating on index 18 in the code, back to GPM.

Also, index 13 - that value seems strange to be inlet temperature. Guess I will have to keep watching and comparing that value to the front panel of the unit and see what it says.

I got my module today. Navilink Lite is using STM32F412 for MCU and MXCHIP EMC3280-E for WIFI.

Making some progress here! Again - thanks to @suva for all his decoding! I started with some generic python code to make sure I could actually talk/receive information from the device. The code I wrote to receive the information from my equipment is below. I am certain that it’s not efficient, and likely doesn’t follow too many rules for coding. If anyone has any tips here they are appreciated!

import time
import serial

print ("Opening port")
try:
  ser = serial.Serial('COM3',19200,bytesize=8,
                    parity=serial.PARITY_NONE,
                    stopbits=serial.STOPBITS_ONE,
                    timeout=0)
  print ("Port is open")

except serial.SerialException:
  serial.Serial('COM3', 19200).close()
  print ("Port is closed")
  serial_port = serial.Serial("COM3",19200)
  print ("Port is open again")

print ("Ready to use")

buffer = bytearray(b'')
buffer_length = 99
buf_list = []


while True:

  try:
    buffer += ser.read()
    if len(buffer) <= buffer_length:
      buffer += buffer
    elif len(buffer) >= buffer_length:
      buf_list = (list(buffer))
      # print(buf_list)
      ser.flushInput()
      buffer = bytearray(b'')
      # print(buf_list[0:23])
    if buf_list[5] == 34:
      # print("Water Info Received!")
      sys_power = buf_list[9]
      sys_power_text = "Off"
      set_temp = (((buf_list[11]/2)*9/5) + 32)
      outlet_temp = (((buf_list[12]/2)*9/5) + 32)
      inlet_temp = (((buf_list[13]/2)*9/5) + 32)
      wtr_flow = round(((buf_list[18]/10)* 0.2642),2)
      unknown_flow = buf_list[19]
      print(" ")
      print("*************Water Information**************")
      print(" ")
      if sys_power == 5:
        sys_power_text= "    On"
        print("The Power is:   ",sys_power_text)
      elif sys_power ==0:
        sys_power_text= "    Off"
        print("The Power is:   ",sys_power_text)
      else:
        print("The Power is Unknown")
      print("The Outlet Temp is: ",outlet_temp,"",u"\u00b0","F")
      print("The Setpoint is:    ", set_temp,u"\u00b0", "F")
      print("The Water flow is:  ",wtr_flow, "GPM")
      print("The Inlet? Temp is: ",inlet_temp,"",u"\u00b0", "F")
      print("The Unknown flow is:",unknown_flow, "  GPM")
    elif buf_list[5] == 42:
      # print("Gas Info Received!")
      print(" ")
      print("**************Gas Information***************")
      print(" ")
      total_gas_use = round(((buf_list[24]/10)*0.3508),2)
      low_byte_kcal = (buf_list[22])
      high_byte_kcal = (buf_list[23])
      total_kcal = high_byte_kcal + low_byte_kcal
      print("Total Gas Usage is: ", total_gas_use, "therm")
      print("Low Byte kCal is:   ", low_byte_kcal, "kCal")
      print("High Byte kCal is:  ", high_byte_kcal, "kCal")
      print("Total kCal:         ", total_kcal, "kCal")
    else:
      pass
      print("Bad Packet")
    time.sleep(1)
  except:
    pass

This was successful enough to produce the following output.

*************Water Information**************
 
The Power is:        On
The Outlet Temp is:  63.5  ° F
The Setpoint is:     140.0 ° F
The Water flow is:   0.0 GPM
The Inlet? Temp is:  68.9  ° F
The Unknown flow is: 0   GPM
 
**************Gas Information***************
 
Total Gas Usage is:  2.17 therm
Low Byte kCal is:    0 kCal
High Byte kCal is:   0 kCal
Total kCal:          0 kCal

So having this information I went towards my final goal of using ESPHome to create this similar interface. Below is the ESPHome Config.


# Include basic info in all ESP Home Configs
# Basic Info Includes
# logger:
# wifi:
# ap:
# web_server:
# captive_portal:
# button: to restart the device

<<: !include .base1.yaml


esphome:
  name: navien
  friendly_name: Navien Water Heater
  includes:
    - uart_read_line_sensor.h

esp32:
  board: esp32dev
  framework:
    type: arduino

# Enable logging
logger:
  level: NONE #makes uart stream available in esphome logstream
  baud_rate: 0 #disable logging over uart

# Enable Home Assistant API
api:
  encryption:
    key: "redacted"

ota:
  password: "redacted"

substitutions:
  comment: "redacted"
  friendly_name: Navien Water Heater

globals:
- id: byte_timer
  type: int
  initial_value: "1"

interval:
- interval: 5s
  then:
    - lambda: |-
        id(byte_timer) = (id(byte_timer) + 1);
        if (id(byte_timer) > 25) {
          id(byte_timer) = 1;
        }

# Example configuration entry
uart:
  id: uart_bus
  tx_pin: 1
  rx_pin: 3
  baud_rate: 19200
  data_bits: 8
  stop_bits: 1
  parity: NONE
  debug:
    direction: BOTH
    dummy_receiver: false
    after:
      delimiter: "\n"
    sequence:
      - lambda: |-
          UARTDebug::log_string(direction, bytes);
          //looking at this string f7 05 50 50 90 22 42
          //where the 22 represents water information to follow
          //and in the same byte if the number is 34 it is 
          //representative of gas information
          if (bytes[0] == 247 && bytes[5] == 34) {
          //id(uart_byte0).publish_state(bytes[0]);
          //id(uart_byte1).publish_state(bytes[1]);
          //id(uart_byte2).publish_state(bytes[2]);
          //id(uart_byte3).publish_state(bytes[3]);
          //id(uart_byte4).publish_state(bytes[4]);
          //id(uart_byte5).publish_state(bytes[5]);
          //id(uart_byte6).publish_state(bytes[6]);
          //id(uart_byte7).publish_state(bytes[7]);
          //id(uart_byte8).publish_state(bytes[8]);
          //id(uart_byte9).publish_state(bytes[9]);
          //id(uart_byte10).publish_state(bytes[10]);
          id(set_temp).publish_state(((bytes[11]/2)*9/5) + 32);
          id(outlet_temp).publish_state(((bytes[12]/2)*9/5) + 32);
          id(inlet_temp).publish_state(((bytes[13]/2)*9/5) + 32);
          //id(uart_byte14).publish_state(bytes[14]);
          //id(uart_byte15).publish_state(bytes[15]);
          //id(uart_byte16).publish_state(bytes[16]);
          //id(uart_byte17).publish_state(bytes[17]);
          id(water_flow).publish_state((bytes[18]/10)* 0.2642);
          //id(uart_byte19).publish_state(bytes[19]);
          //id(uart_byte20).publish_state(bytes[20]);
          //id(uart_byte21).publish_state(bytes[21]);
          //id(uart_byte22).publish_state(bytes[22]);
          //id(uart_byte23).publish_state(bytes[23]);
          //id(uart_byte24).publish_state(bytes[24]);
          } else if (bytes[0] == 247 && bytes[5] == 42) {
          id(low_byte_kcal).publish_state(bytes[22]);
          id(high_byte_kcal).publish_state(bytes[23]);
          id(total_gas_use).publish_state((bytes[24]/10)*0.3508);
          } else (bytes[0]  != 247); {
          }
# Example configuration entry
switch:
  - platform: template
    name: "HW Heater On/Off"
    id: navien_switch
    restore_mode: ALWAYS_ON
    lambda: |-
      if ((id(navien_switch).state) != (id(navien_switch).state)) {
        return true;
      } else {
        return false;
      }
    turn_on_action:
      - uart.write: !lambda 
          return {0xf7, 0x05, 0x0f, 0x50, 0x10, 0x0c, 0x4f, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xce};
          id(navien_switch).publish_state(true);
    turn_off_action:
      - uart.write: !lambda 
          return {0xf7, 0x05, 0x0f, 0x50, 0x10, 0x0c, 0x4f, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa};
          id(navien_switch).publish_state(false);


text_sensor:
- platform: custom
  lambda: |-
    auto my_custom_sensor = new UartReadLineSensor(id(uart_bus));
    App.register_component(my_custom_sensor);
    return {my_custom_sensor};
  text_sensors:
    id: "uart_readline"

text:
  - platform: template
    name: "Temp Set - Write"
    optimistic: true
    min_length: 0
    max_length: 5
    mode: text
    on_value:
      then:
        - sensor.template.publish:
            id: num_from_text
            state: !lambda |-
              auto n = parse_number<float>(x);
              return n.has_value() ? n.value() : NAN;

sensor:
  # - platform: template
  #   name: "Byte 0"
  #   id: uart_byte0
  # - platform: template
  #   name: "Byte 1"
  #   id: uart_byte1
  # - platform: template
  #   name: "Byte 2"
  #   id: uart_byte2
  # - platform: template
  #   name: "Byte 3"
  #   id: uart_byte3
  # - platform: template
  #   name: "Byte 4"
  #   id: uart_byte4
  # - platform: template
  #   name: "Byte 5"
  #   id: uart_byte5
  # - platform: template
  #   name: "Byte 6"
  #   id: uart_byte6
  # - platform: template
  #   name: "Byte 7"
  #   id: uart_byte7
  # - platform: template
  #   name: "Byte 8"
  #   id: uart_byte8
  # - platform: template
  #   name: "Byte 9"
  #   id: uart_byte9
  # - platform: template
  #   name: "Byte 10"
  #   id: uart_byte10
  - platform: template
    name: "Temp Set - Read"
    id: set_temp
    unit_of_measurement: °F
  - platform: template
    name: "Temp Outlet"
    id: outlet_temp
    unit_of_measurement: °F
    accuracy_decimals: 2
  - platform: template
    name: "Temp Inlet"
    id: inlet_temp
    unit_of_measurement: °F
  # - platform: template
  #   name: "Byte 14"
  #   id: uart_byte14
  # - platform: template
  #   name: "Byte 15"
  #   id: uart_byte15
  # - platform: template
  #   name: "Byte 16"
  #   id: uart_byte16
  # - platform: template
  #   name: "Byte 17"
  #   id: uart_byte17
  - platform: template
    name: "Water Flow"
    id: water_flow
    unit_of_measurement: GPM
  # - platform: template
  #   name: "Byte 19"
  #   id: uart_byte19
  # - platform: template
  #   name: "Byte 20"
  #   id: uart_byte20
  # - platform: template
  #   name: "Byte 21"
  #   id: uart_byte21
  # - platform: template
  #   name: "Byte 22"
  #   id: uart_byte22
  # - platform: template
  #   name: "Byte 23"
  #   id: uart_byte23
  # - platform: template
  #   name: "Byte 24"
  #   id: uart_byte24
  - platform: template
    name: "Total Gas Use"
    id: total_gas_use
    unit_of_measurement: therms
    accuracy_decimals: 2
  - platform: template
    name: "kCal Low Byte"
    id: low_byte_kcal
    unit_of_measurement: kCal
  - platform: template
    name: "kCal High Byte"
    id: high_byte_kcal
    unit_of_measurement: kCal
  - platform: template
    id: num_from_text
    name: "Temp Set - Write"
    internal: True

This seems to produce similar results as what I was seeing with the python code.

It looks like this:

I’m struggling with a few things here that I could use some tips on.

  1. When using the switch I created in ESPHome to turn on/off the Heater, ideally I’d like the switch to set it’s position based on the current status of the unit, then if the switch changes - send the appropriate command. Last, I’d like to know that the command was received and that it has gone into that mode.

  2. Similar question regarding the setpoint. In my config, I created a second “write” setpoint to try and begin configuring this function - but I’m not sure where to go from here.

In both of those cases, I’d like to have a single value/entity show on the ESPHome web page or entity in HA.

Any and all feedback is appreciated if you see something else here that would make this project better!

Hope this helps someone as they dive into their journey of doing the same!

3 Likes