Update Saunum integration to platinum quality (#160824)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
mettolen
2026-01-17 16:44:45 +02:00
committed by GitHub
parent 3e3ec4616c
commit 3539c4bcec
11 changed files with 75 additions and 17 deletions

View File

@@ -455,6 +455,7 @@ homeassistant.components.russound_rio.*
homeassistant.components.ruuvi_gateway.* homeassistant.components.ruuvi_gateway.*
homeassistant.components.ruuvitag_ble.* homeassistant.components.ruuvitag_ble.*
homeassistant.components.samsungtv.* homeassistant.components.samsungtv.*
homeassistant.components.saunum.*
homeassistant.components.scene.* homeassistant.components.scene.*
homeassistant.components.schedule.* homeassistant.components.schedule.*
homeassistant.components.schlage.* homeassistant.components.schlage.*

View File

@@ -17,7 +17,7 @@ from homeassistant.components.climate import (
HVACAction, HVACAction,
HVACMode, HVACMode,
) )
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -57,6 +57,8 @@ class LeilSaunaClimate(LeilSaunaEntity, ClimateEntity):
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
) )
_attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_precision = PRECISION_WHOLE
_attr_target_temperature_step = 1.0
_attr_min_temp = MIN_TEMPERATURE _attr_min_temp = MIN_TEMPERATURE
_attr_max_temp = MAX_TEMPERATURE _attr_max_temp = MAX_TEMPERATURE
_attr_fan_modes = [FAN_OFF, FAN_LOW, FAN_MEDIUM, FAN_HIGH] _attr_fan_modes = [FAN_OFF, FAN_LOW, FAN_MEDIUM, FAN_HIGH]
@@ -143,10 +145,18 @@ class LeilSaunaClimate(LeilSaunaEntity, ClimateEntity):
"""Set new fan mode.""" """Set new fan mode."""
if not self.coordinator.data.session_active: if not self.coordinator.data.session_active:
raise ServiceValidationError( raise ServiceValidationError(
"Cannot change fan mode when sauna session is not active",
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="session_not_active", translation_key="session_not_active",
) )
await self.coordinator.client.async_set_fan_speed(FAN_MODE_TO_SPEED[fan_mode]) try:
await self.coordinator.client.async_set_fan_speed(
FAN_MODE_TO_SPEED[fan_mode]
)
except SaunumException as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="set_fan_mode_failed",
) from err
await self.coordinator.async_request_refresh() await self.coordinator.async_request_refresh()

View File

@@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import TYPE_CHECKING, Any
from pysaunum import SaunumException from pysaunum import SaunumException
@@ -15,6 +15,9 @@ from . import LeilSaunaConfigEntry
from .const import DOMAIN from .const import DOMAIN
from .entity import LeilSaunaEntity from .entity import LeilSaunaEntity
if TYPE_CHECKING:
from .coordinator import LeilSaunaCoordinator
PARALLEL_UPDATES = 1 PARALLEL_UPDATES = 1
@@ -35,7 +38,7 @@ class LeilSaunaLight(LeilSaunaEntity, LightEntity):
_attr_color_mode = ColorMode.ONOFF _attr_color_mode = ColorMode.ONOFF
_attr_supported_color_modes = {ColorMode.ONOFF} _attr_supported_color_modes = {ColorMode.ONOFF}
def __init__(self, coordinator) -> None: def __init__(self, coordinator: LeilSaunaCoordinator) -> None:
"""Initialize the light entity.""" """Initialize the light entity."""
super().__init__(coordinator) super().__init__(coordinator)
# Override unique_id to differentiate from climate entity # Override unique_id to differentiate from climate entity

View File

@@ -7,6 +7,6 @@
"integration_type": "device", "integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["pysaunum"], "loggers": ["pysaunum"],
"quality_scale": "gold", "quality_scale": "platinum",
"requirements": ["pysaunum==0.2.0"] "requirements": ["pysaunum==0.2.0"]
} }

View File

@@ -133,11 +133,7 @@ class LeilSaunaNumber(LeilSaunaEntity, NumberEntity):
except SaunumException as err: except SaunumException as err:
raise HomeAssistantError( raise HomeAssistantError(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="set_value_failed", translation_key=f"set_{self.entity_description.key}_failed",
translation_placeholders={
"entity": self.entity_description.key,
"value": str(value),
},
) from err ) from err
await self.coordinator.async_request_refresh() await self.coordinator.async_request_refresh()

View File

@@ -77,4 +77,4 @@ rules:
inject-websession: inject-websession:
status: exempt status: exempt
comment: Integration uses Modbus TCP protocol and does not make HTTP requests. comment: Integration uses Modbus TCP protocol and does not make HTTP requests.
strict-typing: todo strict-typing: done

View File

@@ -89,6 +89,12 @@
"session_not_active": { "session_not_active": {
"message": "Cannot change fan mode when sauna session is not active" "message": "Cannot change fan mode when sauna session is not active"
}, },
"set_fan_duration_failed": {
"message": "Failed to set fan duration"
},
"set_fan_mode_failed": {
"message": "Failed to set fan mode"
},
"set_hvac_mode_failed": { "set_hvac_mode_failed": {
"message": "Failed to set HVAC mode to {hvac_mode}" "message": "Failed to set HVAC mode to {hvac_mode}"
}, },
@@ -98,11 +104,11 @@
"set_light_on_failed": { "set_light_on_failed": {
"message": "Failed to turn on light" "message": "Failed to turn on light"
}, },
"set_sauna_duration_failed": {
"message": "Failed to set sauna duration"
},
"set_temperature_failed": { "set_temperature_failed": {
"message": "Failed to set temperature to {temperature}" "message": "Failed to set temperature to {temperature}"
},
"set_value_failed": {
"message": "Failed to set {entity} to {value}"
} }
} }
} }

10
mypy.ini generated
View File

@@ -4306,6 +4306,16 @@ disallow_untyped_defs = true
warn_return_any = true warn_return_any = true
warn_unreachable = true warn_unreachable = true
[mypy-homeassistant.components.saunum.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.scene.*] [mypy-homeassistant.components.scene.*]
check_untyped_defs = true check_untyped_defs = true
disallow_incomplete_defs = true disallow_incomplete_defs = true

View File

@@ -17,6 +17,7 @@
]), ]),
'max_temp': 100, 'max_temp': 100,
'min_temp': 40, 'min_temp': 40,
'target_temp_step': 1.0,
}), }),
'config_entry_id': <ANY>, 'config_entry_id': <ANY>,
'config_subentry_id': <ANY>, 'config_subentry_id': <ANY>,
@@ -51,7 +52,7 @@
# name: test_entities[climate.saunum_leil-state] # name: test_entities[climate.saunum_leil-state]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'current_temperature': 75.0, 'current_temperature': 75,
'fan_mode': 'medium', 'fan_mode': 'medium',
'fan_modes': list([ 'fan_modes': list([
'off', 'off',
@@ -68,6 +69,7 @@
'max_temp': 100, 'max_temp': 100,
'min_temp': 40, 'min_temp': 40,
'supported_features': <ClimateEntityFeature: 9>, 'supported_features': <ClimateEntityFeature: 9>,
'target_temp_step': 1.0,
'temperature': 80, 'temperature': 80,
}), }),
'context': <ANY>, 'context': <ANY>,

View File

@@ -378,3 +378,33 @@ async def test_fan_mode_session_not_active_error(
{ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_LOW}, {ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_LOW},
blocking=True, blocking=True,
) )
@pytest.mark.usefixtures("init_integration")
async def test_fan_mode_error_handling(
hass: HomeAssistant,
mock_saunum_client,
) -> None:
"""Test error handling when setting fan mode fails."""
entity_id = "climate.saunum_leil"
# Ensure session is active
mock_saunum_client.async_get_data.return_value.session_active = True
# Make the client method raise an exception
mock_saunum_client.async_set_fan_speed.side_effect = SaunumException(
"Communication error"
)
# Try to call the service and expect HomeAssistantError
with pytest.raises(HomeAssistantError) as exc_info:
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
{ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_LOW},
blocking=True,
)
# Verify the exception has the correct translation key
assert exc_info.value.translation_key == "set_fan_mode_failed"
assert exc_info.value.translation_domain == "saunum"

View File

@@ -52,7 +52,7 @@ async def test_sensor_not_created_when_value_is_none(
assert await hass.config_entries.async_setup(mock_config_entry.entry_id) assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("sensor.saunum_leil_temperature") is None assert hass.states.get("sensor.saunum_leil_current_temperature") is None
assert hass.states.get("sensor.saunum_leil_heater_elements_active") is None assert hass.states.get("sensor.saunum_leil_heater_elements_active") is None
assert hass.states.get("sensor.saunum_leil_on_time") is None assert hass.states.get("sensor.saunum_leil_on_time") is None