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
¶
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
¶
Handle Remote Two disconnect command.
Default implementation: disconnects all configured devices. Override to add custom disconnect logic.
on_r2_enter_standby
async
¶
Handle Remote Two entering standby mode.
Default implementation: disconnects all devices to save resources. Override to customize standby behavior.
on_r2_exit_standby
async
¶
Handle Remote Two exiting standby mode.
Default implementation: reconnects all configured devices. Override to customize wake behavior.
on_subscribe_entities
async
¶
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
¶
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 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 ¶
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 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
¶
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
¶
Handle device disconnection.
Sets all entity states to UNAVAILABLE when device disconnects.
:param device_id: Device identifier
on_device_connection_error
async
¶
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 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 ¶
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 ¶
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 ¶
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 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-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 ¶
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 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 a configured device.
:param device_id: Device identifier
on_device_added ¶
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 ¶
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"