Repository navigation
feat: air fryer refactor - #503
Conversation
for more information, see https://pre-commit.ci
cdnninja
left a comment
There was a problem hiding this comment.
Few thoughts. As well were is state for each chamber? Maybe I missed that.
for more information, see https://pre-commit.ci
for more information, see https://pre-commit.ci
for more information, see https://pre-commit.ci
|
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. |
|
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? |
|
Confirmed. Running locally in HA via my custom integration component; both chambers behave as expected (status, temperature, remaining time). All good. |
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>
|
Has there been any update on this? |
|
Apologies for the wait, releasing this today. |
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>
|
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
One issue: for an unmapped device type, _build_device_instance logs |
|
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 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. |
|
@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. |
|
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. |
…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)
## 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.
Description
This PR rewrites air fryer support around a single
VeSyncFryerinterface 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:readyStartfalse (from #560)dev, then cherry-picked here)Supported fryers
VeSyncAirFryer158VeSyncTurboBlazeFryerVeSyncDualBlazeFryerVeSyncDualAirFryerVeSyncDualAirFryerVeSyncDualAirFryerCommon 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)andupdate().stop(),resume()andcook_from_preheat()are implemented only on the CS137/CS158. On the other fryers they log a message and returnFalsewithout sending a request.Chambers.
AirFryerChamberhasCHAMBER_1,CHAMBER_2,WHOLEandBOTH. The device also has:supported_chambersandis_dual_chamber;get_chamber_state();sync_chambersandwork_chamber.BOTHis sent as the app's Sync mode. Each chamber has its ownFryerState.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 throughget_cook_mode(),get_mode_recipe()andtemp_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.
FryerStatetracks:AirFryerCookStatusenum;Error handling
False, leaves the state as it was, and records the reason inlast_response.ErrorCodes, including the DC111S Sync refusals 11914000, 11915000 and 11926000.UNKNOWN. Either way the API code and message are kept forlast_responseand the logs.Other changes
DeviceContainerskips a device whose class raisesVeSyncErrorwhile it is created, with a warning. One bad device no longer stops discovery of the rest.update_all_devices()logs errors other thanVeSyncErrorwith their traceback, instead of dropping them.VeSync.save_credentials(None)now saves to the configured token file, or to.vesync_authin the working directory if none is set. It used to save nothing. Its docstring still says "If None, no action is taken".pyproject.toml,requirements-dev.txtand the pre-commit config, andERA001(commented-out code) is now enforced.docs/devices/kitchen.mdis rewritten;docs/supported_devices.mdlist the new models;AGENTS.mdand test guides describe the air fryer tests.Breaking changes for CS137/CS158 users (released in 3.4.2)
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_resumablestate.can_resumestate.remaining_timestate.total_time_remainingstate.is_heatingstate.is_preheatingstate.cook_statusas a stringAirFryerCookStatustoggle_switch()is deprecated and sends no request, because air fryers have no on/off command.Type of change
fix:)feat:)docs:)Checklist
DeviceState; models inmodels/; constants inpyvesync.const).ruff check,pylint,mypyandpytestpass locally: 613 passed, 3 skipped; pylint 10/10.src/tests/api/vesynckitchen/coverupdate()for each fryer.test_fryer_captures.py,test_fryer_chambers.py,test_fryer_units.py,test_fryer_resilience.py,test_errors.pyandtest_manager_resilience.pycover the rest.Real-hardware verification
____________Still pending hardware verification:
readyStartfalse. Its status after a cook starts (COOKING) is inferred from the CS158 captures.