Skill: imaloney/roku-control (version 1.0.0)

--- name: roku-control description: "Control Roku devices via local network (ECP protocol). Use when the user wants to control their Roku TV or streaming device, change channels, launch apps (Netflix, YouTube, Hulu, etc.), navigate menus, adjust volume, play/pause content, search for shows, or power off. Works over LAN with no authentication required." --- # Roku Control Control Roku devices over your local network using the External Control Protocol (ECP). No authentication, cloud services, or complex setup required - just local HTTP commands. ## Prerequisites - Roku device on the same network as OpenClaw - Roku's IP address (can be discovered automatically) ## Setup (First Time) **1. Discover your Roku:** ```bash python3 scripts/roku_control.py discover ``` This will show all Roku devices on your network with their IP addresses. **2. Save the IP address:** Note your Roku's IP (e.g., `192.168.1.100`) for use in commands. **3. Test connectivity:** ```bash python3 scripts/roku_control.py --ip 192.168.1.100 info ``` ## Common Operations ### Device Information ```bash # Get device details python3 scripts/roku_control.py --ip 192.168.1.100 info # List all installed apps python3 scripts/roku_control.py --ip 192.168.1.100 apps # See what's currently playing python3 scripts/roku_control.py --ip 192.168.1.100 active ``` ### Navigation & Control ```bash # Navigate menus python3 scripts/roku_control.py --ip 192.168.1.100 key Up python3 scripts/roku_control.py --ip 192.168.1.100 key Down python3 scripts/roku_control.py --ip 192.168.1.100 key Left python3 scripts/roku_control.py --ip 192.168.1.100 key Right python3 scripts/roku_control.py --ip 192.168.1.100 key Select # Go home python3 scripts/roku_control.py --ip 192.168.1.100 key Home # Go back python3 scripts/roku_control.py --ip 192.168.1.100 key Back ``` ### Playback ```bash # Play/pause python3 scripts/roku_control.py --ip 192.168.1.100 key Play python3 scripts/roku_control.py --ip 192.168.1.100 key Pause # Rewind/fast forward python3 scripts/roku_control.py --ip 192.168.1.100 key Rev python3 scripts/roku_control.py --ip 192.168.1.100 key Fwd # Instant replay (back 10 seconds) python3 scripts/roku_control.py --ip 192.168.1.100 key InstantReplay ``` ### Volume & Power ```bash # Volume control (Roku TV or HDMI-CEC enabled) python3 scripts/roku_control.py --ip 192.168.1.100 key VolumeUp python3 scripts/roku_control.py --ip 192.168.1.100 key VolumeDown python3 scripts/roku_control.py --ip 192.168.1.100 key VolumeMute # Power off python3 scripts/roku_control.py --ip 192.168.1.100 key PowerOff ``` ### Launch Apps ```bash # Launch by app ID (faster) python3 scripts/roku_control.py --ip 192.168.1.100 launch 12 # Netflix # Launch by app name (case-insensitive) python3 scripts/roku_control.py --ip 192.168.1.100 launch Netflix python3 scripts/roku_control.py --ip 192.168.1.100 launch YouTube python3 scripts/roku_control.py --ip 192.168.1.100 launch "Disney+" ``` ### Search & Text Input ```bash # Send search text python3 scripts/roku_control.py --ip 192.168.1.100 text "Breaking Bad" # This is much faster than individual key presses for searches ``` ## Natural Language Translation Map user requests to commands: **Navigation:** - "Go to home screen" → `key Home` - "Go back" → `key Back` - "Scroll down" / "Move down" → `key Down` - "Select this" / "Click" → `key Select` **Playback:** - "Play" / "Resume" → `key Play` - "Pause" → `key Pause` - "Rewind" → `key Rev` - "Fast forward" → `key Fwd` - "Go back 10 seconds" / "Replay that" → `key InstantReplay` **Volume:** - "Turn up the volume" → `key VolumeUp` - "Turn down the volume" → `key VolumeDown` - "Mute" / "Unmute" → `key VolumeMute` **Apps:** - "Open Netflix" → `launch Netflix` - "Launch YouTube" → `launch YouTube` - "Start Hulu" → `launch Hulu` **Search:** - "Search for Breaking Bad" → `text "Breaking Bad"` - "Find Stranger Things" → Open search + send text **Power:** - "Turn off the TV" → `key PowerOff` ## Common App IDs See [references/common-apps.md](references/common-apps.md) for a comprehensive list. Quick reference: - Netflix: 12 - YouTube: 837 - Hulu: 2285 - Disney+: 291097 - Amazon Prime Video: 13 - HBO Max: 61322 - The Roku Channel: 151908 To get app IDs for your specific Roku: ```bash python3 scripts/roku_control.py --ip <ip> apps ``` ## Complete Key Reference See [references/remote-keys.md](references/remote-keys.md) for all supported remote keys. Common keys: `Home`, `Back`, `Up`, `Down`, `Left`, `Right`, `Select`, `Play`, `Pause`, `Rev`, `Fwd`, `VolumeUp`, `VolumeDown`, `VolumeMute`, `PowerOff`, `Search`, `Info` ## Advanced Workflows ### Watch Netflix ```bash # Go home, launch Netflix python3 scripts/roku_control.py --ip 192.168.1.100 key Home sleep 1 python3 scripts/roku_control.py --ip 192.168.1.100 launch 12 ``` ### Search and Play ```bash # Open search, send text, select first result python3 scripts/roku_control.py --ip 192.168.1.100 key Search sleep 1 python3 scripts/roku_control.py --ip 192.168.1.100 text "The Office" sleep 1 python3 scripts/roku_control.py --ip 192.168.1.100 key Select ``` ### Quick Replay ```bash # Go back 10 seconds and resume python3 scripts/roku_control.py --ip 192.168.1.100 key InstantReplay sleep 1 python3 scripts/roku_control.py --ip 192.168.1.100 key Play ``` ### Movie Night Setup ```bash # Launch streaming app, adjust volume python3 scripts/roku_control.py --ip 192.168.1.100 launch "Disney+" sleep 2 python3 scripts/roku_control.py --ip 192.168.1.100 key VolumeDown python3 scripts/roku_control.py --ip 192.168.1.100 key VolumeDown ``` ## Device Mapping Store your Roku IP in `references/roku.json`: ```json { "living_room": { "ip": "192.168.1.100", "name": "Living Room TV", "model": "Roku Ultra" }, "bedroom": { "ip": "192.168.1.101", "name": "Bedroom Roku", "model": "Roku Streaming Stick+" } } ``` Then reference by friendly name in conversations. ## Troubleshooting **"No Roku devices found"** - Ensure Roku is powered on and connected to network - Check that OpenClaw and Roku are on same network/subnet - Some routers block SSDP discovery - try manual IP if known - Verify Roku's network settings in Settings → Network **"Connection timeout"** - Verify IP address is correct - Ping the Roku: `ping <roku-ip>` - Check firewall isn't blocking port 8060 - Roku may have changed IP (use DHCP reservation) **"Key not working"** - Some keys only work on Roku TVs (volume, power, inputs) - Volume keys require HDMI-CEC or Roku TV - Power commands may not be supported on older devices - Check [references/remote-keys.md](references/remote-keys.md) for compatibility **App won't launch** - Verify app is installed: run `apps` command - Use correct app ID (case-sensitive for name matching) - Some apps require additional authentication in their own UI **Discovery not finding device** - Try manual IP if you know it: check router DHCP leases - Roku's IP is shown in Settings → Network → About - Set static IP or DHCP reservation for reliability ## Integration with Other Skills ### Movie Night Routine Combine with Govee lights skill: ```bash # Dim lights for light in "living room" "tv lights"; do python3 govee-lights/scripts/govee_control.py brightness "$light" 15 python3 govee-lights/scripts/govee_control.py temp "$light" 2700 done # Launch streaming app python3 roku-control/scripts/roku_control.py --ip 192.168.1.100 launch Netflix # Set comfortable volume python3 roku-control/scripts/roku_control.py --ip 192.168.1.100 key VolumeDown ``` ## Notes - ECP protocol works entirely over LAN (no internet required) - No authentication or API keys needed - Commands are instant (local network speed) - Multiple Roku devices can be controlled independently - Works with Roku TVs, streaming sticks, and boxes - Power-on commands not supported (ECP limitation - Roku must be awake) - For power-on, use HDMI-CEC or network wake features if available ## Limitations - Cannot power on a fully-off Roku (ECP only works when device is on) - Volume/power commands limited to Roku TVs or HDMI-CEC setups - No feedback on success/failure for some commands - Text input is character-by-character (slower for long searches) - Discovery requires SSDP (some networks block multicast)

path: README.md
content: # Roku Control

Control your Roku TV or streaming device directly over your local network. No cloud services, no authentication, no complex setup - just fast, reliable local control.

## Features

✅ **Automatic Discovery** - Find all Roku devices on your network  
✅ **Full Remote Control** - Navigate menus, control playback, adjust volume  
✅ **App Launching** - Open Netflix, YouTube, Hulu, and any installed app  
✅ **Power Control** - Turn TV on/off remotely  
✅ **Text Input** - Fast text entry for searches  
✅ **Works Offline** - No internet required, purely local network control  

## Quick Start

### 1. Install Dependencies

```bash
pip3 install requests
```

### 2. Discover Your Roku

```bash
python3 scripts/roku_control.py discover
```

This will show all Roku devices on your network:

```json
[
  {
    "ip": "192.168.1.100",
    "name": "Living Room TV",
    "model": "Hisense•Roku TV",
    "serial": "X00100XXXXX"
  }
]
```

### 3. Test Control

```bash
# Get device info
python3 scripts/roku_control.py --ip 192.168.1.100 info

# Press the Home button
python3 scripts/roku_control.py --ip 192.168.1.100 key Home

# Launch Netflix
python3 scripts/roku_control.py --ip 192.168.1.100 launch Netflix
```

## Usage Examples

### Navigation

```bash
# Navigate menus
python3 scripts/roku_control.py --ip 192.168.1.100 key Up
python3 scripts/roku_control.py --ip 192.168.1.100 key Down
python3 scripts/roku_control.py --ip 192.168.1.100 key Select

# Go home or back
python3 scripts/roku_control.py --ip 192.168.1.100 key Home
python3 scripts/roku_control.py --ip 192.168.1.100 key Back
```

### Playback

```bash
# Control playback
python3 scripts/roku_control.py --ip 192.168.1.100 key Play
python3 scripts/roku_control.py --ip 192.168.1.100 key Pause
python3 scripts/roku_control.py --ip 192.168.1.100 key Rev
python3 scripts/roku_control.py --ip 192.168.1.100 key Fwd
```

### Apps

```bash
# Launch apps by name
python3 scripts/roku_control.py --ip 192.168.1.100 launch Netflix
python3 scripts/roku_control.py --ip 192.168.1.100 launch YouTube
python3 scripts/roku_control.py --ip 192.168.1.100 launch "Disney Plus"

# Or by app ID (faster)
python3 scripts/roku_control.py --ip 192.168.1.100 launch 12  # Netflix

# List all installed apps
python3 scripts/roku_control.py --ip 192.168.1.100 apps
```

### Volume & Power

```bash
# Volume control
python3 scripts/roku_control.py --ip 192.168.1.100 key VolumeUp
python3 scripts/roku_control.py --ip 192.168.1.100 key VolumeDown
python3 scripts/roku_control.py --ip 192.168.1.100 key VolumeMute

# Power control
python3 scripts/roku_control.py --ip 192.168.1.100 key PowerOff
python3 scripts/roku_control.py --ip 192.168.1.100 key PowerOn
```

### Search

```bash
# Send text for search (much faster than individual key presses)
python3 scripts/roku_control.py --ip 192.168.1.100 text "Breaking Bad"
```

## Configuration

For convenience, save your Roku IP addresses in `references/roku.json`:

```json
{
  "living_room": {
    "ip": "192.168.1.100",
    "name": "Living Room TV",
    "model": "Roku Ultra"
  },
  "bedroom": {
    "ip": "192.168.1.101",
    "name": "Bedroom Roku",
    "model": "Roku Streaming Stick+"
  }
}
```

Then reference them by friendly name in your automation scripts.

## Natural Language Examples

When translating user requests to commands:

| User Says | Command |
|-----------|---------|
| "Turn on the TV" | `key PowerOn` |
| "Open Netflix" | `launch Netflix` |
| "Go to YouTube" | `launch YouTube` |
| "Turn up the volume" | `key VolumeUp` |
| "Pause" | `key Pause` |
| "Go back" | `key Back` |
| "Search for The Office" | `text "The Office"` |

## Technical Details

**Protocol:** Roku External Control Protocol (ECP)  
**Communication:** Local HTTP REST API on port 8060  
**Discovery:** SSDP multicast on 239.255.255.250:1900  
**Authentication:** None required  
**Internet:** Not needed after initial setup  

## Compatibility

✅ All Roku devices (TV, Ultra, Streaming Stick, Express, etc.)  
✅ Works with any Roku OS version  
⚠️ Volume/power commands require Roku TV or HDMI-CEC setup  
⚠️ Device must be on same network/subnet as OpenClaw  

## Troubleshooting

**Discovery not working?**
- Ensure Roku is powered on and connected to network
- Check both devices are on same subnet
- Some routers block SSDP - use manual IP if needed
- Find IP in Roku Settings → Network → About

**Commands timeout?**
- Verify IP address: `ping <roku-ip>`
- Check port 8060 is accessible: `curl http://<roku-ip>:8060`
- Roku may have changed IP (use DHCP reservation)

**Volume not working?**
- Volume keys only work on Roku TVs
- Roku sticks need HDMI-CEC configured
- Check TV volume control settings

## Contributing

Found a bug or have a feature request? Open an issue on GitHub or submit a pull request!

## License

MIT License - Free to use, modify, and distribute.

## Credits

Built for the OpenClaw community. Uses Roku's official ECP protocol.


path: references/common-apps.md
content: # Common Roku Apps & Channels

This reference lists popular Roku channel/app IDs for quick launching.

## Streaming Services

| App Name | App ID | Notes |
|----------|--------|-------|
| Netflix | 12 | Most common streaming service |
| Hulu | 2285 | Subscription streaming |
| Disney+ | 291097 | Disney, Pixar, Marvel, Star Wars |
| Amazon Prime Video | 13 | Prime Video streaming |
| HBO Max | 61322 | HBO and Warner Bros content |
| YouTube | 837 | Free video platform |
| YouTube TV | 41468 | Live TV streaming |
| Apple TV+ | 551012 | Apple's streaming service |
| Peacock | 593099 | NBCUniversal streaming |
| Paramount+ | 31440 | CBS, MTV, Nickelodeon content |
| ESPN | 34376 | Sports streaming |
| ESPN+ | 116014 | ESPN premium streaming |

## Live TV & News

| App Name | App ID | Notes |
|----------|--------|-------|
| The Roku Channel | 151908 | Free live TV and movies |
| Pluto TV | 74519 | Free live TV channels |
| Tubi | 41468 | Free movies and TV |
| Sling TV | 46041 | Live TV subscription |
| FuboTV | 147977 | Live sports and TV |
| Plex | 13535 | Media server and free content |

## Music

| App Name | App ID | Notes |
|----------|--------|-------|
| Spotify | 22297 | Music streaming |
| Pandora | 28 | Internet radio |
| Apple Music | 626209 | Apple's music service |
| Amazon Music | 41387 | Amazon music streaming |
| iHeartRadio | 31533 | Radio and podcasts |

## Gaming

| App Name | App ID | Notes |
|----------|--------|-------|
| Twitch | 46510 | Game streaming |

## Note

App IDs may vary by region and device. To get the exact IDs for your Roku:

```bash
python3 scripts/roku_control.py --ip <your-roku-ip> apps
```

This will list all installed apps with their IDs.


path: references/remote-keys.md
content: # Roku Remote Keys Reference

Complete list of supported remote control keys for the Roku ECP API.

## Navigation

| Key | Function |
|-----|----------|
| `Home` | Go to Roku home screen |
| `Back` | Go back / return to previous screen |
| `Up` | Navigate up |
| `Down` | Navigate down |
| `Left` | Navigate left |
| `Right` | Navigate right |
| `Select` | Select / OK button |

## Playback Control

| Key | Function |
|-----|----------|
| `Play` | Play |
| `Pause` | Pause |
| `Rev` | Rewind |
| `Fwd` | Fast forward |
| `InstantReplay` | Instant replay (back 10 seconds) |

## Channel/Content

| Key | Function |
|-----|----------|
| `ChannelUp` | Channel up |
| `ChannelDown` | Channel down |
| `Info` | Show info/details |
| `Search` | Open search |

## Volume & Power

