Thanks for the help everyone, I have the compensation sensors working now.
For anyone interested in doing the same here’s the process I’m using to calculate time to heat my hot water tank (or immersion as we call it in the UK).
For me I use 2 temperature probe sensors touching the tank, one close to the top and the other at the bottom. You don’t need to use 2, but more sensors may be more accurate.
I specifically use this one, but any kind of probe could be fine:
Then I turn on the immersion for a while and watch the temperature increase in homeassistant. I’m able to control my immersion through homeassistant, but being able to control is not necessary to calculate time to heat.
Once it’s increased enough, I select the heat data history and export it in homeassistant to a CSV file.
It should look like below:
entity_id,state,last_changed
sensor.hot_water_tank_top_temperature_sensor_temperature,17.5,2025-05-08T14:30:00.000Z
sensor.hot_water_tank_top_temperature_sensor_temperature,18.5,2025-05-08T14:33:27.261Z
sensor.hot_water_tank_top_temperature_sensor_temperature,19.8,2025-05-08T14:36:26.922Z
sensor.hot_water_tank_top_temperature_sensor_temperature,21,2025-05-08T14:39:26.553Z
sensor.hot_water_tank_top_temperature_sensor_temperature,22.1,2025-05-08T14:41:26.313Z
...
From there I want to convert that data to something readable by a compensation sensor. To do this I wrote a quick python script:
"""
Python script that converts an exported homeassistant CSV
into compensation sensor list.
Takes 2 args:
1. The name of the input CSV file.
2. The desired max temperature of the compensation sensor.
"""
import csv
from datetime import datetime
import sys
# Define the end time as a datetime object
end_time = None
# Open and read the CSV file
with open(sys.argv[1], mode='r') as file:
reader = csv.reader(file)
# Skip the header row
next(reader)
# Remove temperatures higher than desired temperature
filtered_list = filter(lambda x: float(x[1]) <= float(sys.argv[2]), reader)
# Sort the list by date
sorted_list = sorted(filtered_list,key=lambda x: x[2], reverse=True)
# Iterate over each row in the CSV file
for index, row in enumerate(sorted_list):
_entity_id, state, last_changed = row
last_changed_time = datetime.strptime(last_changed, "%Y-%m-%dT%H:%M:%S.%fZ")
if index == 0:
end_time = last_changed_time
# Calculate the difference between starendt time and last changed time
diff_seconds = (end_time - last_changed_time).total_seconds()
print (f" - [{state}, {diff_seconds}]")
So I would just call this script like so:
python3 convert.py history.csv 38.8
The 38.8
above is the temperature I want to achieve.
Specifically I have found that 38.8°C is the temperature that the bottom temperature sensor reads when the tank is just hot enough to fill a bath.
This number will be different for anyone else trying to set this up themselves, and can only be figured out by running a bath a few times and finding out what the minimum temperature you can fill a bath at is.
If you have multiple temperature sensors, you will have to run this script again for the other sensors.
For me, I like the top temperature sensor to read 46.8°C and the bottom to read 38.8°C to be sure the bath is hot enough so I’ll run the script twice like so:
python3 convert.py top_sensor_history.csv 46.8
python3 convert.py bottom_sensor_history.csv 38.8
I also did some testing and found out my ideal temperature to take a shower, which for me was 38°C at the top and 30°C at the bottom. So again I run the scripts:
python3 convert.py top_sensor_history.csv 38.8
python3 convert.py bottom_sensor_history.csv 30
These scripts all end up producing some yaml like so:
- [17.5, 5040.43]
- [18.5, 4833.169]
- [19.8, 4653.508]
- [21, 4473.877]
- [22.1, 4354.117]
- [23.1, 4234.338]
- [24.1, 4114.588]
...
I just have to copy that yaml into my compensation sensors homeassistant config so I end up with this:
hot_water_tank_bottom_bath_compensation:
source: sensor.hot_water_tank_bottom_temperature_sensor_temperature
unique_id: hot_water_tank_bottom_bath_compensation
data_points:
# First Value: Temperature
# Second value: Seconds away from target temp (38.8)
- [17.2, 5723.929]
- [18.2, 5453.238]
- [19.2, 5213.101]
- [20.2, 4913.558]
- [21.3, 4671.924]
- [22.5, 4374.367]
- [23.5, 4134.724]
- [24.5, 3895.076]
- [25.5, 3655.427]
- [26.7, 3355.889]
- [27.7, 3115.754]
- [28.8, 2816.211]
- [30, 2516.665]
- [31.1, 2217.118]
- [32.2, 1917.565]
- [33.2, 4045.995]
- [34.5, 1677.934]
- [35.7, 1018.44]
- [36.7, 718.895]
- [37.8, 419.355]
- [38.8, 0]
hot_water_tank_bottom_shower_compensation:
source: sensor.hot_water_tank_bottom_temperature_sensor_temperature
unique_id: hot_water_tank_bottom_shower_compensation
data_points:
# First Value: Temperature
# Second value: Seconds away from target temp (30)
- [17.2, 3207.264]
- [18.2, 2936.573]
- [19.2, 5213.101]
- [20.2, 2696.436]
- [21.3, 2155.259]
- [22.5, 1857.702]
- [23.5, 1618.059]
- [24.5, 1378.411]
- [25.5, 1138.762]
- [26.7, 839.224]
- [27.7, 599.089]
- [28.8, 299.546]
- [30, 0]
hot_water_tank_top_bath_compensation:
source: sensor.hot_water_tank_top_temperature_sensor_temperature
unique_id: hot_water_tank_top_bath_compensation
data_points:
# First Value: Temperature
# Second value: Seconds away from target temp (46.8)
- [17.5, 5040.43]
- [18.5, 4833.169]
- [19.8, 4653.508]
- [21, 4473.877]
- [22.1, 4354.117]
- [23.1, 4234.338]
- [24.1, 4114.588]
- [25.5, 3934.933]
- [26.5, 3815.163]
- [27.8, 3635.022]
- [29.1, 1585.055]
- [30.3, 3455.375]
- [31.5, 3096.072]
- [33.7, 2916.428]
- [34.8, 2557.115]
- [35.8, 2377.477]
- [36.8, 2197.818]
- [38, 1958.276]
- [39.1, 1718.255]
- [40.1, 1478.721]
- [41.2, 1239.19]
- [42.3, 999.657]
- [43.4, 700.213]
- [44.5, 460.684]
- [45.6, 221.138]
- [46.8, 0]
hot_water_tank_top_shower_compensation:
source: sensor.hot_water_tank_top_temperature_sensor_temperature
unique_id: hot_water_tank_top_shower_compensation
data_points:
# First Value: Temperature
# Second value: Seconds away from target temp (38)
- [17.5, 3082.154]
- [18.5, 2874.893]
- [19.8, 2695.232]
- [21, 2515.601]
- [22.1, 2395.841]
- [23.1, 2276.062]
- [24.1, 2156.312]
- [25.5, 1976.657]
- [26.5, 1856.887]
- [27.8, 1676.746]
- [29.1, 1585.055]
- [30.3, 1497.099]
- [31.5, 1137.796]
- [33.7, 958.152]
- [34.8, 598.839]
- [35.8, 419.201]
- [36.8, 239.542]
- [38, 0]
From there I can create some template sensors in the UI:
- Shower Ready Time (Bottom)
{{
(now().timestamp() + states('sensor.hot_water_tank_bottom_shower_compensation')| float) |as_datetime
}}
- Shower Ready Time (Top)
{{
(now().timestamp() + states('sensor.hot_water_tank_top_shower_compensation')| float) |as_datetime
}}
- Shower Ready Time
{% if states('sensor.shower_ready_time_bottom_temp_sensor') > states('sensor.shower_ready_time_top_temp_sensor') %}
{{ states('sensor.shower_ready_time_bottom_temp_sensor') }}
{% else %}
{{states('sensor.shower_ready_time_top_temp_sensor') }}
{% endif %}
With all that data combined I can now very accurately estimate when I can have a shower or a bath: