Skip to content

Driver API Reference

The driver is the central coordinator for your integration, managing device lifecycle, entity registration, and Remote events.

BaseIntegrationDriver

ucapi_framework.driver.BaseIntegrationDriver

BaseIntegrationDriver(device_class: type[DeviceT], entity_classes: list[type[Entity] | EntityFactory] | type[Entity], require_connection_before_registry: bool = False, loop: AbstractEventLoop | None = None, driver_id: str | None = None)

Bases: Generic[DeviceT, ConfigT]

Base class for Remote Two integration drivers.

Handles common patterns like: - Event listeners (connect, disconnect, standby, subscribe/unsubscribe) - Device lifecycle management - Entity registration and updates - State propagation from devices to entities

Class Type Parameters:

Name Bound or Constraints Description Default
DeviceT

The device interface class (e.g., YamahaAVR)

required
ConfigT

The device configuration class (e.g., YamahaDevice)

required

Initialize the integration driver.

:param device_class: The device interface class to instantiate :param entity_classes: Entity class or list of entity classes (e.g., MediaPlayer, Light) Single entity class will be converted to a list :param require_connection_before_registry: If True, ensure device connection before subscribing to entities and re-register available entities after connection. Useful for hub-based integrations that populate entities dynamically on connection. :param loop: The asyncio event loop (optional, defaults to asyncio.get_running_loop()) :param driver_id: Optional driver/integration ID. Used for entity ID migration to automatically fetch the current version from the Remote, eliminating manual entry during upgrades.

Functions

on_r2_connect_cmd async

on_r2_connect_cmd() -> None

Handle Remote Two connect command.

Default implementation: connects all configured devices and sets integration state. Override to add custom logic before/after device connections.

Example

async def on_r2_connect_cmd(self) -> None: await super().on_r2_connect_cmd() # Custom logic after connect

on_r2_disconnect_cmd async

on_r2_disconnect_cmd() -> None

Handle Remote Two disconnect command.

Default implementation: disconnects all configured devices. Override to add custom disconnect logic.

on_r2_enter_standby async

on_r2_enter_standby() -> None

Handle Remote Two entering standby mode.

Default implementation: disconnects all devices to save resources. Override to customize standby behavior.

on_r2_exit_standby async

on_r2_exit_standby() -> None

Handle Remote Two exiting standby mode.

Default implementation: reconnects all configured devices. Override to customize wake behavior.

on_subscribe_entities async

on_subscribe_entities(entity_ids: list[str]) -> None

Handle entity subscription events.

Default implementation handles two scenarios:

Standard integrations (require_connection_before_registry=False): - Adds devices for subscribed entities (with background connect) - Calls refresh_entity_state() for each entity

Hub-based integrations (require_connection_before_registry=True): - If device not configured: adds device, connects, then creates entities using factory functions - If device configured but not connected: connects with retries, then creates entities - Factory functions in entity_classes can access device.lights, device.scenes, etc. - Calls refresh_entity_state() for each entity

Override refresh_entity_state() for custom state refresh logic.

:param entity_ids: List of entity identifiers being subscribed

on_unsubscribe_entities async

on_unsubscribe_entities(entity_ids: list[str]) -> None

Handle entity unsubscription events.

Default implementation: disconnects and cleans up devices when all their entities are unsubscribed. Override to customize cleanup behavior.

:param entity_ids: List of entity identifiers being unsubscribed

add_configured_device

add_configured_device(device_config: ConfigT, connect: bool = True) -> None

Add and configure a device (non-blocking).

This method adds a device to the configured devices list and registers its available entities. If connect=True, it will start a background connection task (non-blocking).

Use this for normal device addition where you don't need to wait for connection to complete. For hub-based integrations that need to wait for connection before registering entities, use async_add_configured_device().

:param device_config: Device configuration :param connect: Whether to initiate connection immediately (as background task)

setup_device_event_handlers

setup_device_event_handlers(device: DeviceT) -> None

Attach event handlers to device.

Override this method to add custom event handlers. Call super() first to register the default handlers, then add your custom ones.

:param device: Device instance

register_available_entities

register_available_entities(device_config: ConfigT, device: DeviceT) -> None

Register available entities for a device.

Override this method to customize entity registration logic. Call super() to use the default implementation that calls create_entities().

:param device_config: Device configuration :param device: Device instance

on_device_connected async

on_device_connected(device_id: str) -> None

Handle device connection.

Sets integration device state to CONNECTED and refreshes state for legacy-pattern entities. Coordinator-pattern entities (those with sync_state() overridden) are skipped here — they receive state via push_update() emitted by the device's own connect() implementation, avoiding a redundant double-sync.

:param device_id: Device identifier

on_device_disconnected async

on_device_disconnected(device_id: str) -> None

Handle device disconnection.

Sets all entity states to UNAVAILABLE when device disconnects.

:param device_id: Device identifier

