Skip to content

API Integration Guide

Learn how to integrate the BusyLight Web API into your applications and services using both the modern v1 endpoints and compatibility endpoints.

Server Setup

Basic Server Configuration

# server_config.py
import subprocess
import os

def start_busylight_server():
    """Start BusyLight API server with custom configuration."""
    env = os.environ.copy()

    # Optional: Set authentication
    env['BUSYLIGHT_API_USER'] = 'api_user'
    env['BUSYLIGHT_API_PASS'] = 'secure_password'

    # Optional: Enable CORS for web apps
    env['BUSYLIGHT_API_CORS_ORIGINS_LIST'] = '["http://localhost:3000"]'

    # Start server
    subprocess.Popen([
        'busyserve', 
        '--host', '0.0.0.0',
        '--port', '8000'
    ], env=env)

Docker Deployment

# Dockerfile
FROM python:3.11-slim

# Install system dependencies
RUN apt-get update && apt-get install -y \
    libusb-1.0-0-dev \
    libudev-dev \
    && rm -rf /var/lib/apt/lists/*

# Install BusyLight
RUN pip install busylight-for-humans[webapi]

# Configure environment
ENV BUSYLIGHT_API_USER=admin
ENV BUSYLIGHT_API_PASS=changeme
ENV BUSYLIGHT_API_CORS_ORIGINS_LIST='["*"]'

# Expose port
EXPOSE 8000

# Add udev rules and start server
COPY 99-busylights.rules /etc/udev/rules.d/
CMD ["busyserve", "--host", "0.0.0.0", "--port", "8000"]
# docker-compose.yml
version: '3.8'
services:
  busylight:
    build: .
    ports:
      - "8000:8000"
    devices:
      - /dev/bus/usb:/dev/bus/usb
    privileged: true
    environment:
      - BUSYLIGHT_API_USER=admin
      - BUSYLIGHT_API_PASS=secure_password

Client Libraries

Modern Python Client (V1 API)

import requests
import json
from typing import Optional, Dict, Any, List

class ModernBusyLightClient:
    """Python client using the modern v1 API endpoints."""

    def __init__(self, base_url: str = "http://localhost:8000", 
                 auth: Optional[tuple] = None, use_v1: bool = True):
        self.base_url = base_url.rstrip('/')
        self.auth = auth
        self.use_v1 = use_v1
        self.session = requests.Session()
        if auth:
            self.session.auth = auth

    def _post_request(self, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]:
        """Make POST request with JSON data."""
        url = f"{self.base_url}{endpoint}"
        response = self.session.post(
            url, 
            json=data,
            headers={'Content-Type': 'application/json'}
        )
        response.raise_for_status()
        return response.json()

    def _get_request(self, endpoint: str) -> Dict[str, Any]:
        """Make GET request."""
        url = f"{self.base_url}{endpoint}"
        response = self.session.get(url)
        response.raise_for_status()
        return response.json()

    def get_api_info(self) -> Dict[str, Any]:
        """Get API information and available endpoints."""
        return self._get_request("/")

    def get_system_health(self) -> Dict[str, Any]:
        """Check system health and device availability."""
        endpoint = "/api/v1/system/health" if self.use_v1 else "/system/health"
        return self._get_request(endpoint)

    def list_lights(self) -> List[Dict[str, Any]]:
        """Get status of all lights."""
        endpoint = "/api/v1/lights" if self.use_v1 else "/lights"
        return self._get_request(endpoint)

    def get_light(self, light_id: int) -> Dict[str, Any]:
        """Get status of specific light."""
        endpoint = f"/api/v1/lights/{light_id}/status" if self.use_v1 else f"/lights/{light_id}/status"
        return self._get_request(endpoint)

    def turn_on(self, light_id: Optional[int] = None, color: str = "green", 
                led: int = 0, dim: float = 1.0) -> Dict[str, Any]:
        """Turn on light(s) using v1 API."""
        if not self.use_v1:
            raise ValueError("Use compatibility client for non-v1 endpoints")

        data = {"color": color, "led": led, "dim": dim}

        if light_id is not None:
            endpoint = f"/api/v1/lights/{light_id}/on"
        else:
            endpoint = "/api/v1/lights/on"

        return self._post_request(endpoint, data)

    def turn_off(self, light_id: Optional[int] = None) -> Dict[str, Any]:
        """Turn off light(s) using v1 API."""
        if not self.use_v1:
            raise ValueError("Use compatibility client for non-v1 endpoints")

        if light_id is not None:
            endpoint = f"/api/v1/lights/{light_id}/off"
        else:
            endpoint = "/api/v1/lights/off"

        return self._post_request(endpoint, {})

    def blink(self, light_id: Optional[int] = None, color: str = "red", 
              count: int = 0, speed: str = "slow", led: int = 0, 
              dim: float = 1.0) -> Dict[str, Any]:
        """Start blinking effect using v1 API."""
        if not self.use_v1:
            raise ValueError("Use compatibility client for non-v1 endpoints")

        data = {
            "color": color, "count": count, "speed": speed, 
            "led": led, "dim": dim
        }

        if light_id is not None:
            endpoint = f"/api/v1/lights/{light_id}/blink"
        else:
            endpoint = "/api/v1/lights/blink"

        return self._post_request(endpoint, data)

    def rainbow_effect(self, light_id: Optional[int] = None, 
                      speed: str = "slow", dim: float = 1.0, 
                      led: int = 0) -> Dict[str, Any]:
        """Start rainbow effect using v1 API."""
        if not self.use_v1:
            raise ValueError("Use compatibility client for non-v1 endpoints")

        data = {"speed": speed, "dim": dim, "led": led}

        if light_id is not None:
            endpoint = f"/api/v1/effects/{light_id}/rainbow"
        else:
            endpoint = "/api/v1/effects/rainbow"

        return self._post_request(endpoint, data)

    def pulse_effect(self, light_id: Optional[int] = None, color: str = "red",
                    speed: str = "slow", dim: float = 1.0, count: int = 0,
                    led: int = 0) -> Dict[str, Any]:
        """Start pulse effect using v1 API."""
        if not self.use_v1:
            raise ValueError("Use compatibility client for non-v1 endpoints")

        data = {
            "color": color, "speed": speed, "dim": dim, 
            "count": count, "led": led
        }

        if light_id is not None:
            endpoint = f"/api/v1/effects/{light_id}/pulse"
        else:
            endpoint = "/api/v1/effects/pulse"

        return self._post_request(endpoint, data)

# Usage example
client = ModernBusyLightClient(auth=("admin", "password"))

# Check system health
health = client.get_system_health()
print(f"System status: {health['status']}")

# Turn first light red using v1 API
response = client.turn_on(light_id=0, color="red", dim=0.8)
print(f"Lights affected: {response['lights_affected']}")

# Start rainbow effect on all lights
client.rainbow_effect(speed="fast", dim=0.7)

Compatibility Python Client

class CompatibilityBusyLightClient:
    """Python client for backwards compatibility with original API."""

    def __init__(self, base_url: str = "http://localhost:8000", 
                 auth: Optional[tuple] = None):
        self.base_url = base_url.rstrip('/')
        self.auth = auth
        self.session = requests.Session()
        if auth:
            self.session.auth = auth

    def _request(self, endpoint: str, params: Optional[Dict] = None) -> Dict[Any, Any]:
        """Make API request and return JSON response."""
        url = f"{self.base_url}{endpoint}"
        response = self.session.get(url, params=params or {})
        response.raise_for_status()
        return response.json()

    def turn_on(self, light_id: int, color: str = "green", 
                led: int = 0, dim: float = 1.0) -> Dict[str, Any]:
        """Turn on specific light using compatibility endpoint."""
        params = {"color": color, "led": led, "dim": dim}
        return self._request(f"/light/{light_id}/on", params)

    def turn_on_all(self, color: str = "green", 
                    led: int = 0, dim: float = 1.0) -> Dict[str, Any]:
        """Turn on all lights using compatibility endpoint."""
        params = {"color": color, "led": led, "dim": dim}
        return self._request("/lights/on", params)

    def blink(self, light_id: int, color: str = "red", count: int = 0,
              speed: str = "slow", led: int = 0) -> Dict[str, Any]:
        """Start blinking effect using compatibility endpoint."""
        params = {
            "color": color, "count": count, 
            "speed": speed, "led": led
        }
        return self._request(f"/light/{light_id}/blink", params)

# Usage with existing code - no changes needed
legacy_client = CompatibilityBusyLightClient(auth=("admin", "password"))
legacy_client.turn_on(0, "red")  # Works exactly as before

JavaScript/TypeScript Client (V1 API)

// modern-busylight-client.ts
interface LightOperationRequest {
  color?: string;
  dim?: number;
  led?: number;
  speed?: string;
  count?: number;
}

interface EffectRequest extends LightOperationRequest {
  color_a?: string;
  color_b?: string;
}

interface LightOperationResponse {
  success: boolean;
  action: string;
  lights_affected: number;
  details: Array<{
    light_id: number;
    action: string;
    color?: string;
    rgb?: [number, number, number];
    dim?: number;
    led?: number;
  }>;
}

class ModernBusyLightClient {
  private baseUrl: string;
  private auth: { user: string; pass: string } | null;

  constructor(baseUrl = 'http://localhost:8000', auth: { user: string; pass: string } | null = null) {
    this.baseUrl = baseUrl.replace(/\/$/, '');
    this.auth = auth;
  }

  private async postRequest(endpoint: string, data: any): Promise<any> {
    const options: RequestInit = {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(data)
    };

    if (this.auth) {
      const credentials = btoa(`${this.auth.user}:${this.auth.pass}`);
      options.headers!['Authorization'] = `Basic ${credentials}`;
    }

    const response = await fetch(`${this.baseUrl}${endpoint}`, options);
    if (!response.ok) {
      throw new Error(`API Error: ${response.statusText}`);
    }

    return response.json();
  }

  private async getRequest(endpoint: string): Promise<any> {
    const options: RequestInit = {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json'
      }
    };

    if (this.auth) {
      const credentials = btoa(`${this.auth.user}:${this.auth.pass}`);
      options.headers!['Authorization'] = `Basic ${credentials}`;
    }

    const response = await fetch(`${this.baseUrl}${endpoint}`, options);
    if (!response.ok) {
      throw new Error(`API Error: ${response.statusText}`);
    }

    return response.json();
  }

  async getApiInfo(): Promise<any> {
    return this.getRequest('/');
  }

  async getSystemHealth(): Promise<any> {
    return this.getRequest('/api/v1/system/health');
  }

  async listLights(): Promise<any[]> {
    return this.getRequest('/api/v1/lights');
  }

  async turnOn(lightId?: number, request: LightOperationRequest = {}): Promise<LightOperationResponse> {
    const endpoint = lightId !== undefined 
      ? `/api/v1/lights/${lightId}/on`
      : '/api/v1/lights/on';

    const data = {
      color: 'green',
      dim: 1.0,
      led: 0,
      ...request
    };

    return this.postRequest(endpoint, data);
  }

  async turnOff(lightId?: number): Promise<LightOperationResponse> {
    const endpoint = lightId !== undefined 
      ? `/api/v1/lights/${lightId}/off`
      : '/api/v1/lights/off';

    return this.postRequest(endpoint, {});
  }

  async blink(lightId?: number, request: LightOperationRequest = {}): Promise<LightOperationResponse> {
    const endpoint = lightId !== undefined 
      ? `/api/v1/lights/${lightId}/blink`
      : '/api/v1/lights/blink';

    const data = {
      color: 'red',
      dim: 1.0,
      led: 0,
      speed: 'slow',
      count: 0,
      ...request
    };

    return this.postRequest(endpoint, data);
  }

  async rainbowEffect(lightId?: number, request: Omit<LightOperationRequest, 'color'> = {}): Promise<any> {
    const endpoint = lightId !== undefined 
      ? `/api/v1/effects/${lightId}/rainbow`
      : '/api/v1/effects/rainbow';

    const data = {
      dim: 1.0,
      led: 0,
      speed: 'slow',
      ...request
    };

    return this.postRequest(endpoint, data);
  }
}

// Usage
const client = new ModernBusyLightClient('http://localhost:8000', {
  user: 'admin',
  pass: 'password'
});

// Turn all lights blue at 50% brightness
await client.turnOn(undefined, { color: 'blue', dim: 0.5 });

// Start fast rainbow effect on light 0
await client.rainbowEffect(0, { speed: 'fast', dim: 0.8 });

Integration Examples

CI/CD Pipeline Integration (Updated)

# .github/workflows/ci.yml
name: CI Pipeline with BusyLight v1 API

on: [push, pull_request]

jobs:
  test:
    runs-on: self-hosted  # Requires self-hosted runner with BusyLight
    steps:
      - uses: actions/checkout@v4

      - name: Signal build start
        run: |
          curl -X POST http://localhost:8000/api/v1/lights/blink \
            -H "Content-Type: application/json" \
            -d '{"color": "blue", "speed": "fast"}' || true

      - name: Run tests
        run: npm test

      - name: Signal success
        if: success()
        run: |
          curl -X POST http://localhost:8000/api/v1/lights/on \
            -H "Content-Type: application/json" \
            -d '{"color": "green"}' || true
          sleep 2
          curl -X POST http://localhost:8000/api/v1/lights/off \
            -H "Content-Type: application/json" \
            -d '{}' || true

      - name: Signal failure  
        if: failure()
        run: |
          curl -X POST http://localhost:8000/api/v1/lights/blink \
            -H "Content-Type: application/json" \
            -d '{"color": "red", "count": 10, "speed": "fast"}' || true

Status Dashboard (Modern)

<!DOCTYPE html>
<html>
<head>
    <title>BusyLight Modern Dashboard</title>
    <style>
        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
        .dashboard { max-width: 800px; margin: 0 auto; padding: 20px; }
        .control-group { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 8px; }
        .button-group { display: flex; gap: 10px; flex-wrap: wrap; }
        button { 
            padding: 12px 24px; 
            border: none; 
            border-radius: 6px; 
            cursor: pointer;
            font-size: 14px;
            transition: opacity 0.2s;
        }
        button:hover { opacity: 0.8; }
        .status { 
            font-family: 'SF Mono', Monaco, monospace; 
            background: #f5f5f5; 
            padding: 15px; 
            border-radius: 6px; 
            margin-top: 20px;
            white-space: pre-wrap;
        }
        .health-status { 
            padding: 10px; 
            border-radius: 6px; 
            margin: 10px 0;
            font-weight: bold;
        }
        .healthy { background-color: #d4edda; color: #155724; }
        .unhealthy { background-color: #f8d7da; color: #721c24; }
    </style>
</head>
<body>
    <div class="dashboard">
        <h1>BusyLight Control Dashboard</h1>

        <div class="control-group">
            <h3>System Status</h3>
            <button onclick="checkHealth()">Check Health</button>
            <button onclick="getApiInfo()">API Info</button>
            <div id="health-status"></div>
        </div>

        <div class="control-group">
            <h3>Quick Controls</h3>
            <div class="button-group">
                <button onclick="setStatus('available')" style="background: #28a745; color: white;">Available</button>
                <button onclick="setStatus('busy')" style="background: #ffc107; color: black;">Busy</button>
                <button onclick="setStatus('meeting')" style="background: #dc3545; color: white;">In Meeting</button>
                <button onclick="setStatus('offline')" style="background: #6c757d; color: white;">Offline</button>
            </div>
        </div>

        <div class="control-group">
            <h3>Effects</h3>
            <div class="button-group">
                <button onclick="startEffect('rainbow')" style="background: linear-gradient(45deg, #ff0000, #ff7700, #ffff00, #00ff00, #0000ff, #8b00ff); color: white;">Rainbow</button>
                <button onclick="startEffect('blink')" style="background: #17a2b8; color: white;">Blink</button>
                <button onclick="startEffect('pulse')" style="background: #6610f2; color: white;">Pulse</button>
            </div>
        </div>

        <div class="control-group">
            <h3>Individual Light Control</h3>
            <div id="light-controls"></div>
        </div>

        <div class="status" id="status">Ready</div>
    </div>

    <script>
        const API_BASE = 'http://localhost:8000';

        async function postRequest(endpoint, data = {}) {
            try {
                const response = await fetch(`${API_BASE}${endpoint}`, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify(data)
                });

                if (!response.ok) {
                    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
                }

                const result = await response.json();
                updateStatus(JSON.stringify(result, null, 2));
                return result;
            } catch (error) {
                updateStatus(`Error: ${error.message}`, 'error');
                throw error;
            }
        }

        async function getRequest(endpoint) {
            try {
                const response = await fetch(`${API_BASE}${endpoint}`);
                if (!response.ok) {
                    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
                }

                const result = await response.json();
                updateStatus(JSON.stringify(result, null, 2));
                return result;
            } catch (error) {
                updateStatus(`Error: ${error.message}`, 'error');
                throw error;
            }
        }

        function updateStatus(text, type = 'info') {
            const statusEl = document.getElementById('status');
            statusEl.textContent = text;
            statusEl.style.color = type === 'error' ? '#dc3545' : '#333';
        }

        async function checkHealth() {
            try {
                const health = await getRequest('/api/v1/system/health');
                const healthEl = document.getElementById('health-status');
                const isHealthy = health.status === 'healthy';

                healthEl.innerHTML = `
                    <div class="${isHealthy ? 'healthy' : 'unhealthy'} health-status">
                        Status: ${health.status} | 
                        Lights Available: ${health.lights_available} |
                        ${health.timestamp ? `Updated: ${new Date(health.timestamp).toLocaleTimeString()}` : ''}
                    </div>
                `;
            } catch (error) {
                console.error('Health check failed:', error);
            }
        }

        async function getApiInfo() {
            await getRequest('/');
        }

        async function setStatus(status) {
            const colors = {
                'available': 'green',
                'busy': 'yellow', 
                'meeting': 'red',
                'offline': 'black'
            };

            if (status === 'offline') {
                await postRequest('/api/v1/lights/off');
            } else {
                await postRequest('/api/v1/lights/on', { color: colors[status] });
            }
        }

        async function startEffect(effect) {
            if (effect === 'rainbow') {
                await postRequest('/api/v1/effects/rainbow', { speed: 'medium' });
            } else if (effect === 'blink') {
                await postRequest('/api/v1/lights/blink', { 
                    color: 'red', 
                    count: 5, 
                    speed: 'fast' 
                });
            } else if (effect === 'pulse') {
                await postRequest('/api/v1/effects/pulse', { 
                    color: 'blue', 
                    count: 3,
                    speed: 'medium'
                });
            }
        }

        async function loadLights() {
            try {
                const lights = await getRequest('/api/v1/lights');
                const controlsEl = document.getElementById('light-controls');

                controlsEl.innerHTML = lights.map((light, index) => `
                    <div style="margin: 10px 0; padding: 10px; border: 1px solid #eee; border-radius: 4px;">
                        <strong>Light ${light.light_id}: ${light.name}</strong>
                        <div style="margin-top: 8px;">
                            <button onclick="controlLight(${light.light_id}, 'on', 'red')">Red</button>
                            <button onclick="controlLight(${light.light_id}, 'on', 'green')">Green</button>
                            <button onclick="controlLight(${light.light_id}, 'on', 'blue')">Blue</button>
                            <button onclick="controlLight(${light.light_id}, 'off')">Off</button>
                        </div>
                    </div>
                `).join('');
            } catch (error) {
                console.error('Failed to load lights:', error);
            }
        }

        async function controlLight(lightId, action, color) {
            if (action === 'on') {
                await postRequest(`/api/v1/lights/${lightId}/on`, { color });
            } else {
                await postRequest(`/api/v1/lights/${lightId}/off`);
            }
        }

        // Initialize dashboard
        document.addEventListener('DOMContentLoaded', () => {
            checkHealth();
            loadLights();
        });
    </script>
</body>
</html>

Migration Guide

Gradual Migration Strategy

class HybridBusyLightClient:
    """Client that supports both v1 and compatibility endpoints."""

    def __init__(self, base_url: str = "http://localhost:8000", 
                 auth: Optional[tuple] = None, prefer_v1: bool = True):
        self.modern_client = ModernBusyLightClient(base_url, auth, use_v1=True)
        self.compat_client = CompatibilityBusyLightClient(base_url, auth)
        self.prefer_v1 = prefer_v1

    def turn_on(self, light_id: int, color: str = "green", **kwargs):
        """Turn on light using preferred API version."""
        if self.prefer_v1:
            try:
                return self.modern_client.turn_on(light_id, color, **kwargs)
            except Exception as e:
                print(f"V1 API failed, falling back to compatibility: {e}")
                return self.compat_client.turn_on(light_id, color, **kwargs)
        else:
            return self.compat_client.turn_on(light_id, color, **kwargs)

# Migration steps:
# 1. Use HybridBusyLightClient with prefer_v1=False initially
# 2. Test v1 endpoints by setting prefer_v1=True  
# 3. Switch to ModernBusyLightClient when ready

Error Handling Best Practices

import requests
import time
from typing import Optional

class RobustBusyLightClient:
    def __init__(self, base_url: str, max_retries: int = 3, use_v1: bool = True):
        self.base_url = base_url
        self.max_retries = max_retries
        self.use_v1 = use_v1
        self.session = requests.Session()

    def _safe_request(self, method: str, endpoint: str, 
                     data: Optional[dict] = None, 
                     params: Optional[dict] = None) -> Optional[dict]:
        """Make API request with retry logic and error handling."""
        url = f"{self.base_url}{endpoint}"

        for attempt in range(self.max_retries):
            try:
                if method.upper() == 'POST':
                    response = self.session.post(
                        url, 
                        json=data, 
                        timeout=10,
                        headers={'Content-Type': 'application/json'}
                    )
                else:
                    response = self.session.get(url, params=params, timeout=10)

                response.raise_for_status()
                return response.json()

            except requests.exceptions.ConnectionError:
                print(f"Connection failed (attempt {attempt + 1})")
                if attempt < self.max_retries - 1:
                    time.sleep(2 ** attempt)  # Exponential backoff
                    continue

            except requests.exceptions.HTTPError as e:
                if response.status_code == 401:
                    print("Authentication required")
                elif response.status_code == 503:
                    print("No lights available")
                elif response.status_code == 422:
                    print(f"Validation error: {response.json()}")
                break  # Don't retry HTTP errors

            except requests.exceptions.Timeout:
                print(f"Request timeout (attempt {attempt + 1})")
                if attempt < self.max_retries - 1:
                    continue

        return None

    def safe_turn_on(self, light_id: Optional[int] = None, 
                    color: str = "green") -> bool:
        """Safely turn on light with error handling."""
        if self.use_v1:
            endpoint = f"/api/v1/lights/{light_id}/on" if light_id else "/api/v1/lights/on"
            result = self._safe_request('POST', endpoint, {"color": color})
        else:
            endpoint = f"/light/{light_id}/on" if light_id else "/lights/on"
            result = self._safe_request('GET', endpoint, params={"color": color})

        return result is not None and result.get('success', True)

OpenAPI Code Generation

The v1 API provides OpenAPI 3.0 specifications for automatic client generation:

# Generate TypeScript client
npx @openapitools/openapi-generator-cli generate \
  -i http://localhost:8000/openapi.json \
  -g typescript-fetch \
  -o ./generated-client

# Generate Python client  
openapi-generator generate \
  -i http://localhost:8000/openapi.json \
  -g python \
  -o ./generated-python-client

These examples demonstrate how to integrate with both the modern v1 API and maintain compatibility with existing code using the compatibility endpoints.