pyvesync icon indicating copy to clipboard operation
pyvesync copied to clipboard

Cosori Dual Blaze 6.8-Quart Smart Air Fryer CAF-P583S

Open webdjoe opened this issue 2 months ago • 4 comments

Discussed in https://github.com/webdjoe/pyvesync/discussions/249

Originally posted by webdjoe August 6, 2024 Discussion for Cosori Dual Blaze 6.8-Quart Smart Air Fryer CAF-P583S

#55

webdjoe avatar Dec 09 '25 03:12 webdjoe

https://github.com/webdjoe/pyvesync/issues/55#issuecomment-1781441132

POST /cloud/v2/deviceManaged/configurationsV2 HTTP/2

{
  "traceId": "1698336540120",
  "code": 0,
  "msg": "request success",
  "module": null,
  "stacktrace": null,
  "result": {
    "ownerShip": true,
    "hygrometerInfo": null,
    "switchInfo": null,
    "macID": "4c:75:25:11:11:11",
    "btMacID": "4c:75:25:11:11:11",
    "airPurifierInfo": null,
    "deviceRegion": "US",
    "deviceName": "Air Fryer",
    "deviceImg": "https://image.vesync.com/defaultImages/deviceDefaultImages/wfon_afr_caf-p583s-kus_us_240.png",
    "uuid": "47012820-3541-48e0-b4d4-1111111",
    "connectionType": "WiFi+BTOnboarding+BTNotify",
    "deviceStatus": "off",
    "ovenInfo": null,
    "configModule": "WFON_AFR_CAF-P583S-KUS_US",
    "meatThermometerInfo": null,
    "sousVideCookerInfo": null,
    "wifiName": "Down_IoT",
    "subDevices": null,
    "latestFirmVersion": "1.0.15",
    "deviceProp": null,
    "airFryerInfo": {
      "tempUnit": 1,
      "workTempUnit": "f"
    },
    "deviceType": "CAF-P583S-KUS",
    "rssi": -49,
    "allowNotify": "on",
    "wifiMacID": "4c:75:25:11:11:11",
    "firmwareUrl": "http://fw.vesync.com/WFON_AFR_CAF-P583S-KUS_US/mcuFirmware/v1.0.15/CAF-P583S-KUS_MCU_C1_ota_v1.0.15.bin",
    "currentFirmVersion": "1.0.15",
    "connectionStatus": "online",
    "defaultDeviceImg": "https://image.vesync.com/defaultImages/deviceDefaultImages/wfon_afr_caf-p583s-kus_us_240.png",
    "humidifierInfo": null,
    "outletInfo": null,
    "cid": "vsskf5a9079c41eca7592f65c4111111"
  }
}

-----------
POST /cloud/v2/deviceManaged/bypassV2 HTTP/2

{
  "traceId": "1698336538629",
  "code": 0,
  "msg": "request success",
  "module": null,
  "stacktrace": null,
  "result": {
    "traceId": "1698336538629",
    "code": 0,
    "result": {
      "stepArray": [
        {
          "cookSetTime": 60,
          "cookTemp": 175,
          "mode": "AirFry",
          "cookLastTime": 60,
          "shakeTime": 0,
          "cookEndTime": 0,
          "recipeName": "Air Fry",
          "recipeId": 14,
          "recipeType": 3
        }
      ],
      "stepIndex": 0,
      "cookMode": "normal",
      "cookStatus": "ready",
      "tempUnit": "f",
      "preheatSetTime": 0,
      "preheatLastTime": 0,
      "preheatEndTime": 0,
      "preheatTemp": 0,
      "startTime": 1698336533,
      "totalTimeRemaining": 60,
      "currentTemp": 24,
      "appointLastTime": 0,
      "shakeStatus": 0
    }
  }
}

webdjoe avatar Dec 09 '25 03:12 webdjoe

This needs to be added to the pyvesync library before it can be integrated with HA. In order to do that, I'll need comprehensive packet captures. Follow the directions https://webdjoe.github.io/pyvesync/latest/development/capturing/ or you can share the device with me temporarily and I will capture the packets.

webdjoe avatar Dec 09 '25 03:12 webdjoe

Would this help?

`POST /cloud/v2/deviceManaged/bypassV2 HTTP/2 host: smartapi.vesync.com user-agent: VeSync/VeSync 5.6.60(CPH2655;Android 16) _signosinfo: Android _packfilesignature: v0001-ae2f6d63d00ae9f5d4b866dd472b54d1c66d98fe159801adb494785d84f3bb2e content-type: application/json; charset=UTF-8 content-length: 1104 accept-encoding: gzip

{"acceptLanguage":"en","accountID":"20xxx776","appVersion":"VeSync 5.6.60","cid":"vssk90b73c6401aba3646b45465cbf","configModule":"WFON_AFR_CAF-P583S-KUS_US","debugMode":false,"method":"bypassV2","phoneBrand":"CPH2655","phoneOS":"Android 16","subDeviceNo":0,"subDeviceType":"","timeZone":"America/Chicago","token":"10010111eyJhbGciOiJIUzI1NiJ9..UNFzFxNFK3fA7fjIq3L7YKGcjYQvXscGxuOwlbVzv_8","traceId":"1765251913564","userCountryCode":"US","deviceId":"vssk90b73c6401aba913646b45465cbf","configModel":"WFON_AFR_CAF-P583S-KUS_US","payload":{"data":{"accountId":"20xxx776","hasLinkage":false,"hasPreheat":0,"hasWarm":false,"mode":"French fries","readyStart":true,"recipeId":6,"recipeName":"French Fries","recipeType":3,"startAct":{"cookSetTime":1200,"cookTemp":385,"preheatTemp":0,"shakeTime":0},"tempUnit":"f"},"method":"startCook","subDeviceType":"","subDeviceNo":0,"source":"APP"}}'

Request 'POST /cloud/v2/deviceManaged/bypassV2 HTTP/2 host: smartapi.vesync.com user-agent: VeSync/VeSync 5.6.60(CPH2655;Android 16) _signosinfo: Android _packfilesignature: v0001-a2af15ba8943e8edf16d1680eed70f57d19ba0026d6a9233ea76e25e3c310c70 content-type: application/json; charset=UTF-8 content-length: 852 accept-encoding: gzip

{"acceptLanguage":"en","accountID":"20xxx776","appVersion":"VeSync 5.6.60","cid":"vssk90b73c6401ab13646b45465cbf","configModule":"WFON_AFR_CAF-P583S-KUS_US","debugMode":false,"method":"bypassV2","phoneBrand":"CPH2655","phoneOS":"Android 16","subDeviceNo":0,"subDeviceType":"","timeZone":"America/Chicago","token":"10010111eyJhbGciOiJIUzI1NiJ9..UNFzFxNFK3fA7fjIq3L7YKGcjYQvXscGxuOwlbVzv_8","traceId":"1765251914109","userCountryCode":"US","deviceId":"vssk90b73c6401aba913646b45465cbf","configModel":"WFON_AFR_CAF-P583S-KUS_US","payload":{"data":{},"method":"getAirfryerStatus","subDeviceType":"","subDeviceNo":0,"source":"APP"}}'

Respond 'HTTP/2 200 date: Tue, 09 Dec 2025 03:45:14 GMT content-type: application/json;charset=UTF-8 content-encoding: gzip

{"traceId":"1765251914109","code":0,"msg":"request success","module":null,"stacktrace":null,"result":{"traceId":"1765251914109","code":0,"result":{"stepArray":[{"cookSetTime":1200,"cookTemp":385,"mode":"French fries","cookLastTime":1200,"shakeTime":0,"cookEndTime":0,"recipeName":"French Fries","recipeId":6,"recipeType":3}],"stepIndex":0,"cookMode":"normal","cookStatus":"ready","tempUnit":"f","preheatSetTime":0,"preheatLastTime":0,"preheatEndTime":0,"preheatTemp":0,"startTime":1765251913,"totalTimeRemaining":1200,"currentTemp":51,"appointLastTime":0,"shakeStatus":0,"linkageStatus":0}}}'

mikealanni avatar Dec 09 '25 04:12 mikealanni

@webdjoe After capturing data I have a working python script to start, status and stop and the 11 presets

#!/usr/bin/env python3
"""
VeSync Air Fryer Control – FINAL VERSION
Works perfectly with Cosori Dual Blaze CAF-P583S-KUS (and similar 2025 models)
"""

import asyncio
import json
import os
import sys
import traceback
from pyvesync import VeSync

# Exact recipe map from official app + your model
RECIPE_MAP = {
    "air fry":        {"id": 14, "type": 3, "name": "Air Fry",        "mode": "AirFry"},
    "broil":          {"id": 17, "type": 3, "name": "Broil",         "mode": "Broil"},
    "roast":          {"id": 13, "type": 3, "name": "Roast",         "mode": "Roast"},
    "bake":           {"id": 9,  "type": 3, "name": "Bake",          "mode": "Bake"},
    "reheat":         {"id": 16, "type": 3, "name": "Reheat",        "mode": "Reheat"},
    "steak":          {"id": 1,  "type": 3, "name": "Steak",         "mode": "Steak"},
    "seafood":        {"id": 3,  "type": 3, "name": "Seafood",       "mode": "Seafood"},
    "veggies":        {"id": 15, "type": 3, "name": "Veggies",       "mode": "Veggies"},
    "french fries":   {"id": 6,  "type": 3, "name": "French Fries",  "mode": "FrenchFries"},
    "frozen":         {"id": 5,  "type": 3, "name": "Frozen",        "mode": "Frozen"},
    "chicken":        {"id": 2,  "type": 3, "name": "Chicken",       "mode": "Chicken"},
}

API_URL = "/cloud/v2/deviceManaged/bypassV2"


async def main() -> None:
    action = sys.argv[1].lower() if len(sys.argv) > 1 else "status"

    email = os.getenv("VESYNC_EMAIL")
    password = os.getenv("VESYNC_PASSWORD")
    if not email or not password:
        print(json.dumps({"error": "Missing VESYNC_EMAIL/VESYNC_PASSWORD"}))
        return

    manager = VeSync(email, password, "America/Chicago")

    try:
        if not await manager.login():
            print(json.dumps({"error": "Login failed"}))
            return

        await manager.get_devices()
        device = manager.devices.air_fryers[0] if manager.devices.air_fryers else None
        if not device:
            print(json.dumps({"error": "Air fryer not found"}))
            return

        # ──────────────────────────────────────
        # START
        # ──────────────────────────────────────
        if action == "start":
            temp = int(sys.argv[2]) if len(sys.argv) > 2 else 375
            time_mins = int(sys.argv[3]) if len(sys.argv) > 3 else 10
            mode_input = sys.argv[4] if len(sys.argv) > 4 else "Air Fry"

            recipe = RECIPE_MAP.get(mode_input.lower())
            if not recipe:
                print(json.dumps({
                    "action": "start", "success": False,
                    "error": f"Unknown mode: {mode_input}",
                    "available_modes": [k.title() for k in RECIPE_MAP]
                }))
                return

            # 1. startCook – preset configuration
            config_body = device._build_request()
            config_body.update({
                "method": "bypassV2",
                "payload": {
                    "data": {
                        "accountId": manager.account_id,
                        "hasLinkage": False,
                        "hasPreheat": 0,
                        "hasWarm": False,
                        "mode": recipe["mode"],
                        "readyStart": False,
                        "recipeId": recipe["id"],
                        "recipeName": recipe["name"],
                        "recipeType": recipe["type"],
                        "startAct": {
                            "cookSetTime": time_mins * 60,
                            "cookTemp": temp,
                            "preheatTemp": 0,
                            "shakeTime": 0
                        },
                        "tempUnit": "f"
                    },
                    "method": "startCook",
                    "source": "APP"
                }
            })

            resp, _ = await manager.async_call_api(API_URL, "post", json_object=config_body)
            if not (resp and resp.get("code") == 0 and resp.get("result", {}).get("code") == 0):
                print(json.dumps({
                    "action": "start", "success": False,
                    "error": "Failed to configure device", "api_response": resp
                }))
                return

            await asyncio.sleep(1)

            # 2. setSwitch start
            start_body = device._build_request()
            start_body.update({
                "method": "bypassV2",
                "payload": {"data": {"startStop": "start"}, "method": "setSwitch", "source": "APP"}
            })
            start_resp, _ = await manager.async_call_api(API_URL, "post", json_object=start_body)

            # 3. readyStart = true
            ready_body = device._build_request()
            ready_body.update({
                "method": "bypassV2",
                "payload": {"data": {"readyStart": True}, "method": "updateDeviceStatus", "source": "APP"}
            })
            await manager.async_call_api(API_URL, "post", json_object=ready_body)

            print(json.dumps({
                "action": "start", "success": True,
                "mode": recipe["name"], "temp": temp, "time": time_mins
            }))

        # ──────────────────────────────────────
        # STOP
        # ──────────────────────────────────────
        elif action == "stop":
            body = device._build_request()
            body.update({
                "method": "bypassV2",
                "payload": {"data": {}, "method": "endCook", "source": "APP"}
            })
            r, _ = await manager.async_call_api(API_URL, "post", json_object=body)
            if not (r and r.get("code") == 0):
                body["payload"]["method"] = "setSwitch"
                body["payload"]["data"] = {"startStop": "stop"}
                r, _ = await manager.async_call_api(API_URL, "post", json_object=body)

            print(json.dumps({"action": "stop", "success": bool(r and r.get("code") == 0)}))

        # ──────────────────────────────────────
        # STATUS – now 100% accurate using the real API call
        # ──────────────────────────────────────
        elif action == "status":
            status_body = device._build_request()
            status_body.update({
                "method": "bypassV2",
                "payload": {
                    "data": {},
                    "method": "getAirfryerStatus",
                    "source": "APP"
                }
            })

            resp, _ = await manager.async_call_api(API_URL, "post", json_object=status_body)

            if not resp or resp.get("code") != 0 or resp.get("result", {}).get("code") != 0:
                print(json.dumps({"cook_status": "error", "error": "Failed to get status"}))
                return

            data = resp["result"]["result"]

            # Extract only what we care about – exactly like the official app
            step = data["stepArray"][0] if data["stepArray"] else {}

            status = {
                "cook_status": data.get("cookStatus", "unknown"),
                "current_temp": data.get("currentTemp"),
                "set_temp": step.get("cookTemp"),
                "time_remaining_sec": data.get("totalTimeRemaining", 0),
                "time_remaining_min": round(data.get("totalTimeRemaining", 0) / 60, 1),
                "active_mode": step.get("mode", "N/A"),
                "recipe_name": step.get("recipeName", "Manual"),
                "device_status": "online",
                "connection_status": "connected"
            }

            print(json.dumps(status))

    except Exception as e:
        print(json.dumps({"error": str(e), "traceback": traceback.format_exc()}))
    finally:
        if manager and hasattr(manager, "session"):
            try:
                await manager.session.close()
            except:
                pass


if __name__ == "__main__":
    asyncio.run(main())

mikealanni avatar Dec 11 '25 08:12 mikealanni