Better estimated remaining time
This commit is contained in:
@@ -94,7 +94,7 @@ class MicrowaveScreen:
|
||||
if cooking_state.paused:
|
||||
self.h_centered_text("Paused", 40, 1)
|
||||
elif cooking_state.state != CookingStates.DONE:
|
||||
remaining_time = cooking_state.estimated_remaining_time
|
||||
remaining_time = int(cooking_state.estimated_remaining_time)
|
||||
minutes = remaining_time // 60
|
||||
seconds = remaining_time % 60
|
||||
self.h_centered_text(f"{minutes:02}:{seconds:02}", 40, 1)
|
||||
|
||||
+70
-28
@@ -3,6 +3,7 @@ import time
|
||||
|
||||
class CookingState:
|
||||
TEMPERATURE_TOLERANCE = 1.0
|
||||
MIN_SIGNIFICANT_HEATING_RATE = 0.05 # °C/s threshold to consider valid heating
|
||||
|
||||
def __init__(self, cook_time: int, power_level: int, target_temp: float, temperature_provider=None, on_state_change=None, on_refresh=None):
|
||||
self.cook_time = cook_time
|
||||
@@ -27,6 +28,9 @@ class CookingState:
|
||||
self._last_refresh_signature = None
|
||||
self._stirred = False
|
||||
|
||||
# Moving average filter for heating rate (°C / sec)
|
||||
self._smoothed_heating_rate = 0.0
|
||||
|
||||
def set_temperature_provider(self, temperature_provider):
|
||||
self.temperature_provider = temperature_provider
|
||||
|
||||
@@ -45,9 +49,9 @@ class CookingState:
|
||||
|
||||
self.paused = True
|
||||
self._pause_started_at = time.time()
|
||||
# self._notify_refresh(force=True)
|
||||
if self.on_pause:
|
||||
self.on_pause(self)
|
||||
|
||||
def unpause(self):
|
||||
if not self.paused:
|
||||
return
|
||||
@@ -56,16 +60,15 @@ class CookingState:
|
||||
if self._pause_started_at is not None:
|
||||
self._paused_duration += now - self._pause_started_at
|
||||
|
||||
# self._pause_started_at = None
|
||||
self.paused = False
|
||||
# self._notify_refresh(force=True)
|
||||
|
||||
def toggle_pause(self):
|
||||
if self.paused:
|
||||
self.unpause()
|
||||
else:
|
||||
self.pause()
|
||||
self.on_pause(self)
|
||||
if self.on_pause:
|
||||
self.on_pause(self)
|
||||
|
||||
def set_state(self, state):
|
||||
if self.state == state:
|
||||
@@ -90,20 +93,56 @@ class CookingState:
|
||||
|
||||
def get_remaining_time_estimation(self) -> float:
|
||||
elapsed_time = self.get_elapsed_time()
|
||||
|
||||
# 1. Base timer remaining based on standard cook time
|
||||
timer_remaining = max(0.0, float(self.cook_time) - elapsed_time)
|
||||
|
||||
if self.current_dish_temp is None:
|
||||
# If temperature is unavailable or already met target, rely on standard timer
|
||||
if self.current_dish_temp is None or self.current_dish_temp >= (self.target_temp - self.TEMPERATURE_TOLERANCE):
|
||||
return timer_remaining
|
||||
|
||||
if self.current_dish_temp >= self.target_temp:
|
||||
return timer_remaining
|
||||
# 2. Prevent hitting 00:00 before stirring trigger:
|
||||
# If we passed half cook_time and temp is far from target, extend expected base time to 1.25x cook_time
|
||||
temp_progress = max(0.0, self.current_dish_temp) / max(1.0, self.target_temp)
|
||||
if elapsed_time > (self.cook_time * 0.5) and temp_progress < 0.8:
|
||||
adjusted_cook_time = self.cook_time * 1.25
|
||||
timer_remaining = max(0.0, adjusted_cook_time - elapsed_time)
|
||||
|
||||
# 3. Estimate using heating rate
|
||||
heating_rate = self._estimate_heating_rate()
|
||||
if heating_rate <= 0:
|
||||
return timer_remaining
|
||||
temp_needed = self.target_temp - self.current_dish_temp
|
||||
|
||||
target_remaining = (self.target_temp - self.current_dish_temp) / heating_rate
|
||||
return max(timer_remaining, max(0.0, target_remaining))
|
||||
if heating_rate > 0.01:
|
||||
rate_based_remaining = temp_needed / heating_rate
|
||||
else:
|
||||
# If flat/slow, project remaining time based on remaining missing temperature fraction
|
||||
temp_ratio = max(0.1, temp_needed / self.target_temp)
|
||||
rate_based_remaining = max(timer_remaining, self.cook_time * temp_ratio * 1.25)
|
||||
|
||||
# 4. Strict dynamic cap: Never exceed maximum possible execution window (2.0x cook_time total)
|
||||
max_possible_remaining = max(0.0, (self.cook_time * 2.0) - elapsed_time)
|
||||
bounded_remaining = min(rate_based_remaining, max_possible_remaining)
|
||||
|
||||
# Return the larger of the adjusted timer or the bounded prediction
|
||||
return max(timer_remaining, bounded_remaining)
|
||||
|
||||
def _estimate_heating_rate(self) -> float:
|
||||
"""Calculates heating rate in °C/sec over time interval."""
|
||||
if self._last_temperature_sample is None or self.current_dish_temp is None:
|
||||
return 0.0
|
||||
|
||||
last_time, last_temp = self._last_temperature_sample
|
||||
now = time.time()
|
||||
delta_time = now - last_time
|
||||
|
||||
if delta_time < 0.8: # Skip micro-ticks
|
||||
return 0.0
|
||||
|
||||
delta_temp = self.current_dish_temp - last_temp
|
||||
if delta_temp <= 0:
|
||||
return 0.0
|
||||
|
||||
return delta_temp / delta_time
|
||||
|
||||
def _read_temperatures(self):
|
||||
if self.temperature_provider is None:
|
||||
@@ -118,27 +157,29 @@ class CookingState:
|
||||
|
||||
raise ValueError("temperature_provider must return a pair: (dish_temp, ambient_temp)")
|
||||
|
||||
def _estimate_heating_rate(self):
|
||||
def _update_heating_rate(self):
|
||||
"""Calculates instantaneous rate and updates the Exponential Moving Average."""
|
||||
now = time.time()
|
||||
if self._last_temperature_sample is None:
|
||||
return 0.0
|
||||
self._last_temperature_sample = (now, self.current_dish_temp)
|
||||
return
|
||||
|
||||
last_time, last_temp = self._last_temperature_sample
|
||||
now = time.time()
|
||||
current_temp = self.current_dish_temp
|
||||
|
||||
if current_temp is None:
|
||||
return 0.0
|
||||
|
||||
delta_time = now - last_time
|
||||
if delta_time <= 0:
|
||||
return 0.0
|
||||
|
||||
return (current_temp - last_temp) / delta_time
|
||||
if delta_time <= 0.5 or self.current_dish_temp is None:
|
||||
return
|
||||
|
||||
instant_rate = (self.current_dish_temp - last_temp) / delta_time
|
||||
self._last_temperature_sample = (now, self.current_dish_temp)
|
||||
|
||||
# Exponential Moving Average (EMA) - alpha smooths out sensor noise
|
||||
alpha = 0.2
|
||||
self._smoothed_heating_rate = (alpha * instant_rate) + ((1.0 - alpha) * self._smoothed_heating_rate)
|
||||
|
||||
def _notify_state_change(self):
|
||||
if self.on_state_change is None:
|
||||
return
|
||||
|
||||
self.on_state_change(self)
|
||||
|
||||
def _notify_refresh(self, force=False):
|
||||
@@ -177,21 +218,22 @@ class CookingState:
|
||||
|
||||
now = time.time()
|
||||
elapsed_time = self.get_elapsed_time()
|
||||
self.estimated_remaining_time = self.get_remaining_time_estimation()
|
||||
|
||||
if self.current_dish_temp is not None:
|
||||
if elapsed_time < (self.cook_time / 2.0) and self.current_dish_temp >= self.target_temp and (self._paused_duration == None or self._paused_duration < 5): # If the dish is heating too fast, we require stirring
|
||||
self._update_heating_rate()
|
||||
|
||||
if elapsed_time < (self.cook_time / 2.0) and self.current_dish_temp >= self.target_temp and (self._paused_duration is None or self._paused_duration < 5):
|
||||
self.state = CookingStates.STIRRING_REQUIRED
|
||||
self.pause()
|
||||
elif elapsed_time >= self.cook_time and self.current_dish_temp >= (self.target_temp - self.TEMPERATURE_TOLERANCE):
|
||||
self.state = CookingStates.DONE
|
||||
elif elapsed_time >= self.cook_time * 1.25 and (self._paused_duration == None or self._paused_duration < 5): # If the dish is not heating up
|
||||
elif elapsed_time >= self.cook_time * 1.25 and (self._paused_duration is None or self._paused_duration < 5):
|
||||
self.state = CookingStates.STIRRING_REQUIRED
|
||||
self.pause()
|
||||
elif self._pause_started_at != None and (self._pause_started_at + self._paused_duration) < (now - (self.cook_time * 0.75)): # If the dish had to be pause and it's been a long time, we stop the cooking
|
||||
elif self._pause_started_at is not None and (self._pause_started_at + self._paused_duration) < (now - (self.cook_time * 0.75)):
|
||||
self.state = CookingStates.DONE
|
||||
|
||||
self._last_temperature_sample = (now, self.current_dish_temp)
|
||||
self.estimated_remaining_time = self.get_remaining_time_estimation()
|
||||
|
||||
if self.state != previous_state:
|
||||
self._notify_state_change()
|
||||
|
||||
Reference in New Issue
Block a user