Calculate cook time based on meat probe temperature?

Hi, I’d like to calculate a “estimated time” for a cook based on a value from a bluetooth thermometer (Meater probe) that I already have working with HA through MQTT. I get the temperature values, but I have no idea how to estimate a cook time based on the output of the temperatures.

Basically what I want to do is get something like “Estimated finish time: 7:05 PM” that dynamically updates based on the variations in temperature from the thermometer.

Any ideas? I’ve been following this community for more than 2 years and I haven’t found anything like this in the forums.

@ lbandi2 It sounds to me like in order to get an estimate we may need to feed in more data. My thoughts are you may also need an input_number for the target temperature and another one for “meal size” (weight). If you are using Node-Red you could create a function that takes these three inputs, calculates the difference from current temp to target temp and then do some math adjustments based on the difference and “meal size”.

You would also need an input_datetime to output your results. I would be happy to help you work on an example in Node-Red to test but I am not sure how to set that up using the baked in automations in HA.

Thanks for the quick reply, I already have a target temp that I set through an input_number and I’m not worried about the meal size since I’m basing the whole thing exclusively on internal temperature of the cook so I think the size is pretty much irrelevant.

Oh, I forgot, the probe measures internal temperature of the cook and ambient (grill surface) temperature also, so I have both values.

I haven’t had much experience with Node-Red, but I’m willing to give it a go.

awesome! I was thinking that while you were typing.

My thought would be to create a flow in Node-Red and add an events state node for all of our inputs. and then feed those to a join node and then feed all of them to a function node. The function is written in javascript. Not sure if you have any experience with that but for some simple math like this it should not be bad.

I was suggesting putting in the meal_size so that we have something to build the estimate on. What I mean by that is a meatball will cook far faster that a roast even if we start and end at the same temperature. So without specifying a weight we would have to set a constant for the rate a which the temp rises which would never be close. Do you have an thoughts on how to come up with that estimate?

My though was to create a function that estimates the meat will rise at a rate of 1° per OZ per minute per 50° temp offset above meat temp
(of course the rise rate would have to be adjusted over time to get closer to accurate)

Very interesting topic. Maybe we can bring in a bit of science into this. Let’s look a bit at the ‘physics’ of the problem. I hope this makes sense. Typically, when you buy a piece of meat, the instructions say cook until internal temperature reaches a given value. Essentially what one takes away from that is, once the temp reaches whatever value, the meat is considered cooked.

If the above is true (yet to be determined/confirmed), then one could possibly use a heat equation (differential equation) to calculate the time needed for the internal temp to reach a specific value.

What is needed in this case is few readings of the internal temperature during the internal phases of cooking, then those values can be used to calculate the coefficients and ‘calibrate’ the differential equation, to predict the time at which the temp reaches the target value.

The heat/diffusion equation is essentially a first order differential equation, where one needs to know the thermal ‘capacitance/inertance’ of the piece of meat, and the forcing i.e., how hot the oven is.

With the above approach one does not need to know the size of the portion of meat since the parameters of the differential equation can be ‘estimated’ from the initial readings of the temperatures, taken at known time intervals.

Thoughts?

Also, one must keep in mind that, once the piece of meat is taken out of the oven, the meat will continue to cook for some time due to the internal heat stored in the meat.

I would dynamically calculate based on actual heatup rate.
Take temperature, one minute later take temperature compare to last minute (as long as thermometer has some precision i.e. “100.123F”).
If you don’t have precision on the termometer or it is just slow to warm you may increase the time between measurements.
Divide desired temperature by heatup rate to get time left to target temperature.

I think this is a pretty interesting idea that I can put to use for my smoker too so I spent a bit of time hammering this out.

I expanded a bit on what @GlennHA suggested above since there’s no real way to know the actual heat transfer rate based on the scientific stuff but we can measure the actual heating rate of the food.

Since I don’t have any food oin the smoker right now (drat! :laughing:) I had to do some simulations (which is a bit harder than it sounds).

