199 lines
6.2 KiB
Python
199 lines
6.2 KiB
Python
import time
|
|
|
|
|
|
class CookingState:
|
|
TEMPERATURE_TOLERANCE = 1.0
|
|
|
|
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
|
|
self.power_level = power_level
|
|
self.target_temp = target_temp
|
|
|
|
self.start_time = time.time()
|
|
self.temperature_provider = temperature_provider
|
|
self.on_state_change = on_state_change
|
|
self.on_refresh = on_refresh
|
|
|
|
self.state = CookingStates.COOKING
|
|
self.paused = False
|
|
self._pause_started_at = None
|
|
self._paused_duration = 0.0
|
|
|
|
self.current_dish_temp = None
|
|
self.current_ambient_temp = None
|
|
self.estimated_remaining_time = float(cook_time)
|
|
self._last_temperature_sample = None
|
|
self._last_refresh_signature = None
|
|
|
|
def set_temperature_provider(self, temperature_provider):
|
|
self.temperature_provider = temperature_provider
|
|
|
|
def set_state_change_callback(self, callback):
|
|
self.on_state_change = callback
|
|
|
|
def set_refresh_callback(self, callback):
|
|
self.on_refresh = callback
|
|
|
|
def pause(self):
|
|
if self.paused:
|
|
return
|
|
|
|
self.paused = True
|
|
self._pause_started_at = time.time()
|
|
self._notify_refresh(force=True)
|
|
|
|
def unpause(self):
|
|
if not self.paused:
|
|
return
|
|
|
|
now = time.time()
|
|
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()
|
|
|
|
def set_state(self, state):
|
|
if self.state == state:
|
|
return
|
|
|
|
self.state = state
|
|
self._notify_state_change()
|
|
self._notify_refresh(force=True)
|
|
|
|
def get_elapsed_time(self) -> float:
|
|
now = time.time()
|
|
elapsed = now - self.start_time - self._paused_duration
|
|
|
|
if self.paused and self._pause_started_at is not None:
|
|
elapsed -= now - self._pause_started_at
|
|
|
|
return max(0.0, elapsed)
|
|
|
|
def get_remaining_time(self) -> int:
|
|
"""Returns the estimated remaining cooking time in seconds."""
|
|
return int(max(0.0, self.get_remaining_time_estimation()))
|
|
|
|
def get_remaining_time_estimation(self) -> float:
|
|
elapsed_time = self.get_elapsed_time()
|
|
timer_remaining = max(0.0, float(self.cook_time) - elapsed_time)
|
|
|
|
if self.current_dish_temp is None:
|
|
return timer_remaining
|
|
|
|
if self.current_dish_temp >= self.target_temp:
|
|
return timer_remaining
|
|
|
|
heating_rate = self._estimate_heating_rate()
|
|
if heating_rate <= 0:
|
|
return timer_remaining
|
|
|
|
target_remaining = (self.target_temp - self.current_dish_temp) / heating_rate
|
|
return max(timer_remaining, max(0.0, target_remaining))
|
|
|
|
def _read_temperatures(self):
|
|
if self.temperature_provider is None:
|
|
return None, None
|
|
|
|
temperatures = self.temperature_provider()
|
|
if temperatures is None:
|
|
return None, None
|
|
|
|
if isinstance(temperatures, (list, tuple)) and len(temperatures) >= 2:
|
|
return temperatures[0], temperatures[1]
|
|
|
|
raise ValueError("temperature_provider must return a pair: (dish_temp, ambient_temp)")
|
|
|
|
def _estimate_heating_rate(self):
|
|
if self._last_temperature_sample is None:
|
|
return 0.0
|
|
|
|
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
|
|
|
|
def _notify_state_change(self):
|
|
if self.on_state_change is None:
|
|
return
|
|
|
|
self.on_state_change(self)
|
|
|
|
def _notify_refresh(self, force=False):
|
|
if self.on_refresh is None:
|
|
return
|
|
|
|
signature = (
|
|
int(self.get_elapsed_time()),
|
|
int(self.get_remaining_time_estimation()),
|
|
self.current_dish_temp,
|
|
self.current_ambient_temp,
|
|
self.state,
|
|
self.paused,
|
|
)
|
|
|
|
if not force and signature == self._last_refresh_signature:
|
|
return
|
|
|
|
self._last_refresh_signature = signature
|
|
self.on_refresh(self)
|
|
|
|
def update_tick(self):
|
|
if self.paused:
|
|
self._notify_refresh()
|
|
return self.state
|
|
|
|
previous_state = self.state
|
|
previous_temperature = self.current_dish_temp
|
|
|
|
try:
|
|
self.current_dish_temp, self.current_ambient_temp = self._read_temperatures()
|
|
except Exception:
|
|
self.current_dish_temp = previous_temperature
|
|
|
|
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:
|
|
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 self.state == CookingStates.DONE and self.current_dish_temp < (self.target_temp - self.TEMPERATURE_TOLERANCE):
|
|
self.state = CookingStates.COOKING
|
|
elif elapsed_time >= self.cook_time * 1.25: # If the dish is not heating up
|
|
self.state = CookingStates.STIRRING_REQUIRED
|
|
self.pause()
|
|
|
|
self._last_temperature_sample = (now, self.current_dish_temp)
|
|
|
|
if self.state != previous_state:
|
|
self._notify_state_change()
|
|
|
|
self._notify_refresh()
|
|
return self.state
|
|
|
|
|
|
class CookingStates:
|
|
COOKING = 0
|
|
STIRRING_REQUIRED = 1
|
|
DONE = 2
|
|
ALERT = 3 # Microwave is too hot internally or other alerts
|
|
IDLE = 4 # Waiting for cooking parameters to be set, or after cooking is done |