Skip to main content

Python library for controlling Saunum sauna controllers via Modbus

Project description

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-30 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

Using Factory Method (Recommended)

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())

Context Manager Usage (Recommended)

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,   # 30 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-30)
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.24 and power unit v1.1.43
  • 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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pysaunum-0.6.0.tar.gz (28.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pysaunum-0.6.0-py3-none-any.whl (14.2 kB view details)

Uploaded Python 3

File details

Details for the file pysaunum-0.6.0.tar.gz.

File metadata

  • Download URL: pysaunum-0.6.0.tar.gz
  • Upload date:
  • Size: 28.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for pysaunum-0.6.0.tar.gz
Algorithm Hash digest
SHA256 e41662dff3f528565c1461a131386fc74acbc3f0feee9873023b2ddf17f647f6
MD5 a3c87f15e20eb7b1e7fac68bb9682d45
BLAKE2b-256 6e0fa42be2828d6804f4956fcfb463ff84b0991aa13e52a456486e06df36add2

See more details on using hashes here.

Provenance

The following attestation bundles were made for pysaunum-0.6.0.tar.gz:

Publisher: publish.yml on mettolen/pysaunum

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pysaunum-0.6.0-py3-none-any.whl.

File metadata

  • Download URL: pysaunum-0.6.0-py3-none-any.whl
  • Upload date:
  • Size: 14.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for pysaunum-0.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 faa5073c75fa44547c02c2c581ac692ff0cbaac99c613129a9bab6430fd321ce
MD5 d2980a5619f04789e8e7ac9ad87c5a12
BLAKE2b-256 6a94f24ef8ef6a510472c4545020cd7c22b9b4921cfde7c81013064c5e921a47

See more details on using hashes here.

Provenance

The following attestation bundles were made for pysaunum-0.6.0-py3-none-any.whl:

Publisher: publish.yml on mettolen/pysaunum

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page