steamloop package¶
Local control for thermostat devices over mTLS.
- exception steamloop.AuthenticationError[source]¶
Bases:
SteamloopErrorAuthentication with the thermostat failed.
- exception steamloop.CommandError[source]¶
Bases:
SteamloopErrorA command sent to the thermostat was rejected.
- class steamloop.FanMode(*values)[source]¶
Bases:
IntEnumFan operating mode.
- ALWAYS_ON = 2¶
- AUTO = 1¶
- CIRCULATE = 3¶
- class steamloop.HoldType(*values)[source]¶
Bases:
IntEnumTemperature hold type.
UNDEFINED: No hold type set. MANUAL: Manual override by user (permanent hold). SCHEDULE: Following the programmed schedule. HOLD: Hold until next scheduled period.
- HOLD = 3¶
- MANUAL = 1¶
- SCHEDULE = 2¶
- UNDEFINED = 0¶
- exception steamloop.PairingError[source]¶
Bases:
SteamloopErrorPairing with the thermostat failed.
- exception steamloop.SteamloopConnectionError[source]¶
Bases:
SteamloopErrorConnection to the thermostat failed.
- class steamloop.ThermostatConnection(ip: str, port: int = 7878, *, cert_set: CertSet | None = None, secret_key: str, device_type: str = 'automation', device_id: str = 'module')[source]¶
Bases:
objectAsync connection to a thermostat over mTLS.
After calling connect() and login(), call start_background_tasks() to begin sending heartbeats. Events are dispatched automatically via the protocol’s data_received callback.
If the connection drops, it will automatically reconnect with exponential backoff. Call disconnect() to stop everything.
- add_event_callback(callback: Callable[[dict[str, Any]], None]) Callable[[], None][source]¶
Register an event callback. Returns a callable to unregister it.
- async connect() None[source]¶
Establish the TLS connection to the thermostat.
If no cert_set was specified, tries each cert set in order until one succeeds. If already connected, the existing connection is closed first.
- Raises:
SteamloopConnectionError: If the connection fails.
- property connected: bool¶
Return True if the connection is active.
- async login() LoginResponse[source]¶
Authenticate with the thermostat.
After receiving the login response, waits for the initial burst of state events (zone discovery, temperatures, etc.) to arrive before returning. This ensures
stateis fully populated.- Returns:
LoginResponse on success.
- Raises:
AuthenticationError: If authentication fails. SteamloopConnectionError: If the connection is lost.
- async pair() SetSecretKeyRequest[source]¶
Pair with the thermostat.
The thermostat must be in pairing mode. Sends a login request with an empty secret key and waits for the thermostat to send a SetSecretKey request containing the new secret key.
- Returns:
SetSecretKeyRequest with the new secret_key.
- Raises:
PairingError: If pairing fails or times out. SteamloopConnectionError: If the connection is lost.
- property secret_key: str¶
Return the secret key used for authentication.
- send(msg: dict[str, Any]) None[source]¶
Send a message to the thermostat.
- Raises:
SteamloopConnectionError: If not connected.
- send_request(command: str, data: dict[str, str]) None[source]¶
Send a Request-wrapped command to the thermostat.
- set_temperature_setpoint(zone_id: str, *, heat_setpoint: str | None = None, cool_setpoint: str | None = None, hold_type: HoldType = HoldType.MANUAL) None[source]¶
Set temperature setpoints for a zone.
If a setpoint isn’t provided, the current state value is used. Deadband is always taken from current state. If the resulting setpoints would violate the deadband, the opposite setpoint is automatically adjusted to maintain the minimum gap:
If only heat_setpoint is provided, cool is raised if needed.
If only cool_setpoint is provided, heat is lowered if needed.
If both are provided, cool is raised to maintain the gap.
- class steamloop.ThermostatState(zones: dict[str, ~steamloop.models.Zone]=<factory>, supported_modes: list[ZoneMode] = <factory>, fan_mode: FanMode = FanMode.AUTO, emergency_heat: str = '', relative_humidity: str = '', cooling_active: str = '', heating_active: str = '')[source]¶
Bases:
objectAggregated state of the thermostat and all zones.
- cooling_active: str = ''¶
- emergency_heat: str = ''¶
- heating_active: str = ''¶
- relative_humidity: str = ''¶
- class steamloop.Zone(zone_id: str, name: str = '', mode: ZoneMode = ZoneMode.OFF, indoor_temperature: str = '', heat_setpoint: str = '', cool_setpoint: str = '', deadband: str = '', hold_type: HoldType = HoldType.UNDEFINED)[source]¶
Bases:
objectState of a single thermostat zone.
- cool_setpoint: str = ''¶
- deadband: str = ''¶
- heat_setpoint: str = ''¶
- indoor_temperature: str = ''¶
- name: str = ''¶
- zone_id: str¶
- class steamloop.ZoneMode(*values)[source]¶
Bases:
IntEnumHVAC zone operating mode.
- AUTO = 1¶
- COOL = 2¶
- HEAT = 3¶
- OFF = 0¶
- async steamloop.load_pairing(ip: str, directory: Path | None = None) dict[str, str] | None[source]¶
Load saved pairing data for a thermostat IP.
- Args:
ip: Thermostat IP address. directory: Directory to load from. Defaults to current directory.
- Returns:
Pairing dict with secret_key, device_type, device_id, or None.
- async steamloop.save_pairing(ip: str, login_info: dict[str, str], directory: Path | None = None) None[source]¶
Save pairing data for a thermostat IP.
- Args:
ip: Thermostat IP address. login_info: Dict with secret_key, device_type, device_id. directory: Directory to save to. Defaults to current directory.
Submodules¶
steamloop.certs module¶
Embedded client certificates for thermostat mTLS authentication.
steamloop.cli module¶
Command-line interface for steamloop thermostat control.
steamloop.connection module¶
Async connection to a thermostat over mTLS on port 7878.
- class steamloop.connection.ThermostatConnection(ip: str, port: int = 7878, *, cert_set: CertSet | None = None, secret_key: str, device_type: str = 'automation', device_id: str = 'module')[source]¶
Bases:
objectAsync connection to a thermostat over mTLS.
After calling connect() and login(), call start_background_tasks() to begin sending heartbeats. Events are dispatched automatically via the protocol’s data_received callback.
If the connection drops, it will automatically reconnect with exponential backoff. Call disconnect() to stop everything.
- add_event_callback(callback: Callable[[dict[str, Any]], None]) Callable[[], None][source]¶
Register an event callback. Returns a callable to unregister it.
- async connect() None[source]¶
Establish the TLS connection to the thermostat.
If no cert_set was specified, tries each cert set in order until one succeeds. If already connected, the existing connection is closed first.
- Raises:
SteamloopConnectionError: If the connection fails.
- property connected: bool¶
Return True if the connection is active.
- async login() LoginResponse[source]¶
Authenticate with the thermostat.
After receiving the login response, waits for the initial burst of state events (zone discovery, temperatures, etc.) to arrive before returning. This ensures
stateis fully populated.- Returns:
LoginResponse on success.
- Raises:
AuthenticationError: If authentication fails. SteamloopConnectionError: If the connection is lost.
- async pair() SetSecretKeyRequest[source]¶
Pair with the thermostat.
The thermostat must be in pairing mode. Sends a login request with an empty secret key and waits for the thermostat to send a SetSecretKey request containing the new secret key.
- Returns:
SetSecretKeyRequest with the new secret_key.
- Raises:
PairingError: If pairing fails or times out. SteamloopConnectionError: If the connection is lost.
- property secret_key: str¶
Return the secret key used for authentication.
- send(msg: dict[str, Any]) None[source]¶
Send a message to the thermostat.
- Raises:
SteamloopConnectionError: If not connected.
- send_request(command: str, data: dict[str, str]) None[source]¶
Send a Request-wrapped command to the thermostat.
- set_temperature_setpoint(zone_id: str, *, heat_setpoint: str | None = None, cool_setpoint: str | None = None, hold_type: HoldType = HoldType.MANUAL) None[source]¶
Set temperature setpoints for a zone.
If a setpoint isn’t provided, the current state value is used. Deadband is always taken from current state. If the resulting setpoints would violate the deadband, the opposite setpoint is automatically adjusted to maintain the minimum gap:
If only heat_setpoint is provided, cool is raised if needed.
If only cool_setpoint is provided, heat is lowered if needed.
If both are provided, cool is raised to maintain the gap.
- class steamloop.connection.ThermostatProtocol(connection: ThermostatConnection)[source]¶
Bases:
ProtocolLow-level protocol handler for thermostat communication.
Handles framing (null-byte delimited JSON) and delegates parsed messages to the owning ThermostatConnection.
- async steamloop.connection.load_pairing(ip: str, directory: Path | None = None) dict[str, str] | None[source]¶
Load saved pairing data for a thermostat IP.
- Args:
ip: Thermostat IP address. directory: Directory to load from. Defaults to current directory.
- Returns:
Pairing dict with secret_key, device_type, device_id, or None.
- async steamloop.connection.save_pairing(ip: str, login_info: dict[str, str], directory: Path | None = None) None[source]¶
Save pairing data for a thermostat IP.
- Args:
ip: Thermostat IP address. login_info: Dict with secret_key, device_type, device_id. directory: Directory to save to. Defaults to current directory.
steamloop.const module¶
Constants and enums for the steamloop thermostat protocol.
- class steamloop.const.FanMode(*values)[source]¶
Bases:
IntEnumFan operating mode.
- ALWAYS_ON = 2¶
- AUTO = 1¶
- CIRCULATE = 3¶
- class steamloop.const.HoldType(*values)[source]¶
Bases:
IntEnumTemperature hold type.
UNDEFINED: No hold type set. MANUAL: Manual override by user (permanent hold). SCHEDULE: Following the programmed schedule. HOLD: Hold until next scheduled period.
- HOLD = 3¶
- MANUAL = 1¶
- SCHEDULE = 2¶
- UNDEFINED = 0¶
steamloop.exceptions module¶
Exception classes for steamloop.
- exception steamloop.exceptions.AuthenticationError[source]¶
Bases:
SteamloopErrorAuthentication with the thermostat failed.
- exception steamloop.exceptions.CommandError[source]¶
Bases:
SteamloopErrorA command sent to the thermostat was rejected.
- exception steamloop.exceptions.PairingError[source]¶
Bases:
SteamloopErrorPairing with the thermostat failed.
- exception steamloop.exceptions.SteamloopConnectionError[source]¶
Bases:
SteamloopErrorConnection to the thermostat failed.
steamloop.models module¶
Data models for thermostat events, responses, and state.
- class steamloop.models.CoolingStatusUpdatedEvent[source]¶
Bases:
TypedDictCooling compressor status changed.
- cooling_active: str¶
- class steamloop.models.EmergencyHeatUpdatedEvent[source]¶
Bases:
TypedDictEmergency heat status changed.
- emergency_heat: str¶
- class steamloop.models.ErrorResponse[source]¶
Bases:
TypedDictError response from the thermostat.
- description: str¶
- error_type: str¶
- class steamloop.models.FanModeUpdatedEvent[source]¶
Bases:
TypedDictThe fan mode changed.
- fan_mode: str¶
- class steamloop.models.HeatingStatusUpdatedEvent[source]¶
Bases:
TypedDictHeating system status changed.
- heating_active: str¶
- class steamloop.models.IndoorRelativeHumidityUpdatedEvent[source]¶
Bases:
TypedDictIndoor humidity changed.
- relative_humidity: str¶
- class steamloop.models.IndoorTemperatureUpdatedEvent[source]¶
Bases:
TypedDictA zone’s indoor temperature changed.
- indoor_temperature: str¶
- zone_id: str¶
- class steamloop.models.LoginResponse[source]¶
Bases:
TypedDictLogin response from the thermostat.
- status: str¶
- class steamloop.models.SetSecretKeyRequest[source]¶
Bases:
TypedDictSecret key sent by the thermostat during pairing.
- secret_key: str¶
- class steamloop.models.SupportedZoneModesUpdatedEvent[source]¶
Bases:
TypedDictThe list of supported zone modes was received.
- modes: str¶
- class steamloop.models.TemperatureSetpointUpdatedEvent[source]¶
Bases:
_TemperatureSetpointRequiredA zone’s temperature setpoints changed.
Only zone_id is guaranteed; other fields may be absent when only a subset of setpoints changed.
- cool_setpoint: str¶
- deadband: str¶
- heat_setpoint: str¶
- hold_type: str¶
- zone_id: str¶
- class steamloop.models.ThermostatState(zones: dict[str, ~steamloop.models.Zone]=<factory>, supported_modes: list[ZoneMode] = <factory>, fan_mode: FanMode = FanMode.AUTO, emergency_heat: str = '', relative_humidity: str = '', cooling_active: str = '', heating_active: str = '')[source]¶
Bases:
objectAggregated state of the thermostat and all zones.
- cooling_active: str = ''¶
- emergency_heat: str = ''¶
- heating_active: str = ''¶
- relative_humidity: str = ''¶
- class steamloop.models.Zone(zone_id: str, name: str = '', mode: ZoneMode = ZoneMode.OFF, indoor_temperature: str = '', heat_setpoint: str = '', cool_setpoint: str = '', deadband: str = '', hold_type: HoldType = HoldType.UNDEFINED)[source]¶
Bases:
objectState of a single thermostat zone.
- cool_setpoint: str = ''¶
- deadband: str = ''¶
- heat_setpoint: str = ''¶
- indoor_temperature: str = ''¶
- name: str = ''¶
- zone_id: str¶
- class steamloop.models.ZoneAddedEvent[source]¶
Bases:
TypedDictA new zone was discovered.
- zone_id: str¶