Skip to main content

pysaunum

CI codecov PyPI version PyPI Downloads Python versions License: MIT

Python library for controlling Saunum sauna controllers via Modbus TCP.


This library is used by the Saunum Home Assistant integration.

Dynamic Regex Badge Dynamic Regex Badge Static Badge

Features

  • 🔌 Async/await support using asyncio with comprehensive error handling
  • 🏠 Sauna type configuration (Type 1, 2, or 3 with 0-indexed values)
  • 🌡️ Temperature control with zero-value support (40-100°C range, 0 = type defined)
  • ⏱️ Session management with configurable duration (0-720 minutes, default 120, 0 = type defined)
  • ⏲️ Fan duration control (0-15 minutes, 0 = type defined)
  • 💨 Fan control with discrete speeds (0=Off, 1=Low, 2=Medium, 3=High)
  • 💡 Light control for sauna lighting
  • 🔥 Heater monitoring with element count (0-3 active elements)
  • 🚨 Alarm status monitoring (door, temperature, sensor alarms)
  • 🛡️ Type hints for better IDE support and development experience
  • 📊 Comprehensive data model with optional fields and proper null handling

Installation

pip install pysaunum

Quick Start

The factory method automatically establishes a connection before returning the client:

import asyncio
from pysaunum import SaunumClient, SaunumConnectionError

async def main():
    try:
        # Create and connect - client is ready to use immediately
        client = await SaunumClient.create("192.168.1.100")

        # Read current state
        data = await client.async_get_data()
        print(f"Current temperature: {data.current_temperature}°C")
        print(f"Target temperature: {data.target_temperature}°C")
        print(f"Session active: {data.session_active}")
        print(f"Heater elements active: {data.heater_elements_active}")

        # Start a sauna session with configuration
        await client.async_set_target_temperature(80)  # Set to 80°C
        await client.async_set_sauna_duration(120)     # 2 hours
        await client.async_set_fan_speed(2)            # Medium fan
        await client.async_start_session()

        # Stop the session when done
        await client.async_stop_session()

    except SaunumConnectionError as err:
        print(f"Connection error: {err}")
    finally:
        # Close connection
        await client.async_close()

asyncio.run(main())

Traditional Method

You can also create and connect manually:

import asyncio
from pysaunum import SaunumClient, SaunumConnectionError

async def main():
    # Create client - replace with your sauna controller's IP
    client = SaunumClient(host="192.168.1.100", port=502, device_id=1)

    try:
        # Must explicitly connect before using
        await client.connect()

        # Read current state
        data = await client.async_get_data()
        print(f"Current temperature: {data.current_temperature}°C")

    except SaunumConnectionError as err:
        print(f"Connection error: {err}")
    finally:
        # Close connection
        await client.async_close()

asyncio.run(main())
import asyncio
from pysaunum import SaunumClient
from pysaunum.const import FanSpeed, SaunaType

async def main():
    try:
        # Using async context manager automatically handles connection cleanup
        async with SaunumClient(host="192.168.1.100") as client:
            # Configure sauna
            await client.async_set_sauna_type(SaunaType.TYPE_2)  # Type 2 sauna
            await client.async_set_target_temperature(85)
            await client.async_set_fan_speed(FanSpeed.HIGH)

            # Start session
            await client.async_start_session()

            # Read updated state
            data = await client.async_get_data()
            print(f"Session started: {data.session_active}")
            print(f"Heater elements: {data.heater_elements_active}/3")

    except Exception as err:
        print(f"Error: {err}")

asyncio.run(main())

Available Constants

from pysaunum.const import (
    # Fan speed enum (IntEnum)
    FanSpeed,           # FanSpeed.OFF=0, LOW=1, MEDIUM=2, HIGH=3

    # Sauna type enum (IntEnum, 0-indexed)
    SaunaType,          # SaunaType.TYPE_1=0, TYPE_2=1, TYPE_3=2

    # Limits
    MIN_TEMPERATURE,    # 40°C
    MAX_TEMPERATURE,    # 100°C
    MIN_DURATION,       # 0 minutes
    MAX_DURATION,       # 720 minutes (12 hours)
    DEFAULT_DURATION,   # 120 minutes (2 hours)
    MIN_FAN_DURATION,   # 0 minutes
    MAX_FAN_DURATION,   # 15 minutes
)

# Fan speed values are IntEnum members
assert FanSpeed.OFF == 0
assert FanSpeed.HIGH == 3
assert 2 in FanSpeed  # Membership testing

# Sauna type values are IntEnum members
assert SaunaType.TYPE_1 == 0
assert SaunaType.TYPE_3 == 2

API Reference

Main Client Methods

Method Description Parameters
async_get_data() Read all current sauna data None
async_start_session() Start sauna session None
async_stop_session() Stop sauna session None
async_set_target_temperature(temp) Set target temperature temp: int (0, 40-100°C)
async_set_sauna_duration(minutes) Set session duration minutes: int (0-720)
async_set_fan_speed(speed) Set fan speed speed: int (0-3)
async_set_fan_duration(minutes) Set fan duration minutes: int (0-15)
async_set_sauna_type(type) Set sauna type type: int (0-2)
async_set_light_control(enabled) Control sauna light enabled: bool

Data Model (SaunumData)

