Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
name: Python package

on:
push:
workflow_dispatch:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
build:
Expand All @@ -16,7 +19,9 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v6
uses: "astral-sh/setup-uv@v8.0.0"
with:
cache-suffix: ${{ matrix.python-version }}
- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
- name: Install dependencies
Expand Down
93 changes: 91 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ or
pip install roku
```

To use the async client, install with the `async` extra:

```
uv add "roku[async]"
```

To use the CLI, install with the `cli` extra:

```
uv add "roku[cli]"
```

## Usage

### The Basics
Expand Down Expand Up @@ -123,6 +135,85 @@ What if I now want to watch *The Informant!*? Again, with the search open and wa

This will iterate over each character, sending it individually to the Roku.

## Async

An async client is available for use with `asyncio`. The `AsyncRoku` class provides the same functionality as the synchronous `Roku` class, but with async methods.

```python
>>> import asyncio
>>> from roku._async import AsyncRoku
```

Create an instance and use it as an async context manager:

```python
>>> async def main():
... async with AsyncRoku('192.168.10.163') as roku:
... await roku.home()
... await roku.right()
... await roku.select()
...
>>> asyncio.run(main())
```

Properties like `apps`, `active_app`, and `device_info` are replaced with async methods:

```python
>>> async def main():
... async with AsyncRoku('192.168.10.163') as roku:
... apps = await roku.get_apps()
... current = await roku.get_active_app()
... info = await roku.get_device_info()
...
>>> asyncio.run(main())
```

Discovery works as an async class method:

```python
>>> async def main():
... rokus = await AsyncRoku.discover()
... for roku in rokus:
... async with roku:
... info = await roku.get_device_info()
... print(info.user_device_name)
...
>>> asyncio.run(main())
```

## CLI

A command-line interface is available for device discovery. Install with the `cli` extra and use the `roku` command:

```
$ roku discover
192.168.10.163:8060
```

Use `-i` / `--inspect` to display device details:

```
$ roku discover -i
192.168.10.163:8060
Name: Living Room Roku
Model: Roku Ultra (4800X)
Type: Box
Software: 11.5.0.4312
Serial: YH009N854321
```

You can adjust the discovery `--timeout` and `--retries`:

```
$ roku discover --timeout 10 --retries 3
```

The CLI also supports the async client with the `--async` flag:

```
$ roku --async discover
```

## Advanced Stuff

### Discovery
Expand Down Expand Up @@ -196,6 +287,4 @@ More information about input, touch, and sensors is available in the [Roku Exter
## TODO

- Multitouch support.
- A Flask proxy server that can listen to requests and forward them to devices on the local network. Control multiple devices at once, eh?
- A server that mimics the Roku interface so you can make your own Roku-like stuff.
- A task runner that will take a set of commands and run them with delays that are appropriate for most devices.
15 changes: 14 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ authors = [
]
requires-python = ">=3.10"
dependencies = [
"requests<3",
"requests>=2.32,<4",
]

classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
Expand All @@ -24,18 +25,30 @@ classifiers = [
"Programming Language :: Python :: 3.14",
]

[project.optional-dependencies]
async = ["aiohttp>=3.9,<4"]
cli = ["click>=8,<9"]

[project.scripts]
roku = "roku.cli:cli"

[project.urls]
Homepage = "https://github.com/jcarbaugh/python-roku"

[dependency-groups]
dev = [
"aiohttp>=3.9,<4",
"black",
"flake8",
"pytest",
"pytest-asyncio",
"pytest-mock",
"twine",
]

[tool.pytest.ini_options]
asyncio_mode = "auto"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
11 changes: 10 additions & 1 deletion roku/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
from roku.core import Roku, Application, Channel, RokuException, __version__ # noqa
from roku.core import Roku, __version__ # noqa
from roku.models import Application, Channel, RokuException # noqa


def __getattr__(name):
if name == "AsyncRoku":
from roku._async import AsyncRoku

return AsyncRoku
raise AttributeError(f"module 'roku' has no attribute {name}")
1 change: 1 addition & 0 deletions roku/_async/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from roku._async.core import AsyncRoku # noqa
Loading