pysaunum
Python library for controlling Saunum sauna controllers via Modbus TCP.
This library is used by the Saunum Home Assistant integration.
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
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, # 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
- Check IP address: Ensure the sauna controller IP is correct
- Network connectivity: Verify network connection to the controller
- Modbus port: Default port is 502, ensure it's not blocked by firewall
- 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)
| File | Size | Uploaded | |
|---|---|---|---|
| pysaunum-0.7.0.tar.gz | 28.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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