| Key | Function |
|-----|----------|
| `VolumeUp` | Increase volume |
| `VolumeDown` | Decrease volume |
| `VolumeMute` | Toggle mute |
| `PowerOff` | Power off (note: many Rokus don't support PowerOn via ECP) |
| `Power` | Toggle power (if supported) |

## Special Keys

| Key | Function |
|-----|----------|
| `Backspace` | Delete character (in text entry) |
| `Enter` | Enter/submit (in text entry) |
| `FindRemote` | Activate remote finder beep |
| `InputTuner` | Switch to antenna/tuner input (if applicable) |
| `InputHDMI1` | Switch to HDMI 1 input |
| `InputHDMI2` | Switch to HDMI 2 input |
| `InputHDMI3` | Switch to HDMI 3 input |
| `InputHDMI4` | Switch to HDMI 4 input |
| `InputAV1` | Switch to composite input |

## Text Entry

For entering text (searches, logins, etc.), use the `text` command instead of individual key presses:

```bash
python3 scripts/roku_control.py --ip <ip> text "search query"
```

This is much faster than pressing individual letter keys.

## Usage Examples

```bash
# Go home
python3 scripts/roku_control.py --ip 192.168.1.100 key Home

# Navigate menu
python3 scripts/roku_control.py --ip 192.168.1.100 key Down
python3 scripts/roku_control.py --ip 192.168.1.100 key Select

# Volume control
python3 scripts/roku_control.py --ip 192.168.1.100 key VolumeUp

# Playback
python3 scripts/roku_control.py --ip 192.168.1.100 key Play
python3 scripts/roku_control.py --ip 192.168.1.100 key Pause
```

## Key Sequences

For complex operations, send multiple keys in sequence:

```bash
# Navigate to second item and select
python3 scripts/roku_control.py --ip 192.168.1.100 key Down
sleep 0.5
python3 scripts/roku_control.py --ip 192.168.1.100 key Select
```

## Notes

- Not all keys are supported on all Roku models
- Power commands may not work on all devices (especially older models)
- Input switching keys only work on Roku TVs, not streaming sticks
- Volume keys require Roku TV or Roku with HDMI-CEC volume control configured


path: references/roku.json
content: {
  "_comment": "Roku device mapping - friendly names to IP addresses. Run 'python3 scripts/roku_control.py discover' to find your Roku devices.",
  
  "living_room": {
    "ip": "REPLACE_WITH_YOUR_ROKU_IP",
    "name": "Living Room TV",
    "model": "Roku Ultra"
  },
  
  "bedroom": {
    "ip": "REPLACE_WITH_YOUR_ROKU_IP",
    "name": "Bedroom Roku",
    "model": "Roku Streaming Stick+"
  }
}


path: scripts/roku_control.py
content: #!/usr/bin/env python3
"""
Roku Control Script - Local network control via ECP (External Control Protocol)
No authentication required - works over LAN
"""

import os
import sys
import json
import socket
import requests
import argparse
import time
from typing import Optional, List, Dict, Any
from xml.etree import ElementTree as ET

# Roku ECP defaults
ROKU_PORT = 8060
SSDP_ADDR = "239.255.255.250"
SSDP_PORT = 1900
DISCOVERY_TIMEOUT = 3

class RokuController:
    def __init__(self, roku_ip: str = None):
        self.roku_ip = roku_ip
        self.device_info = None
    
    def discover(self) -> List[Dict[str, str]]:
        """Discover Roku devices on the local network using SSDP"""
        devices = []
        
        # SSDP discovery message
        ssdp_request = "\r\n".join([
            "M-SEARCH * HTTP/1.1",
            f"HOST: {SSDP_ADDR}:{SSDP_PORT}",
            "MAN: \"ssdp:discover\"",
            "MX: 3",
            "ST: roku:ecp",
            "",
            ""
        ])
        
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        sock.settimeout(DISCOVERY_TIMEOUT)
        
        try:
            sock.sendto(ssdp_request.encode(), (SSDP_ADDR, SSDP_PORT))
            
            seen_locations = set()
            start_time = time.time()
            
            while time.time() - start_time < DISCOVERY_TIMEOUT:
                try:
                    data, addr = sock.recvfrom(1024)
                    response = data.decode()
                    
                    # Parse SSDP response
                    for line in response.split("\r\n"):
                        if line.startswith("LOCATION:"):
                            location = line.split("LOCATION:")[1].strip()
                            
                            if location not in seen_locations:
                                seen_locations.add(location)
                                
                                # Extract IP from location URL
                                ip = location.split("//")[1].split(":")[0]
                                
                                # Get device info
                                try:
                                    device_info = self._get_device_info_from_ip(ip)
                                    devices.append({
                                        "ip": ip,
                                        "location": location,
                                        "name": device_info.get("user-device-name", "Unknown Roku"),
                                        "model": device_info.get("model-name", "Unknown"),
                                        "serial": device_info.get("serial-number", ""),
                                    })
                                except:
                                    devices.append({
                                        "ip": ip,
                                        "location": location,
                                        "name": "Roku Device",
                                        "model": "Unknown"
                                    })
                
                except socket.timeout:
                    break
        
        finally:
            sock.close()
        
        return devices
    
    def _get_device_info_from_ip(self, ip: str) -> Dict[str, str]:
        """Get device info from a specific IP"""
        url = f"http://{ip}:{ROKU_PORT}/query/device-info"
        response = requests.get(url, timeout=2)
        
        if response.status_code == 200:
            root = ET.fromstring(response.content)
            return {child.tag: child.text for child in root}
        
        return {}
    
    def get_device_info(self) -> Dict[str, Any]:
        """Get detailed device information"""
        if not self.roku_ip:
            return {"error": "No Roku IP configured"}
        
        try:
            url = f"http://{self.roku_ip}:{ROKU_PORT}/query/device-info"
            response = requests.get(url, timeout=5)
            
            if response.status_code == 200:
                root = ET.fromstring(response.content)
                self.device_info = {child.tag: child.text for child in root}
                return self.device_info
            else:
                return {"error": f"HTTP {response.status_code}"}
        
        except Exception as e:
            return {"error": str(e)}
    
    def get_apps(self) -> List[Dict[str, str]]:
        """Get list of installed apps/channels"""
        if not self.roku_ip:
            return []
        
        try:
            url = f"http://{self.roku_ip}:{ROKU_PORT}/query/apps"
            response = requests.get(url, timeout=5)
            
            if response.status_code == 200:
                root = ET.fromstring(response.content)
                apps = []
                for app in root.findall("app"):
                    apps.append({
                        "id": app.get("id"),
                        "name": app.text,
                        "type": app.get("type", "appl"),
                        "version": app.get("version", "")
                    })
                return apps
            
        except Exception as e:
            print(f"Error getting apps: {e}", file=sys.stderr)
        
        return []
    
    def get_active_app(self) -> Dict[str, str]:
        """Get currently active app"""
        if not self.roku_ip:
            return {}
        
        try:
            url = f"http://{self.roku_ip}:{ROKU_PORT}/query/active-app"
            response = requests.get(url, timeout=5)
            
            if response.status_code == 200:
                root = ET.fromstring(response.content)
                app = root.find("app")
                if app is not None:
                    return {
                        "id": app.get("id"),
                        "name": app.text,
                        "version": app.get("version", "")
                    }
        
        except Exception as e:
            print(f"Error getting active app: {e}", file=sys.stderr)
        
        return {}
    
    def press_key(self, key: str) -> bool:
        """Press a remote key (Home, Up, Down, Select, etc.)"""
        if not self.roku_ip:
            return False
        
        try:
            url = f"http://{self.roku_ip}:{ROKU_PORT}/keypress/{key}"
            response = requests.post(url, timeout=5)
            return response.status_code == 200
        
        except Exception as e:
            print(f"Error pressing key '{key}': {e}", file=sys.stderr)
            return False
    
    def launch_app(self, app_id: str) -> bool:
        """Launch an app by ID"""
        if not self.roku_ip:
            return False
        
        try:
            url = f"http://{self.roku_ip}:{ROKU_PORT}/launch/{app_id}"
            response = requests.post(url, timeout=5)
            return response.status_code == 200
        
        except Exception as e:
            print(f"Error launching app '{app_id}': {e}", file=sys.stderr)
            return False
    
    def send_text(self, text: str) -> bool:
        """Send text input (for search, etc.)"""
        if not self.roku_ip:
            return False
        
        try:
            for char in text:
                url = f"http://{self.roku_ip}:{ROKU_PORT}/keypress/Lit_{requests.utils.quote(char)}"
                response = requests.post(url, timeout=5)
                if response.status_code != 200:
                    return False
                time.sleep(0.1)  # Small delay between characters
            return True
        
        except Exception as e:
            print(f"Error sending text: {e}", file=sys.stderr)
            return False


def main():
    parser = argparse.ArgumentParser(
        description="Control Roku devices via ECP (External Control Protocol)",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Common Keys:
  Home, Back, Up, Down, Left, Right, Select, Play, Pause,
  Rev, Fwd, InstantReplay, Info, Search, VolumeUp, VolumeDown,
  VolumeMute, PowerOff, ChannelUp, ChannelDown

Examples:
  %(prog)s discover
  %(prog)s --ip 192.168.1.100 info
  %(prog)s --ip 192.168.1.100 apps
  %(prog)s --ip 192.168.1.100 key Home
  %(prog)s --ip 192.168.1.100 launch 12
  %(prog)s --ip 192.168.1.100 text "Breaking Bad"
        """
    )
    
    parser.add_argument("--ip", help="Roku device IP address")
    
    subparsers = parser.add_subparsers(dest="command", help="Command to execute")
    
    # Discover
    subparsers.add_parser("discover", help="Discover Roku devices on network")
    
    # Device info
    subparsers.add_parser("info", help="Get device information")
    
    # Apps
    subparsers.add_parser("apps", help="List installed apps")
    
    # Active app
    subparsers.add_parser("active", help="Get currently active app")
    
    # Key press
    key_parser = subparsers.add_parser("key", help="Press a remote key")
    key_parser.add_argument("key", help="Key name (Home, Up, Select, etc.)")
    
    # Launch app
    launch_parser = subparsers.add_parser("launch", help="Launch an app")
    launch_parser.add_argument("app_id", help="App ID or name")
    
    # Send text
    text_parser = subparsers.add_parser("text", help="Send text input")
    text_parser.add_argument("text", help="Text to send")
    
    args = parser.parse_args()
    
    # Handle discovery separately (doesn't need IP)
    if args.command == "discover":
        print("Discovering Roku devices on network...\n")
        controller = RokuController()
        devices = controller.discover()
        
        if devices:
            print(f"Found {len(devices)} Roku device(s):\n")
            print(json.dumps(devices, indent=2))
        else:
            print("No Roku devices found on network")
        
        sys.exit(0)
    
    # All other commands need an IP
    if not args.ip:
        print("Error: --ip required for this command", file=sys.stderr)
        print("Run 'discover' first to find your Roku's IP address", file=sys.stderr)
        sys.exit(1)
    
    controller = RokuController(args.ip)
    
    # Execute command
    if args.command == "info":
        result = controller.get_device_info()
        print(json.dumps(result, indent=2))
    
    elif args.command == "apps":
        apps = controller.get_apps()
        print(json.dumps(apps, indent=2))
    
    elif args.command == "active":
        app = controller.get_active_app()
        print(json.dumps(app, indent=2))
    
    elif args.command == "key":
        success = controller.press_key(args.key)
        if success:
            print(f"✓ Pressed key: {args.key}")
        else:
            print(f"✗ Failed to press key: {args.key}", file=sys.stderr)
            sys.exit(1)
    
    elif args.command == "launch":
        # Try to find app by name or use ID directly
        app_id = args.app_id
        
        # If not a number, search for app by name
        if not app_id.isdigit():
            apps = controller.get_apps()
            for app in apps:
                if app["name"].lower() == args.app_id.lower():
                    app_id = app["id"]
                    break
        
        success = controller.launch_app(app_id)
        if success:
            print(f"✓ Launched app: {args.app_id}")
        else:
            print(f"✗ Failed to launch app: {args.app_id}", file=sys.stderr)
            sys.exit(1)
    
    elif args.command == "text":
        success = controller.send_text(args.text)
        if success:
            print(f"✓ Sent text: {args.text}")
        else:
            print(f"✗ Failed to send text", file=sys.stderr)
            sys.exit(1)
    
    else:
        parser.print_help()
        sys.exit(1)


if __name__ == "__main__":
    main()


path: skill.json
content: {
  "name": "roku-control",
  "version": "1.0.0",
  "description": "Control Roku devices via local network (ECP protocol). No authentication or cloud services required - works entirely over LAN.",
  "author": "IMaloney1",
  "tags": ["roku", "tv", "streaming", "remote-control", "home-automation", "lan", "ecp"],
  "requirements": {
    "python3": true,
    "packages": ["requests"]
  },
  "capabilities": [
    "Device discovery via SSDP",
    "Remote control (navigation, playback, volume)",
    "App launching (Netflix, YouTube, Hulu, etc.)",
    "Text input for search",
    "Power control (off/on)",
    "Device info and status"
  ],
  "compatibility": {
    "devices": ["Roku TV", "Roku Ultra", "Roku Streaming Stick", "Roku Express", "All Roku devices"],
    "network": "Must be on same LAN as Roku device"
  }
}


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)