Skip to main content

Python async API for Unfolded Circle remotes and docks

Project description

unfurled

An async Python library for controlling Unfolded Circle Remote Two and Remote 3 devices.


Features

  • Async-first design built on aiohttp and websockets
  • Full REST API coverage via CoreAPI
  • Real-time WebSocket event stream with auto-reconnect
  • High-level Remote class for common operations (activities, IR, media, docks)
  • Dock class for IR learning, firmware updates, and codeset management
  • mDNS discovery via zeroconf
  • Clean exception hierarchy
  • Typed with mypy

Installation

# Using uv (recommended)
uv add unfurled

# Or pip
pip install unfurled

To install for development:

git clone https://github.com/you/unfurled
cd unfurled
uv venv --python 3.11
uv pip install -e ".[dev]"

Quick Start

import asyncio
from unfurled.remote import Remote

async def main():
    remote = Remote("http://192.168.1.100:80", api_key="your-api-key")
    await remote.init()

    print(remote.name)                # "My Remote Two" (or auto-derived)
    print(remote.info.model_name)     # "Remote Two"
    print(remote.info.hw_revision)    # "Revision 2"
    print(remote.sw_version)          # "2.1.0"

    # List activities
    for act in remote.activities:
        print(act.name, "-", "ON" if act.is_on else "off")

    # Turn on an activity
    await remote.find_activity("my-activity-id").turn_on()

asyncio.run(main())

Discovery

Find remotes on the local network via mDNS:

from unfurled.discovery import discover_remotes

async def main():
    devices = await discover_remotes(timeout=5)
    for d in devices:
        print(d.hostname, d.address, d.port)

Remote

Construction

# API key (preferred)
remote = Remote("http://192.168.1.100:80", api_key="your-api-key")

# PIN
remote = Remote("http://192.168.1.100:80", pin="1234")

Initialisation

await remote.init() fetches the full device state in one call: configuration, activities, entities, docks, IR emitters, update info.

Key Properties

Property Description
name Device display name (falls back to model name)
info.model_name Marketing model name (e.g. "Remote Two")
info.hw_revision Human-readable hardware revision (e.g. "Revision 2")
info.serial_number Device serial number
sw_version Currently running firmware
latest_sw_version Latest available firmware
available_update True when an update is ready
settings.network.wifi_enabled Wi-Fi radio state
settings.network.bt_enabled Bluetooth radio state
settings.display.brightness Display brightness (0-100)
settings.power_saving.standby_sec Display sleep timeout (seconds)
activities list[Activity]
docks list[Dock]

Activities

# List
for act in remote.activities:
    print(act.name, act.state)

# Find and control
act = remote.find_activity("activity-id")
await act.turn_on()
await act.turn_off()

# All off
await remote.turn_off_all_activities()

Settings

Configuration is grouped under remote.settings:

# Adjust display brightness
await remote.settings.update_display(brightness=80)

# Enable Wi-Fi wake-on-LAN
await remote.settings.update_network(wake_on_wlan=True)

# Change sound volume
await remote.settings.update_sound(volume=60)

IR

# Send a raw HEX or PRONTO code
await remote.ir.send(
    code="0000 006C ...",
    format="PRONTO",
    emitter_name="Dock IR",   # or emitter_id="device-id"
    repeat=1,
)

# Send from a loaded codeset
await remote.ir.send_from_codeset("Samsung TV", "VOLUME_UP")

# List available emitters
for e in remote.ir.emitters:
    print(e.name, e.device_id)

Integrations / External Systems

# Find a specific integration driver instance
instance = await remote.integrations.get_by_driver("hass")

# Set an API token for an external system (e.g. Home Assistant)
await remote.auth.set_external_token(
    system="hass",
    token_id="primary",
    token="long-lived-token",
    name="Home Assistant",
)

Authentication / API Keys

# Create a persistent API key
key = await remote.auth.create_key()
print(key["api_key"])

# Revoke a key
await remote.auth.revoke_key(key["key_id"])

Firmware Updates

# Force an update check
result = await remote.api.post_force_update_check()

# Current status
print(remote.update_info.in_progress, remote.update_info.update_percent)

WebSocket Events

from unfurled.websocket import RemoteWebSocketClient

async def on_message(msg: str):
    print("WS event:", msg)

ws = RemoteWebSocketClient(api_url, api_key)
ws.on_message(on_message)
await ws.connect()

Or use the built-in client on Remote:

remote.add_listener(my_callback)   # raw WS message handler
await remote.connect_websocket()

Dock

dock = remote.docks[0]

# Refresh state
await dock.update()

# IR learning
result = await dock.start_ir_learning()
await dock.stop_ir_learning()

# Firmware update
info = await dock.get_update_status()
if info.get("update_available"):
    await dock.update_firmware()

# Custom codesets
codesets = await dock.get_custom_codesets()
await dock.delete_custom_codeset("my-codeset-id")

Exceptions

Exception When raised
AuthenticationError Wrong API key / PIN
HTTPError Non-2xx response
RemoteIsSleeping Device is asleep; wake it first
ExternalSystemNotSupported Unknown external system ID

Interactive Tester

A built-in REPL tester is included:

uv run main.py

It discovers remotes on the local network, prompts for credentials, then offers a numbered menu to inspect state and send commands.


Development

# Run tests
uv run pytest

# Type checking
uv run mypy unfurled/

# Lint / format
uv run ruff check unfurled/
uv run ruff format unfurled/

Licence

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

unfurled-0.1.3.tar.gz (152.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

unfurled-0.1.3-py3-none-any.whl (50.0 kB view details)

Uploaded Python 3

File details

Details for the file unfurled-0.1.3.tar.gz.

File metadata

  • Download URL: unfurled-0.1.3.tar.gz
  • Upload date:
  • Size: 152.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for unfurled-0.1.3.tar.gz
Algorithm Hash digest
SHA256 f1c3fc2fb61effdb5b902b19b7ba2e9fc9a50be315bbfc4e71ae0c0034504972
MD5 0d78a936215cd16d1544fdcec0783788
BLAKE2b-256 4cff52102e3fba45ea9900836f44219bdb48c0c88f7f6d0f4d51dcc82fe24197

See more details on using hashes here.

Provenance

The following attestation bundles were made for unfurled-0.1.3.tar.gz:

Publisher: publish.yml on JackJPowell/unfurled

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file unfurled-0.1.3-py3-none-any.whl.

File metadata

  • Download URL: unfurled-0.1.3-py3-none-any.whl
  • Upload date:
  • Size: 50.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for unfurled-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 87478e2331adc3c5a60868878ec1eeff88997c1263c5d4407880e611e16d8cc0
MD5 86345d71ac22e3f32c9283e189d0590a
BLAKE2b-256 714d2f3aa27ea2432d525f1d13890d9a355ab2b9393cceb0699a2d5203310cda

See more details on using hashes here.

Provenance

The following attestation bundles were made for unfurled-0.1.3-py3-none-any.whl:

Publisher: publish.yml on JackJPowell/unfurled

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page