Skip to content

fix: TurboBlaze fryer preheat state after set_mode and on pullOut - #560

Closed
ScribleSean wants to merge 1 commit into
webdjoe:air-fryer-refactorfrom
ScribleSean:fix/turboblaze-preheat-state
Closed

ScribleSean wants to merge 1 commit into
webdjoe:air-fryer-refactorfrom
ScribleSean:fix/turboblaze-preheat-state

Conversation

@ScribleSean

Copy link
Copy Markdown
Contributor

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_mode with preheat leaves the state as COOKING

set_mode_from_recipe always set the local status to COOKING. When the recipe has a preheat time the unit starts in the preheat phase, so until the next update() the state was wrong: is_in_preheat_mode was False and cook_from_preheat() could not see the preheat.

It now sets HEATING when the recipe has a preheat time, and COOKING otherwise.

2. A basket pulled during a cook reads as preheat

getAirfryerStatus reports preheatSetTime as 0 when the program has no preheat. get_details passed that 0 straight to set_state. FryerState.is_in_preheat_mode and is_in_cook_mode test for None, so a pullOut during a plain cook gave is_in_preheat_mode=True and is_in_cook_mode=False.

get_details now passes None for both preheat times when preheatSetTime is 0.

Tests

src/tests/test_fryer_turboblaze_state.py, using the CAF-DC601S test data:

  • set_mode with a preheat time ends in HEATING and preheat mode
  • set_mode without one ends in COOKING
  • pullOut with preheatSetTime 0 is cook mode
  • pullOut with a preheat time set is preheat mode

The first and third fail without the fix. pytest src/tests: all pass locally. ruff check and ruff format --check are clean.

Not covered

On my CS358-AF, a cook that follows a preheat still reports the original preheatSetTime while cooking, with preheatLastTime at 0. A basket pulled in that phase is still classified as preheat. Telling it apart from a pull at preheatEnd needs more than these two fields, so I left it out.

I only have a CS358-AF. The 0 for preheatSetTime matches the existing CAF-DC601S test data, but I could not check the behaviour on that model's hardware.

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.
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>
@webdjoe

webdjoe commented Nov 1, 2026

Copy link
Copy Markdown
Owner

Thank you for the PR! I've incorporated these changes in #575. Will release shortly.

@webdjoe webdjoe closed this Nov 1, 2026
@webdjoe webdjoe mentioned this pull request Nov 5, 2026
6 of 13 tasks
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants