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
aiohttpandwebsockets - Full REST API coverage via
CoreAPI - Real-time WebSocket event stream with auto-reconnect
- High-level
Remoteclass for common operations (activities, IR, media, docks) Dockclass 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file unfurled-0.1.0.tar.gz.
File metadata
- Download URL: unfurled-0.1.0.tar.gz
- Upload date:
- Size: 172.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
44cb518aad6131bfd6cac68c86c010aaed1c8b87d7e120afb63ea1b74760f034
|
|
| MD5 |
47e9f95932bcf3e39efdaa10d0d796d9
|
|
| BLAKE2b-256 |
5b1d4d2b92e2ae13ae67b61470bec3685c876e4adab1dc811e922636a3d50b69
|
Provenance
The following attestation bundles were made for unfurled-0.1.0.tar.gz:
Publisher:
publish.yml on JackJPowell/unfurled
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
unfurled-0.1.0.tar.gz -
Subject digest:
44cb518aad6131bfd6cac68c86c010aaed1c8b87d7e120afb63ea1b74760f034 - Sigstore transparency entry: 1526644091
- Sigstore integration time:
-
Permalink:
JackJPowell/unfurled@31994c85d0fbe918d3d64d9ecbf3480389198f3c -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/JackJPowell
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@31994c85d0fbe918d3d64d9ecbf3480389198f3c -
Trigger Event:
push
-
Statement type:
File details
Details for the file unfurled-0.1.0-py3-none-any.whl.
File metadata
- Download URL: unfurled-0.1.0-py3-none-any.whl
- Upload date:
- Size: 47.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
07b89570946b692ce947793dffea3aab377a8df4faa15dbb3e0e8312b764b301
|
|
| MD5 |
6edd90fa651119274def96971af9b750
|
|
| BLAKE2b-256 |
6d130e960057a6947975bcf92366ee791a31660d0232a3d4ab3ce3a1e31c7307
|
Provenance
The following attestation bundles were made for unfurled-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on JackJPowell/unfurled
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
unfurled-0.1.0-py3-none-any.whl -
Subject digest:
07b89570946b692ce947793dffea3aab377a8df4faa15dbb3e0e8312b764b301 - Sigstore transparency entry: 1526644324
- Sigstore integration time:
-
Permalink:
JackJPowell/unfurled@31994c85d0fbe918d3d64d9ecbf3480389198f3c -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/JackJPowell
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@31994c85d0fbe918d3d64d9ecbf3480389198f3c -
Trigger Event:
push
-
Statement type: