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.BUTTON,
|
||||||
Platform.LIGHT,
|
Platform.LIGHT,
|
||||||
Platform.NUMBER,
|
Platform.NUMBER,
|
||||||
|
Platform.SELECT,
|
||||||
Platform.SENSOR,
|
Platform.SENSOR,
|
||||||
Platform.SWITCH,
|
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."""
|
"""Common methods for SleepIQ."""
|
||||||
from __future__ import annotations
|
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 (
|
from asyncsleepiq import (
|
||||||
SleepIQActuator,
|
SleepIQActuator,
|
||||||
SleepIQBed,
|
SleepIQBed,
|
||||||
SleepIQFoundation,
|
SleepIQFoundation,
|
||||||
SleepIQLight,
|
SleepIQLight,
|
||||||
|
SleepIQPreset,
|
||||||
SleepIQSleeper,
|
SleepIQSleeper,
|
||||||
)
|
)
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -28,6 +30,8 @@ SLEEPER_L_NAME = "SleeperL"
|
||||||
SLEEPER_R_NAME = "Sleeper R"
|
SLEEPER_R_NAME = "Sleeper R"
|
||||||
SLEEPER_L_NAME_LOWER = SLEEPER_L_NAME.lower().replace(" ", "_")
|
SLEEPER_L_NAME_LOWER = SLEEPER_L_NAME.lower().replace(" ", "_")
|
||||||
SLEEPER_R_NAME_LOWER = SLEEPER_R_NAME.lower().replace(" ", "_")
|
SLEEPER_R_NAME_LOWER = SLEEPER_R_NAME.lower().replace(" ", "_")
|
||||||
|
PRESET_L_STATE = "Watch TV"
|
||||||
|
PRESET_R_STATE = "Flat"
|
||||||
|
|
||||||
SLEEPIQ_CONFIG = {
|
SLEEPIQ_CONFIG = {
|
||||||
CONF_USERNAME: "user@email.com",
|
CONF_USERNAME: "user@email.com",
|
||||||
|
@ -36,48 +40,88 @@ SLEEPIQ_CONFIG = {
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_asyncsleepiq():
|
def mock_bed() -> MagicMock:
|
||||||
"""Mock an AsyncSleepIQ object."""
|
"""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:
|
with patch("homeassistant.components.sleepiq.AsyncSleepIQ", autospec=True) as mock:
|
||||||
client = mock.return_value
|
client = mock.return_value
|
||||||
bed = create_autospec(SleepIQBed)
|
client.beds = {BED_ID: mock_bed}
|
||||||
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]
|
|
||||||
|
|
||||||
sleeper_l.side = "L"
|
actuator_h = create_autospec(SleepIQActuator)
|
||||||
sleeper_l.name = SLEEPER_L_NAME
|
actuator_f = create_autospec(SleepIQActuator)
|
||||||
sleeper_l.in_bed = True
|
mock_bed.foundation.actuators = [actuator_h, actuator_f]
|
||||||
sleeper_l.sleep_number = 40
|
|
||||||
sleeper_l.pressure = 1000
|
|
||||||
sleeper_l.sleeper_id = SLEEPER_L_ID
|
|
||||||
|
|
||||||
sleeper_r.side = "R"
|
actuator_h.side = "R"
|
||||||
sleeper_r.name = SLEEPER_R_NAME
|
actuator_h.side_full = "Right"
|
||||||
sleeper_r.in_bed = False
|
actuator_h.actuator = "H"
|
||||||
sleeper_r.sleep_number = 80
|
actuator_h.actuator_full = "Head"
|
||||||
sleeper_r.pressure = 1400
|
actuator_h.position = 60
|
||||||
sleeper_r.sleeper_id = SLEEPER_R_ID
|
|
||||||
|
|
||||||
bed.foundation = create_autospec(SleepIQFoundation)
|
actuator_f.side = None
|
||||||
light_1 = create_autospec(SleepIQLight)
|
actuator_f.actuator = "F"
|
||||||
light_1.outlet_id = 1
|
actuator_f.actuator_full = "Foot"
|
||||||
light_1.is_on = False
|
actuator_f.position = 10
|
||||||
light_2 = create_autospec(SleepIQLight)
|
|
||||||
light_2.outlet_id = 2
|
preset = create_autospec(SleepIQPreset)
|
||||||
light_2.is_on = False
|
mock_bed.foundation.presets = [preset]
|
||||||
bed.foundation.lights = [light_1, light_2]
|
|
||||||
|
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_r = create_autospec(SleepIQActuator)
|
||||||
actuator_h_l = create_autospec(SleepIQActuator)
|
actuator_h_l = create_autospec(SleepIQActuator)
|
||||||
actuator_f = 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 = "R"
|
||||||
actuator_h_r.side_full = "Right"
|
actuator_h_r.side_full = "Right"
|
||||||
|
@ -96,6 +140,18 @@ def mock_asyncsleepiq():
|
||||||
actuator_f.actuator_full = "Foot"
|
actuator_f.actuator_full = "Foot"
|
||||||
actuator_f.position = 10
|
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
|
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