Skip to content

feat: air fryer refactor - #503

Merged
webdjoe merged 30 commits into
devfrom
air-fryer-refactor
Nov 5, 2026
Merged

webdjoe merged 30 commits into
devfrom
air-fryer-refactor

Conversation

@webdjoe

@webdjoe webdjoe commented Jan 29, 2026 •

Copy link
Copy Markdown
Owner

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:)
  • 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).
  • 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. 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.
  • ruff check, pylint, mypy and pytest pass locally: 613 passed, 3 skipped; pylint 10/10.
  • 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.
  • Documentation is updated.

Real-hardware verification

  • Tested on the physical device.
  • 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 feat: add CAF-DC111S-AEU air fryer support #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.

@webdjoe webdjoe added the WIP label Jan 29, 2026

@cdnninja cdnninja left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few thoughts. As well were is state for each chamber? Maybe I missed that.

Comment thread src/pyvesync/base_devices/fryer_base.py Resolved
Comment thread src/pyvesync/base_devices/fryer_base.py Resolved
Comment thread src/pyvesync/base_devices/fryer_base.py Resolved
Comment thread src/pyvesync/devices/vesynckitchen.py Resolved
Comment thread src/pyvesync/device_map.py Resolved
@lackas

lackas commented Apr 26, 2026

Copy link
Copy Markdown

Added CAF-TF102S (Cosori Dual Blaze TwinFry 10L) support on top of this branch: lackas/pyvesync@e86d102

We also built a custom HA integration component with dual-chamber sensor entities (per-zone cook status, temperature, remaining time) based on this refactor: https://github.com/lackas/ha-core/tree/vesync-air-fryer

Both have been running in production. Happy to help get this over the finish line if needed.

@webdjoe

webdjoe commented May 23, 2026

Copy link
Copy Markdown
Owner Author

Hi @lackas thank you for the support! I just want to confirm that you successfully used this PR with the dual blaze? Everything is working as it should?

@lackas

lackas commented May 23, 2026

Copy link
Copy Markdown

Confirmed. Running locally in HA via my custom integration component; both chambers behave as expected (status, temperature, remaining time). All good.

RedsGT and others added 3 commits July 12, 2026 00:10
Adds support for the Cosori Dual Blaze 6.8qt smart air fryer
(CAF-P583S-KUS / CAF-P583S-KEU). Resolves the request in #477.

The Dual Blaze is a single-chamber model with dual heating elements
that uses the same bypassV2 protocol as TurboBlaze (`startCook`,
`endCook`, `getAirfryerStatus`), so it can reuse `VeSyncTurboBlazeFryer`
without needing a new device class.

## Commits

1. **Register CAF-P583S** in `device_map.py` (basic support, default
   AirFry preset).
2. **Add 10 Dual Blaze presets + 5 cook modes** — recipe IDs from
   packet captures of the official VeSync app, default temp/time per
   the CAF-P583S-KUS user manual.

## Tested on

- CAF-P583S-KUS, firmware v1.0.15
- Verified: remote start, remote stop, status polling (cookStatus,
  currentTemp, cookSetTemp, totalTimeRemaining, stepArray), all 11
  presets accepted by the device

## Notes

- The Dual Blaze has no preheat function.
- Temperature range per manual: 175–400°F / 80–205°C.

🤖 The patches in this PR were authored with assistance from Claude
(Anthropic).

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Joe Trabulsy <jtrabulsy@gmail.com>
Co-authored-by: Darren Griffin <darren.griffin@live.co.uk>
…nits

All air fryer temperatures (method arguments, state, and API payloads) are
now consistently expressed in the device's own unit, with the device map as
the single source of truth for each model's unit and step.

- AirFryerMap gains a temp_unit field that seeds VeSyncFryer._temp_unit at
  init, replacing the country-code guess. The API-reported tempUnit still
  wins: each device's get_details sets device.temp_unit explicitly from the
  response, and FryerState.set_state no longer mutates device-level config.
- The map declares a single Fahrenheit step (temperature_step_f); the Celsius
  step is derived from AIRFRYER_STEP_F_TO_C (5F -> 2C, 10F -> 5C) via a new
  temperature_step_c property, and temperature_step/min_temp/max_temp select
  by the device's current unit. round_temperature uses the declared step,
  dropping the imprecise 5/9-derived Celsius rounding.
- The Dual Blaze is split into US (CAF-P583S, Fahrenheit) and EU
  (CAF-P583S-KEU, Celsius) entries so each device type declares one unit.
- set_mode rounds-then-validates via a shared prepare_temperature helper,
  adding the validation TurboBlaze previously skipped and fixing
  validate-before-round edge cases.
- FryerState exposes temp_unit as a read-through property; docstrings
  document that all temperatures are in the device's current unit.

Adds src/tests/test_fryer_units.py covering unit seeding, API-wins updates,
unit-aware steps/ranges, the F-to-C step map, and round-then-validate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@webdjoe webdjoe changed the title [wip] feat: air fryer refactor feat: air fryer refactor Jul 12, 2026
@webdjoe
webdjoe marked this pull request as ready for review July 12, 2026 18:55
@fma965

fma965 commented Sep 7, 2026

Copy link
Copy Markdown

Has there been any update on this?

@webdjoe

webdjoe commented Oct 31, 2026

Copy link
Copy Markdown
Owner Author

Apologies for the wait, releasing this today.

Comment thread src/tests/call_json_fryers.py Resolved
@webdjoe webdjoe removed the WIP label Oct 31, 2026
webdjoe and others added 2 commits October 31, 2026 18:22
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>
@Klaestrup

Copy link
Copy Markdown

