Trinnov Altitude Python Library
Async Trinnov Altitude client for long-running integrations (Home Assistant primary target).
Version 2.0
Version 2.x is a clean break from 1.x.
- No compatibility shims
- New lifecycle (
start/wait_synced/stop) - New state model (
client.state) - Optional command ACK handling
Read the migration guide: docs/MIGRATION_V2.md
Installation
pip install trinnov-altitude
Quick Start
import asyncio
from trinnov_altitude.client import TrinnovAltitudeClient
async def main() -> None:
client = TrinnovAltitudeClient(host="192.168.1.90")
try:
await client.start()
await client.wait_synced(timeout=10)
await client.volume_set(-30.0)
await client.mute_on()
print(client.state.volume)
print(client.state.source)
finally:
await client.stop()
asyncio.run(main())
Lifecycle
await client.start()connects, bootstraps, and starts the read loop.await client.wait_synced()waits until welcome + catalogs + current indices are observed.await client.stop()stops listener and disconnects cleanly.
Liveness & reconnect
The control connection is a long-lived TCP push session, so a silent read is
ambiguous: a healthy link is quiet whenever nothing is changing, but a dead link
(an idle-killed half-open socket, or a processor whose control thread has wedged
while its TCP stack still ACKs) looks identical. To tell them apart, when the link
has been quiet for heartbeat_interval seconds the client sends a read-only probe
(get_current_state); if no traffic arrives within heartbeat_timeout, the link is
treated as dead and auto_reconnect kicks in. TcpTransport also enables
SO_KEEPALIVE as an OS-level backstop.
client = TrinnovAltitudeClient(
host="192.168.1.90",
heartbeat_interval=20.0, # seconds of quiet before a liveness probe (None disables)
heartbeat_timeout=5.0, # seconds to wait for the probe response before reconnecting
)
State reconciliation
Push messages remain the fast path for state updates. As a backstop for an
individual dropped notification, the client also requests the processor's
current state every reconcile_interval seconds. This schedule is independent
of ordinary push traffic, so unrelated messages cannot leave one cached field
stale indefinitely.
client = TrinnovAltitudeClient(
host="192.168.1.90",
reconcile_interval=30.0, # None disables periodic reconciliation
)
Protocol Semantics
The client parses raw messages first, then normalizes them into canonical state events. This keeps protocol quirks isolated and keeps the state reducer deterministic.
- Canonical identity:
CURRENT_PRESET <n>CURRENT_PROFILE <n>or index-onlyPROFILE <n>DECODER ... UPMIXER <mode>
- Optional catalogs:
- Presets via
LABELS_CLEAR+LABEL <n>: <name> - Sources via
PROFILES_CLEAR+PROFILE <n>: <name>
- Presets via
- Quirk profiles:
altitude_ciis selected whenIDENTSincludesaltitude_ci- In that profile,
META_PRESET_LOADED <n>is normalized as a source-change signal
Catalog messages may arrive late, be refreshed, or be absent. Consumers should not assume labels are always present.
Events
def on_event(event, message):
if event == "connected":
...
elif event == "disconnected":
...
elif event == "received_message":
...
client.register_callback(on_event)
Callback exceptions are isolated and logged (they do not crash the listener).
HA Adapter
Use trinnov_altitude.adapter.AltitudeStateAdapter to convert mutable runtime state into immutable snapshots plus typed deltas/events:
snapshot: stable full-state view for coordinator datadeltas: field-level changes since previous snapshotevents: integration-friendly event stream (volume, mute, source, preset, etc.)
You can wire this directly through the client:
from trinnov_altitude.adapter import AltitudeStateAdapter
adapter = AltitudeStateAdapter()
def on_update(snapshot, deltas, events):
...
handle = client.register_adapter_callback(adapter, on_update)
# later: client.deregister_adapter_callback(handle)
For Home Assistant coordinator/event-bus integration, use trinnov_altitude.ha_bridge:
coordinator_payload(snapshot)to_ha_events(events)build_bridge_update(snapshot, deltas, events)
Command ACKs
You can use fire-and-forget commands (default) or explicit ACK waiting:
await client.volume_set(-20.0)
await client.command("volume -20", wait_for_ack=True, ack_timeout=2.0)
Development
uv sync --group dev
uv run ruff check .
uv run ruff format --check .
uv run ty check trinnov_altitude
uv run pytest -v
Or use task wrappers:
task dev
task check
Real Device Integration Tests (Read-Only)
The test suite includes a manual, read-only integration tier for validating behavior against real hardware.
- Marker:
integration_real - Opt-in gate:
TRINNOV_ITEST=1 - Target host:
TRINNOV_HOST=<ip-or-hostname> - Optional port override:
TRINNOV_PORT=44100 - If the device is offline/unreachable, tests are skipped.
These tests intentionally avoid mutating commands (no power/preset/source/volume state changes).
TRINNOV_ITEST=1 TRINNOV_HOST=192.168.30.3 task test:integration-real
Pyx (optional)
Pyx is optional in this repo. You can keep publishing to PyPI/TestPyPI only.
- Install via Pyx: authenticate
uvwithPYX_API_KEYand configure your Pyx index URL inuv(uv add --index .../uv sync). - Publish to Pyx: run the
Releaseworkflow manually withtarget=pyxafter setting repository secretsPYX_API_KEYandPYX_PUBLISH_URL. - No dual-publish requirement: use Pyx when you need private/internal package distribution or policy control.
Release
- Merge conventional-commit changes to
master. - Wait for the
release-pleaseworkflow to open/update a release PR. - Review and merge the release PR (this updates
CHANGELOG.mdand__version__). - Release Please creates the GitHub Release and tag.
- The
Releaseworkflow publishes artifacts to PyPI automatically for published releases. - For TestPyPI or Pyx-only publishing, run
Releasemanually withworkflow_dispatch.
Maintenance
- Migration guide: docs/MIGRATION_V2.md
- Maintainer runbook: docs/MAINTAINERS.md
- Protocol reference used for implementation: docs/Altitude Protocol.pdf (v1.15, 2019-04-19)
Release files for trinnov-altitude 3.3.7
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| trinnov_altitude-3.3.7.tar.gz | 258.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| trinnov_altitude-3.3.7-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 290.7 kB
Release files / trinnov_altitude-3.3.7.tar.gz
| Download URL | trinnov_altitude-3.3.7.tar.gz |
|---|---|
| Size | 258.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
66748d66b7565d597e2d545e221ccf1eed03aec4d31b371e15a5d9acb0e10b98
|
|
BLAKE2b-256 checksum How to use checksums |
2b1daababf4afbdcd4e71dfedbcbc6c615b8ac5575947f54e59916ac25ef6c38
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.13
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 29, 2026.
Transparency logRelease files / trinnov_altitude-3.3.7-py3-none-any.whl
| Download URL | trinnov_altitude-3.3.7-py3-none-any.whl |
|---|---|
| Size | 32.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
59d61d3d3872d28aa52db1920fef3c8fe06e2a7e78b1aa2efe64f79868a93106
|
|
BLAKE2b-256 checksum How to use checksums |
670bfc4534d25de65c2947dc92667cca413220e86b0c20dfd8fffb0a275a22dd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.13
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 29, 2026.
Transparency log