My baseline was to sample the food temp every 5 minutes and then store those values (current and previous) to a variable (using the hass-variables custom integration). That sampling rate may be too fast depending on the size of the food you are cooking or how “low ands slow” you go so therefore may result in getting a heating rate of 0. And since division by 0 causes issues the time calculation needs to take that into account.

If you are working from actual sensors then you will need the following:

  • actual food temp (measured from sensor)
  • variable to hold the current and last reading of food temp (variable using hass-variables or you could use two input numbers)
  • rate of change of food temp sensor (sensor value set by automation)
  • desired finished temperature (constant set by input number)
  • calculated finish time estimate (an input_datetime set via automation)
  • two automations (one to calculate the rate of change of food temp & one to calculate the food done datetime)

To do the simulation you will need a couple of additional things:

  • a counter to simulate the steadily increasing food temp that gets incremented by an automation
  • an automation to increment the counter
  • an input number to simulate the current food temp

The counter simulates a heating rate of 2 degrees per minute.

so all of that said here is the simulation code. You can just dump all of this into a package and everything should just work. The result of this calculation should end up setting a time that the food is done to 1 hour and 15 minutes in the future updated every 5 minutes:

counter:
  food_counter:
    initial: 0
    step: 2

input_datetime:
  food_done:
    name: Estimated Time Food Will Be Done
    has_date: true
    has_time: true
    
input_number:
  food_temp_current:
    name: Food Current Temp
    initial: 45
    min: -20
    max: 200
    step: 1
  food_temp_done:
    name: Food Done Temp
    initial: 195
    min: 0
    max: 200
    step: 1

sensor:
  - platform: template
    sensors:
      food_temp_rate:
        friendly_name: Food Temp Rate
        value_template: "{{ (states('variable.food_temp_history') | float - state_attr('variable.food_temp_history', 'history_01') | float) / 5 }}"
        unit_of_measurement: "degrees/min"
      
automation:
  - alias: simulate 2 degrees per minute
    trigger:
      - platform: time_pattern
        minutes: "/1"
    action:
      - service: counter.increment
        entity_id: counter.food_counter
        
  - alias: calc rate of change of food temp
    trigger:
      - platform: time_pattern
        minutes: "/5"
    action:
      - service: variable.set_variable
        data:
          variable: food_temp_history
          attributes: 
            history_01: "{{ states('variable.food_temp_history') }}"
      - service: variable.set_variable
        data:
          variable: food_temp_history
          value: "{{ states('counter.food_counter') }}"
          
  - alias: set time food done
    trigger:
      - platform: time_pattern
        minutes: "/5"
    action:
      - service: input_datetime.set_datetime
        entity_id: input_datetime.food_done
        data:
          timestamp: >
            {% if states('sensor.food_temp_rate') | float != 0.0 %}
              {{ (((states('input_number.food_temp_done') | float - states('input_number.food_temp_current') | float) / states('sensor.food_temp_rate') | float) * 60) + as_timestamp(now()) }}
            {% else %}
              {{ as_timestamp(now()) + 86400 }}
            {% endif %}
    
variable:
  food_temp_history:
    value: 'Not set'
    restore: true
    attributes:
      name: 'Food Temp History'

to use it with just the real sensors:

input_datetime:
  food_done:
    name: Estimated Time Food Will Be Done
    has_date: true
    has_time: true
    
input_number:
  food_temp_done:
    name: Food Done Temp
    initial: 195
    min: 0
    max: 200
    step: 1

sensor:
  - platform: template
    sensors:
      food_temp_rate:
        friendly_name: Food Temp Rate
        value_template: "{{ (states('variable.food_temp_history') | float - state_attr('variable.food_temp_history', 'history_01') | float) / 5 }}"
        unit_of_measurement: "degrees/min"
      
automation:
  - alias: set sampling rate of food temp
    trigger:
      - platform: time_pattern
        minutes: "/5"
    action:
      - service: variable.set_variable
        data:
          variable: food_temp_history
          attributes: 
            history_01: "{{ states('variable.food_temp_history') }}"
      - service: variable.set_variable
        data:
          variable: food_temp_history
          value: "{{ states('sensor.food_temp_current') }}"
          
  - alias: set time food done
    trigger:
      - platform: time_pattern
        minutes: "/5"
    action:
      - service: input_datetime.set_datetime
        entity_id: input_datetime.food_done
        data:
          timestamp: >
            {% if states('sensor.food_temp_rate') | float != 0.0 %}
              {{ (((states('input_number.food_temp_done') | float - states('sensor.food_temp_current') | float) / states('sensor.food_temp_rate') | float) * 60) + as_timestamp(now()) }}
            {% else %}
              {{ as_timestamp(now()) + 86400 }}
            {% endif %}
    
variable:
  food_temp_history:
    value: 'Not set'
    restore: true
    attributes:
      name: 'Food Temp History'

The division by zero issue is handled by just setting the finishing time to 24 hours from the current time because if the food temp isn’t increasing then the food will technically never get done.

This isn’t obviously going to be very precise but it will basically get you a “time to doneness” that will get more accurate as the doneness temperature is approached.

Let me knows what you think. I threw this together pretty quickly so I may have screwed something up.

1 Like

@finity … Beautiful! That brings me back to my grad school days, when I took classical & modern feedback control courses. I had no idea back then one could apply those concepts in the kitchen.

1 Like

This is great stuff, I’ve been running some tests yesterday and for the most part I think your code was spot on. I had to make a new automation because when saving the thermometer value to variable.food_temp_history sometimes it would save ‘unknown’ and that would throw the calculations way off, so I added a new automation to correct that in case it happens. Other than that, here’s what I modified:

  • Added automation to correct ‘unknown’ value that sometimes was being saved to variable.food_temp_history
  • Lowered automation repetition time to 2 minutes instead of 5 to get quicker updates
  • Added some variable names to read the code more easily

Something I noticed while running tests is that if variable.food_temp_rate becomes negative (I tested this thinking of a scenario where the heat element begins to die down, the estimated final time would actually decrease instead of increasing. I’m kind of stumped there, but I would like to correct it.

I also added some variable names to make the code more easily readable to me.

Here’s the revised code:

input_datetime:
  food_done:
    name: Estimated Time Food Will Be Done
    has_date: true
    has_time: true
    
input_number:
  food_temp_done:
    name: Food Done Temp
    initial: 195
    min: 0
    max: 200
    step: 1

sensor:
  - platform: template
    sensors:
      food_temp_rate:
        friendly_name: Food Temp Rate
        value_template: >
            {% set temp_hist = states('variable.food_temp_history') | float %}
            {% set temp_hist01 = state_attr('variable.food_temp_history', 'history_01') | float %}
            {{ (temp_hist - temp_hist01) / 2 }}
        unit_of_measurement: "degrees/min"
      
automation:
  - alias: set sampling rate of food temp
    trigger:
      - platform: time_pattern
        minutes: "/2"
    action:
      - service: variable.set_variable
        data:
          variable: food_temp_history
          attributes: 
            history_01: "{{ states('variable.food_temp_history') }}"
      - service: variable.set_variable
        data:
          variable: food_temp_history
          value: "{{ states('sensor.food_temp_current') }}"
          
  - alias: set time food done
    trigger:
      - platform: time_pattern
        minutes: "/2"
    action:
      - service: input_datetime.set_datetime
        entity_id: input_datetime.food_done
        data:
          timestamp: >
            {% set temp_rate = states('sensor.food_temp_rate') | float %}
            {% set temp_done = states('input_number.food_temp_done') | float %}
            {% set int_temp = states('sensor.food_temp_current') | float %}
            {% set calc = (((temp_done - int_temp) / temp_rate) * 60) + as_timestamp(now()) %}
            
            {% if temp_rate != 0.0 %}
              {{ calc }}
            {% else %}
              {{ as_timestamp(now()) + 86400 }}
            {% endif %}        

  - alias: sampling rate correction for unknown value
    trigger:
      platform: state
      entity_id: variable.food_temp_history
      to: "unknown"
      for: "00:00:01"
    action:
      - service: variable.set_variable
        data:
          variable: food_temp_history
          value: "{{ states('sensor.food_temp_current') }}"

variable:
  food_temp_history:
    value: 'Not set'
    restore: true
    attributes:
      name: 'Food Temp History'       

Can you share the specific hardware you’re using in this application? … a thermometer that tracks both the oven temperature (outside meat) and that of the piece being cooked (inside meat)?

I mean, technically, it’s not really wrong since if the food temp really is going down then the finish time will never arrive. :wink: But it’s very similar to what I did for the division by 0 issue so you could simply change the test to eliminate all 0 and negative numbers (> 0 instead of != 0):

{% if temp_rate > 0.0 %}
  {{ calc }}
{% else %}
  {{ as_timestamp(now()) + 86400 }}
{% endif %}

and instead of creating another automation to handle the 'unknown" just put that filter into your original sampling rate automation:

action:
  - service: variable.set_variable
    data:
      variable: food_temp_history
      attributes: 
        history_01: "{{ states('variable.food_temp_history') }}"
  - service: variable.set_variable
    data:
      variable: food_temp_history
      value: >
        {% if  states('sensor.food_temp_current') == 'unknown' %}
          {{ states('variable.food_temp') }}
        {% else %}
          {{ states('sensor.food_temp_current') }}
        {% endif %}

That should prevent an ‘unknown’ from ever getting into the history variable by simply using the last value of the variable if the sensor value is ‘unknown’.

EDIT:

I think I realized an even easier way to filter out the ‘unknown’ values. the float function will force any non-numerical values to 0.0 so you could just do this instead of the filter:

- service: variable.set_variable
  data:
    variable: food_temp_history
    value: "{{ states('counter.food_counter') | float }}"

I know your not asking me…but I’ll throw in a shameless plug for a DIY sensor I made. :laughing:

Instead of re-writing it all out here again here’s a post I made about it in another thread:

Thank you @finity, that’s a very brave contraption you have. I was curious about a piece of electronics that resides in the oven and remains functional. Unless the sensors are inside, and the electronics way out.

yeah, the electronics are outside of the smoker. The box sits on a handrail of my deck right beside the smoker when in use. Otherwise the probes are disconnected (using the plugs) and the controller is inside since it’s not water-proof :slightly_smiling_face:

Oh yeah, I forgot about that, I’m using a Meater BLE wireless grill thermometer.

I followed this guide https://github.com/jcallaghan/home-assistant-config/issues/50 and got it working through MQTT reporting to a Raspeberry Pi Zero.

1 Like

I’ll try this and get back to you. Today I’m firing up the grill to try this automation so wish me luck!

1 Like

That is an expensive toy (per amazon). You sure do love your steaks :slight_smile:

You aren’t kidding.

My setup only cost about $50 and gives similar functionality.

The only downside is that I only have one food probe.

But if i needed more I could either add to the existing controller or build another complete unit and still be at around the same price as having a single probe meager.

No offense meant to the OP since I had looked at those before and didn’t know how to get them connected into HA so I dug in to making my own. And im glad i did.

1 Like

I did not put that together before; that explains some of my sensors that should be unknown. I could not figure out why my print time countdown was 0 when it should have unknown.

1 Like

Here is some examples I have for formatting the time:

    prusa_connect_time_done:
      friendly_name: "Estimated Completion Time"
      value_template: "{{ (as_timestamp(now()) + states('sensor.prusa_connect_time_est') | float) | timestamp_custom('%H:%M')}}"
      icon_template: mdi:clock
      availability_template: '{{ not is_state("sensor.prusa_connect", "unavailable") }}'
    prusa_connect_time_countdown:
      friendly_name: "Estimated Completion Countdown"
      value_template: "{{ states('sensor.prusa_connect_time_est') | float | timestamp_custom('%H hr %M min', false)}}"
      icon_template: mdi:timer-sand
      availability_template: '{{ not is_state("sensor.prusa_connect", "unavailable") }}'

I like them both even thou they are technically the same, but it saves me brain power.
One displays the time in hours and minutes left the other displays the actual completion time.

1 Like