Tested using Claude, and the following report is also generated by Claude. I hope it helps:

Tested this branch (commit 685d5ed) on a Cosori Dual Blaze Chef Edition, EU model
(label: CAF-P583S-KEUR), via a custom copy of the HA 2026.10.4 VeSync integration.

  • The cloud reports deviceType "CAF-P583S-KEU", so the KEU entry matches as is.
  • Status polling works: standby -> cooking -> cookEnd, currentTemp in °C,
    totalTimeRemaining counts down, and a cook time changed on the device during
    cooking is picked up.
  • Not tested yet: remote start/stop, basket pulled out.

One issue: for an unmapped device type, _build_device_instance logs
device.to_json(indent=True), which raises TypeError with mashumaro 3.23
(as shipped with HA 2026.10). Since the argument is evaluated even with debug
logging off, any unmapped device on the account aborts the whole setup.

@charlie508010

Copy link
Copy Markdown

Hello,

I had another look at the different air fryer PRs. #550 and #557 already build on your shared base in #503. The Turbo Tower support from #536, #552 and my PR #564 is still separate from that.

I can see a lot of similarities in the commands, for example getAirfryerMultiStatus, startMultiCook and endCook. But there are also differences in the programmes, the start behaviour and the basket states.

I have now tested my CAF-DC111S-AEU directly with pyvesync, without Home Assistant. I went through all seven programmes as presets on both baskets in Celsius and Fahrenheit. I also checked stopping, the status responses, SYNC and MATCH. The results are in #564.

Would it help you if I adapted the Turbo Tower support to your base in #503 and only added the differences for my device? I would like to build on your existing work as much as possible and make it easier for you to bring the changes together.

Of course, I cannot verify the other models with my device.

@webdjoe

webdjoe commented Nov 2, 2026

Copy link
Copy Markdown
Owner Author

@charlie508010 I've already begun to rebuild the DC111S PR's on top of this branch. I'm not sure if was you or someone else that sent me the captures to make it possible.

@charlie508010

Copy link
Copy Markdown

Hello, thanks for the update. Then I don’t need to work on the adaptation in parallel.

My direct device tests and results are in #564. I tested the CAF-DC111S-AEU without Home Assistant: all seven programmes as presets on both baskets in Celsius and Fahrenheit, as well as stopping, status responses, SYNC and MATCH.

When your updated version is ready, I’ll be happy to test it again directly on my device.

webdjoe and others added 5 commits November 4, 2026 00:26
…rrors (#578)

- DeviceContainer skips a device whose class raises VeSyncError in its
  constructor (for example a CS137/CS158 config module missing from
  AIRFRYER_PID_MAP) with a warning, so one bad device no longer stops
  discovery of the rest.
- update_all_devices() logs non-VeSyncError exceptions with their
  traceback instead of dropping them silently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- AirFryerChamber (CHAMBER_1, CHAMBER_2, WHOLE, BOTH) for every control
  method; supported_chambers, is_dual_chamber, get_chamber_state(),
  sync_chambers and work_chamber on VeSyncFryer. BOTH is sent as the
  app's Sync mode on the CAF-TF101S/TF102S.
- Cook modes, presets and per-mode temperature limits and steps come from
  the device map (AirFryerModeConfig, AirFryerTempLimits), with
  get_cook_mode(), get_mode_recipe(), temp_limits() and unit conversion
  for set_mode(temp_unit=...) and recipes.
- Add CAF-TF102S; FryerState tracks total_time_remaining and the
  remaining-time countdowns across stop, resume and preheat.
- Rewrite docs/devices/kitchen.md and update the supported device lists
  and test guides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#580)

- Bad temperature units are logged and the current unit kept; a recipe
  with an invalid unit or temperature returns False instead of raising.
- A failed or unreadable poll (no response, error code, null result,
  unknown status, out-of-range stepIndex) leaves the state unchanged. On
  the CAF-TF101S/TF102S chamber 2 takes the connection status and update
  time from each response, and a chamber with an unknown status keeps
  its state.
- CS137/CS158 stop() and resume() are always sent (the preheat command in
  a preheat phase, otherwise the cook command) and every successful
  command sets the state to the phase it sent; resume keeps the
  remaining time.
- CAF-TF101S/TF102S end(CHAMBER_1) during a whole-basket cook ends the
  whole basket (chamber 3); WHOLE starts set work_chamber, and ending
  WHOLE or BOTH clears it.
- Cook and preheat times are validated before sending (at least one API
  time unit; whole seconds), and set_mode_from_recipe() rejects a cook
  mode the fryer lacks and sends the mode's API string.
- default_preset is a per-device copy; get_chamber_state() returns None
  instead of raising; AirFryer158Command replaces hardcoded status
  strings; the Fahrenheit conversion uses TemperatureUnits.to_fahrenheit.
- Remove the unused FryerState.set_cooking_state/set_preheating_state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
ErrorCodes.get_error_info() raised KeyError on an unknown code before it
set the code and appended the API msg, so last_response and the log line
showed "code: None, message: Unknown error". An unknown code now falls
back to the UNKNOWN entry and keeps both, e.g. a CAF-DC111S-AEU endCook on
an idle chamber gives UNKNOWN, code 11923000, "Unknown error -
af_iot_bypass_end_cook". Callers still treat it as a failure.

- Add const.UNKNOWN_ERROR_CODE for the UNKNOWN key.
- Truncate to the thousand with integer math toward zero, as the app's
  CloudErrorCode lookup does, instead of float division.
- A non-numeric code keeps the API msg.

(cherry picked from commit 668e9b0)
@webdjoe
webdjoe merged commit adab169 into dev Nov 5, 2026
10 checks passed
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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants