Calculate cook time based on meat probe 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

I agree, Meater probes are crazy expensive, but I wanted an option that didn’t involve wires so I didn’t find any other solutions. I never intended to make it work with HA, but the downside of these gadgets (at least the original meater) is range. Their website claims that they’re good for 30 feet. I have a kettle grill and most of the time when I’d put the lid on, signal would be lost (probes report to your phone via BLE thorugh an app). So that made me quite furious, I contacted them and they suggested me to buy an accessory to amplify the signal (around $50 pfff), so I went to google looking for a DIY option. I was surprised to find out there were some attempts at making it work and I tried a couple of them and found that hanging a Raspberry Pi Zero in a small box in direct line of sight of the grill would get me a perfect signal (I guess the Raspberry Pi Zero has a better antenna than my phone… or phones in general?).

So that ended up better than I anticipated :slight_smile:.

And I obviously take no offence, I know I payed a lot, but I wanted my setup to be as wireless as possible.

1 Like

BTW, yesterday’s test was a success, estimated time was incredibly stable and did not jump around as it did when I was running tests earlier by heating a pot with water (test setup was a little sketchy hehe) and the result was spot on.

Any ideas of what I could do with the other value I get which is the “ambient” temperature (ie. grill temperature)?

I’m not sure how big an effect the ambient temperature affects the end result. However, if we go back to the Heat diffusion equation (my first post), then conceptually the ambient temperature would affect how fast the meat heats-up. Of course this makes more sense if the grill is open and there is snow outside. However, if the cover is closed, then the outside temp effect is likely to be very small.

I use mine just as a smoker setpoint indication to know whether my 'low and slow" temperature is at the desired temp.

It probably wouldn’t be as useful when grilling when you are using direct high heat tho.

I have a bunch of automations set up around my smoker…

it announces the smoker temp over my Alexa devices every 15 minutes during the cook and it announces every time the smoker temp deviates from the setpoint by more than 10 degrees.

I also have an announcement at the 2 hour mark (and then every 30 minutes thereafter) to remind me to check the water pan level.

And then I have announcements to tell me the food is getting close to being done (within 20 degrees and then within 10 degrees of the “done” setpoint) and again when the food actually reaches the “done” setpoint.

I also plot both the food temp and smoker temp on a mini-graph card.

And now, thanks to your inspiration, I have a calculated time the food will be done! :laughing: Thanks!

But I still haven’t had a chance to actually try mine out in the real world yet… :disappointed: Hopefully soon tho.

1 Like

This is really getting fancy now … and you’re giving me nice ideas. I wonder, if one grills chicken/steak/shrimp at the same time … one can have Alexa dictate the time in/out and time to flip the meat as well.

1 Like

As long as you have enough probes, I only have 2 so I’m probably more limited than you guys.

Here’s something I did which is not so much a calculation, but a handy addition:

I set up an input_select with an assortment of meats and their corresponding usda safe to consume temperatures. That way I can set up my cook selecting for instance “Beef” and when I select the option in the input_select combo box that sets my target temperature to the corresponding value. I can also fine tune this number with a slider, but I rarely do that since we like our meat medium-rare.

So that way you only have to look up the temperatures once and set them up for poultry, fish, beef, lamb, etc.

You get the idea, the action that sets the temperature looks like this:

      - service: input_number.set_value
        data:
          entity_id: input_number.meater_target_temp
          value: >
            {% set cook = states('input_select.meater_cook') | lower %}
            {% if 'pork' in cook %}
              62
            {% elif 'chicken' in cook %}
              65
            {% elif 'salmon' in cook %}
              52
            {% elif 'lamb' in cook %}
              60
            {% else %}
              57
            {% endif %}

2 Likes