Skip to content

feat: add CAF-DC111S-AEU air fryer support - #536

Closed
arypien wants to merge 4 commits into
webdjoe:devfrom
arypien:feat/caf-dc111s-aeu
Closed

arypien wants to merge 4 commits into
webdjoe:devfrom
arypien:feat/caf-dc111s-aeu

Conversation

@arypien

@arypien arypien commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Add support for the Cosori CAF-DC111S-AEU Turbo Tower Pro Smart Air Fryer

This PR adds support for the European Cosori CAF-DC111S-AEU Turbo Tower Pro Smart Air Fryer.

Device information

  • Device type: CAF-DC111S-AEU
  • Product type: SKA
  • Config module: VS_WFON_CAF-DC111S-AEU_EU
  • Region: EU

API

Unlike previous Cosori air fryers, this model communicates through the new:

POST /cloud/v2/deviceManaged/bypassV2

The following methods have been implemented:

  • getAirfryerMultiStatus
  • startMultiCook
  • endCook

Features

  • Automatic device detection
  • Read the status of both cooking chambers
  • Read cooking mode, temperature and remaining time
  • Read active chamber and synchronization state
  • Prepare a cooking program for either chamber
  • Stop a prepared or running program

startMultiCook prepares the selected program, but the appliance still requires physical confirmation by pressing the Start button on the device.

Testing

The implementation was developed and validated using a real European CAF-DC111S-AEU device.

The API was reverse-engineered by observing the official VeSync application and verifying every implemented request against the physical device.

The PR also includes unit tests covering:

  • device detection
  • reading chamber status
  • preparing a cooking program
  • stopping a program
  • handling an empty chamber (11923000)

All added unit tests pass successfully.

Development note

AI-assisted tooling (ChatGPT) was used during development to help analyze the protocol, explore the codebase and draft parts of the implementation. All code, API behaviour and functionality were manually reviewed, tested and validated on a real device before submission.

@webdjoe

webdjoe commented Nov 1, 2026

Copy link
Copy Markdown
Owner

@arypien Thank you for the PR. Did you capture packets? I will need full packet captures with all of the available states for future maintenance and debugging. Feel free to reach out on discord: homefire1608.

This should be based on https://github.com/webdjoe/pyvesync/tree/air-fryer-refactor. I refactored the air fryer architecture. I can refactor your PR on top once you send the captures.

@webdjoe
webdjoe force-pushed the feat/caf-dc111s-aeu branch from a89d8cc to 04a54fd Compare November 2, 2026 02:42
@charlie508010 charlie508010 mentioned this pull request Nov 2, 2026
6 of 13 tasks
webdjoe added a commit that referenced this pull request Nov 5, 2026
Add the Cosori Turbo Tower Pro Smart Air Fryer (CAF-DC111S-AEU) as a
dual-chamber VeSyncDualAirFryer with its seven cook modes and per-mode
temperature limits from the APK.

- Map the DC111S `hold` cook status (Sync cook wait) to
  AirFryerCookStatus.HOLD. The wait counts down only while the other
  chamber cooks.
- Add the APK-backed DC111S Sync error codes 11914000, 11915000 and
  11926000 to ErrorCodes.
- Tests from the PR #536 captures; docs, README and supported devices.

Hardware testing (model + firmware) must be done by a human with the
device.

Co-authored-by: arypien <andrzejrypien@proton.me>
@webdjoe

webdjoe commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Thank you @arypien for this work and for the captures behind it. The CAF-DC111S-AEU support was ported onto the new air fryer chamber interface and merged in #581 (into air-fryer-refactor), with you credited as co-author. Closing this in favor of #581.

@webdjoe webdjoe closed this Nov 5, 2026
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>
webdjoe added a commit that referenced this pull request Nov 5, 2026
## Description

Releases pyvesync 3.5.0 from `dev` to `master`. Merging publishes to
PyPI and deploys the docs.

**Main changes:** air fryer support is rewritten around a single
`VeSyncFryer` interface, with new Cosori models added. New devices and
fixes for outlets, humidifiers and the thermostat are also included.
**CS137/CS158 air fryer users have breaking changes, listed below.**

### New features and devices

