I replaced my storage tank water heater. The new heater is a Navien NPE-A2. It has a recirculation pump built-in, and it also has an optional feature called HotButton that lets you call for hot water recirculation on demand. That is pretty dandy, but there are some quirks to their implementation that are not the choices I would make.
This project is about how I am using the awesome power of inexpensive electronics, rich open source software, and personal stubbornness to get things to operate the way I want.
- A relay is wired directly to the water heater's HotButton terminals. The relay is controlled by a microprocessor and simulates a person pressing the physical button of the HotButton add-on. (I don't have the official HotButton add-on hardware.)
- Remote hot water call buttons are placed in upstairs bathrooms. The call buttons are microprocessors with an RGB LED for feedback.
- Home Assistant automation is used to tie it all together. It senses a press on any of the remote call buttons, activates the relay, and provides visual feedback via the RGB LEDs on the remote call buttons.
- Communications between the remote call buttons, the Home Assistant server, and the relay are over wifi.
Clever collaborators worked out a lot of details of the Navien RS485 interface, and I'm capitalizing on their discoveries (mentioned a couple of places below). The rest of the project implementation is all mine.
Depending on how you are counting, this is either the second or third iteration of this scheme for my water heater. If you are interested, you can read about the first and second iterations here and here.
What's a recirculation pump?Just in case you don't even know what I'm talking about, I'll briefly describe the idea of a recirculation pump.
If you have a centralized hot water heater, which is overwhelmingly common in the US, you sometimes have to wait for hot water in your sink or shower. Besides the waiting, running the water until it gets hot lets an unfortunate amount of water be wasted down the drain. In theory, that water isn't really wasted and just makes its way back into the hydrologic cycle. In practice, it's a pretty inefficient way of dealing with things because it typically goes through wastewater treatment and all kinds of other resource wasting processes.
A recirculation pump tries to overcome the problem by making sure there is hot water nearby in the pipes when you want it. There are two main approaches to this:
- If you are in a commercial building of any size and any recent vintage, it's quite likely that there is a hot water loop that makes its way around to all of the sinks, showers, tubs, and so on, and includes a segment that returns to the water heater. The recirculation pump sends hot water around through that loop. Any water returned is not wasted. It's just part of the closed system and goes around and around and around until it gets used.
- In a residential house, you might have that same sort of hot water loop, but it's more likely you have hot water lines with no way to return any water to the water heater. For this situation, recirculation pump systems can work with a bypass valve that's usually installed between the hot and cold water pipes at the farthest (in water pipe length) from the water heater. The bypass valve is thermostatically controlled. When the water is below the set temperature (for example, 95F), the bypass valve is open. When the pump is on and the valve is open, the hot water gets pushed down into the cold water line. When the "hot enough" water arrives, the valve closes, with hot water in the pipe ready for use.
There are a couple of trade-offs in using a recirculation system. First, any time you are recirculating hot water when you are not actually going to use hot water, you are wasting energy. You're heating water which goes into the plumbing pipes and starts to cool off. Second, for the type of system that uses a bypass valve, the cold water tap at the bypass valve location will emit the pushed-back water that is warm or hot until it is all expelled.
Recirculation pump systems typically have some kind of scheduled timer, which is useful for the "popular" hot water times, but it's no good for other times. Even better would be to have the pump read your mind and switch itself on a couple of minutes before you need hot water. That mind reading is a tricky business. The next best thing is to be able to tell the pump that you want hot water now, and then you just have to wait that couple of minutes to get it. That on-demand pattern is the aim of this project.
The Navien A2 series has 3 modes of operating the recirculation pump. As far as I can tell from reading the documentation, they are all mutually exclusive. They can't be combined.
- A "smart" mode that watches your hot water usage for a week and then sets its own schedule based on what it saw during that week. I don't know if it adjusts things if your usage changes later.
- A timer mode that uses a configured schedule.
- An on-demand mode, which they call HotButton, to start the pump in response to a button press. The button can be either hard-wired (a simple normally-open switch between two contacts) or a few different wireless options for the same thing.
This project uses the on-demand feature integrated with Home Assistant. That integration will not only give me job security in the household, but it will also let me implement any of the other schemes if I see fit.
Spoiler alertI'll dispel the suspense by showing the final hardware result.
This is the component mounted directly on the side of the water heater:
Here's one of the remote units, which is just an M5 Atom Lite plugged into a USB charge port:
My original project implementation had been operating for a while in a satisfactory way. I did still have one unsolved problem (knowing when the pump stops pumping), though for a while I've had some ideas about how to tackle that. In the meanwhile, some clever and industrious people over in the Home Assistant forums have been making progress on reverse engineering the wire protocol between Navien's NaviLink unit and the tankless water heater. That gives me the answer about the pump (more or less). To use that status information, I needed an RS485 interface to the water heater control board.
If you look around for an RS485 interface board (as some of the folks in that forum thread have done), you come across some components from M5stack. Their stuff is particularly appealing for one-off projects since it mostly comes in reasonably attractive packages. I already have a little box full of assorted M5stack stuff, so I decided to make use of that and a few things I had to order from them to re-implement the hardware.
At the water heater end, requirements are minimal. Just about any ESP32 variant (or even ESP8266) would fit the bill. I decided to use something from their Atom series since they are inexpensive and I already have some on hand. Specifically, I'm using the Atom Lite, which has two physical buttons and a single RGB LED.
They have a couple different RS485 interface components that work with the Atom series. The cheapest option is their RS485 to TTL Converter Unit.
For a tiny amount more, their ATOM Tail485 - RS485 Converter for ATOM makes for a tidier package since it plugs directly into the Atom Lite.
In the end, I didn't choose either of those. I spent a few bucks more and chose the ATOMIC RS485 Base.
The reason for that choice is because it leaves the 4-pin port on the Atom Lite exposed. (It also leaves the USB-C port exposed for power, though once it's wired up to an RS485 circuit, that circuit can power everything.) I'm not sure if the reverse-engineered RS485 conversation will let me remotely "push the button" to call for hot water. I might do that later, but, for insurance, my first step is to implement the hot button feature using a relay. This Mini 3A Relay Unit interfaces to the Atom Lite via that 4-pin port.
The total cost for these three components was under US$20, which is not too bad for a one-off project.
Physical assemblyThe RS485 unit and the relay unit both have LEGO-compatible holes in their bodies. I decided to make a 3D-printed slab with a few LEGO studs in the right places to take advantage of that. I started with this nice custom LEGO component generating script: https://github.com/cfinke/LEGO.scad. My requirement was a little unusual, so I made a couple of local modifications to that OpenSCAD script. (You can find my changes in this pull request: https://github.com/cfinke/LEGO.scad/pull/24)
- The standard height for a LEGO stud in that script is 1.8mm. I wanted them taller to give a more snug fit, so I changed that to 8.0mm. That's no good for normal LEGO stacking, but it works great with the M5stack stuff.
- I changed the code a bit to suppress the generation of the studs except in the four locations where I wanted them. I spent a little time trying to see if I could parameterize that, but OpenSCAD makes that at best kind of klunky and hacky. If I later figure out a reasonably elegant way to do that, I'll send another PR to the author.
The script does include a parametric scaling factor for the diameter size of the studs. Through trial and error, I settled on 1.01 (101%) to allow for filament shrinkage and still have a snug fit.
Here are the bottom sides of the components along with the 6x6 base I created.
And here is what it looks like when it's press-fit together:
I think it makes a pretty tidy bundle.
WiresIn the forum thread I mentioned, other people have already figured out the connectors and wiring arrangement for the Navien board's RS485 interface. For example, in this post, user aruffell provides this clear picture of a custom cable they created:
The connector in the picture is a 5-pin JST-XH, though the slightly preferred connector is JST-XHB (the "B" is for "buckle", the integrated clip that keeps the connector from coming out). Both have been reported to work without issue.
In this post, user tsquared shows the wiring with the connector inserted into the socket on the Navien board:
RS485 uses differential signalling, and there is an A and a B signal line. Only 4 pins of the JST-XHB connector are used. Combining the info from the above pictures, the pin assignments are:
- 12vdc
- B
- A
- (not connected)
- Ground
In addition to the RS485 wiring, there is also the relay wiring for the Hot Button feature. That's somewhat simpler since it's just 2 wires and orientation doesn't matter. They're connected to the relay pins Common and NO (normally open).
ESPHome implementation alternativesBesides the collaborative reverse-engineering effort documented in the forum thread, there are at least 2 concrete ESPHome interface configurations. They differ a bit in approach. The github repositories for both include documentation of the packet formats. They expect bytes to be read/written over the UART interface, implying a separate component that translates the microcontroller's logic levels to RS485 signals.
The repository https://github.com/evanjarrett/ESPHome-Navien takes the approach of defining everything inside a YAML file. Debug logging within that configuration facilitates the iterative discovery process for reverse engineering. Most of the packet data bytes are made available as simple values, but some well-understood items are given semantically meaningful names. Some ideas for this repository seem to have been taken from or inspired by https://github.com/esphome-econet/esphome-econet.
The repository https://github.com/htumanyan/navien takes the approach of isolating the low-level manipulation into an ESPHome custom component. That simplifies the YAML configuration and, in the long term, is more consistent with how other components are usually referenced in ESPHome. However, it comes at the expense of being a bit more cumbersome for iterative discovery. Only a handful of data items are exposed as sensors and switches for use in YAML. Adding more requires manipulating the code for the custom component and making related changes to the YAML.
There is also a non-ESPHome implementation in repository https://github.com/dacarson/NavienManager. It uses the Arduino environment but is still useful as an information source.
My short-term aims are these:
- Only read from the water heater; never write. That's just a safety measure. If something goes wrong, it can't be the case that I sent a command that gave the Navien brain damage. The only thing I'm really interested in commanding is the press of the Hot Button, which I am doing with the tried and true relay technique.
- I want to be able to see when the recirculation pump turns off. That will let me give better user feedback for my remote button presses.
Taking those into account and the current state of the two GitHub repositories, I started with the pure YAML implementation. Further details are given below in the software section.
Re-wiringSomeone in the forum thread who owns a NaviLink unit was able to identify the connector needed for the RS485 interface. Someone also identified a source for the connectors with long pigtail wires for the 5 pins. They are readily available in lengths up to 30 cm.
That's the good news. I got some of those connectors with pigtails. I got the 30 cm length because I knew I could always trim off some excess wire for tidiness. There are two bits of bad news. First, since I wanted to mount my device on the outside of the cabinet, I'd need to feed the cable through the knock-out grommet in the water heater case. That turns out to be about 40 cm from the socket on the control board where the cable plugs in. Second, the conductors inside the pigtail wires that I got are pretty flimsy. It was really fiddly getting them stripped and then willing to stay in place in the screw-down terminals of the RS485 device.
I was able to get things assembled, which at least let me prove that the 12v power supply and the signals on the RS485 interface were as expected. I had to mount my device on the bottom of the cabinet, with wires hanging out and held in place by the cabinet cover's weatherstripping. Yikes, like a barbarian!
Another common use for JST-XH connectors is in battery chargers for remote-controlled cars and other devices. There, the cables are called "balance cables" because of the way they work with multi-cell lithium ion battery packs. As a trap for the unwary, the nomenclature of those cables uses a number that is one less than the number of conductors. So, for a 5-pin cable, it's called "4S". It's typically a single red wire and all the rest are black.
Balance cables are also readily available, but again in relatively short lengths. I didn't find any longer than the 30 cm cable I already had. But you can also buy cables that act as extensions, with a male connector on one end and a female connector on the other end. I bought a few of those in 20 cm length and strung them together to make an 80 cm cable. (When using these for charging batteries, there could very well be some rules about how long they can be, how many you can string together, etc. I don't think any of that applies for RS485 since it's intended to stretch over distances in noisy environments.) On the end toward the RS485 device, it was pretty easy to pull the connector off, leaving the metal pins that were already attached to the individual wires. Those made much better connections to the screw-down terminals of the RS485 device, so I dropped my original pigtail cable out of the picture entirely.
Here is the final, mounted device. The Atom Lite on the RS485 base is on the right, and the relay is on the left. They are connected with a Grove connector cable (the shortest I had on hand). The device is mounted with some rare-earth magnets that I glued to the bottom of the 3D printed base.
For the relay contact wires, I just reached into my junk drawer an pulled out some wires that I thought would be easy to clamp into the screw-down terminal of the relay. They're pretty thick (blue in the photo) even though they need to carry next to no current. That also made them easy to bind to the screw terminals on the Navien control board
Except for possibly pondering some of the additional information reported over the RS485 interface, this project is done. I don't intend to implement any additional controls from my electronics to the water heater.
SoftwareThe software environment operates on the Atom Lite on the remote units, the Atom Lite on the unit on the water heater end, and Home Assistant in the middle to tie things together.
I used ESPHome to configure all of the Atom Lite modules. The configuration for the two remote units differs only in the node names, so I factored out the common elements into a YAML include file.
I wanted all of the feedback to go to all of the devices, and I also wanted to know reliably that the button press had the desired effect. For those reasons, the button press is detected by Home Assistant, and Home Assistant controls the RGB LED.
The button press is simple to configure in ESPHome and is readily detected as a trigger by Home Assistant. The RGB LED control takes a little bit more work. I defined three services on the Atom Lite: set_led_hot, set_led_cold, and set_led_off. That abstracts the device behavior from Home Assistant's intentions when it calls the services. Each service just calls a script to do something with the RGB LED. The Atom boots up as "cold" and is typically in that state. The RGB LED emits a soft and steady blue. The "hot" display is orange-yellow and blinking. The "off" display is obvious, though I don't currently have a use for it. All of the colors, brightness values, and blinking behavior were set by trial and error to get something I liked.
hotbuttonmb.yaml:
substitutions:
node_name: hotbuttonmb
api_key: !secret hotbuttonmb_apikey
<<: !include { file: hotbutton.yaml.inc}
hotbutton.yaml.inc:
esphome:
name: $node_name
on_boot:
then:
# delay to allow other boot time stuff to settle down
- delay: 5s
- script.execute: set_led_cold
esp32:
board: m5stack-atom
framework:
type: arduino
wifi:
networks:
- ssid: !secret wifi_ssid
password: !secret wifi_password
sensor:
- platform: wifi_signal
name: ${node_name} WiFi Signal
logger:
level: DEBUG
ota:
platform: esphome
password: !secret ota_password
api:
encryption:
key: ${api_key}
reboot_timeout: 60min
services:
- service: set_led_hot
then:
- logger.log:
tag: 'hotbutton'
level: INFO
format: "service: set_led_hot"
- script.execute: set_led_hot
- service: set_led_cold
then:
- logger.log:
tag: 'hotbutton'
level: INFO
format: "service: set_led_cold"
- script.execute: set_led_cold
- service: set_led_off
then:
- logger.log:
tag: 'hotbutton'
level: INFO
format: "service: set_led_off"
- script.execute: set_led_off
binary_sensor:
- platform: gpio
pin:
number: GPIO39
inverted: true
name: ${node_name} Button
id: big_button
internal: false
light:
- platform: fastled_clockless
id: bright_light
name: ${node_name} LED
chipset: SK6812
pin: GPIO27
num_leds: 1
rgb_order: GRB # required
default_transition_length: 0s
effects:
- pulse:
name: fast_pulse
transition_length: 0.25s
update_interval: 0.25s
min_brightness: 20%
max_brightness: 99%
script:
- id: set_led_hot
then:
- light.turn_on:
id: bright_light
red: 100%
green: 60%
blue: 0%
effect: fast_pulse
- id: set_led_cold
then:
- light.turn_on:
id: bright_light
color_brightness: 20%
red: 0%
green: 0%
blue: 100%
effect: none
- id: set_led_off
then:
- light.turn_on:
id: bright_light
color_brightness: 0%
red: 0%
green: 0%
blue: 0%
effect: none
Having great frameworks like Home Assistant and ESPHome makes it simple to do simple things. It's like many other IRL infrastructure things that act as multipliers for add-on projects. There are lots of ways to accomplish what I wanted in those frameworks, and I'm using mechanisms that make the most sense to me.
For this project, I've chosen to make Home Assistant automation the centerpiece tying things together. The overall flow looks like this:
- Someone presses one of the buttons.
- Home Assistant detects that button press as a trigger for an automation.
- Home Assistant "presses the button" that momentarily activates the relay.
- The water heater starts the recirculation pump.
- Home Assistant calls the "set_led_hot" service on each Atom Lite device.
- Home Assistant waits for the water flow to stop, with a 2 minute timeout just in case.
- Home Assistant calls the "set_led_cold" service on each Atom device.
My aim has always been to have the remote Atom Lite units blink orange-yellow when the pump is running and stop blinking when the pump stops. Until now, I've been unable to do that, and I've just been using a simple timer that reflects the worst case value. It takes just slightly over 2 minutes for the NaviCirc cross-over valve to close when the water in my pipes starts out cool, so my timer was 2 minutes and 15 seconds. If the water is already hot, the cross-over valve stays closed and the pump hardly runs at all.
With these new RS485 reports, I still have not found a bit that definitively indicates that the recirculation is active. Some people have seen such a bit flip in one of the bytes, but they have different Navien units than I have (I think). That bit position does not change for me. There is a bit that I am using as a proxy for that. The bit indicates if hot water is flowing, which I call "Consumption active" in my sensors. There is also a report of the water flow rate. The bit for consumption active is set whenever hot water flows, which means it may or may not be due to the recirculation pump. If hot water is not actually flowing out of any faucet, shower, or appliance, then that bit is an accurate proxy for the pump.
I expect that, most of the time when the remote buttons are pressed, other hot water is not being used. Sometimes that expectation will not be met. If the dishwasher is running or if someone is washing their hands while waiting for the hot water, that consumption bit will not reset when the pump stops. For now, I'll just live with that, and the 2 minute timeout eventually signals the end.
The ESPHome configuration for the Atom Lite unit at the water heater has the same button and LED feedback implementation as remote units. That's helpful for troubleshooting and research when at the water heater. It also has the implementation of reading and reporting selected values from the RS485 interface from the water heater. For that, it's definitely a case of exploiting the work done earlier by others, though I made many stylistic and functional changes. Several status values are reported, and they show up as entities in Home Assistant, but they are otherwise uninteresting for this project. Pressing the button on that Atom Lite unit does not locally command the activity on the relay. Instead, it reports the button press to Home Assistant, just like the remote units. Home Assistant then signals the relay switch to close. Local logic on the Atom Lite opens that relay switch again after a short delay, simulating a human button press.
Here is the rather long ESPHome configuration for the Atom Lite at the water heater (at some hazy future time, I'll pull out the common parts so I can use the include file that the remote units use):
substitutions:
node_name: hotbuttonrelay-al
RELAY: 'GPIO26'
LED: 'GPIO27'
BUTTON: 'GPIO39'
UART_TX: 'GPIO19'
UART_RX: 'GPIO22'
log_level: 'INFO'
# used in therm calculation
BTU_FACTOR: '1.155563'
esphome:
name: $node_name
on_boot:
then:
# delay to allow other boot time stuff to settle down
- delay: 5s
- script.execute: set_led_cold
esp32:
board: m5stack-atom
framework:
type: arduino
wifi:
networks:
- ssid: !secret wifi_ssid
password: !secret wifi_password
logger:
level: ${log_level}
globals:
# We use these so that we don't have to share the UART receive
# buffer among scripts.
- id: water_packet_bytes
type: std::vector<std::uint8_t>
- id: gas_packet_bytes
type: std::vector<std::uint8_t>
api:
encryption:
key: !secret hotbuttonrelay-al_apikey
reboot_timeout: 60min
services:
- service: set_led_hot
then:
- logger.log:
tag: 'hotbutton'
level: INFO
format: "service: set_led_hot"
- script.execute: set_led_hot
- service: set_led_cold
then:
- logger.log:
tag: 'hotbutton'
level: INFO
format: "service: set_led_cold"
- script.execute: set_led_cold
- service: set_led_off
then:
- logger.log:
tag: 'hotbutton'
level: INFO
format: "service: set_led_off"
- script.execute: set_led_off
ota:
platform: esphome
password: !secret ota_password
switch:
- platform: restart
name: "${node_name} Reboot"
- platform: gpio
id: i_relay
name: '${node_name} relay'
pin: '${RELAY}'
restore_mode: ALWAYS_OFF
icon: mdi:hot-tub
on_turn_on:
- logger.log: "RELAY on"
- light.turn_on:
id: bright_light
red: 0%
green: 100%
blue: 0%
- delay: 500ms
- switch.turn_off: i_relay
- logger.log: "RELAY off"
- light.turn_off:
id: bright_light
light:
- platform: fastled_clockless
id: bright_light
name: ${node_name} LED
chipset: SK6812
pin: '${LED}'
num_leds: 1
rgb_order: GRB # required
default_transition_length: 0s
icon: mdi:led-on
effects:
- pulse:
name: fast_pulse
transition_length: 0.25s
update_interval: 0.25s
min_brightness: 20%
max_brightness: 99%
binary_sensor:
# Local test button. This goes through the relay logic,
# so it's not an analog press of the water heater button.
- platform: gpio
name: "${node_name} Atom Button"
id: i_button
icon: mdi:toggle-switch-off
pin:
number: '${BUTTON}'
inverted: true
mode:
input: true
filters:
- delayed_on: 10ms
- delayed_off: 10ms
# real sensors after this
- platform: template
id: dc_consumption_active
name: "Consumption active"
icon: mdi:check-circle
- platform: template
id: dc_schedule_configured
name: "Schedule configured"
icon: mdi:check-circle
- platform: template
id: dc_recirculation_available
name: "Recirculation available"
icon: mdi:check-circle
sensor:
- platform: template
id: dc_set_temp
name: "Target temperature"
icon: mdi:water-thermometer
filters:
- lambda: return (x/2.0) * (9.0/5.0) + 32.0;
- throttle_average: 2s
unit_of_measurement: "°F"
accuracy_decimals: 1
- platform: template
id: dc_inlet_temp
name: "Inlet temperature"
icon: mdi:water-thermometer
filters:
- lambda: return (x/2.0) * (9.0/5.0) + 32.0;
- throttle_average: 2s
unit_of_measurement: "°F"
accuracy_decimals: 1
- platform: template
id: dc_outlet_temp
name: "Outlet temperature"
icon: mdi:water-thermometer
filters:
- lambda: return (x/2.0) * (9.0/5.0) + 32.0;
- throttle_average: 2s
unit_of_measurement: "°F"
accuracy_decimals: 1
- platform: template
id: dc_inlet_temp_ex
name: "Inlet temperature H/Ex"
icon: mdi:water-thermometer
filters:
- lambda: return (x/2.0) * (9.0/5.0) + 32.0;
- throttle_average: 2s
unit_of_measurement: "°F"
accuracy_decimals: 1
- platform: template
id: dc_outlet_temp_ex
name: "Outlet temperature H/Ex"
icon: mdi:water-thermometer
filters:
- lambda: return (x/2.0) * (9.0/5.0) + 32.0;
- throttle_average: 2s
unit_of_measurement: "°F"
accuracy_decimals: 1
- platform: template
id: dc_operating_capacity
name: "Operating capacity"
icon: mdi:percent-box
filters:
- multiply: 0.5 # for scaling
unit_of_measurement: '%'
accuracy_decimals: 0
- platform: template
id: dc_flow_lpm
name: "Water flow rate"
icon: mdi:water
# convert from liters to gallons (also divide by 10 for scaling)
filters:
- multiply: 0.0264172
unit_of_measurement: GPM
accuracy_decimals: 1
- platform: template
id: dc_accumulated_gas_usage
name: "Gas usage (monthly)"
icon: mdi:gas-burner
# convert from cubic meters to CCF (also divide by 10 for scaling)
filters:
- multiply: 0.0353107345
unit_of_measurement: 'CCF'
accuracy_decimals: 0
- platform: template
id: dc_current_gas_usage
name: "Current gas usage"
icon: mdi:gas-burner
# convert from kcal to BTU
filters:
- multiply: 3.965667
unit_of_measurement: 'BTU/hr'
accuracy_decimals: 1
- platform: template
id: dc_total_operating_time
name: "Operating time"
icon: mdi:calendar
unit_of_measurement: hours
accuracy_decimals: 0
- platform: template
id: dc_accumulated_domestic_usage_cnt
name: "Hot water counter"
icon: mdi:counter
filters:
- multiply: 10
unit_of_measurement: times
accuracy_decimals: 0
- platform: template
id: dc_wbyte8
name: "dc_wbyte8"
icon: mdi:dots-horizontal
unit_of_measurement: 'bits'
accuracy_decimals: 0
- platform: template
id: dc_wbyte9
name: "dc_wbyte9"
icon: mdi:dots-horizontal
unit_of_measurement: 'bits'
accuracy_decimals: 0
- platform: template
id: dc_wbyte19
name: "dc_wbyte19"
icon: mdi:dots-horizontal
unit_of_measurement: 'bits'
accuracy_decimals: 0
- platform: template
id: dc_wbyte3031
name: dc_wbyte3031
unit_of_measurement: times
accuracy_decimals: 0
- platform: template
id: dc_gbyte3334
name: dc_gbyte3334
unit_of_measurement: times
accuracy_decimals: 0
uart:
id: uart_bus
tx_pin: ${UART_TX}
rx_pin: ${UART_RX}
baud_rate: 19200
data_bits: 8
stop_bits: 1
parity: NONE
debug:
direction: BOTH
dummy_receiver: True
after:
delimiter: "\n"
sequence:
- lambda: |-
UARTDebug::log_hex(direction, bytes, ' ');
if (
bytes[0] == 247
&& bytes[1] == 5
&& bytes[2] == 80
&& bytes[3] == 80
&& bytes[4] == 144
&& bytes[5] == 34) {
id(water_packet_bytes) = bytes;
id(decode_water_packet)->execute();
}
else if (
bytes[0] == 247
&& bytes[1] == 5
&& bytes[2] == 80
&& bytes[3] == 15
&& bytes[4] == 144
&& bytes[5] == 42) {
id(gas_packet_bytes) = bytes;
id(decode_gas_packet)->execute();
} else {
// want to log these even if DEBUG is not turned on
ESP_LOGI("rs485", "Navien OTHER packet, type %d-%d-%d, length %d", bytes[2], bytes[3], bytes[4], bytes[5]);
}
script:
- id: set_led_hot
then:
- logger.log: "set_led_hot"
- delay: 600ms # give the relay blink a chance to show
- light.turn_on:
id: bright_light
red: 100%
green: 60%
blue: 0%
effect: fast_pulse
- id: set_led_cold
then:
- logger.log: "set_led_cold"
- light.turn_on:
id: bright_light
color_brightness: 20%
red: 0%
green: 0%
blue: 100%
effect: none
- id: set_led_off
then:
- logger.log: "set_led_off"
- light.turn_on:
id: bright_light
color_brightness: 0%
red: 0%
green: 0%
blue: 0%
effect: none
- id: decode_water_packet
mode: parallel
max_runs: 1
then:
- logger.log: "Navien WATER packet ^^^^"
- lambda: |-
std::vector<uint8_t>& bytes = id(water_packet_bytes);
byte w_byte8 = bytes[8];
byte w_byte9 = bytes[9];
byte w_set_temp = bytes[11];
byte w_outlet_temp = bytes[12];
byte w_inlet_temp = bytes[13];
byte w_system_status = bytes[24];
byte w_operating_capacity = bytes[17];
byte w_water_flow = bytes[18];
// could byte 19 be a high byte that pairs with low byte 18?
byte w_byte19 = bytes[19];
byte w_recirculation_enabled = bytes[33];
byte w_byte3031_lo = bytes[30];
byte w_byte3031_hi = bytes[31];
uint32_t w_byte3031 = (w_byte3031_hi << 8 | w_byte3031_lo);
bool consumption_active = (w_byte8 & 0x20);
float set_temp = w_set_temp;
float outlet_temp = w_outlet_temp;
float inlet_temp = w_inlet_temp;
bool schedule_configured = (w_system_status & 0x2) ? true : false;
float operating_capacity = w_operating_capacity; // 0.5 increments
float flow_lpm = w_water_flow;
bool recirculation_available = (w_recirculation_enabled & 0x2) ? true : false;
id(dc_wbyte8).publish_state(w_byte8);
id(dc_consumption_active).publish_state(consumption_active);
id(dc_set_temp).publish_state(set_temp);
id(dc_outlet_temp).publish_state(outlet_temp);
id(dc_inlet_temp).publish_state(inlet_temp);
id(dc_schedule_configured).publish_state(schedule_configured);
id(dc_operating_capacity).publish_state(operating_capacity);
id(dc_flow_lpm).publish_state(flow_lpm);
id(dc_recirculation_available).publish_state(recirculation_available);
id(dc_wbyte9).publish_state(w_byte9);
id(dc_wbyte19).publish_state(w_byte19);
id(dc_wbyte3031).publish_state(w_byte3031);
- id: decode_gas_packet
mode: parallel
max_runs: 1
then:
#- logger.log: "Navien GAS packet ^^^^"
- lambda: |-
std::vector<uint8_t>& bytes = id(gas_packet_bytes);
byte g_set_temp = bytes[14];
byte g_outlet_temp = bytes[15];
byte g_inlet_temp = bytes[16];
byte g_cumulative_gas_lo = bytes[24];
byte g_cumulative_gas_hi = bytes[25];
byte g_current_gas_lo = bytes[22];
byte g_current_gas_hi = bytes[23];
byte g_total_operating_time_lo = bytes[36];
byte g_total_operating_time_hi = bytes[37];
byte g_cumulative_domestic_usage_cnt_lo = bytes[30];
byte g_cumulative_domestic_usage_cnt_hi = bytes[31];
byte g_byte3334_lo = bytes[33];
byte g_byte3334_hi = bytes[34];
uint32_t g_byte3334 = (g_byte3334_hi << 8 | g_byte3334_lo);
float set_temp = g_set_temp;
float outlet_temp = g_outlet_temp;
float inlet_temp = g_inlet_temp;
uint32_t raw_gas = (g_cumulative_gas_hi << 8 | g_cumulative_gas_lo);
float accumulated_gas_usage = raw_gas;
float current_gas_usage = g_current_gas_hi << 8 | g_current_gas_lo;
uint32_t raw_time = (g_total_operating_time_hi << 8 | g_total_operating_time_lo);
uint32_t total_operating_time = raw_time;
uint32_t raw_usage = (g_cumulative_domestic_usage_cnt_hi << 8 | g_cumulative_domestic_usage_cnt_lo);
uint32_t accumulated_domestic_usage_cnt = raw_usage;
id(dc_set_temp).publish_state(set_temp);
id(dc_outlet_temp_ex).publish_state(outlet_temp);
id(dc_inlet_temp_ex).publish_state(inlet_temp);
id(dc_accumulated_gas_usage).publish_state(accumulated_gas_usage);
id(dc_current_gas_usage).publish_state(current_gas_usage);
id(dc_total_operating_time).publish_state(total_operating_time);
id(dc_accumulated_domestic_usage_cnt).publish_state(accumulated_domestic_usage_cnt);
id(dc_gbyte3334).publish_state(g_byte3334);
Here an abbreviated look at the entities for this Atom Lite in Home Assistant:
There is a simple Home Assistant automation that is triggered by any of the Atom Lite buttons being pressed.
Here is the YAML for that automation:
alias: Tankless hot button
description: Press the button, get hot water
triggers:
- entity_id:
- binary_sensor.hotbuttonkb_button
from: "off"
to: "on"
trigger: state
- entity_id:
- binary_sensor.hotbuttonmb_button
from: "off"
to: "on"
trigger: state
- trigger: state
entity_id:
- binary_sensor.hotbuttonrelay_al_atom_button
from: "off"
to: "on"
conditions: []
actions:
- data: {}
action: script.tankless_button_cycle
mode: parallel
max: 10
The complex action is factored out into a separate Home Assistant script. Here's the graphical view of that "Tankless button cycle" script:
and the YAML for that script:
alias: Tankless button cycle
sequence:
- type: turn_on
device_id: 7c8657a0b747b7584ec3775223c14045
entity_id: ea0d9e3eb5631aa2876a1112f6c8c8cc
domain: switch
- parallel:
- data: {}
action: esphome.hotbuttonkb_set_led_hot
continue_on_error: true
- data: {}
action: esphome.hotbuttonmb_set_led_hot
continue_on_error: true
- action: esphome.hotbuttonrelay_al_set_led_hot
data: {}
continue_on_error: true
- delay:
hours: 0
minutes: 0
seconds: 5
milliseconds: 0
- wait_template: "{{not states('binary_sensor.consumption_active')|bool}}"
continue_on_timeout: true
timeout: "00:02:00"
- parallel:
- data: {}
action: esphome.hotbuttonkb_set_led_cold
continue_on_error: true
- data: {}
action: esphome.hotbuttonmb_set_led_cold
continue_on_error: true
- action: esphome.hotbuttonrelay_al_set_led_cold
data: {}
continue_on_error: true
mode: restart
icon: mdi:hot-tub
description: ""
ConclusionThis hardware iteration has been in use for a couple of months now. The remote units operate the same way they did on the previous hardware iteration, except that they more accurately indicate the end of the recirculation cycle. So, for other household members, it's not much of a visible change.
The hardware at the water heater end is a much tidier bundle than the previous iteration, which makes me happy even though I seldom actually catch a glimpse of it.
I had originally thought of tracking some of the reported metrics from the RS485 interface for the usual obsessive smart home reasons. Maybe I'll do that at some future point. For now, I'm content to bask in the warm glow of the on-demand hot water.
Comments