on_device_connection_error async

on_device_connection_error(device_id: str, message: str) -> None

Handle device connection error.

Sets all entity states to UNAVAILABLE when device connection fails.

:param device_id: Device identifier :param message: Error message

on_device_update async

on_device_update(entity_id: str | None = None, update: dict[str, Any] | None = None, clear_media_when_off: bool = True) -> None

Handle device state updates.

This handler is wired to DeviceEvents.UPDATE and supports two patterns:

Coordinator pattern (recommended): Entities that override sync_state() and call subscribe_to_device() manage their own state. The device simply emits DeviceEvents.UPDATE with no arguments — entity_id and update are ignored, and this handler returns immediately without doing any work.

Legacy / attribute-routing pattern: The device emits DeviceEvents.UPDATE with an entity_id and an update dict of raw attribute values. This handler extracts the entity-type-specific attributes and pushes them to the Remote. Override this method to customise the attribute routing or state mapping.

:param entity_id: Entity identifier. Required for the legacy pattern; omit (or pass None) when using the coordinator pattern. :param update: Dictionary of raw attribute values to apply. Required for the legacy pattern; omit (or pass None) when using the coordinator pattern. :param clear_media_when_off: Legacy pattern only. If True, clears all media player attributes when the state transitions to OFF. Has no effect in the coordinator pattern.

get_device_config

get_device_config(device_id: str) -> ConfigT | None

Get device configuration for the given device ID.

Default implementation: checks _device_instances first, then falls back to self._config_manager.get() if config manager is available. Override this if your integration uses a different config structure.

:param device_id: Device identifier :return: Device configuration or None

get_device_id

get_device_id(device_config: ConfigT) -> str

Extract device ID from device configuration.

Default implementation: tries common attribute names (identifier, id, device_id). Override this if your config uses a different attribute name.

:param device_config: Device configuration :return: Device identifier :raises AttributeError: If no valid ID attribute is found

get_device_name

get_device_name(device_config: ConfigT) -> str

Extract device name from device configuration.

Default implementation: tries common attribute names (name, friendly_name, device_name). Override this if your config uses a different attribute name.

:param device_config: Device configuration :return: Device name :raises AttributeError: If no valid name attribute is found

get_device_address

get_device_address(device_config: ConfigT) -> str

Extract device address from device configuration.

Default implementation: tries common attribute names (address, host_address, ip_address, device_address, host). Override this if your config uses a different attribute name.

:param device_config: Device configuration :return: Device address :raises AttributeError: If no valid address attribute is found

create_entities

create_entities(device_config: ConfigT, device: DeviceT) -> list[Entity]

Create entity instances for a device.

DEFAULT IMPLEMENTATION: Creates one instance per entity class/factory passed to init. Supports both entity classes and factory functions: - Classes are called as: entity_class(device_config, device) - Factories are called as: factory(device_config, device) and can return Entity | list[Entity]

After entity creation, the framework automatically sets entity._api = self.api for entities that inherit from the framework Entity ABC. This gives entities access to the API without requiring it as a constructor parameter.

This works automatically for simple integrations. Override this method only when you need: - Complex conditional logic that can't be expressed in a factory function - Custom parameters beyond (device_config, device) - Special initialization sequences

Using Factory Functions (recommended for most multi-entity patterns):

Example - Static sensor list
In main function or driver init:

driver = BaseIntegrationDriver( device_class=MyDevice, entity_classes=[ MyMediaPlayer, MyRemote, lambda cfg, dev: [ MySensor(cfg, dev, sensor_config) for sensor_config in SENSOR_TYPES ] ] )

Example - Hub-based discovery
In main function or driver init:

driver = BaseIntegrationDriver( device_class=MyHub, entity_classes=[ lambda cfg, dev: [ MyLight(cfg, dev, light) for light in dev.lights ], lambda cfg, dev: [ MyButton(cfg, dev, scene) for scene in dev.scenes ] ], require_connection_before_registry=True )

Override Method (for complex cases):

Example - Multi-zone receiver with custom logic

