Back to skills
SkillHub ClubShip Full StackFull StackBackend

anova-oven

Control Anova Precision Ovens and Precision Cookers (sous vide) via WiFi WebSocket API. Start cooking modes (sous vide, roasting, steam), set temperatures, monitor status, and stop cooking remotely.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
3,087
Hot score
99
Updated
March 20, 2026
Overall rating
C4.0
Composite score
4.0
Best-practice grade
B76.0

Install command

npx @skill-hub/cli install openclaw-skills-anova-skill

Repository

openclaw/skills

Skill path: skills/dodeja/anova-skill

Control Anova Precision Ovens and Precision Cookers (sous vide) via WiFi WebSocket API. Start cooking modes (sous vide, roasting, steam), set temperatures, monitor status, and stop cooking remotely.

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack, Backend.

Target audience: everyone.

License: Apache-2.0.

Original source

Catalog source: SkillHub Club.

Repository owner: openclaw.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install anova-oven into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/openclaw/skills before adding anova-oven to shared team environments
  • Use anova-oven for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: anova-oven
description: Control Anova Precision Ovens and Precision Cookers (sous vide) via WiFi WebSocket API. Start cooking modes (sous vide, roasting, steam), set temperatures, monitor status, and stop cooking remotely.
license: Apache-2.0
compatibility: Requires Python 3.7+, websockets library, and internet access to Anova cloud API
metadata:
  author: Akshay Dodeja
  version: "1.0.0"
  repository: https://github.com/dodeja/anova-skill
---

# Anova Oven & Precision Cooker Control

Control Anova WiFi devices including Precision Ovens (APO) and Precision Cookers (APC) via WebSocket API.

## Prerequisites

1. **Personal Access Token** from Anova app
   - Download Anova Oven app (iOS/Android)
   - Go to: More → Developer → Personal Access Tokens
   - Create token (starts with `anova-`)
   - Store in `~/.config/anova/token`

2. **Python dependencies**
   ```bash
   pip3 install websockets
   ```

3. **Device Setup**
   - Anova device connected to WiFi
   - Paired with your Anova account

## Installation

```bash
# Install Python dependency
pip3 install websockets

# Store your token
mkdir -p ~/.config/anova
echo "anova-YOUR_TOKEN_HERE" > ~/.config/anova/token
chmod 600 ~/.config/anova/token
```

## Usage

### List Devices
```bash
python3 scripts/anova.py list
```

### Basic Cooking
```bash
# Simple cook at 350°F for 30 minutes
python3 scripts/anova.py cook --temp 350 --duration 30

# Cook at 175°C for 45 minutes
python3 scripts/anova.py cook --temp 175 --unit C --duration 45
```

### Advanced Controls

**Custom Elements:**
```bash
# Rear element only (low-temp slow cook)
python3 scripts/anova.py cook --temp 225 --elements rear --duration 180

# Bottom + rear (standard roasting)
python3 scripts/anova.py cook --temp 375 --elements bottom,rear --duration 45

# All elements (maximum heat)
python3 scripts/anova.py cook --temp 450 --elements top,bottom,rear --duration 20
```

**Custom Fan Speed:**
```bash
# Low fan (gentle cooking)
python3 scripts/anova.py cook --temp 250 --fan-speed 25 --duration 120

# High fan (fast heat circulation)
python3 scripts/anova.py cook --temp 400 --fan-speed 100 --duration 30
```

**Probe Cooking:**
```bash
# Cook to internal temperature (not time-based)
python3 scripts/anova.py cook --temp 350 --probe-temp 165

# Low-temp probe cook
python3 scripts/anova.py cook --temp 225 --elements rear --fan-speed 25 --probe-temp 135
```

**Combined Advanced Settings:**
```bash
# Precision low-temp cook
python3 scripts/anova.py cook --temp 225 --elements rear --fan-speed 25 --duration 180

# High-heat sear
python3 scripts/anova.py cook --temp 500 --elements top,bottom,rear --fan-speed 100 --duration 5
```

### Stop Cooking
```bash
python3 scripts/anova.py stop
```

### Monitor (Real-time Stream)
```bash
python3 scripts/anova.py monitor --monitor-duration 60
```

## Natural Language Examples

**Agent prompts:**
- "Preheat the oven to 375°F for roasting"
- "Start sous vide at 135°F for 2 hours"
- "What's the current oven temperature?"
- "Stop cooking"
- "Steam vegetables at 212°F for 15 minutes"

## Features

### Anova Precision Oven (APO)
- Sous vide cooking (wet bulb mode)
- Roasting (dry bulb mode)
- Steam cooking with humidity control
- Temperature control (C/F)
- Real-time status monitoring
- Telemetry export

### Anova Precision Cooker (APC)
- Sous vide cooking
- Temperature control
- Timer management
- Real-time status

## API Reference

**WebSocket Endpoint:** Via Anova cloud service
**Authentication:** Personal Access Token (Bearer token)
**Protocol:** WebSocket with JSON messages

## Configuration

**Token file:** `~/.config/anova/token`
**Default device:** First device found (or specify with `--device-id`)

## Troubleshooting

**"No token found":**
```bash
echo "anova-YOUR_TOKEN" > ~/.config/anova/token
```

**"No devices found":**
- Check device is online in Anova app
- Verify WiFi connection
- Generate new token

**"Connection failed":**
- Check internet connection
- Verify token is valid
- Ensure device is paired with account

## Safety Notes

- Always verify temperature before starting long cooks
- Use timers to prevent overcooking
- Monitor remotely but check in-person for safety
- Default timeout: 4 hours max

## References

- [Anova Developer Portal](https://developer.anovaculinary.com)
- [GitHub: anova-wifi-device-controller](https://github.com/anova-culinary/developer-project-wifi)


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### scripts/anova.py

```python
#!/usr/bin/env python3
"""Anova Oven Control - Fixed command structure"""
import asyncio, websockets, json, argparse, sys, uuid
from pathlib import Path
from datetime import datetime

TOKEN_FILE = Path.home() / ".config" / "anova" / "token"

class AnovaDevice:
    def __init__(self, token):
        self.token = token.strip()
        self.ws = None
        self.devices = []
        self.current_device = None
        self.message_history = []
        
    def generate_uuid(self):
        return str(uuid.uuid4())
        
    async def connect(self):
        uri = f"wss://devices.anovaculinary.io?token={self.token}&supportedAccessories=APC,APO"
        try:
            self.ws = await websockets.connect(uri)
            await self.wait_for_discovery()
        except Exception as e:
            print(f"Connection error: {e}")
            raise
    
    async def wait_for_discovery(self):
        timeout = 5
        start = asyncio.get_event_loop().time()
        while (asyncio.get_event_loop().time() - start) < timeout:
            try:
                message = await asyncio.wait_for(self.ws.recv(), timeout=1.0)
                data = json.loads(message)
                self.message_history.append(data)
                
                if data.get("command") == "EVENT_APC_WIFI_LIST" and "payload" in data:
                    for device in data["payload"]:
                        if not any(d["id"] == device["cookerId"] for d in self.devices):
                            self.devices.append({
                                "id": device["cookerId"],
                                "name": device.get("name", "Anova Precision Cooker"),
                                "type": "APC",
                                "device_type": device.get("type", "unknown"),
                                "data": device
                            })
                
                elif data.get("command") == "EVENT_APO_WIFI_LIST" and "payload" in data:
                    for device in data["payload"]:
                        if not any(d["id"] == device["cookerId"] for d in self.devices):
                            self.devices.append({
                                "id": device["cookerId"],
                                "name": device.get("name", "Anova Precision Oven"),
                                "type": "APO",
                                "device_type": device.get("type", "unknown"),
                                "data": device
                            })
                
                if self.devices:
                    break
            except asyncio.TimeoutError:
                continue
            except json.JSONDecodeError:
                continue
    
    async def discover_devices(self):
        await self.connect()
        return self.devices
    
    async def get_working_elements(self):
        """Detect which heating elements are working (not failed)"""
        # Wait for a state message with element status
        for _ in range(20):
            try:
                message = await asyncio.wait_for(self.ws.recv(), timeout=0.5)
                data = json.loads(message)
                self.message_history.append(data)
                
                if data.get("command") == "EVENT_APO_STATE":
                    payload = data.get("payload", {})
                    nodes = payload.get("state", {}).get("nodes", {})
                    elements = nodes.get("heatingElements", {})
                    
                    working = []
                    for element in ["top", "bottom", "rear"]:
                        if element in elements and not elements[element].get("failed", False):
                            working.append(element)
                    
                    if working:
                        return working
            except (asyncio.TimeoutError, json.JSONDecodeError):
                continue
        
        # Fallback: return bottom + rear (safe default)
        return ["bottom", "rear"]
    
    async def send_command(self, command):
        await self.ws.send(json.dumps(command))
        try:
            response = await asyncio.wait_for(self.ws.recv(), timeout=5.0)
            return json.loads(response)
        except asyncio.TimeoutError:
            return None
    
    async def start_cook(self, temp, unit="F", duration=None, elements=None, fan_speed=None, 
                        humidity=None, probe_temp=None):
        """Start cooking - simplified to match official Anova structure"""
        temp_c = temp if unit == "C" else (temp - 32) * 5/9
        temp_f = temp if unit == "F" else (temp * 9/5) + 32
        
        # Default settings - auto-detect working elements
        if elements is None:
            # Try to detect which elements are working
            working_elements = await self.get_working_elements()
            elements = working_elements
            print(f"Auto-detected working elements: {', '.join(elements)}")
        if fan_speed is None:
            fan_speed = 50
            
        heating_elements = {
            "top": {"on": "top" in elements},
            "bottom": {"on": "bottom" in elements},
            "rear": {"on": "rear" in elements}
        }
        
        # Build temperature bulbs - always use DRY mode
        temp_bulbs = {
            "mode": "dry",
            "dry": {
                "setpoint": {
                    "celsius": temp_c,
                    "fahrenheit": temp_f
                }
            }
        }
        
        # Preheat stage
        preheat_stage = {
            "stepType": "stage",
            "id": self.generate_uuid(),
            "title": "",
            "description": "",
            "type": "preheat",
            "userActionRequired": False,
            "temperatureBulbs": temp_bulbs,
            "heatingElements": heating_elements,
            "fan": {"speed": fan_speed},
            "vent": {"open": False},
            "rackPosition": 3,
            "stageTransitionType": "automatic"
        }
        
        # Add steam if humidity specified
        if humidity is not None:
            preheat_stage["steamGenerators"] = {
                "mode": "relative-humidity",
                "relativeHumidity": {"setpoint": humidity}
            }
        
        # Cook stage
        cook_stage = {
            "stepType": "stage",
            "id": self.generate_uuid(),
            "title": "",
            "description": "",
            "type": "cook",
            "userActionRequired": False,
            "temperatureBulbs": temp_bulbs,
            "heatingElements": heating_elements,
            "fan": {"speed": fan_speed},
            "vent": {"open": False},
            "rackPosition": 3,
            "stageTransitionType": "automatic"
        }
        
        if humidity is not None:
            cook_stage["steamGenerators"] = {
                "mode": "relative-humidity",
                "relativeHumidity": {"setpoint": humidity}
            }
        
        # Timer or probe
        if probe_temp is not None:
            probe_temp_c = probe_temp if unit == "C" else (probe_temp - 32) * 5/9
            probe_temp_f = probe_temp if unit == "F" else (probe_temp * 9/5) + 32
            cook_stage["temperatureProbe"] = {
                "setpoint": {
                    "celsius": probe_temp_c,
                    "fahrenheit": probe_temp_f
                }
            }
        elif duration:
            cook_stage["timer"] = {
                "initial": duration * 60
            }
        
        command = {
            "command": "CMD_APO_START",
            "payload": {
                "id": self.current_device["id"],
                "payload": {
                    "cookId": self.generate_uuid(),
                    "cookerId": self.current_device["id"],
                    "cookableId": "",
                    "title": "",
                    "type": self.current_device["device_type"],
                    "originSource": "api",
                    "cookableType": "manual",
                    "stages": [preheat_stage, cook_stage]
                },
                "type": "CMD_APO_START"
            },
            "requestId": self.generate_uuid()
        }
        
        return await self.send_command(command)
    
    async def stop_cook(self):
        command = {
            "command": "CMD_APO_STOP",
            "payload": {
                "id": self.current_device["id"],
                "type": "CMD_APO_STOP"
            },
            "requestId": self.generate_uuid()
        }
        return await self.send_command(command)
    
    async def monitor(self, duration=60):
        start = datetime.now()
        print(f"Monitoring device for {duration} seconds...\n")
        try:
            while (datetime.now() - start).seconds < duration:
                try:
                    message = await asyncio.wait_for(self.ws.recv(), timeout=1.0)
                    data = json.loads(message)
                    self.message_history.append(data)
                    if data.get("command") == f"EVENT_{self.current_device['type']}_STATE":
                        timestamp = datetime.now().strftime("%H:%M:%S")
                        payload = data.get("payload", {})
                        print(f"[{timestamp}] {json.dumps(payload, indent=2)}")
                except asyncio.TimeoutError:
                    continue
                except json.JSONDecodeError:
                    continue
        except KeyboardInterrupt:
            print("\nMonitoring stopped")
    
    async def close(self):
        if self.ws:
            await self.ws.close()

async def main():
    parser = argparse.ArgumentParser(description="Anova Oven Control")
    parser.add_argument("action", choices=["list", "cook", "stop", "monitor"])
    parser.add_argument("--temp", type=float)
    parser.add_argument("--unit", choices=["C", "F"], default="F")
    parser.add_argument("--duration", type=int, help="Minutes")
    parser.add_argument("--elements", help="Comma-separated: top,bottom,rear")
    parser.add_argument("--fan-speed", type=int, help="0-100")
    parser.add_argument("--humidity", type=int, help="0-100")
    parser.add_argument("--probe-temp", type=float)
    parser.add_argument("--monitor-duration", type=int, default=60)
    args = parser.parse_args()
    
    if not TOKEN_FILE.exists():
        print(f"Error: Token file not found at {TOKEN_FILE}")
        sys.exit(1)
    
    token = TOKEN_FILE.read_text().strip()
    device = AnovaDevice(token)
    
    try:
        if args.action == "list":
            devices = await device.discover_devices()
            if not devices:
                print("No devices found")
                sys.exit(1)
            print(f"Found {len(devices)} device(s):\n")
            for i, dev in enumerate(devices):
                print(f"{i+1}. {dev['name']} ({dev['type']})")
                print(f"   ID: {dev['id']}\n")
        else:
            devices = await device.discover_devices()
            if not devices:
                print("No devices found")
                sys.exit(1)
            device.current_device = devices[0]
            print(f"Using device: {device.current_device['name']}\n")
            
            if args.action == "cook":
                if args.temp is None:
                    print("Error: --temp required")
                    sys.exit(1)
                if not args.duration and not args.probe_temp:
                    print("Error: Either --duration or --probe-temp required")
                    sys.exit(1)
                
                elements = None
                if args.elements:
                    elements = [e.strip() for e in args.elements.split(",")]
                
                desc = f"Cooking at {args.temp}°{args.unit}"
                if args.duration:
                    desc += f" for {args.duration} min"
                if args.probe_temp:
                    desc += f" until probe {args.probe_temp}°{args.unit}"
                if args.humidity:
                    desc += f" with {args.humidity}% humidity"
                if args.elements:
                    desc += f" [elements: {args.elements}]"
                if args.fan_speed:
                    desc += f" [fan: {args.fan_speed}]"
                print(f"{desc}...")
                
                response = await device.start_cook(
                    temp=args.temp,
                    unit=args.unit,
                    duration=args.duration,
                    elements=elements,
                    fan_speed=args.fan_speed,
                    humidity=args.humidity,
                    probe_temp=args.probe_temp
                )
                print("✓ Cook started!")
                if response:
                    print(f"Response: {json.dumps(response, indent=2)}")
            
            elif args.action == "stop":
                print("Stopping cook...")
                response = await device.stop_cook()
                print("✓ Stopped!")
                if response:
                    print(f"Response: {json.dumps(response, indent=2)}")
            
            elif args.action == "monitor":
                await device.monitor(args.monitor_duration)
    
    except Exception as e:
        print(f"Error: {e}")
        import traceback
        traceback.print_exc()
        sys.exit(1)
    finally:
        await device.close()

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

```



---

## Skill Companion Files

> Additional files collected from the skill directory layout.

### README.md

```markdown
# Anova Oven & Precision Cooker Skill

An Agent Skill for controlling Anova Precision Ovens (APO) and Precision Cookers (APC) via their WiFi WebSocket API.

**Example prompts:**
- "Start sous vide at 135°F for 2 hours"
- "Preheat to 375°F for roasting"
- "What's the oven temperature?"
- "Stop cooking"

See [AGENTS.md](AGENTS.md) for agent instructions. Works with Claude, Cursor, ChatGPT, and other LLMs.

## Prerequisites

1. **Anova Device**
   - Anova Precision Oven (APO) or Precision Cooker (APC)
   - Connected to WiFi and paired with your Anova account

2. **Personal Access Token**
   - Download the Anova Oven app (iOS/Android)
   - Navigate to: More → Developer → Personal Access Tokens
   - Generate a new token (starts with `anova-`)
3. **Python Environment**
   - Python 3.7 or higher
   - `websockets` library: `pip3 install websockets`

## Installation

1. **Store your Anova token:**
   ```bash
   mkdir -p ~/.config/anova
   echo "anova-YOUR_TOKEN_HERE" > ~/.config/anova/token
   chmod 600 ~/.config/anova/token
   ```

2. **Install Python dependencies:**
   ```bash
   pip3 install websockets
   ```

3. **Test the connection:**
   ```bash
   python3 scripts/anova.py list
   ```

## Usage

See [SKILL.md](SKILL.md) for complete usage instructions and examples.

**Quick examples:**
```bash
# List your devices
python3 scripts/anova.py list

# Basic cooking
python3 scripts/anova.py cook --temp 350 --duration 30

# Advanced: Custom elements and fan speed
python3 scripts/anova.py cook --temp 225 --elements rear --fan-speed 25 --duration 180

# Probe cooking (cook to internal temperature)
python3 scripts/anova.py cook --temp 350 --probe-temp 165

# Monitor real-time status
python3 scripts/anova.py monitor --monitor-duration 60

# Stop cooking
python3 scripts/anova.py stop
```

## Advanced Controls

The skill supports full control over:
- **Temperature**: Any value in °F or °C
- **Heating Elements**: Individual control (top, bottom, rear) or combinations
- **Fan Speed**: 0-100 for precise air circulation control
- **Cooking Mode**: Timer-based or probe-based (cook to internal temperature)

This enables advanced techniques like:
- Low-temp slow roasting (rear element only, low fan)
- High-heat searing (all elements, high fan)
- Probe-based cooking (stop when meat reaches target temp)

## Safety Notes

- Always verify temperatures before starting long cooks
- Monitor cooking remotely but check in person for safety
- Use timers to prevent overcooking
- Default timeout: 4 hours maximum

## Testing

Tested January 2026 with Anova Precision Oven (APO).

| Feature | Status |
|---------|--------|
| Device discovery | ✓ |
| Sous-vide mode | ✓ |
| Roast mode | ✓ |
| Steam mode | ✓ |
| Stop cooking | ✓ |
| Real-time monitoring | ✓ |
| Temperature units (F/C) | ✓ |
| APC (Precision Cooker) | Not tested |

## License

Apache 2.0 - See [LICENSE](LICENSE) for details.

## Credits

Built with the [Anova Developer API](https://developer.anovaculinary.com).

## Contributing

Issues and pull requests welcome! This skill is part of the [Agent Skills](https://agentskills.io) ecosystem.

```

### _meta.json

```json
{
  "owner": "dodeja",
  "slug": "anova-skill",
  "displayName": "Anova Oven",
  "latest": {
    "version": "0.1.0",
    "publishedAt": 1769217537909,
    "commit": "https://github.com/clawdbot/skills/commit/a689ad5ebc96609ca2282817c8cc6349fcb8f5bf"
  },
  "history": []
}

```

anova-oven | SkillHub