From c5e0c78cbcf9f2cb02c1854046ffe36b7fba17a5 Mon Sep 17 00:00:00 2001 From: mettolen <1007649+mettolen@users.noreply.github.com> Date: Mon, 9 Mar 2026 20:22:27 +0200 Subject: [PATCH] Minor Saunum integration improvements (#164705) --- homeassistant/components/saunum/__init__.py | 4 ++-- homeassistant/components/saunum/climate.py | 17 ++++++++++++----- homeassistant/components/saunum/number.py | 14 ++++++-------- homeassistant/components/saunum/services.py | 20 ++++++++++++++++---- tests/components/saunum/test_number.py | 18 +++++++++--------- 5 files changed, 45 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/saunum/__init__.py b/homeassistant/components/saunum/__init__.py index 208f0ab9861..6248ac8dd72 100644 --- a/homeassistant/components/saunum/__init__.py +++ b/homeassistant/components/saunum/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations -from pysaunum import SaunumClient, SaunumConnectionError +from pysaunum import SaunumClient, SaunumConnectionError, SaunumTimeoutError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, Platform @@ -40,7 +40,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LeilSaunaConfigEntry) -> try: client = await SaunumClient.create(host) - except SaunumConnectionError as exc: + except (SaunumConnectionError, SaunumTimeoutError) as exc: raise ConfigEntryNotReady(f"Error connecting to {host}: {exc}") from exc entry.async_on_unload(client.async_close) diff --git a/homeassistant/components/saunum/climate.py b/homeassistant/components/saunum/climate.py index 411d456c3c7..f2615593cbc 100644 --- a/homeassistant/components/saunum/climate.py +++ b/homeassistant/components/saunum/climate.py @@ -6,7 +6,14 @@ import asyncio from datetime import timedelta from typing import Any -from pysaunum import MAX_TEMPERATURE, MIN_TEMPERATURE, SaunumException +from pysaunum import ( + DEFAULT_DURATION, + DEFAULT_FAN_DURATION, + DEFAULT_TEMPERATURE, + MAX_TEMPERATURE, + MIN_TEMPERATURE, + SaunumException, +) from homeassistant.components.climate import ( FAN_HIGH, @@ -149,7 +156,7 @@ class LeilSaunaClimate(LeilSaunaEntity, ClimateEntity): def preset_mode(self) -> str | None: """Return the current preset mode.""" sauna_type = self.coordinator.data.sauna_type - if sauna_type is not None and sauna_type in self._preset_name_map: + if sauna_type in self._preset_name_map: return self._preset_name_map[sauna_type] return self._preset_name_map[0] @@ -242,9 +249,9 @@ class LeilSaunaClimate(LeilSaunaEntity, ClimateEntity): async def async_start_session( self, - duration: timedelta = timedelta(minutes=120), - target_temperature: int = 80, - fan_duration: timedelta = timedelta(minutes=10), + duration: timedelta = timedelta(minutes=DEFAULT_DURATION), + target_temperature: int = DEFAULT_TEMPERATURE, + fan_duration: timedelta = timedelta(minutes=DEFAULT_FAN_DURATION), ) -> None: """Start a sauna session with custom parameters.""" if self.coordinator.data.door_open: diff --git a/homeassistant/components/saunum/number.py b/homeassistant/components/saunum/number.py index 0a59127ffd6..d6da69deede 100644 --- a/homeassistant/components/saunum/number.py +++ b/homeassistant/components/saunum/number.py @@ -7,6 +7,8 @@ from dataclasses import dataclass from typing import TYPE_CHECKING from pysaunum import ( + DEFAULT_DURATION, + DEFAULT_FAN_DURATION, MAX_DURATION, MAX_FAN_DURATION, MIN_DURATION, @@ -35,10 +37,6 @@ if TYPE_CHECKING: PARALLEL_UPDATES = 0 -# Default values when device returns None or invalid data -DEFAULT_DURATION_MIN = 120 -DEFAULT_FAN_DURATION_MIN = 15 - @dataclass(frozen=True, kw_only=True) class LeilSaunaNumberEntityDescription(NumberEntityDescription): @@ -59,8 +57,8 @@ NUMBERS: tuple[LeilSaunaNumberEntityDescription, ...] = ( native_step=1, value_fn=lambda data: ( duration - if (duration := data.sauna_duration) is not None and duration > MIN_DURATION - else DEFAULT_DURATION_MIN + if (duration := data.sauna_duration) > MIN_DURATION + else DEFAULT_DURATION ), set_value_fn=lambda client, value: client.async_set_sauna_duration(int(value)), ), @@ -74,8 +72,8 @@ NUMBERS: tuple[LeilSaunaNumberEntityDescription, ...] = ( native_step=1, value_fn=lambda data: ( fan_dur - if (fan_dur := data.fan_duration) is not None and fan_dur > MIN_FAN_DURATION - else DEFAULT_FAN_DURATION_MIN + if (fan_dur := data.fan_duration) > MIN_FAN_DURATION + else DEFAULT_FAN_DURATION ), set_value_fn=lambda client, value: client.async_set_fan_duration(int(value)), ), diff --git a/homeassistant/components/saunum/services.py b/homeassistant/components/saunum/services.py index 88b074af15d..c45c412e164 100644 --- a/homeassistant/components/saunum/services.py +++ b/homeassistant/components/saunum/services.py @@ -4,7 +4,15 @@ from __future__ import annotations from datetime import timedelta -from pysaunum import MAX_DURATION, MAX_FAN_DURATION, MAX_TEMPERATURE, MIN_TEMPERATURE +from pysaunum import ( + DEFAULT_DURATION, + DEFAULT_FAN_DURATION, + DEFAULT_TEMPERATURE, + MAX_DURATION, + MAX_FAN_DURATION, + MAX_TEMPERATURE, + MIN_TEMPERATURE, +) import voluptuous as vol from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN @@ -29,17 +37,21 @@ def async_setup_services(hass: HomeAssistant) -> None: SERVICE_START_SESSION, entity_domain=CLIMATE_DOMAIN, schema={ - vol.Optional(ATTR_DURATION, default=timedelta(minutes=120)): vol.All( + vol.Optional( + ATTR_DURATION, default=timedelta(minutes=DEFAULT_DURATION) + ): vol.All( cv.time_period, vol.Range( min=timedelta(minutes=1), max=timedelta(minutes=MAX_DURATION), ), ), - vol.Optional(ATTR_TARGET_TEMPERATURE, default=80): vol.All( + vol.Optional(ATTR_TARGET_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All( cv.positive_int, vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE) ), - vol.Optional(ATTR_FAN_DURATION, default=timedelta(minutes=10)): vol.All( + vol.Optional( + ATTR_FAN_DURATION, default=timedelta(minutes=DEFAULT_FAN_DURATION) + ): vol.All( cv.time_period, vol.Range( min=timedelta(minutes=1), diff --git a/tests/components/saunum/test_number.py b/tests/components/saunum/test_number.py index 80b5dcd68fa..03f81a109cb 100644 --- a/tests/components/saunum/test_number.py +++ b/tests/components/saunum/test_number.py @@ -5,7 +5,7 @@ from __future__ import annotations from dataclasses import replace from unittest.mock import MagicMock -from pysaunum import SaunumException +from pysaunum import DEFAULT_DURATION, DEFAULT_FAN_DURATION, SaunumException import pytest from syrupy.assertion import SnapshotAssertion @@ -50,7 +50,7 @@ async def test_set_sauna_duration( # Verify initial state state = hass.states.get(entity_id) assert state is not None - assert state.state == "120" + assert state.state == str(DEFAULT_DURATION) # Set new duration await hass.services.async_call( @@ -75,7 +75,7 @@ async def test_set_fan_duration( # Verify initial state state = hass.states.get(entity_id) assert state is not None - assert state.state == "10" + assert state.state == str(DEFAULT_FAN_DURATION) # Set new duration await hass.services.async_call( @@ -151,13 +151,13 @@ async def test_number_with_default_duration( mock_config_entry: MockConfigEntry, mock_saunum_client: MagicMock, ) -> None: - """Test number entities use default when device returns None.""" - # Set duration to None (device hasn't set it yet) + """Test number entities use default when device returns 0.""" + # Set duration to 0 (device hasn't set it yet / sauna type default) base_data = mock_saunum_client.async_get_data.return_value mock_saunum_client.async_get_data.return_value = replace( base_data, - sauna_duration=None, - fan_duration=None, + sauna_duration=0, + fan_duration=0, ) mock_config_entry.add_to_hass(hass) @@ -167,11 +167,11 @@ async def test_number_with_default_duration( # Should show default values sauna_duration_state = hass.states.get("number.saunum_leil_sauna_duration") assert sauna_duration_state is not None - assert sauna_duration_state.state == "120" # DEFAULT_DURATION_MIN + assert sauna_duration_state.state == str(DEFAULT_DURATION) fan_duration_state = hass.states.get("number.saunum_leil_fan_duration") assert fan_duration_state is not None - assert fan_duration_state.state == "15" # DEFAULT_FAN_DURATION_MIN + assert fan_duration_state.state == str(DEFAULT_FAN_DURATION) async def test_number_with_valid_duration_from_device(