diff --git a/homeassistant/components/snooz/const.py b/homeassistant/components/snooz/const.py index 9ce16b80e05..7a39ed49105 100644 --- a/homeassistant/components/snooz/const.py +++ b/homeassistant/components/snooz/const.py @@ -4,3 +4,11 @@ from homeassistant.const import Platform DOMAIN = "snooz" PLATFORMS: list[Platform] = [Platform.FAN] + +SERVICE_TRANSITION_ON = "transition_on" +SERVICE_TRANSITION_OFF = "transition_off" + +ATTR_VOLUME = "volume" +ATTR_DURATION = "duration" + +DEFAULT_TRANSITION_DURATION = 20 diff --git a/homeassistant/components/snooz/fan.py b/homeassistant/components/snooz/fan.py index d8c8f54d7bb..a34989d1a03 100644 --- a/homeassistant/components/snooz/fan.py +++ b/homeassistant/components/snooz/fan.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Callable +from datetime import timedelta from typing import Any from pysnooz.api import UnknownSnoozState @@ -12,16 +13,25 @@ from pysnooz.commands import ( turn_off, turn_on, ) +import voluptuous as vol from homeassistant.components.fan import ATTR_PERCENTAGE, FanEntity, FanEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from .const import DOMAIN +from .const import ( + ATTR_DURATION, + ATTR_VOLUME, + DEFAULT_TRANSITION_DURATION, + DOMAIN, + SERVICE_TRANSITION_OFF, + SERVICE_TRANSITION_ON, +) from .models import SnoozConfigurationData @@ -30,6 +40,29 @@ async def async_setup_entry( ) -> None: """Set up Snooz device from a config entry.""" + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + SERVICE_TRANSITION_ON, + { + vol.Optional(ATTR_VOLUME): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ), + vol.Optional(ATTR_DURATION, default=DEFAULT_TRANSITION_DURATION): vol.All( + vol.Coerce(int), vol.Range(min=1, max=300) + ), + }, + "async_transition_on", + ) + platform.async_register_entity_service( + SERVICE_TRANSITION_OFF, + { + vol.Optional(ATTR_DURATION, default=DEFAULT_TRANSITION_DURATION): vol.All( + vol.Coerce(int), vol.Range(min=1, max=300) + ), + }, + "async_transition_off", + ) + data: SnoozConfigurationData = hass.data[DOMAIN][entry.entry_id] async_add_entities([SnoozFan(data)]) @@ -108,6 +141,18 @@ class SnoozFan(FanEntity, RestoreEntity): set_volume(percentage) if percentage > 0 else turn_off() ) + async def async_transition_on(self, duration: int, **kwargs: Any) -> None: + """Transition on the device.""" + await self._async_execute_command( + turn_on(volume=kwargs.get("volume"), duration=timedelta(seconds=duration)) + ) + + async def async_transition_off(self, duration: int, **kwargs: Any) -> None: + """Transition off the device.""" + await self._async_execute_command( + turn_off(duration=timedelta(seconds=duration)) + ) + async def _async_execute_command(self, command: SnoozCommandData) -> None: result = await self._device.async_execute_command(command) diff --git a/homeassistant/components/snooz/services.yaml b/homeassistant/components/snooz/services.yaml new file mode 100644 index 00000000000..f795edf213a --- /dev/null +++ b/homeassistant/components/snooz/services.yaml @@ -0,0 +1,43 @@ +transition_on: + name: Transition on + description: Transition to a target volume level over time. + target: + entity: + integration: snooz + domain: fan + fields: + duration: + name: Transition duration + description: Time it takes to reach the target volume level. + selector: + number: + min: 1 + max: 300 + unit_of_measurement: seconds + mode: box + volume: + name: Target volume + description: If not specified, the volume level is read from the device. + selector: + number: + min: 1 + max: 100 + unit_of_measurement: "%" + +transition_off: + name: Transition off + description: Transition volume off over time. + target: + entity: + integration: snooz + domain: fan + fields: + duration: + name: Transition duration + description: Time it takes to turn off. + selector: + number: + min: 1 + max: 300 + unit_of_measurement: seconds + mode: box diff --git a/tests/components/snooz/test_fan.py b/tests/components/snooz/test_fan.py index 30528336e2d..19c794d6f04 100644 --- a/tests/components/snooz/test_fan.py +++ b/tests/components/snooz/test_fan.py @@ -10,7 +10,12 @@ from pysnooz.testing import MockSnoozDevice import pytest from homeassistant.components import fan -from homeassistant.components.snooz.const import DOMAIN +from homeassistant.components.snooz.const import ( + ATTR_DURATION, + DOMAIN, + SERVICE_TRANSITION_OFF, + SERVICE_TRANSITION_ON, +) from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, @@ -41,6 +46,20 @@ async def test_turn_on(hass: HomeAssistant, snooz_fan_entity_id: str): assert ATTR_ASSUMED_STATE not in state.attributes +async def test_transition_on(hass: HomeAssistant, snooz_fan_entity_id: str): + """Test transitioning on the device.""" + await hass.services.async_call( + DOMAIN, + SERVICE_TRANSITION_ON, + {ATTR_ENTITY_ID: [snooz_fan_entity_id], ATTR_DURATION: 1}, + blocking=True, + ) + + state = hass.states.get(snooz_fan_entity_id) + assert state.state == STATE_ON + assert ATTR_ASSUMED_STATE not in state.attributes + + @pytest.mark.parametrize("percentage", [1, 22, 50, 99, 100]) async def test_turn_on_with_percentage( hass: HomeAssistant, snooz_fan_entity_id: str, percentage: int @@ -115,6 +134,20 @@ async def test_turn_off(hass: HomeAssistant, snooz_fan_entity_id: str): assert ATTR_ASSUMED_STATE not in state.attributes +async def test_transition_off(hass: HomeAssistant, snooz_fan_entity_id: str): + """Test transitioning off the device.""" + await hass.services.async_call( + DOMAIN, + SERVICE_TRANSITION_OFF, + {ATTR_ENTITY_ID: [snooz_fan_entity_id], ATTR_DURATION: 1}, + blocking=True, + ) + + state = hass.states.get(snooz_fan_entity_id) + assert state.state == STATE_OFF + assert ATTR_ASSUMED_STATE not in state.attributes + + async def test_push_events( hass: HomeAssistant, mock_connected_snooz: SnoozFixture, snooz_fan_entity_id: str ):