Async Python library for local control of thermostat devices over mTLS (port 7878).
pip install steamloopPut the thermostat in pairing mode (Menu > Settings > Network > Advanced Setup > Remote Connection > Pair), then:
steamloop 192.168.1.100 --pairThis saves a pairing file in the current directory with the secret key.
steamloop 192.168.1.100If already paired, you can pass the secret key directly to skip the pairing file:
steamloop 192.168.1.100 --key YOUR_SECRET_KEYInteractive commands: status, heat <temp>, cool <temp>, mode <off|auto|cool|heat>, fan <auto|on|circulate>, eheat <on|off>, help.
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())pair() returns the secret key directly — store it however you like:
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:
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"])def on_event(msg):
print("Received:", msg)
remove = conn.add_event_callback(on_event)
# later: remove() to unregisterKey design points for using steamloop in a Home Assistant integration:
- Commands are sync —
set_zone_mode(),set_fan_mode(),set_temperature_setpoint()usetransport.write()internally, so they won't block the event loop. Noawaitneeded. - State is always fresh — the
asyncio.Protocolreceives events viadata_received()and updatesconn.stateautomatically. 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 triggerasync_write_ha_state()when the thermostat pushes updates. - Multi-zone — create one
ClimateEntityperconn.state.zonesentry. Zones are populated automatically after login.
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:
async with ThermostatConnection(ip, secret_key=key) as conn:
... # connected, logged in, background tasks running
# automatically disconnectedZoneMode—OFF,AUTO,COOL,HEATFanMode—AUTO,ALWAYS_ON,CIRCULATEHoldType—UNDEFINED,MANUAL,SCHEDULE,HOLD
conn.state.zones—dict[str, Zone]with temperature, setpoints, mode per zoneconn.state.fan_mode— currentFanModeconn.state.supported_modes—list[ZoneMode]conn.state.emergency_heat/relative_humidity/cooling_active/heating_active
Thanks goes to these wonderful people (emoji key):
This project follows the all-contributors specification. Contributions of any kind welcome!
This package was created with Copier and the browniebroke/pypackage-template project template.