Metadata-Version: 2.4 Name: steamloop Version: 1.2.0 Summary: Local control for choochoo based thermostats Author-email: "J. Nick Koston" License-Expression: Apache-2.0 Project-URL: Bug Tracker, https://github.com/hvaclibs/steamloop/issues Project-URL: Changelog, https://github.com/hvaclibs/steamloop/blob/main/CHANGELOG.md Project-URL: documentation, https://steamloop.readthedocs.io Project-URL: repository, https://github.com/hvaclibs/steamloop Classifier: Development Status :: 2 - Pre-Alpha Classifier: Intended Audience :: Developers Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.14 Classifier: Topic :: Software Development :: Libraries Requires-Python: >=3.12 Description-Content-Type: text/markdown License-File: LICENSE Requires-Dist: orjson>=3.10 Dynamic: license-file # steamloop

CI Status Documentation Status Test coverage percentage

uv Ruff pre-commit

PyPI Version Supported Python versions License

--- Async Python library for local control of thermostat devices over mTLS (port 7878). ## Installation ```bash pip install steamloop ``` ## CLI ### Pairing Put the thermostat in pairing mode (Menu > Settings > Network > Advanced Setup > Remote Connection > Pair), then: ```bash steamloop 192.168.1.100 --pair ``` This saves a pairing file in the current directory with the secret key. ### Monitoring ```bash steamloop 192.168.1.100 ``` If already paired, you can pass the secret key directly to skip the pairing file: ```bash steamloop 192.168.1.100 --key YOUR_SECRET_KEY ``` Interactive commands: `status`, `heat `, `cool `, `mode `, `fan `, `eheat `, `help`. ## Library Usage ```python import asyncio from steamloop import ThermostatConnection, ZoneMode, FanMode async def main(): conn = ThermostatConnection( "192.168.1.100", secret_key="your-secret-key-from-pairing", ) async with conn: # State is populated automatically from thermostat events for zone_id, zone in conn.state.zones.items(): print(f"{zone.name}: {zone.indoor_temperature}°F") # Send commands (sync — no await needed) conn.set_temperature_setpoint("1", heat_setpoint="72") conn.set_zone_mode("1", ZoneMode.COOL) conn.set_fan_mode(FanMode.AUTO) asyncio.run(main()) ``` ### Pairing Programmatically `pair()` returns the secret key directly — store it however you like: ```python from steamloop import ThermostatConnection async def pair(ip: str) -> str: conn = ThermostatConnection(ip, secret_key="") try: await conn.connect() ssk = await conn.pair() return ssk["secret_key"] # store in a database, config entry, etc. finally: await conn.disconnect() ``` Or use the built-in file helpers to save/load pairing data to disk: ```python from steamloop import ThermostatConnection, save_pairing, load_pairing # Save after pairing await save_pairing(ip, { "secret_key": secret_key, "device_type": "automation", "device_id": "module", }) # Load later pairing = await load_pairing(ip) conn = ThermostatConnection(ip, secret_key=pairing["secret_key"]) ``` ### Event Callbacks ```python def on_event(msg): print("Received:", msg) remove = conn.add_event_callback(on_event) # later: remove() to unregister ``` ## Home Assistant Integration Key design points for using steamloop in a Home Assistant integration: - **Commands are sync** — `set_zone_mode()`, `set_fan_mode()`, `set_temperature_setpoint()` use `transport.write()` internally, so they won't block the event loop. No `await` needed. - **State is always fresh** — the `asyncio.Protocol` receives events via `data_received()` and updates `conn.state` automatically. Just read properties directly. - **Auto-reconnect** — after calling `start_background_tasks()`, the connection automatically reconnects with exponential backoff (5s, 10s, 20s, ... up to 5 min). - **Event callbacks** — use `add_event_callback()` to trigger `async_write_ha_state()` when the thermostat pushes updates. - **Multi-zone** — create one `ClimateEntity` per `conn.state.zones` entry. Zones are populated automatically after login. ## API Reference ### `ThermostatConnection(ip, port=7878, *, secret_key, cert_set=None, device_type="automation", device_id="module")` | Method | Async | Description | | ------------------------------------------------------------------------------- | ----- | ----------------------------------------------------- | | `connect()` | yes | Establish mTLS connection | | `login()` | yes | Authenticate with secret key | | `pair()` | yes | Pair and receive secret key | | `start_background_tasks()` | no | Start heartbeat + auto-reconnect | | `disconnect()` | yes | Close connection and stop tasks | | `set_temperature_setpoint(zone_id, *, heat_setpoint, cool_setpoint, hold_type)` | no | Set zone temperature | | `set_zone_mode(zone_id, mode)` | no | Set zone HVAC mode | | `set_fan_mode(mode)` | no | Set fan mode | | `set_emergency_heat(enabled)` | no | Toggle emergency heat | | `add_event_callback(fn)` | no | Register event listener (returns unregister callable) | Supports `async with` for automatic connect/login/disconnect: ```python async with ThermostatConnection(ip, secret_key=key) as conn: ... # connected, logged in, background tasks running # automatically disconnected ``` ### Enums - `ZoneMode` — `OFF`, `AUTO`, `COOL`, `HEAT` - `FanMode` — `AUTO`, `ALWAYS_ON`, `CIRCULATE` - `HoldType` — `UNDEFINED`, `MANUAL`, `SCHEDULE`, `HOLD` ### State - `conn.state.zones` — `dict[str, Zone]` with temperature, setpoints, mode per zone - `conn.state.fan_mode` — current `FanMode` - `conn.state.supported_modes` — `list[ZoneMode]` - `conn.state.emergency_heat` / `relative_humidity` / `cooling_active` / `heating_active` ## Contributors Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! ## Credits [![Copier](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/copier-org/copier/master/img/badge/badge-grayscale-inverted-border-orange.json)](https://github.com/copier-org/copier) This package was created with [Copier](https://copier.readthedocs.io/) and the [browniebroke/pypackage-template](https://github.com/browniebroke/pypackage-template) project template.