mirror of
https://github.com/home-assistant/core.git
synced 2026-02-15 07:36:16 +00:00
Add Start session action to Saunum integration (#162177)
This commit is contained in:
@@ -8,8 +8,12 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import LeilSaunaCoordinator
|
||||
from .services import async_setup_services
|
||||
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.BINARY_SENSOR,
|
||||
@@ -19,9 +23,17 @@ PLATFORMS: list[Platform] = [
|
||||
Platform.SENSOR,
|
||||
]
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
type LeilSaunaConfigEntry = ConfigEntry[LeilSaunaCoordinator]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Saunum component."""
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: LeilSaunaConfigEntry) -> bool:
|
||||
"""Set up Saunum Leil Sauna from a config entry."""
|
||||
host = entry.data[CONF_HOST]
|
||||
|
||||
@@ -238,3 +238,33 @@ class LeilSaunaClimate(LeilSaunaEntity, ClimateEntity):
|
||||
) from err
|
||||
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_start_session(
|
||||
self,
|
||||
duration: int = 120,
|
||||
target_temperature: int = 80,
|
||||
fan_duration: int = 10,
|
||||
) -> None:
|
||||
"""Start a sauna session with custom parameters."""
|
||||
if self.coordinator.data.door_open:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="door_open",
|
||||
)
|
||||
|
||||
try:
|
||||
# Set all parameters before starting the session
|
||||
await self.coordinator.client.async_set_sauna_duration(duration)
|
||||
await self.coordinator.client.async_set_target_temperature(
|
||||
target_temperature
|
||||
)
|
||||
await self.coordinator.client.async_set_fan_duration(fan_duration)
|
||||
await self.coordinator.client.async_start_session()
|
||||
except SaunumException as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="start_session_failed",
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@@ -27,5 +27,10 @@
|
||||
"default": "mdi:radiator"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"start_session": {
|
||||
"service": "mdi:heat-wave"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: Integration does not register custom actions.
|
||||
action-setup: done
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: Integration does not register custom actions.
|
||||
docs-actions: done
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
|
||||
41
homeassistant/components/saunum/services.py
Normal file
41
homeassistant/components/saunum/services.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""Define services for the Saunum integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pysaunum import MAX_DURATION, MAX_FAN_DURATION, MAX_TEMPERATURE, MIN_TEMPERATURE
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, service
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
ATTR_DURATION = "duration"
|
||||
ATTR_TARGET_TEMPERATURE = "target_temperature"
|
||||
ATTR_FAN_DURATION = "fan_duration"
|
||||
|
||||
SERVICE_START_SESSION = "start_session"
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register services for the Saunum integration."""
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_START_SESSION,
|
||||
entity_domain=CLIMATE_DOMAIN,
|
||||
schema={
|
||||
vol.Optional(ATTR_DURATION, default=120): vol.All(
|
||||
cv.positive_int, vol.Range(min=1, max=MAX_DURATION)
|
||||
),
|
||||
vol.Optional(ATTR_TARGET_TEMPERATURE, default=80): vol.All(
|
||||
cv.positive_int, vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
|
||||
),
|
||||
vol.Optional(ATTR_FAN_DURATION, default=10): vol.All(
|
||||
cv.positive_int, vol.Range(min=1, max=MAX_FAN_DURATION)
|
||||
),
|
||||
},
|
||||
func="async_start_session",
|
||||
)
|
||||
23
homeassistant/components/saunum/services.yaml
Normal file
23
homeassistant/components/saunum/services.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
start_session:
|
||||
target:
|
||||
entity:
|
||||
domain: climate
|
||||
integration: saunum
|
||||
fields:
|
||||
duration:
|
||||
required: false
|
||||
selector:
|
||||
duration:
|
||||
target_temperature:
|
||||
required: false
|
||||
example: 80
|
||||
default: 80
|
||||
selector:
|
||||
number:
|
||||
min: 40
|
||||
max: 100
|
||||
unit_of_measurement: "°C"
|
||||
fan_duration:
|
||||
required: false
|
||||
selector:
|
||||
duration:
|
||||
@@ -128,6 +128,9 @@
|
||||
},
|
||||
"set_temperature_failed": {
|
||||
"message": "Failed to set temperature to {temperature}"
|
||||
},
|
||||
"start_session_failed": {
|
||||
"message": "Failed to start sauna session: {error}"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
@@ -146,5 +149,25 @@
|
||||
"description": "Customize the names of the three sauna type preset modes"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"start_session": {
|
||||
"description": "Starts a sauna session with custom duration, target temperature, and fan duration.",
|
||||
"fields": {
|
||||
"duration": {
|
||||
"description": "Session duration in minutes.",
|
||||
"name": "Duration"
|
||||
},
|
||||
"fan_duration": {
|
||||
"description": "Fan duration in minutes.",
|
||||
"name": "Fan duration"
|
||||
},
|
||||
"target_temperature": {
|
||||
"description": "Target temperature in Celsius.",
|
||||
"name": "Target temperature"
|
||||
}
|
||||
},
|
||||
"name": "Start session"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,31 @@ def mock_config_entry() -> MockConfigEntry:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_saunum_client_class() -> Generator[MagicMock]:
|
||||
def mock_saunum_data() -> SaunumData:
|
||||
"""Return default mock Saunum data."""
|
||||
return SaunumData(
|
||||
session_active=False,
|
||||
sauna_type=0,
|
||||
sauna_duration=120,
|
||||
fan_duration=10,
|
||||
target_temperature=80,
|
||||
fan_speed=2,
|
||||
light_on=False,
|
||||
current_temperature=75.0,
|
||||
on_time=3600,
|
||||
heater_elements_active=0,
|
||||
door_open=False,
|
||||
alarm_door_open=False,
|
||||
alarm_door_sensor=False,
|
||||
alarm_thermal_cutoff=False,
|
||||
alarm_internal_temp=False,
|
||||
alarm_temp_sensor_short=False,
|
||||
alarm_temp_sensor_open=False,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_saunum_client_class(mock_saunum_data: SaunumData) -> Generator[MagicMock]:
|
||||
"""Return a mocked Saunum client class for config flow and integration tests."""
|
||||
with (
|
||||
patch(
|
||||
@@ -54,29 +78,7 @@ def mock_saunum_client_class() -> Generator[MagicMock]:
|
||||
mock_client.is_connected = True
|
||||
|
||||
mock_client_class.create = AsyncMock(return_value=mock_client)
|
||||
|
||||
# Create mock data for async_get_data
|
||||
mock_data = SaunumData(
|
||||
session_active=False,
|
||||
sauna_type=0,
|
||||
sauna_duration=120,
|
||||
fan_duration=10,
|
||||
target_temperature=80,
|
||||
fan_speed=2,
|
||||
light_on=False,
|
||||
current_temperature=75.0,
|
||||
on_time=3600,
|
||||
heater_elements_active=0,
|
||||
door_open=False,
|
||||
alarm_door_open=False,
|
||||
alarm_door_sensor=False,
|
||||
alarm_thermal_cutoff=False,
|
||||
alarm_internal_temp=False,
|
||||
alarm_temp_sensor_short=False,
|
||||
alarm_temp_sensor_open=False,
|
||||
)
|
||||
|
||||
mock_client.async_get_data.return_value = mock_data
|
||||
mock_client.async_get_data.return_value = mock_saunum_data
|
||||
|
||||
yield mock_client_class
|
||||
|
||||
|
||||
115
tests/components/saunum/test_services.py
Normal file
115
tests/components/saunum/test_services.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""Tests for Saunum services."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from pysaunum import SaunumData, SaunumException
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.saunum.const import DOMAIN
|
||||
from homeassistant.components.saunum.services import (
|
||||
ATTR_DURATION,
|
||||
ATTR_FAN_DURATION,
|
||||
ATTR_TARGET_TEMPERATURE,
|
||||
SERVICE_START_SESSION,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Fixture to specify platforms to test."""
|
||||
return [Platform.CLIMATE]
|
||||
|
||||
|
||||
async def test_start_session_success(
|
||||
hass: HomeAssistant,
|
||||
init_integration: MockConfigEntry,
|
||||
mock_saunum_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test start_session service success."""
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_START_SESSION,
|
||||
{
|
||||
ATTR_ENTITY_ID: "climate.saunum_leil",
|
||||
ATTR_DURATION: 120,
|
||||
ATTR_TARGET_TEMPERATURE: 80,
|
||||
ATTR_FAN_DURATION: 10,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_saunum_client.async_set_sauna_duration.assert_called_once_with(120)
|
||||
mock_saunum_client.async_set_target_temperature.assert_called_once_with(80)
|
||||
mock_saunum_client.async_set_fan_duration.assert_called_once_with(10)
|
||||
mock_saunum_client.async_start_session.assert_called_once()
|
||||
|
||||
|
||||
async def test_start_session_with_defaults(
|
||||
hass: HomeAssistant,
|
||||
init_integration: MockConfigEntry,
|
||||
mock_saunum_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test start_session service uses defaults when optional fields omitted."""
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_START_SESSION,
|
||||
{ATTR_ENTITY_ID: "climate.saunum_leil"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Defaults: duration=120, target_temperature=80, fan_duration=10
|
||||
mock_saunum_client.async_set_sauna_duration.assert_called_once_with(120)
|
||||
mock_saunum_client.async_set_target_temperature.assert_called_once_with(80)
|
||||
mock_saunum_client.async_set_fan_duration.assert_called_once_with(10)
|
||||
mock_saunum_client.async_start_session.assert_called_once()
|
||||
|
||||
|
||||
async def test_start_session_door_open(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_saunum_client: MagicMock,
|
||||
mock_saunum_data: SaunumData,
|
||||
) -> None:
|
||||
"""Test start_session service fails when door is open."""
|
||||
mock_saunum_client.async_get_data.return_value = SaunumData(
|
||||
**{**mock_saunum_data.__dict__, "door_open": True}
|
||||
)
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with pytest.raises(ServiceValidationError) as exc_info:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_START_SESSION,
|
||||
{ATTR_ENTITY_ID: "climate.saunum_leil"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert exc_info.value.translation_key == "door_open"
|
||||
|
||||
|
||||
async def test_start_session_communication_error(
|
||||
hass: HomeAssistant,
|
||||
init_integration: MockConfigEntry,
|
||||
mock_saunum_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test start_session service handles communication error."""
|
||||
mock_saunum_client.async_set_sauna_duration.side_effect = SaunumException(
|
||||
"Connection lost"
|
||||
)
|
||||
|
||||
with pytest.raises(HomeAssistantError) as exc_info:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_START_SESSION,
|
||||
{ATTR_ENTITY_ID: "climate.saunum_leil"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert exc_info.value.translation_key == "start_session_failed"
|
||||
Reference in New Issue
Block a user