- **Air fryers (#503):** a common interface for cooking, chambers,
presets, temperature limits and units.
- New models: CAF-DC601S TurboBlaze, CAF-P583S Dual Blaze (KUS and KEU),
CAF-TF101S, CAF-TF102S and CAF-DC111S-AEU Turbo Tower Pro.
- A failed or unreadable response now leaves the fryer's state
unchanged, and a refused command returns `False`.
- **WYLDR16A1081 smart plug (#508):** uses `VeSyncBSDOGPlug` without
energy history.
- This adds `OutletFeatures.ENERGY_HISTORY` and
`supports_energy_history`. Weekly, monthly and yearly energy requests
now do nothing on outlets without the flag.
- **RGB nightlight for LUH-O451S-WEU (#502):** adds
`set_rgb_nightlight()` and `supports_rgb_nightlight`.

### Breaking changes

For CS137/CS158 air fryers:

| 3.4.2 | 3.5.0 |
| --- | --- |
| `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` |

- `AirFryer158138State` is removed; use `FryerState`.
- The device map `setup_entry` changes from `CS137-AF/CS158-AF` to
`CS158-AF`.
- `toggle_switch()` on air fryers is deprecated and sends no request,
because they have no on/off command.

Other changes that may affect you:

- The minimum `aiohttp` version rises from 3.8.1 to 3.14.4 (#573),
because older releases have published security advisories.
- `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 do nothing (#503).

### Fixes

- **Sprout Humidifier LEH-B381S:** `set_nightlight_brightness()` and
`toggle_nightlight()` now call the API instead of the base-class stub,
and toggling no longer sends a null brightness (#528).
- **Aura thermostat:** details processing read `supportMode` from the
dataclass instead of the response instance (#555).
- **Unknown API error codes:** `last_response` and logs now keep the
API's code and message (#582).
- **Device discovery:** a device whose class raises `VeSyncError` during
setup is skipped with a warning, so it no longer stops discovery of the
others (#503).
- **`update_all_devices()`:** errors other than `VeSyncError` are now
logged with a traceback instead of being dropped (#503).
- **Test suite:** the event loop is closed in teardown, preventing a
file descriptor leak (#514).

### Docs, tooling and CI

- **Contribution docs (#572):**
  - Adds `AGENTS.md`, a PR template and issue forms.
  - Rewrites the contribution guides.
  - Refreshes the README and docs.
- **Python and mypy (#574):**
  - CI and tox now test Python 3.14.
  - mypy targets Python 3.11.
  - Removes the unused `python-dateutil` from `requirements.txt`.
- **Ruff and pre-commit:**
- `ruff` is pinned to 0.15.21 in `pyproject.toml` and
`requirements-dev.txt`, and `ERA001` is enforced (#503).
  - The pre-commit hooks move to mypy v2.4.1 and ruff v0.17.3 (#498).
- **GitHub Actions:** `actions/checkout` moves to v7 (#527) and
`actions/setup-python` to v7 (#534).

### Known issues

- **RGB nightlight:** the API can return stale values for several
minutes after a change, and the timeout added to handle this isn't fully
reliable. Minimum brightness is 40% (#502).
- **`save_credentials()` docstring:** it still says "If None, no action
is taken", which is no longer true.
- **Ruff versions:** the pre-commit hook (v0.17.3) doesn't match the
pinned version (0.15.21).

## Type of change

- [ ] Bug fix (`fix:`)
- [ ] New feature or device support (`feat:`)
- [ ] Documentation (`docs:`)
- [x] Refactor / test / chore (no functional change): release PR. The
functional changes were reviewed in the PRs linked above.

## Checklist

- [ ] This PR is a **single** feature, fix, or edit. This is a release
PR, so it combines the PRs 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`).
- [ ] No API request/response fields were invented. Not checked because
some models have no capture: see "Mapped or inferred without a capture"
below.
- [x] `ruff check`, `pylint`, `mypy`, and `pytest` pass locally: 613
passed, 3 skipped on Python 3.12; pylint 10/10.
- [x] Tests added/updated and API fixtures written (`pytest
--write_api`) where relevant.
- [x] Documentation updated if behavior or public API changed.

## Real-hardware verification

- [ ] Tested on the physical device. Partial; status by model:

| Model | Status | Firmware | Source |
| --- | --- | --- | --- |
| CAF-P583S-KUS | Tested: remote start and stop, status polling, 11
presets | v1.0.15 | #517, #503 |
| LTM-A401S-WUS | Tested | 1.0.60 | #555 |
| LUH-O451S-WEU | Tested | `____` | #502 |
| LEH-B381S | Tested by the reporter | `____` | #528 |
| WYLDR16A1081 | Not stated in the PR | `____` | #508 |
| CS137-AF / CS158-AF | Not recorded | `____` | #503 |
| CAF-DC601S | Not recorded | `____` | #503 |
| CAF-TF101S | Not recorded | `____` | #503 |
| CAF-TF102S | Pending | `____` | #503 |
| CAF-DC111S-AEU | Pending | `____` | #503, #581 |
| CAF-P583S-KEU | Pending | `____` | #503 |

Mapped or inferred without a capture:

- **CAF-TF102S:** mapped from the VeSync app; there is no capture.
- **CAF-DC111S-AEU:** status captures come from #536. 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, so its
`COOKING` status after a cook starts is inferred from the CS158
captures.

- [ ] Not applicable (explain why): `____`
- [ ] I do not own the device; a packet capture is provided / the device
has been shared with a maintainer for verification.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants