Repository navigation
fix: TurboBlaze fryer preheat state after set_mode and on pullOut - #560
Closed
ScribleSean wants to merge 1 commit into
Closed
ScribleSean wants to merge 1 commit into
ScribleSean wants to merge 1 commit into
Conversation
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.
8 of 10 tasks
webdjoe
added a commit
that referenced
this pull request
Nov 1, 2026
TurboBlaze set_mode_from_recipe always set the local status to COOKING. A program with preheat runs the preheat phase first, so it now sets HEATING when the recipe has a preheat time. The DC601S capture reports heating with preheatSetTime/preheatLastTime while a preheat runs. CS158 and TF101S now send readyStart false, like TurboBlaze, so a cook starts without the device's start button. The CS158 captures show readyStart false reporting cooking/heating at once. TF101S has no capture with readyStart false; its local status after start is now COOKING instead of READY, inferred from CS158. From PR #560. The pullOut half of that PR was already fixed in 8cbc6f4. <!-- Thanks for contributing to pyvesync! Please read the contribution rules first: https://webdjoe.github.io/pyvesync/latest/development/contributing These checkboxes are self-attested and enforced through review, not CI. Please fill them in honestly. PRs that bundle unrelated changes or add/modify device behavior without real-hardware testing will be asked to change before review. --> ## Description <!-- What does this PR change, and why? Link any related issue. --> ## Type of change <!-- This PR must be a SINGLE feature, fix, or edit. Pick one. --> - [x] Bug fix (`fix:`) - [ ] New feature or device support (`feat:`) - [ ] Documentation (`docs:`) - [x] Refactor / test / chore (no functional change) ## Checklist - [x] This PR is a **single** feature, fix, or edit (not a bundle of unrelated changes). - [x] The change follows the intended architecture (base class → device + API mixin → device map; state in `DeviceState`; models in `models/`; constants in `pyvesync.const`). - [x] No API request/response fields were invented — all come from a real packet capture. - [x] `ruff check`, `pylint`, `mypy`, and `pytest` pass locally. - [x] Tests added/updated and API fixtures written (`pytest --write_api`) where relevant. - [x] Documentation updated if behavior or public API changed. Co-authored-by: ScribleSean <56331494+ScribleSean@users.noreply.github.com>
Owner
|
Thank you for the PR! I've incorporated these changes in #575. Will release shortly. |
webdjoe
added a commit
that referenced
this pull request
Nov 5, 2026
## Description This PR rewrites air fryer support around a single `VeSyncFryer` interface and adds five new Cosori models. It is the integration branch for the air fryer work. Each part was reviewed and squash-merged here as its own PR: | PR | Change | | --- | --- | | #517 | Add the Dual Blaze (CAF-P583S) | | #575 | Report preheat on the TurboBlaze, and send `readyStart` false (from #560) | | #578 | Skip devices that fail to initialize, and log unexpected update errors | | #579 | Add the chamber interface, presets and temperature limits from the device map, and unit handling | | #580 | Make the fryers resilient to failed or unreadable responses, and keep local state consistent | | #581 | Add the CAF-DC111S-AEU Turbo Tower Pro (replaces #536, from arypien's captures) | | #582 | Keep the API code and message for unknown error codes (merged to `dev`, then cherry-picked here) | ### Supported fryers | Model | Class | Chambers | Preheat | Pause / resume | | --- | --- | --- | --- | --- | | CS137-AF, CS158-AF | `VeSyncAirFryer158` | 1 | ✔ | ✔ | | CAF-DC601S TurboBlaze (new) | `VeSyncTurboBlazeFryer` | 1 | ✔ | | | CAF-P583S Dual Blaze, KUS and KEU (new) | `VeSyncDualBlazeFryer` | 1 | | | | CAF-TF101S Dual Air Fryer (new) | `VeSyncDualAirFryer` | 1, 2, whole, both | | | | CAF-TF102S (new) | `VeSyncDualAirFryer` | 1, 2, whole, both | | | | CAF-DC111S-AEU Turbo Tower Pro (new) | `VeSyncDualAirFryer` | 1 (upper), 2 (lower), both | | | ### Common interface These are on `VeSyncFryer` (`base_devices/fryer_base.py`): - **Cook controls.** Every fryer has: - `set_mode(cook_time, cook_temp, *, temp_unit, preheat_time, cook_mode, chamber)`; - `set_mode_from_recipe(recipe, chamber)`; - `end(chamber)` and `update()`. `stop()`, `resume()` and `cook_from_preheat()` are implemented only on the CS137/CS158. On the other fryers they log a message and return `False` without sending a request. - **Chambers.** `AirFryerChamber` has `CHAMBER_1`, `CHAMBER_2`, `WHOLE` and `BOTH`. The device also has: - `supported_chambers` and `is_dual_chamber`; - `get_chamber_state()`; - `sync_chambers` and `work_chamber`. `BOTH` is sent as the app's Sync mode. Each chamber has its own `FryerState`. - **Cook modes and presets come from the device map.** Each model's map entry defines its cook modes (`AirFryerModeConfig`) and per-mode temperature limits and steps (`AirFryerTempLimits`). The fryer exposes them through `get_cook_mode()`, `get_mode_recipe()` and `temp_limits()`. A temperature is rounded to the mode's step, then checked against the mode's range. - **Temperature units.** The device map gives each model its starting unit and step, and the unit the API reports takes over once the fryer updates. Times are in seconds. Temperatures are in the fryer's unit, unless you pass `temp_unit`. - **State.** `FryerState` tracks: - the cook status, as the `AirFryerCookStatus` enum; - the preheat, cook and total remaining times, which count down locally between updates and carry across stop, resume and preheat; - the set time and temperature, and the cook mode. ### Error handling - A command the fryer refuses returns `False`, leaves the state as it was, and records the reason in `last_response`. - A failed or unreadable update also leaves the state unchanged. That covers no response, an error code, a null result, an unknown status, or an out-of-range step index. - Air fryer error codes are added to `ErrorCodes`, including the DC111S Sync refusals 11914000, 11915000 and 11926000. - An unknown error code is looked up the way the VeSync app does it: the code is truncated to the thousand, and if that doesn't match, it falls back to `UNKNOWN`. Either way the API code and message are kept for `last_response` and the logs. ### Other changes - `DeviceContainer` skips a device whose class raises `VeSyncError` while it is created, with a warning. One bad device no longer stops discovery of the rest. - `update_all_devices()` logs errors other than `VeSyncError` with their traceback, instead of dropping them. - `VeSync.save_credentials(None)` now saves to the configured token file, or to `.vesync_auth` in the working directory if none is set. It used to save nothing. Its docstring still says "If None, no action is taken". - Ruff is pinned to 0.15.21 in `pyproject.toml`, `requirements-dev.txt` and the pre-commit config, and `ERA001` (commented-out code) is now enforced. - Docs: - `docs/devices/kitchen.md` is rewritten; - the README and `docs/supported_devices.md` list the new models; - the `AGENTS.md` and test guides describe the air fryer tests. ### Breaking changes for CS137/CS158 users (released in 3.4.2) | 3.4.2 | This PR | | --- | --- | | `cook(set_temp, set_time)` (minutes) | `set_mode(cook_time, cook_temp)` (seconds) | | `set_preheat(target_temp, cook_time)` | `set_mode(cook_time, cook_temp, preheat_time=...)` | | `pause()` | `stop()` | | `check_status()` | `update()` | | `state.is_resumable` | `state.can_resume` | | `state.remaining_time` | `state.total_time_remaining` | | `state.is_heating` | `state.is_preheating` | | `state.cook_status` as a string | `AirFryerCookStatus` | `toggle_switch()` is deprecated and sends no request, because air fryers have no on/off command. ## Type of change - [ ] Bug fix (`fix:`) - [x] New feature or device support (`feat:`) - [ ] Documentation (`docs:`) - [ ] Refactor / test / chore (no functional change) ## Checklist - [ ] This PR is a **single** feature, fix, or edit. It is an integration branch: each part was reviewed and merged as its own PR (see the table above). - [x] The change follows the intended architecture (base class → device + API mixin → device map; state in `DeviceState`; models in `models/`; constants in `pyvesync.const`). - [x] No API request/response fields were invented. All come from real packet captures, and the cook modes and limits come from the VeSync app. The exceptions are flagged under hardware verification below. - [x] `ruff check`, `pylint`, `mypy` and `pytest` pass locally: 613 passed, 3 skipped; pylint 10/10. - [x] Tests are added and the API fixtures are written. The YAML fixtures under `src/tests/api/vesynckitchen/` cover `update()` for each fryer. `test_fryer_captures.py`, `test_fryer_chambers.py`, `test_fryer_units.py`, `test_fryer_resilience.py`, `test_errors.py` and `test_manager_resilience.py` cover the rest. - [x] Documentation is updated. ## Real-hardware verification - [ ] Tested on the physical device. - CAF-P583S-KUS, firmware v1.0.15: remote start and stop, status polling, and all 11 presets (#517). - CS137-AF / CS158-AF: model and firmware `____` - CAF-DC601S: model and firmware `____` - CAF-TF101S: model and firmware `____` - [ ] Not applicable. - [ ] I do not own the device; a packet capture is provided / the device has been shared with a maintainer for verification. Still pending hardware verification: - **CAF-TF102S:** mapped from the VeSync app. There is no capture. - **CAF-DC111S-AEU:** the status captures come from #536. The cook and end requests have not been tested on the device. - **CAF-P583S-KEU:** there is no capture. It is assumed to match the KUS, apart from using Celsius. - **CAF-TF101S:** there is no capture with `readyStart` false. Its status after a cook starts (`COOKING`) is inferred from the CS158 captures. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Red Hodgerson <1190796+RedsGT@users.noreply.github.com> Co-authored-by: Darren Griffin <darren.griffin@live.co.uk> Co-authored-by: ScribleSean <56331494+ScribleSean@users.noreply.github.com> Co-authored-by: arypien <andrzejrypien@proton.me>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two small state fixes for
VeSyncTurboBlazeFryer, against the air fryer refactor in #503. I ran into both while testing a CS358-AF for #557.1.
set_modewith preheat leaves the state asCOOKINGset_mode_from_recipealways set the local status toCOOKING. When the recipe has a preheat time the unit starts in the preheat phase, so until the nextupdate()the state was wrong:is_in_preheat_modewasFalseandcook_from_preheat()could not see the preheat.It now sets
HEATINGwhen the recipe has a preheat time, andCOOKINGotherwise.2. A basket pulled during a cook reads as preheat
getAirfryerStatusreportspreheatSetTimeas0when the program has no preheat.get_detailspassed that0straight toset_state.FryerState.is_in_preheat_modeandis_in_cook_modetest forNone, so apullOutduring a plain cook gaveis_in_preheat_mode=Trueandis_in_cook_mode=False.get_detailsnow passesNonefor both preheat times whenpreheatSetTimeis0.Tests
src/tests/test_fryer_turboblaze_state.py, using the CAF-DC601S test data:set_modewith a preheat time ends inHEATINGand preheat modeset_modewithout one ends inCOOKINGpullOutwithpreheatSetTime0 is cook modepullOutwith a preheat time set is preheat modeThe first and third fail without the fix.
pytest src/tests: all pass locally.ruff checkandruff format --checkare clean.Not covered
On my CS358-AF, a cook that follows a preheat still reports the original
preheatSetTimewhile cooking, withpreheatLastTimeat 0. A basket pulled in that phase is still classified as preheat. Telling it apart from a pull atpreheatEndneeds more than these two fields, so I left it out.I only have a CS358-AF. The
0forpreheatSetTimematches the existing CAF-DC601S test data, but I could not check the behaviour on that model's hardware.