Wow that thing is pricey if it’s just an ESP and ToF sensor. I don’t know if a nice case is worth $130.
i agree, but i priced out making a custom pcb with the components and the printing & build of the enclosure and realized that it’s just easier to pay for the ezsalt hardware. spending $100 won’t change my life and the hardware build is actually really nice.
@weswitt Thank you for sharing! While I like their build, the price is too high… but from your post I see they used a VL53L0X time of flight distance sensor which I was unaware of! I made a very cheap sensor using an ESP and an ultrasonic sensor. The only drawback is that the sensor cannot be fully enclosed in its case so it is getting a bit corroded but it is just a few dollars and it has been running for at least 2 years. The other issue, which I could possibly mitigate in software but don’t really care, is that the speed of sound changes with temperature which may be what is causing the fluctuations in the graph shown below. The peaks seem to match the daily temperature excursion here in Texas. I could definitely reduce the data I take > 100x and more which would likely hide the issue but it works so I won’t mess with it for now.
Note: The levels shown are just meant to match what the Water Softener shows and the numbered bar inside it. When I get to the yellow zone, the toggle (that I should just hide or turn into just a boolean indicator) turns on and other things happen in HA that serve to remind me to buy salt.
As for the case and PCB I used very cheap stuff I had on hand:
For version 2, I will consider the VL53L0X time of flight distance sensor however I need to verify that it can actually do 1m as my salt container is over 1m depending on where I install the sensor. The current sensor is installed 97cm above the bottom so just 3cm from the ToF’s sensor limit (at least according to Adafruit):
@weswitt - You inspired me and I built v2 using the same sensor in the device you used. This version is now powered by the water softener’s power supply (24Vac) and in the future (just wiring up needed), I might be able to detect when the ws runs as I included 2 opto isolated inputs, one of which I plan on connecting to a switch that gets triggered when the valve is turned during operation. Except for the galvanized bracket, everything is stainless steel or covered up (black paint on small screws) to prevent salt induced corrosion that I had on v1. The TOF sensor required me to cut a hole in the lid which I was hoping not to need doing but it didn’t work through the transparent lid. Luckily I purchased a TOF sensor that comes with its own cover so that opening it sealed too. Fuse is just to prevent shorting the power supply in case there is a meltdown in my build.
The one thing that puzzles me is that I seem to be getting an error of -9cm in my readings (-3.5 inches) whole the sensor was quite accurate when I tested it at my desk. How accurate is yours?
great work. i think mine is also off in terms of accuracy. my theory is that the textured surface of the salt confuses the sensor. my idea to improve this is to put a piece of plywood or plastic on top of the salt for reflection.
Hi, I build my own hardware as well. Adding a derivative helper sensor (on HA, not supported by esphome) allows to track current regeneration phase. I explained it here How can I track water softener schedule (using ESPHome and Ultrasonic) - #4 by lcavalli
Looks great.
I have a bunch of TOF sensors left over from another project.
Would you mind sharing the wiring diagram and ESP code so that I can see what I can cobble together.
Thank you so much
@carltonwb - Here is the code however it is customized to my Whirlpool water softener so the levels, percentage, distance of sensor to bottom are all things you may have to change. My goal was to have ESPhome output the same thing shown on the water softener’s screen.
@weswitt has posted code at the very top that is much easier to implement as you only have to make minor changes to adjust it to your water softener.
substitutions:
devicename: water-softener
friendly_devicename: "Water Softener"
device_description: "Water Softener with TOF200C sensor"
#Distance between bottom of tank and lower edge of lip where sensor is attached.
#Edge lip bottom to the bottom of canisater is 96.5cm but sensor is a bit lower down so about 95cm?
#Value in cm as sensor readings are converted to cm right away
distance_sensor_to_bottom: "95.0"
#Whirlpool WHES48 has 9 markers starting from 0. 8th can/should be ignored as it is too far up
#Distance from bottom to top of level number
top_lvl_7: "79.5" #100%
top_lvl_6: "70.5" #89
top_lvl_5: "61.0" #77
top_lvl_4: "51.5" #65
top_lvl_3: "42.0" #53
top_lvl_2: "32.5" #41
top_lvl_1: "22.5" #28
top_lvl_0: "12.5" #16
#Set to 4hrs. Needs to be very long otherwise it overrides the frequency set in the main sensor
update_interval_s: "60s"
#Only reason not to set it very long it for wifi troubleshooting
update_interval_wifi: "120s"
distance_calibration: "-0.085" #In meters. Must be the salt as outside the tank was more accurate
esphome:
name: ${devicename}
comment: ${device_description}
friendly_name: ${friendly_devicename}
esp32:
board: lolin_s2_mini
variant: esp32s2
framework:
type: esp-idf
version: recommended
wifi:
ssid: !secret iot_wifi_ssid
password: !secret iot_wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "${devicename}"
password: !secret iot_wifi_password
#Faster than DHCP. Also use if can't reach because of name change
manual_ip:
static_ip: 10.1.3.200
gateway: 10.1.2.1
subnet: 255.255.254.0
dns1: 10.1.0.50
dns2: 10.1.0.51
#Manually override what address to use to connect to the ESP.
#Defaults to auto-generated value. Example, if you have changed your
#static IP and want to flash OTA to the previously configured IP address.
use_address: 10.1.3.200
logger:
#baud_rate: 0 #disabled
# Enable Home Assistant API
api:
encryption:
key: "bEjyWk4O0mgiWhwcPzPXkT6o45M3MZNIlsTtc4gBgHk="
ota:
web_server:
port: 80
include_internal: true
# Sync time with Home Assistant
time:
- platform: homeassistant
id: ha_time
text_sensor:
- platform: wifi_info
ip_address:
name: "IP"
icon: "mdi:ip-outline"
update_interval: ${update_interval_wifi}
dns_address:
name: "DNS"
icon: "mdi:dns-outline"
update_interval: ${update_interval_wifi}
ssid:
name: "SSID"
icon: "mdi:wifi-settings"
update_interval: ${update_interval_wifi}
bssid:
name: "BSSID"
icon: "mdi:wifi-settings"
update_interval: ${update_interval_wifi}
mac_address:
name: "MAC"
icon: "mdi:network-outline"
scan_results:
name: "Wifi Scan"
icon: "mdi:wifi-refresh"
update_interval: ${update_interval_wifi}
disabled_by_default: true
i2c:
- id: bus_a
sda: 16
scl: 18
scan: true
frequency: 800khz
binary_sensor:
- platform: status
name: "Status"
id: connection_status
entity_category: diagnostic
- platform: gpio
pin:
number: 7
mode:
input: true
pulldown: true
name: Input 1
- platform: gpio
pin:
number: 9
mode:
input: true
pulldown: true
name: Input 2
sensor:
- platform: wifi_signal
name: "WiFi Signal"
update_interval: ${update_interval_wifi}
device_class: signal_strength
- platform: vl53l0x
name: "Sensor to salt"
icon: "mdi:format-vertical-align-bottom"
address: 0x29
update_interval: 3s
long_range: true
timeout: 200us
id: sensor_to_salt_cm
state_class: measurement
unit_of_measurement : "cm"
accuracy_decimals: 1
filters:
- filter_out: nan
- offset: ${distance_calibration}
- sliding_window_moving_average:
window_size: 20 #Both 60 and 120 seem to not affect the cadence, however I would use a smaller number to get rid of outliers sooner
send_every: 20 #Works out to 10 minutes but not sure why
send_first_at: 1
- lambda: return (x*100); #convert from m to cm
on_value:
- sensor.template.publish:
id: saltLevel
state: !lambda 'return ${distance_sensor_to_bottom} - id(sensor_to_salt_cm).state ;'
- sensor.template.publish:
id: saltTankLevel #Percentage
state: !lambda |-
x = (id(saltLevel).state / ${top_lvl_7})*100;
ESP_LOGD("DEBUG", "saltLevel / top_lvl_7 = %f", x);
return x;
- sensor.template.publish:
id: saltTankLevelNumber
#Numbers are not equally spaced likely due to widening shape of tank as you go up so
#can't use simple formula as the one below:
state: !lambda |-
if (id(saltLevel).state <= ${top_lvl_0}) {
x = 0;
}
else if ((id(saltLevel).state > ${top_lvl_0}) and (id(saltLevel).state <= ${top_lvl_1})) {
x = 1;
}
else if ((id(saltLevel).state > ${top_lvl_1}) and (id(saltLevel).state <= ${top_lvl_2})) {
x = 2;
}
else if ((id(saltLevel).state > ${top_lvl_2}) and (id(saltLevel).state <= ${top_lvl_3})) {
x = 3;
}
else if ((id(saltLevel).state > ${top_lvl_3}) and (id(saltLevel).state <= ${top_lvl_4})) {
x = 4;
}
else if ((id(saltLevel).state > ${top_lvl_4}) and (id(saltLevel).state <= ${top_lvl_5})) {
x = 5;
}
else if ((id(saltLevel).state > ${top_lvl_5}) and (id(saltLevel).state <= ${top_lvl_6})) {
x = 6;
}
else if ((id(saltLevel).state > ${top_lvl_6}) and (id(saltLevel).state <= ${top_lvl_7})) {
x = 7;
}
else if (id(saltLevel).state > ${top_lvl_7}) {
x = 8;
ESP_LOGD("ALERT", "SALT TANK TOO FULL! LEVEL 7 is 100");
}
return x;
#Salt Level
- platform: template
id: saltLevel
name: "Salt Level"
icon: "mdi:arrow-expand-vertical"
#lambda: return ${distance_sensor_to_bottom} - id(sensor_to_salt_cm).state ;
state_class: measurement
unit_of_measurement: "cm"
accuracy_decimals: 1
update_interval: ${update_interval_s}
#Salt Tank Fill Percentage
- platform: template
id: saltTankLevel
name: "Salt Tank Level"
icon: "mdi:arrow-expand-vertical"
state_class: measurement
unit_of_measurement: "%"
accuracy_decimals: 0
update_interval: ${update_interval_s}
# lambda: |-
# auto d = (id(saltLevel).state / ${top_lvl_7})*100;
# ESP_LOGD("DEBUG", "top_lvl_7 / saltLevel = %f", d);
# return d;
#Manufacturer specific Tank Fill Level
- platform: template
id: saltTankLevelNumber
name: "Salt Tank Level Number"
icon: "mdi:arrow-expand-vertical"
#lambda: return (${distance_sensor_to_bottom} - id(sensor_to_salt_cm).state) / ${cm_per_salt_level_marker};
state_class: measurement
unit_of_measurement: "Lvl"
accuracy_decimals: 0
update_interval: ${update_interval_s}
filters:
- filter_out: nan
button:
- platform: safe_mode
name: "Restart (Safe Mode)"
entity_category: diagnostic
- platform: restart
name: "Restart"
entity_category: diagnostic
@lcavalli - Thanks for sharing your code in the other thread. Unfortunately, I cannot see the brine as it never covers the salt so I cannot use that to detect in what phase it is. There is a switch that I can use to detect when the valve turns but I need to find a way to make the connections without modifying the water softener’s original wiring… or maybe I will as it is quite old by now so who cares… I can also see a few extra watts of power consumption when the valve motor is on so technically I can already see when it is active but I wanted the same ESP device to tell me everything and since I had many available GPIO… (Luca, sono di Roma ma vivo in USA…ciao!)
Would you be willing to share the config file you flashed and how? TIA!
Here you go. You may need to adjust the height based on your softener container height, but I think they’re pretty standard.
esp8266:
name: "**yourname**"
friendly_name: "**yourfriendlyname**"
board: d1_mini
wifi:
ssid: "**yourssid**"
password: "**youroassword**"
logger:
baud_rate: 0
api:
ota:
web_server:
port: 80
i2c:
sda: D6
scl: D7
scan: True
button:
- platform: restart
name: Restart
text_sensor:
- platform: wifi_info
ip_address:
name: "IP Address"
id: ip_address
icon: mdi:ip-network
ssid:
name: SSID
id: ssid
icon: mdi:wifi
sensor:
- platform: uptime
name: Uptime
- platform: wifi_signal
name: "WiFi Signal"
id: wifi_signal_strength
update_interval: 60s
icon: mdi:wifi-strength-1
- platform: vl53l0x
name: "Salt Level"
id: salt_level
update_interval: 10s
address: 0x29
unit_of_measurement: "Inches"
filters:
- multiply: 39.37 # convert from mm to inches
- offset: -35 # height of salt container
- multiply: -1 # back to a positive value
Thanks! But what do I do with this? (Be nice. LOL)
Using the instructions here you need to install EspHome:
https://esphome.io/guides/installing_esphome
Then you need to compile the YAML file: “esphome compile yaml-file”
Then flash the compiled firmware to your device: “esphome upload yaml-file”
Got my EZ today. Tried following the instructions here for Linux. However, I’m using HA on a Pi5 with the HAOS and these instructions don’t work. Missing GCC (which I resolved) and then missing Python.h file. Trying esphome on the command line produces “file not found” even after I export the correct PATH. So I reverted to installing ESPhome from here instead: Getting Started with ESPHome and Home Assistant — ESPHome (I chose ESP8266) and the install worked. Now I’m just stuck what to do next to 1) compile the yaml you provided and 2) getting it on to the EZ.
I got this and flashed it with your code. Admittedly, I’m an HA NOOB. Would you mind posting the code for your card? I’m unsure how to get it to link up. I apologize for being so new - BUT I learn real fast once I see an example. TIA!
@aruffell
I’m going to build my Sensor inspired by your Vers.2 with the TOF Sensor.
Just one question, what is the reason for the two (1/4W?) resistors at your green PCB?
@gohakn - If I recall correctly, they are pull ups or pull downs (can’t recall) for me to read 1 or 2 external switches part of the water softener. I believe you can activate baked in pull ups / pull downs in some ESPs so I can’t recall why I went this route instead. Anyhow, the intent was to read when the water softener runs but I never implemented that part.
The black component under the regulator board is a rectifier bridge as this is powered by the 24Vac of the WS.
I just looked at the latest EZ Salt 3.0 linked by OP and am somewhat shocked they used a USB connector to power it. In my v1 build all metal parts exposed inside the tank got corroded after some time so just imagine what may happen to that connector. Even if the contacts on both the connector and the plug were gold plated I would still not trust it over time… plus it is likely an opening to the inside of the enclosure.
Did you face any Problems with the TOF Sensor beeing exposed to the salty environment until now?
I’ve made a small 3D Printed case for the D1 mini, Buck Converter and the Sensor ( whenever it will arrive from China ) which will sit on top of the softeners tank, and so I just have to drill a small hole through the lid for measuring.
I’ve had mine for 3 years and have had zero issues. No corrosion at all. None.
So I picked up the EZSalt 3 based on this post. The problem is, when it got here, I couldn’t get my mac or my son’s windows computer to see the esp board in order to flash it. As it turns out, the issue was most likely the USB cable the sensor came with doesn’t support data, but during my frustration, I found a post from maybe a year ago where the guy who created the EZSalt sensor was hyping it up and said “you don’t have to use our software - it runs tasmota, so you can point it to whatever MQTT broker you want”…
I had no idea what tasmota or MQTT was, but I remember seeing MQTT in my HA Yellow somewhere - turns out, I did need their app in order to connect the sensor to my wifi, but once I did that, I was able to log into the local web server on the device (part of tasmota) and point it to the MQTT broker I setup on HA. From there, I was able to see the sensor in HA BUT there was a problem.
The sensor sends distance data in mm but states its in cm - that means the resulting distance is 10x bigger than it should be. Also, because I’m not using their app, the data is just distance. For me to turn it into a meaningful dashboard item, I needed to convert the distance into the proper units and into a meaningful number (amount of salt left, instead of distance from sensor to salt.
I created a helper sensor to address both - and now have a dashboard that looks very similar to yours.