def create_entities(self, device_config, device): entities = [] for zone in device_config.zones: if zone.enabled: entities.append(AnthemMediaPlayer( entity_id=create_entity_id( EntityTypes.MEDIA_PLAYER, device_config.id, f"zone_{zone.id}" ), device=device, device_config=device_config, zone_config=zone # Custom parameter )) return entities

Example - Conditional creation

def create_entities(self, device_config, device): entities = [] if device.supports_playback: entities.append(YamahaMediaPlayer(device_config, device)) if device.supports_remote: entities.append(YamahaRemote(device_config, device)) return entities

:param device_config: Device configuration :param device: Device instance :return: List of entity instances (MediaPlayer, Remote, etc.)

map_device_state

map_device_state(device_state: Any) -> media_player.States

Map device-specific state to ucapi media player state.

DEFAULT IMPLEMENTATION: Uses map_state_to_media_player() helper to convert device_state to uppercase string and map common state values to media_player.States:

  • UNAVAILABLE → UNAVAILABLE
  • UNKNOWN → UNKNOWN
  • ON, MENU, IDLE, ACTIVE, READY → ON
  • OFF, POWER_OFF, POWERED_OFF, STOPPED → OFF
  • PLAYING, PLAY, SEEKING → PLAYING
  • PAUSED, PAUSE → PAUSED
  • STANDBY, SLEEP → STANDBY
  • BUFFERING, LOADING → BUFFERING
  • Everything else → UNKNOWN

Override this method if you need: - Different state mappings - Device-specific state enum handling - Complex state logic

Example override

def map_device_state(self, device_state): if isinstance(device_state, MyDeviceState): match device_state: case MyDeviceState.POWERED_ON: return media_player.States.ON case MyDeviceState.POWERED_OFF: return media_player.States.OFF case _: return media_player.States.UNKNOWN return super().map_device_state(device_state)

:param device_state: Device-specific state (string, enum, or any object with str) :return: Media player state

device_from_entity_id

device_from_entity_id(entity_id: str) -> str | None

Extract device identifier from entity identifier.

DEFAULT IMPLEMENTATION: Parses entity IDs using the configured separator (defaults to "."). Handles both formats: - Simple: "entity_type.device_id" → returns "device_id" - With sub-entity: "entity_type.device_id.entity_id" → returns "device_id"

If you use a custom entity ID format that doesn't use the standard separator, either: 1. Set driver.entity_id_separator to your custom separator, OR 2. Override this method to parse your custom format

Example with custom separator

def init(self, ...): super().init(...) self.entity_id_separator = "_" # Use underscore instead of period

Example custom override

def device_from_entity_id(self, entity_id: str) -> str | None: # For PSN, entity_id IS the device_id return entity_id

:param entity_id: Entity identifier (e.g., "media_player.device_123") :return: Device identifier or None :raises ValueError: If entity_id doesn't contain the expected separator

get_entity_ids_for_device

get_entity_ids_for_device(device_id: str) -> list[str]

Get all entity identifiers for a device.

DEFAULT IMPLEMENTATION: Queries all registered entities from the API and filters them by device_id using device_from_entity_id().

This works automatically with the standard entity ID format from create_entity_id(). For integrations using custom entity ID formats, this will work as long as device_from_entity_id() is properly overridden to parse your custom format.

Override this method only if you need: - Performance optimization for integrations with many entities - Special filtering logic beyond device_id matching - Caching or pre-computed entity lists

Example override for performance

def get_entity_ids_for_device(self, device_id: str) -> list[str]: # Cache entity IDs per device for faster lookups if device_id not in self._entity_cache: self._entity_cache[device_id] = [ f"media_player.{device_id}", f"remote.{device_id}", ] return self._entity_cache[device_id]

:param device_id: Device identifier :return: List of entity identifiers for this device

remove_device

remove_device(device_id: str) -> None

Remove a configured device.

:param device_id: Device identifier

clear_devices

clear_devices() -> None

Remove all configured devices.

on_device_added

on_device_added(device_config: ConfigT | None) -> None

Handle a newly added device in the configuration.

Default implementation: - If require_connection_before_registry=True: schedules async_add_configured_device as a background task (connects and registers entities after connection) - Otherwise: adds the device without connecting

Override if you need custom behavior.

:param device_config: Device configuration that was added

on_device_removed

on_device_removed(device_config: ConfigT | None) -> None

Handle a removed device in the configuration.

Default implementation: Removes the device or clears all if None. Override if you need custom behavior.

:param device_config: Device configuration that was removed, or None to clear all

Helper Functions

ucapi_framework.driver.create_entity_id

create_entity_id(entity_type: EntityTypes | str, device_id: str, sub_device_id: str | None = None) -> str

Create a unique entity identifier for the given device and entity type.

Entity IDs follow the format: - Simple: "{entity_type}.{device_id}" - With sub-device: "{entity_type}.{device_id}.{sub_device_id}"

Use the optional sub_device_id parameter for devices that expose multiple entities of the same type, such as a hub with multiple lights or zones.

Examples:

>>> create_entity_id(EntityTypes.MEDIA_PLAYER, "device_123")
'media_player.device_123'
>>> create_entity_id(EntityTypes.LIGHT, "hub_1", "light_bedroom")
'light.hub_1.light_bedroom'
>>> create_entity_id("media_player", "receiver_abc", "zone_2")
'media_player.receiver_abc.zone_2'

:param entity_type: The entity type (EntityTypes enum or string) :param device_id: The device identifier (hub or parent device) :param sub_device_id: Optional sub-device identifier (e.g., light ID, zone ID) :return: Entity identifier in the format "entity_type.device_id" or "entity_type.device_id.sub_device_id"