@dataclass(frozen=True)
class SaunumData:
    # Session control
    session_active: bool                   # Session status
    sauna_type: SaunaType | int             # Sauna type enum (or raw int)
    sauna_duration: int                    # Duration in minutes (0=type default)
    fan_duration: int                      # Fan duration in minutes (0=type default)
    target_temperature: int                # Target temp in °C (0=type default)
    fan_speed: FanSpeed | None             # Fan speed enum
    light_on: bool                         # Light status

    # Status sensors
    current_temperature: float             # Current temp in °C
    on_time: int                           # Device uptime in seconds
    heater_elements_active: int            # Active heater elements
    door_open: bool                        # Door status

    # Alarm status
    alarm_door_open: bool                  # Door alarm during heating
    alarm_door_sensor: bool                # Door open too long
    alarm_thermal_cutoff: bool             # Thermal protection
    alarm_internal_temp: bool              # Overheating alarm
    alarm_temp_sensor_short: bool          # Sensor short circuit
    alarm_temp_sensor_open: bool           # Sensor disconnected

Exception Handling

from pysaunum import (
    SaunumConnectionError,      # Connection issues
    SaunumCommunicationError,   # Modbus communication errors
    SaunumTimeoutError,         # Timeout errors
    SaunumInvalidDataError,     # Invalid data received
)

try:
    async with SaunumClient("192.168.1.100") as client:
        data = await client.async_get_data()
except SaunumConnectionError:
    print("Failed to connect to sauna controller")
except SaunumCommunicationError:
    print("Communication error with sauna controller")
except SaunumTimeoutError:
    print("Operation timed out")

Development

Setup

# Clone the repository
git clone https://github.com/mettolen/pysaunum.git
cd pysaunum

# Create virtual environment
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

# Install in development mode with dev dependencies
pip install -e ".[dev]"

Testing & Quality

# Run all tests with coverage
pytest --cov=pysaunum --cov-report=term-missing

# Run type checking
mypy src/pysaunum

# Run linting and formatting
ruff check src/pysaunum
ruff format src/pysaunum

# Run pre-commit hooks (if installed)
pre-commit run --all-files

Current Test Coverage

The library maintains 100% test coverage with comprehensive tests including:

  • ✅ Connection handling and error scenarios
  • ✅ All API methods with valid and invalid inputs
  • ✅ Modbus communication error handling
  • ✅ Data parsing and validation
  • ✅ Context manager functionality
  • ✅ Exception hierarchy and error messages

Requirements

  • Python 3.12+
  • Dependencies:
    • pymodbus >= 3.0.0 (Modbus TCP communication)
    • asyncio (built-in, async/await support)

Compatibility

This library is tested and compatible with:

  • Saunum sauna controllers with Modbus TCP interface
    • Tested with control panel v1.1.49 and power unit v1.1.49
  • Home Assistant integration
  • Python 3.12, 3.13+

Advanced Usage

Monitoring Heater Elements

data = await client.async_get_data()
print(f"Active heater elements: {data.heater_elements_active}/3")

# Heater elements show how many of the 3 elements are currently active
# 0 = No heating, 1-3 = Number of elements heating

Alarm Monitoring

data = await client.async_get_data()

# Check for any active alarms
alarms = [
    ("Door open during heating", data.alarm_door_open),
    ("Door sensor alarm", data.alarm_door_sensor),
    ("Thermal cutoff", data.alarm_thermal_cutoff),
    ("Internal overheating", data.alarm_internal_temp),
    ("Temperature sensor short", data.alarm_temp_sensor_short),
    ("Temperature sensor open", data.alarm_temp_sensor_open),
]

active_alarms = [name for name, active in alarms if active]
if active_alarms:
    print(f"Active alarms: {', '.join(active_alarms)}")

Troubleshooting

Connection Issues

  1. Check IP address: Ensure the sauna controller IP is correct
  2. Network connectivity: Verify network connection to the controller
  3. Modbus port: Default port is 502, ensure it's not blocked by firewall
  4. Device ID: Default device ID is 1, check controller configuration

Common Error Patterns

# Handle specific error types
try:
    await client.connect()
except SaunumConnectionError as err:
    if "timeout" in str(err).lower():
        print("Connection timeout - controller may be offline")
    elif "refused" in str(err).lower():
        print("Connection refused - check IP and port")
    else:
        print(f"Connection error: {err}")

License

MIT License - see LICENSE file for details.

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for detailed guidelines on:

  • Setting up your development environment
  • Code style and testing requirements
  • Submitting pull requests
  • Reporting bugs and requesting features

For major changes, please open an issue first to discuss what you would like to change.

Metadata

Release files for pysaunum 0.7.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pysaunum 0.7.0
File Size Uploaded
pysaunum-0.7.0.tar.gz 28.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pysaunum 0.7.0
File Interpreter ABI Platform
pysaunum-0.7.0-py3-none-any.whl Python 3 none any Details

Total release size: 42.8 kB

Release files / pysaunum-0.7.0.tar.gz

Download URL pysaunum-0.7.0.tar.gz
Size 28.6 kB
Tags Source
SHA-256 checksum
How to use checksums
80f15ea7d4d8ca804c8e4573d620459b23d2111f8cfe78ce12d038a628ec8179
BLAKE2b-256 checksum
How to use checksums
afd41a77cc3a32b8ff53b34769a15f4e3716461b79c994f56844195b894805d3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

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 5, 2026.

Transparency log

Release files / pysaunum-0.7.0-py3-none-any.whl

Download URL pysaunum-0.7.0-py3-none-any.whl
Size 14.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
354e544e1851cdc745a038799ba66c86c88cfef13a5f8fae03a80288b531c8c8
BLAKE2b-256 checksum
How to use checksums
d9455f15f73558d1b20d2497f7d331221f690da14861dfd7eef157b1ecf3ed3d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

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 5, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.7.0 This release

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page