Getting Started¶
This guide will walk you through creating your first Unfolded Circle Remote integration using the UCAPI Framework.
Installation¶
Install the framework using pip or uv:
Project Structure¶
A typical integration has this structure:
my-integration/
├── intg-mydevice/
│ ├── driver.py # Driver implementation
│ ├── device.py # Device interface
│ ├── setup_flow.py # Setup flow
│ └── config.py # Configuration dataclass
│ └── media_player.py # Media Player Entity
├── pyproject.toml
└── README.md
Quick Example: REST API Device¶
Let's build a simple integration for a device with a REST API.
1. Define Your Configuration¶
# config.py
from dataclasses import dataclass
@dataclass
class MyDeviceConfig:
"""Device configuration."""
identifier: str
name: str
host: str
api_key: str = ""
2. Implement Your Device¶
# device.py
from ucapi_framework import StatelessHTTPDevice
import aiohttp
class MyDevice(StatelessHTTPDevice):
"""Device implementation."""
@property
def identifier(self) -> str:
return self._device_config.identifier
@property
def name(self) -> str:
return self._device_config.name
@property
def address(self) -> str:
return self._device_config.host
@property
def log_id(self) -> str:
return f"MyDevice[{self.identifier}]"
async def verify_connection(self) -> None:
"""Verify device is reachable."""
url = f"http://{self.address}/api/status"
headers = {"Authorization": f"Bearer {self._device_config.api_key}"}
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as response:
response.raise_for_status()
async def send_command(self, command: str) -> None:
"""Send a command to the device."""
url = f"http://{self.address}/api/command"
headers = {"Authorization": f"Bearer {self._device_config.api_key}"}
data = {"command": command}
async with aiohttp.ClientSession() as session:
async with session.post(url, json=data, headers=headers) as response:
response.raise_for_status()
3. Create Your Entities¶
For simple integrations, you often don't need to create your own driver class - you can use BaseIntegrationDriver directly in your main function (see step 5). However, you do need to define entity classes:
# entities.py
from ucapi import MediaPlayer, media_player
from ucapi_framework import create_entity_id, EntityTypes, Entity
class MyMediaPlayer(MediaPlayer, Entity):
"""Media player entity for MyDevice."""
def __init__(self, device_config, device):
"""Initialize the media player entity."""
entity_id = create_entity_id(
EntityTypes.MEDIA_PLAYER,
device_config.identifier,
"player"
)
super().__init__(
entity_id,
device_config.name,
features=[
media_player.Features.ON_OFF,
media_player.Features.VOLUME,
media_player.Features.VOLUME_UP_DOWN,
],
attributes={
media_player.Attributes.STATE: media_player.States.UNKNOWN,
media_player.Attributes.VOLUME: 0,
},
cmd_handler=self.handle_command,
)
self._device = device
self._device_config = device_config
async def handle_command(self, entity_id: str, cmd_id: str, params: dict | None) -> None:
"""Handle entity commands."""
if cmd_id == media_player.Commands.ON:
await self._device.send_command("power_on")
elif cmd_id == media_player.Commands.OFF:
await self._device.send_command("power_off")
elif cmd_id == media_player.Commands.VOLUME:
await self._device.send_command(f"volume_{params['volume']}")
# ... handle other commands
4. Implement Setup Flow¶
# setup_flow.py
from ucapi_framework import BaseSetupFlow
from ucapi.api_definitions import RequestUserInput
from .config import MyDeviceConfig
class MySetupFlow(BaseSetupFlow[MyDeviceConfig]):
"""Setup flow for manual device entry."""
def get_manual_entry_form(self) -> RequestUserInput:
"""Return the manual entry form."""
return RequestUserInput(
title="Add Device",
settings=[
{
"id": "host",
"label": {"en": "Device IP Address", "de": "Geräte-IP-Adresse"},
"field": {"text": {"value": ""}},
},
{
"id": "name",
"label": {"en": "Device Name", "de": "Gerätename"},
"field": {"text": {"value": ""}},
},
{
"id": "api_key",
"label": {"en": "API Key", "de": "API-Schlüssel"},
"field": {"text": {"value": ""}},
},
],
)
async def query_device(self, input_values: dict) -> MyDeviceConfig:
"""Create device config from user input."""
return MyDeviceConfig(
identifier=input_values.get("identifier", input_values["host"].replace(".", "_")),
name=input_values["name"],
host=input_values["host"],
api_key=input_values.get("api_key", ""),
)
5. Wire It All Up¶
# __main__.py
import asyncio
import logging
import ucapi
from ucapi_framework import BaseIntegrationDriver, BaseConfigManager
from .device import MyDevice
from .entities import MyMediaPlayer
from .setup_flow import MySetupFlow
from .config import MyDeviceConfig
_LOG = logging.getLogger(__name__)
async def main():
"""Main entry point."""
logging.basicConfig(level=logging.INFO)
driver = BaseIntegrationDriver(
device_class=MyDevice,
entity_classes=[MyMediaPlayer],
)
# Initialize configuration manager with device callbacks
driver.config_manager = BaseConfigManager(
get_config_path(driver.api.config_dir_path),
driver.on_device_added,
driver.on_device_removed,
config_class=MyDeviceConfig,
)
# Register all configured PowerView hubs
await driver.register_all_device_instances()
setup_handler = PowerviewSetupFlow.create_handler(driver, None)
await driver.api.init("driver.json", setup_handler)
await asyncio.Future()
if __name__ == "__main__":
asyncio.run(main())
Key Points:
- No custom driver class needed: For simple integrations, use
BaseIntegrationDriverdirectly - Entity classes list: Pass your entity classes to
entity_classesparameter - Automatic entity creation: The driver creates entity instances automatically for each device
- Config manager callbacks: Use
add_listenerandremove_listenerto wire up device lifecycle events
Next Steps¶
Now that you have a basic integration:
- Add Discovery - Implement device discovery if your devices support it
- Add Multiple Entities - Use factory functions for creating multiple entities
- Customize Entity Behavior - Use the Entity ABC for per-entity state mapping and attribute filtering
- Handle Events - Override event handlers for custom behavior (see Driver Integration)
- Add Polling - Use
PollingDeviceif your device needs state polling - Add WebSocket - Use
WebSocketDevicefor real-time updates - Hub-based Integrations - Learn about hub-based discovery patterns for devices that expose multiple entities
Check out the User Guide for detailed information on each component!