diff --git a/homeassistant/components/sleepiq/__init__.py b/homeassistant/components/sleepiq/__init__.py index c12eace2f03..b98be3dcfe1 100644 --- a/homeassistant/components/sleepiq/__init__.py +++ b/homeassistant/components/sleepiq/__init__.py @@ -35,6 +35,7 @@ PLATFORMS = [ Platform.BUTTON, Platform.LIGHT, Platform.NUMBER, + Platform.SELECT, Platform.SENSOR, Platform.SWITCH, ] diff --git a/homeassistant/components/sleepiq/select.py b/homeassistant/components/sleepiq/select.py new file mode 100644 index 00000000000..2bf305007ca --- /dev/null +++ b/homeassistant/components/sleepiq/select.py @@ -0,0 +1,82 @@ +"""Support for SleepIQ foundation preset selection.""" +from __future__ import annotations + +from asyncsleepiq import ( + FAVORITE, + FLAT, + READ, + SNORE, + WATCH_TV, + ZERO_G, + SleepIQBed, + SleepIQPreset, +) + +from homeassistant.components.select import SelectEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN +from .coordinator import SleepIQData +from .entity import SleepIQBedEntity + +FOUNDATION_PRESET_NAMES = { + "Not at preset": 0, + "Favorite": FAVORITE, + "Read": READ, + "Watch TV": WATCH_TV, + "Flat": FLAT, + "Zero G": ZERO_G, + "Snore": SNORE, +} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the SleepIQ foundation preset select entities.""" + data: SleepIQData = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + SleepIQSelectEntity(data.data_coordinator, bed, preset) + for bed in data.client.beds.values() + for preset in bed.foundation.presets + ) + + +class SleepIQSelectEntity(SleepIQBedEntity, SelectEntity): + """Representation of a SleepIQ select entity.""" + + _attr_options = list(FOUNDATION_PRESET_NAMES.keys()) + + def __init__( + self, coordinator: DataUpdateCoordinator, bed: SleepIQBed, preset: SleepIQPreset + ) -> None: + """Initialize the select entity.""" + self.preset = preset + + if preset.side: + self._attr_name = ( + f"SleepNumber {bed.name} Foundation Preset {preset.side_full}" + ) + self._attr_unique_id = f"{bed.id}_preset_{preset.side}" + else: + self._attr_name = f"SleepNumber {bed.name} Foundation Preset" + self._attr_unique_id = f"{bed.id}_preset" + + super().__init__(coordinator, bed) + self._async_update_attrs() + + @callback + def _async_update_attrs(self) -> None: + """Update entity attributes.""" + self._attr_current_option = self.preset.preset + + async def async_select_option(self, option: str) -> None: + """Change the current preset.""" + await self.preset.set_preset(FOUNDATION_PRESET_NAMES[option]) + self._attr_current_option = option + self.async_write_ha_state() diff --git a/tests/components/sleepiq/conftest.py b/tests/components/sleepiq/conftest.py index a6f6ba78aba..4408f41035b 100644 --- a/tests/components/sleepiq/conftest.py +++ b/tests/components/sleepiq/conftest.py @@ -1,13 +1,15 @@ """Common methods for SleepIQ.""" from __future__ import annotations -from unittest.mock import create_autospec, patch +from collections.abc import Generator +from unittest.mock import MagicMock, create_autospec, patch from asyncsleepiq import ( SleepIQActuator, SleepIQBed, SleepIQFoundation, SleepIQLight, + SleepIQPreset, SleepIQSleeper, ) import pytest @@ -28,6 +30,8 @@ SLEEPER_L_NAME = "SleeperL" SLEEPER_R_NAME = "Sleeper R" SLEEPER_L_NAME_LOWER = SLEEPER_L_NAME.lower().replace(" ", "_") SLEEPER_R_NAME_LOWER = SLEEPER_R_NAME.lower().replace(" ", "_") +PRESET_L_STATE = "Watch TV" +PRESET_R_STATE = "Flat" SLEEPIQ_CONFIG = { CONF_USERNAME: "user@email.com", @@ -36,48 +40,88 @@ SLEEPIQ_CONFIG = { @pytest.fixture -def mock_asyncsleepiq(): - """Mock an AsyncSleepIQ object.""" +def mock_bed() -> MagicMock: + """Mock a SleepIQBed object with sleepers and lights.""" + bed = create_autospec(SleepIQBed) + bed.name = BED_NAME + bed.id = BED_ID + bed.mac_addr = "12:34:56:78:AB:CD" + bed.model = "C10" + bed.paused = False + sleeper_l = create_autospec(SleepIQSleeper) + sleeper_r = create_autospec(SleepIQSleeper) + bed.sleepers = [sleeper_l, sleeper_r] + + sleeper_l.side = "L" + sleeper_l.name = SLEEPER_L_NAME + sleeper_l.in_bed = True + sleeper_l.sleep_number = 40 + sleeper_l.pressure = 1000 + sleeper_l.sleeper_id = SLEEPER_L_ID + + sleeper_r.side = "R" + sleeper_r.name = SLEEPER_R_NAME + sleeper_r.in_bed = False + sleeper_r.sleep_number = 80 + sleeper_r.pressure = 1400 + sleeper_r.sleeper_id = SLEEPER_R_ID + + bed.foundation = create_autospec(SleepIQFoundation) + light_1 = create_autospec(SleepIQLight) + light_1.outlet_id = 1 + light_1.is_on = False + light_2 = create_autospec(SleepIQLight) + light_2.outlet_id = 2 + light_2.is_on = False + bed.foundation.lights = [light_1, light_2] + + return bed + + +@pytest.fixture +def mock_asyncsleepiq_single_foundation( + mock_bed: MagicMock, +) -> Generator[MagicMock, None, None]: + """Mock an AsyncSleepIQ object with a single foundation.""" with patch("homeassistant.components.sleepiq.AsyncSleepIQ", autospec=True) as mock: client = mock.return_value - bed = create_autospec(SleepIQBed) - client.beds = {BED_ID: bed} - bed.name = BED_NAME - bed.id = BED_ID - bed.mac_addr = "12:34:56:78:AB:CD" - bed.model = "C10" - bed.paused = False - sleeper_l = create_autospec(SleepIQSleeper) - sleeper_r = create_autospec(SleepIQSleeper) - bed.sleepers = [sleeper_l, sleeper_r] + client.beds = {BED_ID: mock_bed} - sleeper_l.side = "L" - sleeper_l.name = SLEEPER_L_NAME - sleeper_l.in_bed = True - sleeper_l.sleep_number = 40 - sleeper_l.pressure = 1000 - sleeper_l.sleeper_id = SLEEPER_L_ID + actuator_h = create_autospec(SleepIQActuator) + actuator_f = create_autospec(SleepIQActuator) + mock_bed.foundation.actuators = [actuator_h, actuator_f] - sleeper_r.side = "R" - sleeper_r.name = SLEEPER_R_NAME - sleeper_r.in_bed = False - sleeper_r.sleep_number = 80 - sleeper_r.pressure = 1400 - sleeper_r.sleeper_id = SLEEPER_R_ID + actuator_h.side = "R" + actuator_h.side_full = "Right" + actuator_h.actuator = "H" + actuator_h.actuator_full = "Head" + actuator_h.position = 60 - bed.foundation = create_autospec(SleepIQFoundation) - light_1 = create_autospec(SleepIQLight) - light_1.outlet_id = 1 - light_1.is_on = False - light_2 = create_autospec(SleepIQLight) - light_2.outlet_id = 2 - light_2.is_on = False - bed.foundation.lights = [light_1, light_2] + actuator_f.side = None + actuator_f.actuator = "F" + actuator_f.actuator_full = "Foot" + actuator_f.position = 10 + + preset = create_autospec(SleepIQPreset) + mock_bed.foundation.presets = [preset] + + preset.preset = PRESET_R_STATE + preset.side = None + preset.side_full = None + yield client + + +@pytest.fixture +def mock_asyncsleepiq(mock_bed: MagicMock) -> Generator[MagicMock, None, None]: + """Mock an AsyncSleepIQ object with a split foundation.""" + with patch("homeassistant.components.sleepiq.AsyncSleepIQ", autospec=True) as mock: + client = mock.return_value + client.beds = {BED_ID: mock_bed} actuator_h_r = create_autospec(SleepIQActuator) actuator_h_l = create_autospec(SleepIQActuator) actuator_f = create_autospec(SleepIQActuator) - bed.foundation.actuators = [actuator_h_r, actuator_h_l, actuator_f] + mock_bed.foundation.actuators = [actuator_h_r, actuator_h_l, actuator_f] actuator_h_r.side = "R" actuator_h_r.side_full = "Right" @@ -96,6 +140,18 @@ def mock_asyncsleepiq(): actuator_f.actuator_full = "Foot" actuator_f.position = 10 + preset_l = create_autospec(SleepIQPreset) + preset_r = create_autospec(SleepIQPreset) + mock_bed.foundation.presets = [preset_l, preset_r] + + preset_l.preset = PRESET_L_STATE + preset_l.side = "L" + preset_l.side_full = "Left" + + preset_r.preset = PRESET_R_STATE + preset_r.side = "R" + preset_r.side_full = "Right" + yield client diff --git a/tests/components/sleepiq/test_select.py b/tests/components/sleepiq/test_select.py new file mode 100644 index 00000000000..1b0c34d167c --- /dev/null +++ b/tests/components/sleepiq/test_select.py @@ -0,0 +1,119 @@ +"""Tests for the SleepIQ select platform.""" +from unittest.mock import MagicMock + +from asyncsleepiq import ZERO_G + +from homeassistant.components.select import DOMAIN, SERVICE_SELECT_OPTION +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_ICON, + ATTR_OPTION, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.components.sleepiq.conftest import ( + BED_ID, + BED_NAME, + BED_NAME_LOWER, + PRESET_L_STATE, + PRESET_R_STATE, + setup_platform, +) + + +async def test_split_foundation_preset( + hass: HomeAssistant, mock_asyncsleepiq: MagicMock +) -> None: + """Test the SleepIQ select entity for split foundation presets.""" + entry = await setup_platform(hass, DOMAIN) + entity_registry = er.async_get(hass) + + state = hass.states.get( + f"select.sleepnumber_{BED_NAME_LOWER}_foundation_preset_right" + ) + assert state.state == PRESET_R_STATE + assert state.attributes.get(ATTR_ICON) == "mdi:bed" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == f"SleepNumber {BED_NAME} Foundation Preset Right" + ) + + entry = entity_registry.async_get( + f"select.sleepnumber_{BED_NAME_LOWER}_foundation_preset_right" + ) + assert entry + assert entry.unique_id == f"{BED_ID}_preset_R" + + state = hass.states.get( + f"select.sleepnumber_{BED_NAME_LOWER}_foundation_preset_left" + ) + assert state.state == PRESET_L_STATE + assert state.attributes.get(ATTR_ICON) == "mdi:bed" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == f"SleepNumber {BED_NAME} Foundation Preset Left" + ) + + entry = entity_registry.async_get( + f"select.sleepnumber_{BED_NAME_LOWER}_foundation_preset_left" + ) + assert entry + assert entry.unique_id == f"{BED_ID}_preset_L" + + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: f"select.sleepnumber_{BED_NAME_LOWER}_foundation_preset_left", + ATTR_OPTION: "Zero G", + }, + blocking=True, + ) + await hass.async_block_till_done() + + mock_asyncsleepiq.beds[BED_ID].foundation.presets[0].set_preset.assert_called_once() + mock_asyncsleepiq.beds[BED_ID].foundation.presets[0].set_preset.assert_called_with( + ZERO_G + ) + + +async def test_single_foundation_preset( + hass: HomeAssistant, mock_asyncsleepiq_single_foundation: MagicMock +) -> None: + """Test the SleepIQ select entity for single foundation presets.""" + entry = await setup_platform(hass, DOMAIN) + entity_registry = er.async_get(hass) + + state = hass.states.get(f"select.sleepnumber_{BED_NAME_LOWER}_foundation_preset") + assert state.state == PRESET_R_STATE + assert state.attributes.get(ATTR_ICON) == "mdi:bed" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == f"SleepNumber {BED_NAME} Foundation Preset" + ) + + entry = entity_registry.async_get( + f"select.sleepnumber_{BED_NAME_LOWER}_foundation_preset" + ) + assert entry + assert entry.unique_id == f"{BED_ID}_preset" + + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: f"select.sleepnumber_{BED_NAME_LOWER}_foundation_preset", + ATTR_OPTION: "Zero G", + }, + blocking=True, + ) + await hass.async_block_till_done() + + mock_asyncsleepiq_single_foundation.beds[BED_ID].foundation.presets[ + 0 + ].set_preset.assert_called_once() + mock_asyncsleepiq_single_foundation.beds[BED_ID].foundation.presets[ + 0 + ].set_preset.assert_called_with(ZERO_G)