March 6, 20242 yr Hi all, Thought I'd share my journey in creating an accurate water level measuring system for my Jojo tanks. I've finally managed to find a solution which is about 99.6% accurate. This equates to a water height difference of +-5mm between the actual height in the tank vs what the probe is reading. I'd call this a success 🙃 Some of the other ESP32 + sensor solutions which I've tried and given up on: Waterproof ultrasonic sensor - difficult to implement as the minimum distance between the sensor and the water level is +-20CM if I recall correctly. This meant that I would need to 3D print a cone to raise the ultrasonic sensor and cut a large hole in the Jojo Tank. Plus the issues of condensation on the sensor. Analog water pressure sensor - this was attached to the bottom of the outlet of the tank. The readings were all over the place and I'd see random drops for no apparent reason so I gave up on this. Throw in pressure sensor + ADS1115 A/D Module + voltage regulation - this was probably the longest standing solution that I had in place. The results were very erratic and I'd see random fluctuations for no apparent reason. Landed up having to re-calibrate multiple times - definitely something was wrong. After advice from others on the Home Assistant Forum, I tried a solution which was suggested by others and it's been spot on and much easier to implment! Hardware: ESP32 24V Power Supply DC - DC Voltage Regulator DF Robot Throw in submersible pressure sensor INA219 Current Sensor USB Power Supply Misc conduit and enclosure 2 Measurements were taken to calibrate: 1: when the sensor was not inside the tank 2: when the sensor was in the tank and the tank was full (manual float switch had stopped filling). As seen in the code below: float full_voltage = 0.00092; // Voltage at full condition float empty_voltage = 0.00042; // Voltage at empty condition & float max_full_reading = 9.2; // Adjust the maximum reading at full water level float min_empty_reading = 4.2; // Adjust the minimum reading at empty water level As there are 2 tanks and only 1 sensor, the total volume is multiplied by 2. Both tank levels equalize when they're full and are thus same. YAML: captive_portal: i2c: sda: 21 scl: 22 sensor: - platform: ina219 address: 0x40 shunt_resistance: 0.1 ohm current: name: "INA219 Current" id: ina_current accuracy_decimals: 5 filters: - multiply: 1000 #convert from Amps to mA unit_of_measurement: "mA" power: name: "INA219 Power" accuracy_decimals: 5 bus_voltage: name: "INA219 Bus Voltage" accuracy_decimals: 2 shunt_voltage: name: "INA219 Shunt Voltage" id: ina219_shunt_voltage accuracy_decimals: 5 max_voltage: 32.0V max_current: 400mA update_interval: 10s - platform: template name: "Water Level" id: water_level unit_of_measurement: 'm' accuracy_decimals: 3 update_interval: 60s lambda: |- float shunt_voltage = id(ina219_shunt_voltage).state; float full_voltage = 0.00092; // Voltage at full condition float empty_voltage = 0.00042; // Voltage at empty condition float max_height = 1.570; // Adjust the max height as needed // Calculate water level based on calibration float slope = (max_height - 0.0) / (full_voltage - empty_voltage); float intercept = max_height - slope * full_voltage; float water_level = slope * shunt_voltage + intercept; // Ensure water level is within bounds water_level = water_level < 0.0 ? 0.0 : water_level; water_level = water_level > max_height ? max_height : water_level; ESP_LOGD("custom", "Shunt Voltage: %.5f V", id(ina219_shunt_voltage).state); ESP_LOGD("custom", "Water Level: %.5f m", water_level); return water_level; - platform: template name: "Current Full Percentage" id: current_full_percentage unit_of_measurement: '%' accuracy_decimals: 1 update_interval: 60s lambda: |- float max_full_reading = 9.2; // Adjust the maximum reading at full water level float min_empty_reading = 4.2; // Adjust the minimum reading at empty water level float current_reading = id(ina_current).state; if (current_reading <= min_empty_reading) { return 0.0; } else if (current_reading >= max_full_reading) { return 100.0; } else { float current_full_percentage = 100 * (current_reading - min_empty_reading) / (max_full_reading - min_empty_reading); return current_full_percentage; } - platform: template name: "Tank Volume" id: tank_volume unit_of_measurement: "litres" lambda: |- float tank_radius = 0.71; // Adjust the tank radius as needed float max_height = 1.57; // Adjust the max height as needed float total_volume_single_tank = 3.14159265 * tank_radius * tank_radius * max_height * 1000; float percentage_full = id(current_full_percentage).state; float water_level = max_height * (percentage_full / 100.0); float current_volume_single_tank = 3.14159265 * tank_radius * tank_radius * water_level * 1000; float total_volume_both_tanks = 2 * current_volume_single_tank; return total_volume_both_tanks; We had a water outage today so I had an opportunity to test the sensor value (height) vs the actual height with a tape measure and I was off by 5mm, so I'll take this as a success. Hope this is useful to you guys - shout if I can answer any questions
March 7, 20242 yr Sensor looks a bit cheaper on Digikey https://www.digikey.co.za/en/products/detail/dfrobot/KIT0139/10279708
March 7, 20242 yr 1 hour ago, Sc00bs said: Sensor looks a bit cheaper on Digikey https://www.digikey.co.za/en/products/detail/dfrobot/KIT0139/10279708 You still have to pay for import tax and shipping, or order through the local digikey re-distributor and pay their markup. I guess it would be cheaper if you order enough things from digikey and get free shipping.
March 7, 20242 yr Author 10 hours ago, Sc00bs said: Sensor looks a bit cheaper on Digikey https://www.digikey.co.za/en/products/detail/dfrobot/KIT0139/10279708 Found it cheaper for you from DIY Electronics
August 21, 20241 yr Author Added this to the code above which gives a number of days left of water left based on a daily usage of 1100l per day: - platform: template name: "Days of Water Left" id: days_of_water_left unit_of_measurement: "days" accuracy_decimals: 1 update_interval: 60s lambda: |- float total_volume = id(tank_volume).state; float daily_usage = 1100.0; // 1100 liters per day float days_left = total_volume / daily_usage; // Cap the maximum days to 5 when tanks are full if (days_left > 5.0) { return 5.0; } return std::max(0.0f, days_left); New sensor: sensor.days_of_water_left
August 21, 20241 yr Looks great! How about to ditch te 1100L constant and use derivate over last X days instead?
August 21, 20241 yr Author 1 minute ago, Youda said: Looks great! How about to ditch te 1100L constant and use derivate over last X days instead? Thanks! Been busy testing that actually
January 17, 20251 yr Hi, I like it a lot! I bought all the parts and performed the installation. I am struggling quite a bit. I'm using this in a 10 000L JoJo Tank and am getting confusing readings. Please see the attached screenshots. Below is my code (I did calibrate the readings): esphome: name: jojo-1 friendly_name: JoJo 1 esp32: board: nodemcu-32s # Enable logging logger: # Enable Home Assistant API api: encryption: key: "uxBTnmmzUw6yiCtJxGYd26Qqoiivswu8GHfUO8Xr+3U=" ota: - platform: esphome password: "620d601b87d0f7bebb1365ae886fd934" wifi: ssid: password: manual_ip: static_ip: 192.168.0.26 subnet: 255.255.255.0 gateway: 192.168.0.254 web_server: captive_portal: i2c: sda: 21 scl: 22 sensor: - platform: ina219 address: 0x45 shunt_resistance: 0.1 ohm current: name: "INA219 Current" id: ina_current accuracy_decimals: 5 filters: - multiply: 1000 #convert from Amps to mA unit_of_measurement: "mA" power: name: "INA219 Power" accuracy_decimals: 5 bus_voltage: name: "INA219 Bus Voltage" accuracy_decimals: 2 shunt_voltage: name: "INA219 Shunt Voltage" id: ina219_shunt_voltage accuracy_decimals: 5 max_voltage: 32.0V max_current: 400mA update_interval: 10s - platform: template name: "Water Level" id: water_level unit_of_measurement: 'm' accuracy_decimals: 3 update_interval: 60s lambda: |- float shunt_voltage = id(ina219_shunt_voltage).state; float full_voltage = 0.00012; // Voltage at full condition float empty_voltage = 0.00004; // Voltage at empty condition float max_height = 2.7; // Adjust the max height as needed // Calculate water level based on calibration float slope = (max_height - 0.0) / (full_voltage - empty_voltage); float intercept = max_height - slope * full_voltage; float water_level = slope * shunt_voltage + intercept; // Ensure water level is within bounds water_level = water_level < 0.0 ? 0.0 : water_level; water_level = water_level > max_height ? max_height : water_level; ESP_LOGD("custom", "Shunt Voltage: %.5f V", id(ina219_shunt_voltage).state); ESP_LOGD("custom", "Water Level: %.5f m", water_level); return water_level; - platform: template name: "Current Full Percentage" id: current_full_percentage unit_of_measurement: '%' accuracy_decimals: 1 update_interval: 60s lambda: |- float max_full_reading = 12; // Adjust the maximum reading at full water level float min_empty_reading = 4; // Adjust the minimum reading at empty water level float current_reading = id(ina_current).state; if (current_reading <= min_empty_reading) { return 0.0; } else if (current_reading >= max_full_reading) { return 100.0; } else { float current_full_percentage = 100 * (current_reading - min_empty_reading) / (max_full_reading - min_empty_reading); return current_full_percentage; } - platform: template name: "Tank Volume" id: tank_volume unit_of_measurement: "litres" lambda: |- float tank_radius = 1.1; // Adjust the tank radius as needed float max_height = 2.7; // Adjust the max height as needed float total_volume_single_tank = 3.14159265 * tank_radius * tank_radius * max_height * 1000; float percentage_full = id(current_full_percentage).state; float water_level = max_height * (percentage_full / 100.0); float current_volume_single_tank = 3.14159265 * tank_radius * tank_radius * water_level * 1000; return total_volume_single_tank; Can someone kindly please assist? Kind regards, Robbie
January 17, 20251 yr Author 31 minutes ago, Robbie3337 said: Hi, I like it a lot! I bought all the parts and performed the installation. I am struggling quite a bit. I'm using this in a 10 000L JoJo Tank and am getting confusing readings. Please see the attached screenshots. Below is my code (I did calibrate the readings): esphome: name: jojo-1 friendly_name: JoJo 1 esp32: board: nodemcu-32s # Enable logging logger: # Enable Home Assistant API api: encryption: key: "uxBTnmmzUw6yiCtJxGYd26Qqoiivswu8GHfUO8Xr+3U=" ota: - platform: esphome password: "620d601b87d0f7bebb1365ae886fd934" wifi: ssid: password: manual_ip: static_ip: 192.168.0.26 subnet: 255.255.255.0 gateway: 192.168.0.254 web_server: captive_portal: i2c: sda: 21 scl: 22 sensor: - platform: ina219 address: 0x45 shunt_resistance: 0.1 ohm current: name: "INA219 Current" id: ina_current accuracy_decimals: 5 filters: - multiply: 1000 #convert from Amps to mA unit_of_measurement: "mA" power: name: "INA219 Power" accuracy_decimals: 5 bus_voltage: name: "INA219 Bus Voltage" accuracy_decimals: 2 shunt_voltage: name: "INA219 Shunt Voltage" id: ina219_shunt_voltage accuracy_decimals: 5 max_voltage: 32.0V max_current: 400mA update_interval: 10s - platform: template name: "Water Level" id: water_level unit_of_measurement: 'm' accuracy_decimals: 3 update_interval: 60s lambda: |- float shunt_voltage = id(ina219_shunt_voltage).state; float full_voltage = 0.00012; // Voltage at full condition float empty_voltage = 0.00004; // Voltage at empty condition float max_height = 2.7; // Adjust the max height as needed // Calculate water level based on calibration float slope = (max_height - 0.0) / (full_voltage - empty_voltage); float intercept = max_height - slope * full_voltage; float water_level = slope * shunt_voltage + intercept; // Ensure water level is within bounds water_level = water_level < 0.0 ? 0.0 : water_level; water_level = water_level > max_height ? max_height : water_level; ESP_LOGD("custom", "Shunt Voltage: %.5f V", id(ina219_shunt_voltage).state); ESP_LOGD("custom", "Water Level: %.5f m", water_level); return water_level; - platform: template name: "Current Full Percentage" id: current_full_percentage unit_of_measurement: '%' accuracy_decimals: 1 update_interval: 60s lambda: |- float max_full_reading = 12; // Adjust the maximum reading at full water level float min_empty_reading = 4; // Adjust the minimum reading at empty water level float current_reading = id(ina_current).state; if (current_reading <= min_empty_reading) { return 0.0; } else if (current_reading >= max_full_reading) { return 100.0; } else { float current_full_percentage = 100 * (current_reading - min_empty_reading) / (max_full_reading - min_empty_reading); return current_full_percentage; } - platform: template name: "Tank Volume" id: tank_volume unit_of_measurement: "litres" lambda: |- float tank_radius = 1.1; // Adjust the tank radius as needed float max_height = 2.7; // Adjust the max height as needed float total_volume_single_tank = 3.14159265 * tank_radius * tank_radius * max_height * 1000; float percentage_full = id(current_full_percentage).state; float water_level = max_height * (percentage_full / 100.0); float current_volume_single_tank = 3.14159265 * tank_radius * tank_radius * water_level * 1000; return total_volume_single_tank; Can someone kindly please assist? Kind regards, Robbie So 3 things that I noticed when compared to my code: - your ina219 address is 0x45 - mine is 0x40 - I also noticed that your calibration numbers seem very low - perhaps adjust the point above and below and try take new calibrations. - the float max_height is only 1 decimal place - perhaps make this 3 Here's my code in case : captive_portal: i2c: sda: 21 scl: 22 sensor: - platform: ina219 address: 0x40 shunt_resistance: 0.1 ohm current: name: "INA219 Current" id: ina_current accuracy_decimals: 5 filters: - multiply: 1000 #convert from Amps to mA unit_of_measurement: "mA" power: name: "INA219 Power" accuracy_decimals: 5 bus_voltage: name: "INA219 Bus Voltage" accuracy_decimals: 2 shunt_voltage: name: "INA219 Shunt Voltage" id: ina219_shunt_voltage accuracy_decimals: 5 max_voltage: 32.0V max_current: 400mA update_interval: 10s - platform: template name: "Water Level" id: water_level unit_of_measurement: 'm' accuracy_decimals: 3 update_interval: 60s lambda: |- float shunt_voltage = id(ina219_shunt_voltage).state; float full_voltage = 0.00092; // Voltage at full condition float empty_voltage = 0.00042; // Voltage at empty condition float max_height = 1.570; // Adjust the max height as needed // Calculate water level based on calibration float slope = (max_height - 0.0) / (full_voltage - empty_voltage); float intercept = max_height - slope * full_voltage; float water_level = slope * shunt_voltage + intercept; // Ensure water level is within bounds water_level = std::max(0.0f, std::min(water_level, max_height)); ESP_LOGD("custom", "Shunt Voltage: %.5f V", shunt_voltage); ESP_LOGD("custom", "Water Level: %.5f m", water_level); return water_level; - platform: template name: "Current Full Percentage" id: current_full_percentage unit_of_measurement: '%' accuracy_decimals: 1 update_interval: 60s lambda: |- float max_full_reading = 9.2; // Adjust the maximum reading at full water level float min_empty_reading = 4.2; // Adjust the minimum reading at empty water level float current_reading = id(ina_current).state; float current_full_percentage = 100.0f * (current_reading - min_empty_reading) / (max_full_reading - min_empty_reading); return std::max(0.0f, std::min(current_full_percentage, 100.0f)); - platform: template name: "Tank Volume" id: tank_volume unit_of_measurement: "litres" lambda: |- constexpr float tank_radius = 0.71; // Adjust the tank radius as needed constexpr float max_height = 1.57; // Adjust the max height as needed constexpr float pi = 3.14159265f; constexpr float total_volume_single_tank = pi * tank_radius * tank_radius * max_height * 1000; float percentage_full = id(current_full_percentage).state; float water_level = max_height * (percentage_full / 100.0f); float current_volume_single_tank = pi * tank_radius * tank_radius * water_level * 1000; float total_volume_both_tanks = 2 * current_volume_single_tank; return total_volume_both_tanks; - platform: template name: "Days of Water Left" id: days_of_water_left unit_of_measurement: "days" accuracy_decimals: 1 update_interval: 60s lambda: |- float total_volume = id(tank_volume).state; float daily_usage = 1100.0; // 1100 liters per day float days_left = total_volume / daily_usage; // Cap the maximum days to 5 when tanks are full if (days_left > 5.0) { return 5.0; } return std::max(0.0f, days_left); - platform: template name: "Daily Water Consumption Rate" id: daily_water_consumption unit_of_measurement: "L/day" accuracy_decimals: 1 update_interval: 3600s # Update hourly lambda: |- static float last_volume = id(tank_volume).state; static unsigned long last_update = 0; float current_volume = id(tank_volume).state; unsigned long current_time = millis(); float consumption_rate = 0; if (last_update > 0) { float volume_change = last_volume - current_volume; float days_elapsed = (current_time - last_update) / (1000.0 * 60 * 60 * 24); consumption_rate = volume_change / days_elapsed; } last_volume = current_volume; last_update = current_time; return std::max(0.0f, consumption_rate); - platform: template name: "Tank Refill Rate" id: tank_refill_rate unit_of_measurement: "L/hour" accuracy_decimals: 1 update_interval: 300s # Update every 5 minutes lambda: |- static float last_volume = id(tank_volume).state; static unsigned long last_update = 0; float current_volume = id(tank_volume).state; unsigned long current_time = millis(); float refill_rate = 0; if (last_update > 0 && current_volume > last_volume) { float volume_change = current_volume - last_volume; float hours_elapsed = (current_time - last_update) / (1000.0 * 60 * 60); refill_rate = volume_change / hours_elapsed; } last_volume = current_volume; last_update = current_time; return refill_rate; - platform: template name: "Time to Empty" id: time_to_empty unit_of_measurement: "hours" accuracy_decimals: 1 update_interval: 60s lambda: |- float current_volume = id(tank_volume).state; float consumption_rate = id(daily_water_consumption).state / 24.0; // L/hour if (consumption_rate > 0) { return current_volume / consumption_rate; } else { return 0; // Avoid division by zero } - platform: template name: "Time to Full" id: time_to_full unit_of_measurement: "hours" accuracy_decimals: 1 update_interval: 60s lambda: |- float current_volume = id(tank_volume).state; float max_volume = 4973.0; // Maximum volume of both tanks float refill_rate = id(tank_refill_rate).state; if (refill_rate > 0) { return (max_volume - current_volume) / refill_rate; } else { return 0; // Not currently filling } - platform: template name: "Water Usage Efficiency" id: water_usage_efficiency unit_of_measurement: "%" accuracy_decimals: 1 update_interval: 3600s # Update hourly lambda: |- float actual_consumption = id(daily_water_consumption).state; float expected_consumption = 1100.0; // Expected daily usage if (expected_consumption > 0) { float efficiency = (expected_consumption - actual_consumption) / expected_consumption * 100; return std::min(100.0f, std::max(0.0f, efficiency)); } else { return 0; } - platform: template name: "Water Self-Sufficiency Duration" id: water_self_sufficiency unit_of_measurement: "days" accuracy_decimals: 1 lambda: |- float total_volume = id(tank_volume).state; float actual_consumption = id(daily_water_consumption).state; if (actual_consumption > 0) { return total_volume / actual_consumption; } else { return id(days_of_water_left).state; // Fallback to the standard calculation } Edited January 17, 20251 yr by Muttley typo
January 18, 20251 yr 14 hours ago, Muttley said: So 3 things that I noticed when compared to my code: - your ina219 address is 0x45 - mine is 0x40 - I also noticed that your calibration numbers seem very low - perhaps adjust the point above and below and try take new calibrations. - the float max_height is only 1 decimal place - perhaps make this 3 Here's my code in case : captive_portal: i2c: sda: 21 scl: 22 sensor: - platform: ina219 address: 0x40 shunt_resistance: 0.1 ohm current: name: "INA219 Current" id: ina_current accuracy_decimals: 5 filters: - multiply: 1000 #convert from Amps to mA unit_of_measurement: "mA" power: name: "INA219 Power" accuracy_decimals: 5 bus_voltage: name: "INA219 Bus Voltage" accuracy_decimals: 2 shunt_voltage: name: "INA219 Shunt Voltage" id: ina219_shunt_voltage accuracy_decimals: 5 max_voltage: 32.0V max_current: 400mA update_interval: 10s - platform: template name: "Water Level" id: water_level unit_of_measurement: 'm' accuracy_decimals: 3 update_interval: 60s lambda: |- float shunt_voltage = id(ina219_shunt_voltage).state; float full_voltage = 0.00092; // Voltage at full condition float empty_voltage = 0.00042; // Voltage at empty condition float max_height = 1.570; // Adjust the max height as needed // Calculate water level based on calibration float slope = (max_height - 0.0) / (full_voltage - empty_voltage); float intercept = max_height - slope * full_voltage; float water_level = slope * shunt_voltage + intercept; // Ensure water level is within bounds water_level = std::max(0.0f, std::min(water_level, max_height)); ESP_LOGD("custom", "Shunt Voltage: %.5f V", shunt_voltage); ESP_LOGD("custom", "Water Level: %.5f m", water_level); return water_level; - platform: template name: "Current Full Percentage" id: current_full_percentage unit_of_measurement: '%' accuracy_decimals: 1 update_interval: 60s lambda: |- float max_full_reading = 9.2; // Adjust the maximum reading at full water level float min_empty_reading = 4.2; // Adjust the minimum reading at empty water level float current_reading = id(ina_current).state; float current_full_percentage = 100.0f * (current_reading - min_empty_reading) / (max_full_reading - min_empty_reading); return std::max(0.0f, std::min(current_full_percentage, 100.0f)); - platform: template name: "Tank Volume" id: tank_volume unit_of_measurement: "litres" lambda: |- constexpr float tank_radius = 0.71; // Adjust the tank radius as needed constexpr float max_height = 1.57; // Adjust the max height as needed constexpr float pi = 3.14159265f; constexpr float total_volume_single_tank = pi * tank_radius * tank_radius * max_height * 1000; float percentage_full = id(current_full_percentage).state; float water_level = max_height * (percentage_full / 100.0f); float current_volume_single_tank = pi * tank_radius * tank_radius * water_level * 1000; float total_volume_both_tanks = 2 * current_volume_single_tank; return total_volume_both_tanks; - platform: template name: "Days of Water Left" id: days_of_water_left unit_of_measurement: "days" accuracy_decimals: 1 update_interval: 60s lambda: |- float total_volume = id(tank_volume).state; float daily_usage = 1100.0; // 1100 liters per day float days_left = total_volume / daily_usage; // Cap the maximum days to 5 when tanks are full if (days_left > 5.0) { return 5.0; } return std::max(0.0f, days_left); - platform: template name: "Daily Water Consumption Rate" id: daily_water_consumption unit_of_measurement: "L/day" accuracy_decimals: 1 update_interval: 3600s # Update hourly lambda: |- static float last_volume = id(tank_volume).state; static unsigned long last_update = 0; float current_volume = id(tank_volume).state; unsigned long current_time = millis(); float consumption_rate = 0; if (last_update > 0) { float volume_change = last_volume - current_volume; float days_elapsed = (current_time - last_update) / (1000.0 * 60 * 60 * 24); consumption_rate = volume_change / days_elapsed; } last_volume = current_volume; last_update = current_time; return std::max(0.0f, consumption_rate); - platform: template name: "Tank Refill Rate" id: tank_refill_rate unit_of_measurement: "L/hour" accuracy_decimals: 1 update_interval: 300s # Update every 5 minutes lambda: |- static float last_volume = id(tank_volume).state; static unsigned long last_update = 0; float current_volume = id(tank_volume).state; unsigned long current_time = millis(); float refill_rate = 0; if (last_update > 0 && current_volume > last_volume) { float volume_change = current_volume - last_volume; float hours_elapsed = (current_time - last_update) / (1000.0 * 60 * 60); refill_rate = volume_change / hours_elapsed; } last_volume = current_volume; last_update = current_time; return refill_rate; - platform: template name: "Time to Empty" id: time_to_empty unit_of_measurement: "hours" accuracy_decimals: 1 update_interval: 60s lambda: |- float current_volume = id(tank_volume).state; float consumption_rate = id(daily_water_consumption).state / 24.0; // L/hour if (consumption_rate > 0) { return current_volume / consumption_rate; } else { return 0; // Avoid division by zero } - platform: template name: "Time to Full" id: time_to_full unit_of_measurement: "hours" accuracy_decimals: 1 update_interval: 60s lambda: |- float current_volume = id(tank_volume).state; float max_volume = 4973.0; // Maximum volume of both tanks float refill_rate = id(tank_refill_rate).state; if (refill_rate > 0) { return (max_volume - current_volume) / refill_rate; } else { return 0; // Not currently filling } - platform: template name: "Water Usage Efficiency" id: water_usage_efficiency unit_of_measurement: "%" accuracy_decimals: 1 update_interval: 3600s # Update hourly lambda: |- float actual_consumption = id(daily_water_consumption).state; float expected_consumption = 1100.0; // Expected daily usage if (expected_consumption > 0) { float efficiency = (expected_consumption - actual_consumption) / expected_consumption * 100; return std::min(100.0f, std::max(0.0f, efficiency)); } else { return 0; } - platform: template name: "Water Self-Sufficiency Duration" id: water_self_sufficiency unit_of_measurement: "days" accuracy_decimals: 1 lambda: |- float total_volume = id(tank_volume).state; float actual_consumption = id(daily_water_consumption).state; if (actual_consumption > 0) { return total_volume / actual_consumption; } else { return id(days_of_water_left).state; // Fallback to the standard calculation } Thank you so much for your swift reply! Regarding the ina219 sensor - I could not find the one that you linked in stock, so I bought this one instead. According to this wiki, I matched the address with the DIP switch configuration. I added a "0" after the "2.7" Yesterday after your reply, and I think that was the problem. I'm still monitoring, but it looks positive, thank you! I'm busy draining the tank to recalibrate everything. I will keep you posted. Do you maybe know why my "Tank Volume" and "Current Full Percentage" aren't showing any data? Again, thank you so much!
January 18, 20251 yr Author 7 hours ago, Robbie3337 said: Thank you so much for your swift reply! Regarding the ina219 sensor - I could not find the one that you linked in stock, so I bought this one instead. According to this wiki, I matched the address with the DIP switch configuration. I added a "0" after the "2.7" Yesterday after your reply, and I think that was the problem. I'm still monitoring, but it looks positive, thank you! I'm busy draining the tank to recalibrate everything. I will keep you posted. Do you maybe know why my "Tank Volume" and "Current Full Percentage" aren't showing any data? Again, thank you so much! No worries - glad you got sorted, I'm sure your INA219 is good! NB: no need to drain the tank - just pull out the probe and take it a reading. Because it's only 2 calibrations points, full and empty and thus you can just take an empty reading with the probe out the tank and one when it's full, ie: at the bottom of the tank when it's full. Regarding the missing values, I forgot to mention that you'll need to add in a template sensor to your configuration.yaml file: template: - sensor: - name: "Water Tank Level" unique_id: water_tank_level state: > {% set current = states('sensor.ina219_current') | float(default=0) %} {% set height = ((current * 5.0663) - 24.86) | round(2) %} {% if 0.000 <= height <= 72.000 %} {{ height }} {% else %} unknown {% endif %} I completely forgot the logic for the above ^ so just use claude.ai - share your esp32 code with it and this and ask it to make the necessary changes. Or even ask it to explain the above to you. 🙃 Oh and also, chances are that your Jojo tank holds more than 10 000L! Edited January 18, 20251 yr by Muttley added extra info
January 19, 20251 yr 13 hours ago, Muttley said: No worries - glad you got sorted, I'm sure your INA219 is good! NB: no need to drain the tank - just pull out the probe and take it a reading. Because it's only 2 calibrations points, full and empty and thus you can just take an empty reading with the probe out the tank and one when it's full, ie: at the bottom of the tank when it's full. Regarding the missing values, I forgot to mention that you'll need to add in a template sensor to your configuration.yaml file: template: - sensor: - name: "Water Tank Level" unique_id: water_tank_level state: > {% set current = states('sensor.ina219_current') | float(default=0) %} {% set height = ((current * 5.0663) - 24.86) | round(2) %} {% if 0.000 <= height <= 72.000 %} {{ height }} {% else %} unknown {% endif %} I completely forgot the logic for the above ^ so just use claude.ai - share your esp32 code with it and this and ask it to make the necessary changes. Or even ask it to explain the above to you. 🙃 Oh and also, chances are that your Jojo tank holds more than 10 000L! Thanks! I'll test and report back.
January 19, 20251 yr Author 6 hours ago, Robbie3337 said: Thanks! I'll test and report back. The joys of open source software 🤨 Turns out, I had a look at my dashboard a few moments ago and both Volume and Percentage we're missing yet I did absolutely nothing - no fiddling, I promise 🙃 Updated my ESP32 to the latest version and boom... everything was back. Maybe this is part of the issue you're having.
January 20, 20251 yr On 2025/01/19 at 3:56 PM, Muttley said: The joys of open source software 🤨 Turns out, I had a look at my dashboard a few moments ago and both Volume and Percentage we're missing yet I did absolutely nothing - no fiddling, I promise 🙃 Updated my ESP32 to the latest version and boom... everything was back. Maybe this is part of the issue you're having. Haha, you said it 🤣 I honestly hoped it was my problem, but sadly after updating, it did not work. It's as if the tank volume is just static. 3 Things bothering me are: My calibration value outside of the tank is indeed 0.00004V - I bought the one from DIY Electronics. I have attached a spreadsheet (history (9).csv) of the reading last year when I first connected the device. After copying your new code and adding the template sensor in the "configuration.yaml" - I still cannot get the Tank Volume & Current Full % to work. In the Developer Tools, I get an "unknown" output when testing the code for the template sensor. I attached the sensor ID I have as well as the states that Home Assistant is reading. The "Shunt Voltage" graph seemed to work fine, but after all the fiddling, it seems like I'm back to square one. Please see the "Correct" and "Incorrect" graph screenshots attached. I'm sorry to trouble you so much! I do appreciate your help though. history (9).csv
January 20, 20251 yr Author 1 hour ago, Robbie3337 said: Haha, you said it 🤣 I honestly hoped it was my problem, but sadly after updating, it did not work. It's as if the tank volume is just static. 3 Things bothering me are: My calibration value outside of the tank is indeed 0.00004V - I bought the one from DIY Electronics. I have attached a spreadsheet (history (9).csv) of the reading last year when I first connected the device. After copying your new code and adding the template sensor in the "configuration.yaml" - I still cannot get the Tank Volume & Current Full % to work. In the Developer Tools, I get an "unknown" output when testing the code for the template sensor. I attached the sensor ID I have as well as the states that Home Assistant is reading. The "Shunt Voltage" graph seemed to work fine, but after all the fiddling, it seems like I'm back to square one. Please see the "Correct" and "Incorrect" graph screenshots attached. I'm sorry to trouble you so much! I do appreciate your help though. history (9).csv 103 B · 0 downloads They said it'll be simple Ok so a few more things come to mind: When I had the issue yesterday, I noticed that I was not getting any values for INA219 Power (W) or and Values for INA219 Current (mA). We need these values to get the tank volume and the tank %. The way that I got this to work was a simple reboot of the 24V power supply which powers the INA219 and the probe. I then started to get W and mA data coming in. Your example from above is showing the exact same scenario as I had: I think the above is step 1 in troubleshooting but I also noticed a few other things: I also noticed a few more things in your esphome code which I've updated based on your original figures shared and adjusted for a single tank like yours: - platform: template name: "Tank Volume" id: tank_volume unit_of_measurement: "litres" lambda: |- constexpr float tank_radius = 1.10; // Adjust the tank radius as needed constexpr float max_height = 2.70; // Adjust the max height as needed constexpr float pi = 3.14159265f; constexpr float total_volume = pi * tank_radius * tank_radius * max_height * 1000; float percentage_full = id(current_full_percentage).state; float water_level = max_height * (percentage_full / 100.0f); float current_volume = pi * tank_radius * tank_radius * water_level * 1000; return current_volume; Your max height seems quite low compared to what it's listed on the Jojo Website - they state that the height is 3150mm and you're only accounting for a height of 2700mm. Did you put a tape measure into the tank to the bottom with water inside to check? I think try the above steps and lets see what it comes up with. You may also need to reboot the ESP32 - I think the sequence I use is: power off probe, power of ESP32, power on probe, power on ESP32.
January 22, 20251 yr On 2025/01/20 at 8:55 PM, Muttley said: They said it'll be simple Ok so a few more things come to mind: When I had the issue yesterday, I noticed that I was not getting any values for INA219 Power (W) or and Values for INA219 Current (mA). We need these values to get the tank volume and the tank %. The way that I got this to work was a simple reboot of the 24V power supply which powers the INA219 and the probe. I then started to get W and mA data coming in. Your example from above is showing the exact same scenario as I had: I think the above is step 1 in troubleshooting but I also noticed a few other things: I also noticed a few more things in your esphome code which I've updated based on your original figures shared and adjusted for a single tank like yours: - platform: template name: "Tank Volume" id: tank_volume unit_of_measurement: "litres" lambda: |- constexpr float tank_radius = 1.10; // Adjust the tank radius as needed constexpr float max_height = 2.70; // Adjust the max height as needed constexpr float pi = 3.14159265f; constexpr float total_volume = pi * tank_radius * tank_radius * max_height * 1000; float percentage_full = id(current_full_percentage).state; float water_level = max_height * (percentage_full / 100.0f); float current_volume = pi * tank_radius * tank_radius * water_level * 1000; return current_volume; Your max height seems quite low compared to what it's listed on the Jojo Website - they state that the height is 3150mm and you're only accounting for a height of 2700mm. Did you put a tape measure into the tank to the bottom with water inside to check? I think try the above steps and lets see what it comes up with. You may also need to reboot the ESP32 - I think the sequence I use is: power off probe, power of ESP32, power on probe, power on ESP32. Haha yes they did 🤣 I did a reboot on my side and my power and current values were restored the same as yours, thanks! It happened twice since your reply, so the second time I added a button to reboot the ESP32 to be able to reboot it remotely which also works. It seems like the jaggered graph is a result of whenever the current and power values go down to 0 or unavailable, because when I reboot the ESP, then the graph begins to normalize after the values restored. The reason I used 2700mm as the height is due to the spec sheet. It seems that the overflow level is at that level. Please see the picture attached. Why do you think that my probe reading outside of the tank has an extra "0" compared to yours? After making the change as per your recommendation in the "Tank Volume" template section in the code, I still cannot get the "Current Full %" and "Tank Volume" readings to work. It just stays at a constant level. I asked claude.ai to adapt the template sensor code to my existing ESPHome code and this is what it came up with: template: - sensor: - name: "Water Level Template" unique_id: water_level_template unit_of_measurement: "m" state: > {% set current = states('sensor.jojo_1_ina219_current') | float(default=0) %} {% set height = ((current - 4) / (12 - 4) * 2.70) | round(3) %} {% if 0.000 <= height <= 2.700 %} {{ height }} {% else %} unknown {% endif %} Does it look fine? Here is my latest ESPHome configuration: web_server: captive_portal: button: - platform: restart name: "JoJo1 Restart" i2c: sda: 21 scl: 22 sensor: - platform: ina219 address: 0x45 shunt_resistance: 0.1 ohm current: name: "INA219 Current" id: ina_current accuracy_decimals: 5 filters: - multiply: 1000 #convert from Amps to mA unit_of_measurement: "mA" power: name: "INA219 Power" accuracy_decimals: 5 bus_voltage: name: "INA219 Bus Voltage" accuracy_decimals: 2 shunt_voltage: name: "INA219 Shunt Voltage" id: ina219_shunt_voltage accuracy_decimals: 5 max_voltage: 32.0V max_current: 400mA update_interval: 10s - platform: template name: "Water Level" id: water_level unit_of_measurement: 'm' accuracy_decimals: 3 update_interval: 60s lambda: |- float shunt_voltage = id(ina219_shunt_voltage).state; float full_voltage = 0.00012; // Voltage at full condition float empty_voltage = 0.00004; // Voltage at empty condition float max_height = 2.70; // Adjust the max height as needed // Calculate water level based on calibration float slope = (max_height - 0.0) / (full_voltage - empty_voltage); float intercept = max_height - slope * full_voltage; float water_level = slope * shunt_voltage + intercept; // Ensure water level is within bounds water_level = water_level < 0.0 ? 0.0 : water_level; water_level = water_level > max_height ? max_height : water_level; ESP_LOGD("custom", "Shunt Voltage: %.5f V", id(ina219_shunt_voltage).state); ESP_LOGD("custom", "Water Level: %.5f m", water_level); return water_level; - platform: template name: "Current Full Percentage" id: current_full_percentage unit_of_measurement: '%' accuracy_decimals: 1 update_interval: 60s lambda: |- float max_full_reading = 12; // Adjust the maximum reading at full water level float min_empty_reading = 4; // Adjust the minimum reading at empty water level float current_reading = id(ina_current).state; if (current_reading <= min_empty_reading) { return 0.0; } else if (current_reading >= max_full_reading) { return 100.0; } else { float current_full_percentage = 100 * (current_reading - min_empty_reading) / (max_full_reading - min_empty_reading); return current_full_percentage; } - platform: template name: "Tank Volume" id: tank_volume unit_of_measurement: "litres" lambda: |- constexpr float tank_radius = 1.10; // Adjust the tank radius as needed constexpr float max_height = 2.70; // Adjust the max height as needed constexpr float pi = 3.14159265f; constexpr float total_volume = pi * tank_radius * tank_radius * max_height * 1000; float percentage_full = id(current_full_percentage).state; float water_level = max_height * (percentage_full / 100.0f); float current_volume = pi * tank_radius * tank_radius * water_level * 1000; return current_volume; Please let me know if you need any additional information Thanks! 😁
January 23, 20251 yr Author Hey, At least we've still got our humour, but we'll get this right eventually So I think we should go back to basics here - I've previously tried using some DF Robot Components and I find them to be at times over engineered and just caused frustration and total randomness in terms of the data. EG: I purchased one of their ADS1115 modules and got terrible data - purchased a generic one and it was perfect. Can I suggest you purchase another INA219, which is like mine from PiShop or Communica ? I think this will remove any additional "calibration" etc that the DF Robot requires and we can use most of my code which works most of the time 🤣 Once this is sorted, we can dive into the code and see where's we're at - I still think that your mV reading is too low but I suspect the suggestion above will fix this.
January 23, 20251 yr I think it's good to have a bit of humour while we struggle with these wonderful devices. It helps taking the focus off of the frustration a bit. But yes, I definitely agree. We will get this right eventually 😂 Thanks for the tip on the DF Robot sensor! I will buy the generic INA219 as per one of your links. I agree with your approach to diagnosing the issue. It will probably only arrive early next week, so I will post back when the new INA219 has been installed. Thanks again!
January 23, 20251 yr Author 1 minute ago, Robbie3337 said: I think it's good to have a bit of humour while we struggle with these wonderful devices. It helps taking the focus off of the frustration a bit. But yes, I definitely agree. We will get this right eventually 😂 Thanks for the tip on the DF Robot sensor! I will buy the generic INA219 as per one of your links. I agree with your approach to diagnosing the issue. It will probably only arrive early next week, so I will post back when the new INA219 has been installed. Thanks again! We apologise for the frustation caused, please continue to hold while we wait for your parts to arrive Let me know when the INA219 arrives and the struggle shall continue 🤓
January 23, 20251 yr LOL, I really hope that won't be the case 🤣🤣 I will surely do so, thank you so much! 😁
January 30, 20251 yr Just an update, I have received the INA219 from PiShop and will be replacing the DFRobot this weekend 😁
February 21, 20251 yr Hi Muttley and Robbie I'll be attempting this project over the weekend, as the ultrasonic sensors are proving to be absolute rubbish inside the tanks. I have 2x ECO Slimline 2220lt tanks, but will initially only use the DF Robot solution in one tank. Problem with these slimline tanks are that they have two cylinders, which join together towards the top, so I'm not sure on which formula to use in the yaml file. Do you guys perhaps have any ideas?
February 21, 20251 yr Author 1 hour ago, stormjp said: Hi Muttley and Robbie I'll be attempting this project over the weekend, as the ultrasonic sensors are proving to be absolute rubbish inside the tanks. I have 2x ECO Slimline 2220lt tanks, but will initially only use the DF Robot solution in one tank. Problem with these slimline tanks are that they have two cylinders, which join together towards the top, so I'm not sure on which formula to use in the yaml file. Do you guys perhaps have any ideas? Good luck on the weekend project - it seems it's going to be slightly different to my setup due to the joined cylinders but I plugged my code into Claude.ai and asked it to provide instructions and described your tank as best as possible. I think it's going to be a bit of a trial and error - here's the code and instructions: # Configuration for dual connected cylindrical water tanks # This configuration assumes two tanks connected at approximately 75% height esphome: name: tankvolume # [Previous WiFi, API, and basic configuration remains the same] sensor: # INA219 sensor configuration remains the same as it's a single probe - platform: ina219 # [Previous INA219 configuration remains unchanged] - platform: template name: "Water Level" id: water_level unit_of_measurement: 'm' accuracy_decimals: 3 update_interval: 60s lambda: |- # CALIBRATION INSTRUCTIONS: # 1. Measure and record the shunt voltage when tanks are empty # 2. Measure and record the shunt voltage when tanks are full # 3. Update these values below: float shunt_voltage = id(ina219_shunt_voltage).state; float full_voltage = 0.00092; // UPDATE: Measure and set your full tank voltage float empty_voltage = 0.00042; // UPDATE: Measure and set your empty tank voltage float max_height = 1.570; // UPDATE: Set your tank height in meters // Calculate water level based on calibration float slope = (max_height - 0.0) / (full_voltage - empty_voltage); float intercept = max_height - slope * full_voltage; float water_level = slope * shunt_voltage + intercept; return std::max(0.0f, std::min(water_level, max_height)); - platform: template name: "Tank Volume" id: tank_volume unit_of_measurement: "litres" lambda: |- // TANK CONFIGURATION PARAMETERS // UPDATE THESE VALUES: constexpr float tank_radius = 0.71; // Set your tank radius in meters constexpr float tank_height = 1.57; // Set total tank height constexpr float connection_height = 1.18; // Set height of connection (75% of total height) constexpr float pi = 3.14159265f; float water_level = id(water_level).state; float total_volume = 0.0f; if (water_level <= connection_height) { // Below connection: Calculate as two separate cylinders total_volume = 2 * (pi * tank_radius * tank_radius * water_level * 1000); } else { // Above connection: Calculate as combined volume // First, volume up to connection float lower_volume = 2 * (pi * tank_radius * tank_radius * connection_height * 1000); // Then add combined volume above connection float upper_volume = pi * tank_radius * tank_radius * (water_level - connection_height) * 2000; total_volume = lower_volume + upper_volume; } return total_volume; # [Rest of the sensors remain largely the same, but update these values:] - platform: template name: "Days of Water Left" id: days_of_water_left unit_of_measurement: "days" lambda: |- float total_volume = id(tank_volume).state; float daily_usage = 1100.0; // UPDATE: Set your expected daily usage in liters float days_left = total_volume / daily_usage; // UPDATE: Adjust maximum days based on your total capacity if (days_left > 7.0) { // Change based on your maximum capacity return 7.0; } return std::max(0.0f, days_left); - platform: template name: "Time to Full" id: time_to_full unit_of_measurement: "hours" lambda: |- float current_volume = id(tank_volume).state; // UPDATE: Calculate your maximum volume based on your tank dimensions float max_volume = 2 * (pi * 0.71 * 0.71 * 1.57 * 1000); // Update with your dimensions float refill_rate = id(tank_refill_rate).state; if (refill_rate > 0) { return (max_volume - current_volume) / refill_rate; } else { return 0; } Here are the key steps for the user to implement this configuration: Physical Measurements Needed: Measure the radius of both tanks Measure the total height of the tanks Measure the height where the tanks are connected (approximately 75% of total height) Calibration Steps: Install the INA219 sensor and connect it to the ESP32 With tanks empty, note the shunt voltage reading With tanks full, note the shunt voltage reading Update these values in the "Water Level" sensor configuration Key Configuration Updates: In the Tank Volume sensor: Update tank_radius with your measured radius Update tank_height with your total tank height Update connection_height with the height where tanks are connected Usage Configuration: Update daily_usage in the "Days of Water Left" sensor with your expected daily water usage Adjust the maximum days cap based on your total tank capacity The main difference in this configuration compared to your original is the volume calculation that accounts for the connected tanks. When the water level is: Below the connection: It calculates volume as two separate cylinders Above the connection: It calculates the combined volume, treating the space above the connection as one larger cylinder
February 21, 20251 yr Hi Muttley Thank you for this - Claude and I are still struggling with each other, maybe because of my very Afrikaans-like English. I've gone through the schematics and cannot figure out the reason for DC-DC voltage regulator, as both the INA219 and the DF Robot can accept 24V input? What voltage setting do you use on the 'out' on the DC-DC voltage regulator then? The 24V power supply I got have two DC outputs, so I'll use a buck converter to power the ESP32 from there, but can I not connect the INA219 and DF Robot directly to the 24V power supply? I'm a little confused And also, can you cut the cable and tube on the DF Robot shorter, or should it remain at the full 5m length?
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.