trmnl-terminus is a small, synchronous Python client for automating a
self-hosted Terminus server. It handles
authentication and token rotation, validates requests and responses, and keeps
the underlying HTTP contract visible.
SDK 0.1.x and 0.2.x support exactly Terminus tag 0.71.0, commit
e0cf90d8ef6d7bc16dfbac8ebab910a9fda9de56.
It makes no compatibility claim for Terminus main, older releases, or later
releases.
Python 3.12 or newer is required.
Add the SDK to an uv project:
uv add trmnl-terminusOr install it with pip:
python -m pip install trmnl-terminusTo work from source, clone this repository and create its locked environment:
git clone https://github.com/hsperker/trmnl-terminus-sdk.git
cd trmnl-terminus-sdk
uv sync --lockedTerminus authenticates with an email address and password. Keep both outside your source code:
export TERMINUS_BASE_URL="https://terminus.example.test"
export TERMINUS_EMAIL="you@example.test"
export TERMINUS_PASSWORD="..."Then inspect your devices without changing the server:
import os
from pydantic import SecretStr
from trmnl_terminus import Credentials, TerminusClient
credentials = Credentials(
email=os.environ["TERMINUS_EMAIL"],
password=SecretStr(os.environ["TERMINUS_PASSWORD"]),
)
with TerminusClient(
os.environ["TERMINUS_BASE_URL"],
credentials=credentials,
) as client:
for device in client.devices.list():
print(device.id, device.label, device.playlist_id)The client logs in on demand. It rotates access and refresh tokens when needed
and never writes them to disk unless you supply a TokenStore.
Point your coding agent to this README and describe what the device should show. The agent should treat these as constraints:
- This SDK controls a self-hosted Terminus server. It does not target the hosted TRMNL developer platform.
- Install
trmnl-terminus, use the typed resource managers, and keep the base URL, email, and password in environment variables or a secret store. - Fetch the target device with
client.devices.get(id)and use itsmodel_idinScreenCreate. Do not select the first model returned by the server. - Generate finished HTML in the application and submit it as
HtmlSource. Usemode="dither"for photographs and other images that need the device palette. - A device display flow is
screens.create(...)→playlists.create(...)orplaylists.update(...)→devices.update(id, DevicePatch(...)). - These calls change real server state. For a temporary proof, save the device's original playlist ID, restore it afterward, and delete temporary screens and playlists in cleanup code.
- Assigning a playlist does not wake the device. The image appears on its next scheduled poll, manual refresh, or power cycle.
Start with the runnable examples and the supported API.
Use client.request(...) only when the pinned Terminus API has a real gap.
Hosted TRMNL plugins, Liquid templates, variables, webhooks, OAuth, and cloud
APIs are outside this SDK. The application owns dynamic data and HTML
generation; the SDK submits the result to Terminus.
The repository includes three runnable examples:
| Example | What it proves | Server changes |
|---|---|---|
examples/list_resources.py |
Lists models, devices, screens, and playlists | None |
examples/render_screen.py |
Creates an HTML screen, downloads its rendered image, then deletes the screen | Temporary screen |
examples/render_photo.py |
Renders a caller-supplied image URL through Terminus's dither path | Temporary screen |
Run the read-only example after setting the three variables above:
uv run python examples/list_resources.pyChoose the target model ID from that output. Mutating examples require it and stop before sending a request when it is missing or invalid.
Rendering requires an explicit mutation opt-in. Remote-screen deletion runs in
a finally block, so cleanup is attempted even when the image download fails:
TERMINUS_MODEL_ID=7 \
TERMINUS_ALLOW_MUTATIONS=1 \
uv run python examples/render_screen.py rendered-screen.pngTERMINUS_ALLOW_MUTATIONS is a local safety interlock used only by the
examples. Mutating examples require the exact value 1 before sending a
request that creates, updates, or deletes server resources. The SDK does not
read the variable, Terminus never receives it, and setting it grants no
permissions.
Render an image that the Terminus server can reach over HTTP or HTTPS. Use a URL you trust: the renderer fetches it from the server's network.
TERMINUS_MODEL_ID=7 \
TERMINUS_ALLOW_MUTATIONS=1 \
uv run python examples/render_photo.py \
"https://example.test/photo.png" rendered-photo.pngThe photo example embeds the URL in full-frame HTML, asks Terminus to dither
the result for the target display palette, downloads the rendered image, and
deletes its temporary screen in a finally block.
For photos or images with little text, pass mode="dither" to
ScreenCreate. Terminus then runs its dither and color-palette conversion
path. Omit mode to keep the default rendering path.
To prove the full path on real hardware, use the guarded
physical-device proof workflow.
It creates a temporary playlist and screen, assigns them to an existing device,
waits for visual confirmation, restores the original playlist, and cleans up.
Read the
real-server smoke guide
before running it.
Playlist assignment does not wake a device. The new image appears on its next scheduled poll, power cycle, or manual refresh.
| Resource | Operations in 0.2 |
|---|---|
client.models |
list() |
client.devices |
list(), get(id), update(id, DevicePatch(...)) |
client.screens |
list(), create(...), delete(id), read_bytes(...), download(...) |
client.playlists |
list(), get(id), create(...), update(...), delete(id) |
client.request(...) |
Raw escape hatch for unsupported Server API endpoints |
Device updates deliberately support playlist assignment only. Terminus
0.71.0 also lacks a native screen lookup endpoint, so the SDK does not fake
one by fetching every screen. The
SDK specification
defines the complete boundary and wire contract.
HTTP failures raise typed TerminusError subclasses. Server responses that use
RFC Problem Details are available as structured ProblemDetails on response
errors. The original httpx.Response remains available for debugging, but
exception strings do not include credentials, tokens, request bodies, or
response bodies.
TLS certificate verification is enabled by default. For a private certificate
authority, pass an ssl.SSLContext; the examples never disable verification.
uv owns the development environment and lockfile:
uv sync --locked
uv run pytest
uv run ruff check .
uv run ruff format --check .
uv run mypy src examples
uv build --no-sourcesThe test suite uses mocked HTTP boundaries. Live server and physical-device checks are separate, guarded workflows because they create and delete real resources.
- Read the SDK specification and honor its Terminus tag and commit pin.
- Prefer the typed resource managers. Use
client.request(...)only when the pinned Server API has a real gap. - Do not infer Server API routes from browser pages or hosted TRMNL API documentation.
- Keep credentials outside source code and leave TLS verification enabled.
- Add public SDK surface only for behavior proven against the pinned server.
Maintainers should follow the
release checklist;
publishing uses a GitHub environment and PyPI Trusted Publishing instead of a
stored API token.
MIT. See
LICENSE.