From 1d0a323d4051e17e528ac68d2d42a4c35e4fa30d Mon Sep 17 00:00:00 2001 From: ScribleSean <56331494+ScribleSean@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:37:00 -0400 Subject: [PATCH] fix: TurboBlaze fryer preheat state after set_mode and on pullOut set_mode_from_recipe set the local status to COOKING even when the program starts with a preheat, so the state read as cooking until the next update. get_details passed preheatSetTime through as 0 when the program has no preheat. FryerState tests for None, so a basket pulled during a cook read as being in preheat mode. --- src/pyvesync/devices/vesynckitchen.py | 16 ++++- src/tests/test_fryer_turboblaze_state.py | 78 ++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 src/tests/test_fryer_turboblaze_state.py diff --git a/src/pyvesync/devices/vesynckitchen.py b/src/pyvesync/devices/vesynckitchen.py index faa8b962..4b284887 100644 --- a/src/pyvesync/devices/vesynckitchen.py +++ b/src/pyvesync/devices/vesynckitchen.py @@ -500,14 +500,21 @@ async def get_details(self) -> None: if _current_temp is not None and self.temp_unit == TemperatureUnits.FAHRENHEIT: _current_temp = round(_current_temp * 9 / 5 + 32) + # The API reports preheatSetTime as 0 when the program has no preheat. + # Pass None in that case so the state does not read as a preheat. + _preheat_set_time = resp_model.preheatSetTime or None + _preheat_last_time = ( + resp_model.preheatLastTime if _preheat_set_time is not None else None + ) + self.state_chamber_1.set_state( cook_status=self.status_map[resp_model.cookStatus], cook_time=cook_step.cookSetTime, cook_last_time=cook_step.cookLastTime, cook_temp=cook_step.cookTemp, cook_mode=cook_step.mode, - preheat_set_time=resp_model.preheatSetTime, - preheat_last_time=resp_model.preheatLastTime, + preheat_set_time=_preheat_set_time, + preheat_last_time=_preheat_last_time, current_temp=_current_temp, ) @@ -533,7 +540,10 @@ async def set_mode_from_recipe(self, recipe: AirFryerPresetRecipe) -> bool: self.state.set_standby() return False self.state.set_state( - cook_status=AirFryerCookStatus.COOKING, + # A program with preheat starts in the preheat phase. + cook_status=AirFryerCookStatus.HEATING + if recipe.preheat_time + else AirFryerCookStatus.COOKING, cook_time=recipe.cook_time, cook_last_time=recipe.cook_time, cook_temp=recipe.target_temp, diff --git a/src/tests/test_fryer_turboblaze_state.py b/src/tests/test_fryer_turboblaze_state.py new file mode 100644 index 00000000..558b6e26 --- /dev/null +++ b/src/tests/test_fryer_turboblaze_state.py @@ -0,0 +1,78 @@ +"""State tests for bypassV2 (TurboBlaze class) fryers around preheat. + +Covers the local state after starting a program with preheat, and how a +pulled-out basket is classified when the API reports ``preheatSetTime`` as 0. +""" + +from copy import deepcopy + +import call_json_fryers +from base_test_cases import TestBase +from defaults import build_bypass_v2_response + +from pyvesync.const import AirFryerCookStatus + +MODEL = 'CAF-DC601S' + + +class TestTurboBlazePreheatState(TestBase): + """Preheat state handling for VeSyncTurboBlazeFryer.""" + + def _update_with(self, fryer, **overrides): + details = deepcopy(call_json_fryers.AIR_FRYER_COOKING_DETAILS[MODEL]) + details.update(overrides) + self.mock_api.return_value = ( + build_bypass_v2_response(inner_result=details), + 200, + ) + self.run_in_loop(fryer.update) + + def test_set_mode_with_preheat_starts_in_heating(self): + fryer = self.get_device('air_fryers', MODEL) + self.mock_api.return_value = (build_bypass_v2_response(inner_result={}), 200) + + result = self.run_in_loop( + fryer.set_mode, cook_time=600, cook_temp=350, preheat_time=300 + ) + + assert result is True + assert fryer.state.cook_status == AirFryerCookStatus.HEATING + assert fryer.state.is_in_preheat_mode is True + assert fryer.state.preheat_set_time == 300 + assert fryer.state.cook_set_time == 600 + assert fryer.state.cook_set_temp == 350 + + def test_set_mode_without_preheat_starts_in_cooking(self): + fryer = self.get_device('air_fryers', MODEL) + self.mock_api.return_value = (build_bypass_v2_response(inner_result={}), 200) + + result = self.run_in_loop(fryer.set_mode, cook_time=600, cook_temp=350) + + assert result is True + assert fryer.state.cook_status == AirFryerCookStatus.COOKING + assert fryer.state.is_in_preheat_mode is False + assert fryer.state.preheat_set_time is None + + def test_pull_out_during_cook_is_cook_mode(self): + """preheatSetTime 0 from the API means the program has no preheat.""" + fryer = self.get_device('air_fryers', MODEL) + self._update_with( + fryer, cookStatus='pullOut', preheatSetTime=0, preheatLastTime=0 + ) + + assert fryer.state.cook_status == AirFryerCookStatus.PULL_OUT + assert fryer.state.preheat_set_time is None + assert fryer.state.is_in_preheat_mode is False + assert fryer.state.is_in_cook_mode is True + + def test_pull_out_during_preheat_is_preheat_mode(self): + fryer = self.get_device('air_fryers', MODEL) + self._update_with( + fryer, cookStatus='pullOut', preheatSetTime=300, preheatLastTime=120 + ) + + assert fryer.state.cook_status == AirFryerCookStatus.PULL_OUT + assert fryer.state.preheat_set_time == 300 + assert fryer.state.preheat_last_time == 120 + assert fryer.state.is_in_preheat_mode is True + assert fryer.state.is_in_cook_mode is False