Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/pyvesync/device_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -1093,6 +1093,17 @@ class ThermostatMap(DeviceMapTemplate):


air_fryer_modules: list[AirFryerMap] = [
AirFryerMap(
class_name='VeSyncAirFryerDC111',
module=vesynckitchen,
dev_types=['CAF-DC111S-AEU'],
device_alias='Turbo Tower Pro Air Fryer',
model_display='CAF-DC111S-AEU',
model_name='Turbo Tower Pro Smart Air Fryer',
setup_entry='CAF-DC111S-AEU',
temperature_range_c=(40, 230),
temperature_range_f=(100, 450),
),
AirFryerMap(
class_name='VeSyncAirFryer158',
module=vesynckitchen,
Expand All @@ -1101,7 +1112,7 @@ class ThermostatMap(DeviceMapTemplate):
model_display='CS158/159/168/169-AF Series',
model_name='Smart/Pro/Pro Gen 2 5.8 Qt. Air Fryer',
setup_entry='CS137-AF/CS158-AF',
)
),
]
"""List of ['AirFryerMap'][pyvesync.device_map.AirFryerMap] configuration
for air fryer devices."""
Expand Down
176 changes: 176 additions & 0 deletions src/pyvesync/devices/vesynckitchen.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@

from pyvesync.base_devices import FryerState, VeSyncFryer
from pyvesync.const import AIRFRYER_PID_MAP, ConnectionStatus, DeviceStatus
from pyvesync.utils.device_mixins import BypassV2Mixin
from pyvesync.utils.errors import VeSyncError
from pyvesync.utils.helpers import Helpers
from pyvesync.utils.logs import LibraryLogger
Expand All @@ -48,6 +49,8 @@

logger = logging.getLogger(__name__)

AIR_FRYER_NO_ACTIVE_PROGRAM = 11923000


# Status refresh interval in seconds
# API calls outside of interval are automatically refreshed
Expand Down Expand Up @@ -651,3 +654,176 @@ async def _status_api(self, json_cmd: dict) -> bool:
self.state.status_request(json_cmd)
await self.update()
return True


class VeSyncAirFryerDC111(BypassV2Mixin, VeSyncFryer):
"""Cosori Turbo Tower Pro dual-chamber air fryer."""

__slots__ = (
'chambers',
'sync_type',
'temp_unit',
'work_chamber',
)

def __init__(
self,
details: ResponseDeviceDetailsModel,
manager: VeSync,
feature_map: AirFryerMap,
) -> None:
"""Initialize CAF-DC111S-AEU."""
super().__init__(details, manager, feature_map)

self.chambers: dict[int, dict] = {}
self.temp_unit: str | None = None
self.sync_type: int | None = None
self.work_chamber: int | None = None

async def get_details(self) -> None:
"""Read status of both chambers."""
response = await self.call_bypassv2_api(
'getAirfryerMultiStatus',
data={},
)

if not response or response.get('code') != 0:
return

outer = response.get('result') or {}
if outer.get('code') not in (None, 0):
return

result = outer.get('result') or {}

self.temp_unit = result.get('tempUnit')
self.sync_type = result.get('syncType')
self.work_chamber = result.get('workChamber')

self.chambers = {
int(item['chamber']): item
for item in result.get('statusList', [])
if item.get('chamber') is not None
}

async def prepare_program(
self,
chamber: int,
temperature: int,
minutes: int,
mode: str = 'AirFry',
) -> bool:
"""Prepare a cooking program.

The appliance still requires physical confirmation with the Start button.
"""
if chamber not in (1, 2):
raise ValueError('chamber must be 1 or 2')

if minutes <= 0:
raise ValueError('minutes must be greater than zero')

data = {
'accountId': self.manager.account_id,
'cookConfigs': [
{
'chamber': chamber,
'cookSetTime': minutes * 60,
'cookTemp': temperature,
'mode': mode,
'recipeId': 14,
'recipeName': 'Air Fry',
'recipeType': 3,
'shakeTime': 0,
}
],
'readyStart': True,
'syncType': 0,
'tempUnit': 'c',
'workChamber': chamber,
}

response = await self.call_bypassv2_api(
'startMultiCook',
data=data,
)

return bool(
response
and response.get('code') == 0
and (response.get('result') or {}).get('code') == 0
)

async def prepare_both_chambers(
self,
chamber_1: dict[str, int | str],
chamber_2: dict[str, int | str],
sync_type: int,
) -> bool:
"""Prepare both cooking chambers.

sync_type 1 synchronizes finishing times.
sync_type 2 applies matching settings to both chambers.
"""
if sync_type not in (1, 2):
msg = 'sync_type must be 1 or 2'
raise ValueError(msg)

def cook_config(
chamber: int,
config: dict[str, int | str],
) -> dict[str, int | str]:
return {
'chamber': chamber,
'cookSetTime': int(config['minutes']) * 60,
'cookTemp': int(config['temperature']),
'mode': str(config.get('mode', 'AirFry')),
'recipeId': 14,
'recipeName': 'Air Fry',
'recipeType': 3,
'shakeTime': 0,
}

data = {
'accountId': self.manager.account_id,
'cookConfigs': [
cook_config(1, chamber_1),
cook_config(2, chamber_2),
],
'readyStart': True,
'syncType': sync_type,
'tempUnit': 'c',
'workChamber': 4,
}

response = await self.call_bypassv2_api(
'startMultiCook',
data=data,
)
return bool(
response
and response.get('code') == 0
and (response.get('result') or {}).get('code') == 0
)

async def stop_chamber(self, chamber: int) -> bool:
"""End the prepared or running program for one chamber."""
if chamber not in (1, 2):
raise ValueError('chamber must be 1 or 2')

response = await self.call_bypassv2_api(
'endCook',
data={'chamber': chamber},
)

if not response or response.get('code') != 0:
return False

inner = response.get('result') or {}
code = inner.get('code')

# The chamber has no prepared or running program to stop.
if code == AIR_FRYER_NO_ACTIVE_PROGRAM:
return False

return code == 0
30 changes: 30 additions & 0 deletions src/pyvesync/models/fryer_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from dataclasses import dataclass

from pyvesync.models.base_models import ResponseBaseModel
from pyvesync.models.bypass_models import BypassV2InnerResult


@dataclass
Expand Down Expand Up @@ -32,3 +33,32 @@ class FryerBaseReturnStatus(ResponseBaseModel):
"""Result returnStatus model for air fryer status."""

cookStatus: str


@dataclass
class AirFryerChamberStatus(ResponseBaseModel):
"""Status of one CAF-DC111S-AEU cooking chamber."""

cookStatus: str
startTime: int
recipeType: int
recipeId: int
recipeName: str
upc: str
holdTime: int
cookSetTime: int
cookTemp: int
mode: str
currentRemainingTime: int
totalTimeRemaining: int
chamber: int


@dataclass
class AirFryerMultiStatusResult(BypassV2InnerResult):
"""Result returned by getAirfryerMultiStatus."""

statusList: list[AirFryerChamberStatus]
tempUnit: str
syncType: int
workChamber: int
Loading