From 423c14e2a1b6b5806a5eab8663009aba87af00e5 Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Thu, 3 Mar 2022 14:42:33 -0500 Subject: [PATCH] Add light entity to SleepIQ (#67363) --- homeassistant/components/sleepiq/__init__.py | 8 +- .../components/sleepiq/binary_sensor.py | 4 +- .../components/sleepiq/coordinator.py | 5 +- homeassistant/components/sleepiq/entity.py | 20 +++-- homeassistant/components/sleepiq/light.py | 59 +++++++++++++++ homeassistant/components/sleepiq/sensor.py | 4 +- homeassistant/components/sleepiq/switch.py | 18 +++-- tests/components/sleepiq/conftest.py | 11 ++- tests/components/sleepiq/test_config_flow.py | 3 + tests/components/sleepiq/test_light.py | 73 +++++++++++++++++++ 10 files changed, 179 insertions(+), 26 deletions(-) create mode 100644 homeassistant/components/sleepiq/light.py create mode 100644 tests/components/sleepiq/test_light.py diff --git a/homeassistant/components/sleepiq/__init__.py b/homeassistant/components/sleepiq/__init__.py index a32e61b972c..da3f38fe560 100644 --- a/homeassistant/components/sleepiq/__init__.py +++ b/homeassistant/components/sleepiq/__init__.py @@ -26,7 +26,13 @@ from .coordinator import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.LIGHT, + Platform.SENSOR, + Platform.SWITCH, +] CONFIG_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/sleepiq/binary_sensor.py b/homeassistant/components/sleepiq/binary_sensor.py index 53611edc66b..b176320e671 100644 --- a/homeassistant/components/sleepiq/binary_sensor.py +++ b/homeassistant/components/sleepiq/binary_sensor.py @@ -12,7 +12,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, ICON_EMPTY, ICON_OCCUPIED, IS_IN_BED from .coordinator import SleepIQData -from .entity import SleepIQSensor +from .entity import SleepIQSleeperEntity async def async_setup_entry( @@ -29,7 +29,7 @@ async def async_setup_entry( ) -class IsInBedBinarySensor(SleepIQSensor, BinarySensorEntity): +class IsInBedBinarySensor(SleepIQSleeperEntity, BinarySensorEntity): """Implementation of a SleepIQ presence sensor.""" _attr_device_class = BinarySensorDeviceClass.OCCUPANCY diff --git a/homeassistant/components/sleepiq/coordinator.py b/homeassistant/components/sleepiq/coordinator.py index a2394de20b1..ef84b17ba9b 100644 --- a/homeassistant/components/sleepiq/coordinator.py +++ b/homeassistant/components/sleepiq/coordinator.py @@ -34,7 +34,10 @@ class SleepIQDataUpdateCoordinator(DataUpdateCoordinator[None]): self.client = client async def _async_update_data(self) -> None: - await self.client.fetch_bed_statuses() + tasks = [self.client.fetch_bed_statuses()] + [ + bed.foundation.update_lights() for bed in self.client.beds.values() + ] + await asyncio.gather(*tasks) class SleepIQPauseUpdateCoordinator(DataUpdateCoordinator[None]): diff --git a/homeassistant/components/sleepiq/entity.py b/homeassistant/components/sleepiq/entity.py index 6d0c8784eec..7fa14f2cbe7 100644 --- a/homeassistant/components/sleepiq/entity.py +++ b/homeassistant/components/sleepiq/entity.py @@ -33,7 +33,7 @@ class SleepIQEntity(Entity): self._attr_device_info = device_from_bed(bed) -class SleepIQSensor(CoordinatorEntity): +class SleepIQBedEntity(CoordinatorEntity): """Implementation of a SleepIQ sensor.""" _attr_icon = ICON_OCCUPIED @@ -42,17 +42,11 @@ class SleepIQSensor(CoordinatorEntity): self, coordinator: DataUpdateCoordinator, bed: SleepIQBed, - sleeper: SleepIQSleeper, - name: str, ) -> None: """Initialize the SleepIQ sensor entity.""" super().__init__(coordinator) - self.sleeper = sleeper self.bed = bed self._attr_device_info = device_from_bed(bed) - - self._attr_name = f"SleepNumber {bed.name} {sleeper.name} {SENSOR_TYPES[name]}" - self._attr_unique_id = f"{bed.id}_{sleeper.name}_{name}" self._async_update_attrs() @callback @@ -67,7 +61,7 @@ class SleepIQSensor(CoordinatorEntity): """Update sensor attributes.""" -class SleepIQBedCoordinator(CoordinatorEntity): +class SleepIQSleeperEntity(SleepIQBedEntity): """Implementation of a SleepIQ sensor.""" _attr_icon = ICON_OCCUPIED @@ -76,8 +70,12 @@ class SleepIQBedCoordinator(CoordinatorEntity): self, coordinator: DataUpdateCoordinator, bed: SleepIQBed, + sleeper: SleepIQSleeper, + name: str, ) -> None: """Initialize the SleepIQ sensor entity.""" - super().__init__(coordinator) - self.bed = bed - self._attr_device_info = device_from_bed(bed) + self.sleeper = sleeper + super().__init__(coordinator, bed) + + self._attr_name = f"SleepNumber {bed.name} {sleeper.name} {SENSOR_TYPES[name]}" + self._attr_unique_id = f"{bed.id}_{sleeper.name}_{name}" diff --git a/homeassistant/components/sleepiq/light.py b/homeassistant/components/sleepiq/light.py new file mode 100644 index 00000000000..1017051f94c --- /dev/null +++ b/homeassistant/components/sleepiq/light.py @@ -0,0 +1,59 @@ +"""Support for SleepIQ outlet lights.""" +import logging +from typing import Any + +from asyncsleepiq import SleepIQBed, SleepIQLight + +from homeassistant.components.light import LightEntity +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 + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the SleepIQ bed lights.""" + data: SleepIQData = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + SleepIQLightEntity(data.data_coordinator, bed, light) + for bed in data.client.beds.values() + for light in bed.foundation.lights + ) + + +class SleepIQLightEntity(SleepIQBedEntity, LightEntity): + """Representation of a light.""" + + def __init__( + self, coordinator: DataUpdateCoordinator, bed: SleepIQBed, light: SleepIQLight + ) -> None: + """Initialize the light.""" + self.light = light + super().__init__(coordinator, bed) + self._attr_name = f"SleepNumber {bed.name} Light {light.outlet_id}" + self._attr_unique_id = f"{bed.id}-light-{light.outlet_id}" + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on light.""" + await self.light.turn_on() + self._handle_coordinator_update() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off light.""" + await self.light.turn_off() + self._handle_coordinator_update() + + @callback + def _async_update_attrs(self) -> None: + """Update light attributes.""" + self._attr_is_on = self.light.is_on diff --git a/homeassistant/components/sleepiq/sensor.py b/homeassistant/components/sleepiq/sensor.py index 7d50876b1b2..cb9bf91cf8a 100644 --- a/homeassistant/components/sleepiq/sensor.py +++ b/homeassistant/components/sleepiq/sensor.py @@ -11,7 +11,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, SLEEP_NUMBER from .coordinator import SleepIQData -from .entity import SleepIQSensor +from .entity import SleepIQSleeperEntity async def async_setup_entry( @@ -28,7 +28,7 @@ async def async_setup_entry( ) -class SleepNumberSensorEntity(SleepIQSensor, SensorEntity): +class SleepNumberSensorEntity(SleepIQSleeperEntity, SensorEntity): """Representation of an SleepIQ Entity with CoordinatorEntity.""" _attr_icon = "mdi:bed" diff --git a/homeassistant/components/sleepiq/switch.py b/homeassistant/components/sleepiq/switch.py index c8977f0ce73..ebc0f720b43 100644 --- a/homeassistant/components/sleepiq/switch.py +++ b/homeassistant/components/sleepiq/switch.py @@ -7,12 +7,12 @@ from asyncsleepiq import SleepIQBed from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .coordinator import SleepIQData, SleepIQPauseUpdateCoordinator -from .entity import SleepIQBedCoordinator +from .entity import SleepIQBedEntity async def async_setup_entry( @@ -28,7 +28,7 @@ async def async_setup_entry( ) -class SleepNumberPrivateSwitch(SleepIQBedCoordinator, SwitchEntity): +class SleepNumberPrivateSwitch(SleepIQBedEntity, SwitchEntity): """Representation of SleepIQ privacy mode.""" def __init__( @@ -39,15 +39,17 @@ class SleepNumberPrivateSwitch(SleepIQBedCoordinator, SwitchEntity): self._attr_name = f"SleepNumber {bed.name} Pause Mode" self._attr_unique_id = f"{bed.id}-pause-mode" - @property - def is_on(self) -> bool: - """Return whether the switch is on or off.""" - return bool(self.bed.paused) - async def async_turn_on(self, **kwargs: Any) -> None: """Turn on switch.""" await self.bed.set_pause_mode(True) + self._handle_coordinator_update() async def async_turn_off(self, **kwargs: Any) -> None: """Turn off switch.""" await self.bed.set_pause_mode(False) + self._handle_coordinator_update() + + @callback + def _async_update_attrs(self) -> None: + """Update switch attributes.""" + self._attr_is_on = self.bed.paused diff --git a/tests/components/sleepiq/conftest.py b/tests/components/sleepiq/conftest.py index 9ecc1edd0b6..caf65a99b3d 100644 --- a/tests/components/sleepiq/conftest.py +++ b/tests/components/sleepiq/conftest.py @@ -3,7 +3,7 @@ from __future__ import annotations from unittest.mock import create_autospec, patch -from asyncsleepiq import SleepIQBed, SleepIQSleeper +from asyncsleepiq import SleepIQBed, SleepIQFoundation, SleepIQLight, SleepIQSleeper import pytest from homeassistant.components.sleepiq import DOMAIN @@ -54,6 +54,15 @@ def mock_asyncsleepiq(): sleeper_r.in_bed = False sleeper_r.sleep_number = 80 + 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] + yield client diff --git a/tests/components/sleepiq/test_config_flow.py b/tests/components/sleepiq/test_config_flow.py index bb6742821f6..3101a7ecdfe 100644 --- a/tests/components/sleepiq/test_config_flow.py +++ b/tests/components/sleepiq/test_config_flow.py @@ -118,6 +118,9 @@ async def test_reauth_password(hass): with patch( "homeassistant.components.sleepiq.config_flow.AsyncSleepIQ.login", return_value=True, + ), patch( + "homeassistant.components.sleepiq.async_setup_entry", + return_value=True, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/sleepiq/test_light.py b/tests/components/sleepiq/test_light.py new file mode 100644 index 00000000000..d7386cceb7b --- /dev/null +++ b/tests/components/sleepiq/test_light.py @@ -0,0 +1,73 @@ +"""The tests for SleepIQ light platform.""" +from homeassistant.components.light import DOMAIN +from homeassistant.components.sleepiq.coordinator import LONGER_UPDATE_INTERVAL +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.helpers import entity_registry as er +from homeassistant.util.dt import utcnow + +from tests.common import async_fire_time_changed +from tests.components.sleepiq.conftest import ( + BED_ID, + BED_NAME, + BED_NAME_LOWER, + setup_platform, +) + + +async def test_setup(hass, mock_asyncsleepiq): + """Test for successfully setting up the SleepIQ platform.""" + entry = await setup_platform(hass, DOMAIN) + entity_registry = er.async_get(hass) + + assert len(entity_registry.entities) == 2 + + entry = entity_registry.async_get(f"light.sleepnumber_{BED_NAME_LOWER}_light_1") + assert entry + assert entry.original_name == f"SleepNumber {BED_NAME} Light 1" + assert entry.unique_id == f"{BED_ID}-light-1" + + entry = entity_registry.async_get(f"light.sleepnumber_{BED_NAME_LOWER}_light_2") + assert entry + assert entry.original_name == f"SleepNumber {BED_NAME} Light 2" + assert entry.unique_id == f"{BED_ID}-light-2" + + +async def test_light_set_states(hass, mock_asyncsleepiq): + """Test light change.""" + await setup_platform(hass, DOMAIN) + + await hass.services.async_call( + DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: f"light.sleepnumber_{BED_NAME_LOWER}_light_1"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_asyncsleepiq.beds[BED_ID].foundation.lights[0].turn_on.assert_called_once() + + await hass.services.async_call( + DOMAIN, + "turn_off", + {ATTR_ENTITY_ID: f"light.sleepnumber_{BED_NAME_LOWER}_light_1"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_asyncsleepiq.beds[BED_ID].foundation.lights[0].turn_off.assert_called_once() + + +async def test_switch_get_states(hass, mock_asyncsleepiq): + """Test light update.""" + await setup_platform(hass, DOMAIN) + + assert ( + hass.states.get(f"light.sleepnumber_{BED_NAME_LOWER}_light_1").state + == STATE_OFF + ) + mock_asyncsleepiq.beds[BED_ID].foundation.lights[0].is_on = True + + async_fire_time_changed(hass, utcnow() + LONGER_UPDATE_INTERVAL) + await hass.async_block_till_done() + + assert ( + hass.states.get(f"light.sleepnumber_{BED_NAME_LOWER}_light_1").state == STATE_ON + )