Add SleepIQ select entity for foundation preset positions (#68489)
This commit is contained in:
parent
661f2fd613
commit
ad7a2c298b
4 changed files with 292 additions and 34 deletions
|
@ -35,6 +35,7 @@ PLATFORMS = [
|
|||
Platform.BUTTON,
|
||||
Platform.LIGHT,
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
|
82
homeassistant/components/sleepiq/select.py
Normal file
82
homeassistant/components/sleepiq/select.py
Normal file
|
@ -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()
|
|
@ -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
|
||||
|
||||
|
||||
|
|
119
tests/components/sleepiq/test_select.py
Normal file
119
tests/components/sleepiq/test_select.py
Normal file
|
@ -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)
|
Loading…
Add table
Add a link
Reference in a new issue