From d59dbbe859c660ed4fbd8444b70336b010e8f0a5 Mon Sep 17 00:00:00 2001 From: Keilin Bickar <TrumpetGod@gmail.com> Date: Sat, 19 Feb 2022 12:54:52 -0500 Subject: [PATCH] Create button entities for SleepIQ (#66849) --- homeassistant/components/sleepiq/__init__.py | 5 +- homeassistant/components/sleepiq/button.py | 81 ++++++++++++++++++++ homeassistant/components/sleepiq/entity.py | 28 +++++-- tests/components/sleepiq/conftest.py | 9 ++- tests/components/sleepiq/test_button.py | 61 +++++++++++++++ 5 files changed, 169 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/sleepiq/button.py create mode 100644 tests/components/sleepiq/test_button.py diff --git a/homeassistant/components/sleepiq/__init__.py b/homeassistant/components/sleepiq/__init__.py index 2fc0c52c706..26557ca6daf 100644 --- a/homeassistant/components/sleepiq/__init__.py +++ b/homeassistant/components/sleepiq/__init__.py @@ -22,7 +22,7 @@ from .coordinator import SleepIQDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR] CONFIG_SCHEMA = vol.Schema( { @@ -35,9 +35,6 @@ CONFIG_SCHEMA = vol.Schema( ) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] - - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up sleepiq component.""" if DOMAIN in config: diff --git a/homeassistant/components/sleepiq/button.py b/homeassistant/components/sleepiq/button.py new file mode 100644 index 00000000000..8cdc0398c2d --- /dev/null +++ b/homeassistant/components/sleepiq/button.py @@ -0,0 +1,81 @@ +"""Support for SleepIQ buttons.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from asyncsleepiq import SleepIQBed + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import SleepIQDataUpdateCoordinator +from .entity import SleepIQEntity + + +@dataclass +class SleepIQButtonEntityDescriptionMixin: + """Describes a SleepIQ Button entity.""" + + press_action: Callable[[SleepIQBed], Any] + + +@dataclass +class SleepIQButtonEntityDescription( + ButtonEntityDescription, SleepIQButtonEntityDescriptionMixin +): + """Class to describe a Button entity.""" + + +ENTITY_DESCRIPTIONS = [ + SleepIQButtonEntityDescription( + key="calibrate", + name="Calibrate", + press_action=lambda client: client.calibrate(), + icon="mdi:target", + ), + SleepIQButtonEntityDescription( + key="stop-pump", + name="Stop Pump", + press_action=lambda client: client.stop_pump(), + icon="mdi:stop", + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the sleep number buttons.""" + coordinator: SleepIQDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + SleepNumberButton(bed, ed) + for bed in coordinator.client.beds.values() + for ed in ENTITY_DESCRIPTIONS + ) + + +class SleepNumberButton(SleepIQEntity, ButtonEntity): + """Representation of an SleepIQ button.""" + + entity_description: SleepIQButtonEntityDescription + + def __init__( + self, bed: SleepIQBed, entity_description: SleepIQButtonEntityDescription + ) -> None: + """Initialize the Button.""" + super().__init__(bed) + self._attr_name = f"SleepNumber {bed.name} {entity_description.name}" + self._attr_unique_id = f"{bed.id}-{entity_description.key}" + self.entity_description = entity_description + + async def async_press(self) -> None: + """Press the button.""" + await self.entity_description.press_action(self.bed) diff --git a/homeassistant/components/sleepiq/entity.py b/homeassistant/components/sleepiq/entity.py index 78c2045bcb6..141b94fa72d 100644 --- a/homeassistant/components/sleepiq/entity.py +++ b/homeassistant/components/sleepiq/entity.py @@ -5,7 +5,7 @@ from asyncsleepiq import SleepIQBed, SleepIQSleeper from homeassistant.core import callback from homeassistant.helpers import device_registry -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -14,6 +14,20 @@ from homeassistant.helpers.update_coordinator import ( from .const import ICON_OCCUPIED, SENSOR_TYPES +class SleepIQEntity(Entity): + """Implementation of a SleepIQ entity.""" + + def __init__(self, bed: SleepIQBed) -> None: + """Initialize the SleepIQ entity.""" + self.bed = bed + self._attr_device_info = DeviceInfo( + connections={(device_registry.CONNECTION_NETWORK_MAC, bed.mac_addr)}, + manufacturer="SleepNumber", + name=bed.name, + model=bed.model, + ) + + class SleepIQSensor(CoordinatorEntity): """Implementation of a SleepIQ sensor.""" @@ -26,14 +40,10 @@ class SleepIQSensor(CoordinatorEntity): sleeper: SleepIQSleeper, name: str, ) -> None: - """Initialize the SleepIQ side entity.""" + """Initialize the SleepIQ sensor entity.""" super().__init__(coordinator) - self.bed = bed self.sleeper = sleeper - self._async_update_attrs() - - self._attr_name = f"SleepNumber {bed.name} {sleeper.name} {SENSOR_TYPES[name]}" - self._attr_unique_id = f"{bed.id}_{sleeper.name}_{name}" + self.bed = bed self._attr_device_info = DeviceInfo( connections={(device_registry.CONNECTION_NETWORK_MAC, bed.mac_addr)}, manufacturer="SleepNumber", @@ -41,6 +51,10 @@ class SleepIQSensor(CoordinatorEntity): model=bed.model, ) + 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 def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" diff --git a/tests/components/sleepiq/conftest.py b/tests/components/sleepiq/conftest.py index b694928a042..3669fd5a7fc 100644 --- a/tests/components/sleepiq/conftest.py +++ b/tests/components/sleepiq/conftest.py @@ -1,6 +1,7 @@ """Common methods for SleepIQ.""" -from unittest.mock import MagicMock, patch +from unittest.mock import create_autospec, patch +from asyncsleepiq import SleepIQBed, SleepIQSleeper import pytest from homeassistant.components.sleepiq import DOMAIN @@ -24,15 +25,15 @@ def mock_asyncsleepiq(): """Mock an AsyncSleepIQ object.""" with patch("homeassistant.components.sleepiq.AsyncSleepIQ", autospec=True) as mock: client = mock.return_value - bed = MagicMock() + 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 = MagicMock() - sleeper_r = MagicMock() + sleeper_l = create_autospec(SleepIQSleeper) + sleeper_r = create_autospec(SleepIQSleeper) bed.sleepers = [sleeper_l, sleeper_r] sleeper_l.side = "L" diff --git a/tests/components/sleepiq/test_button.py b/tests/components/sleepiq/test_button.py new file mode 100644 index 00000000000..cab3f36d73f --- /dev/null +++ b/tests/components/sleepiq/test_button.py @@ -0,0 +1,61 @@ +"""The tests for SleepIQ binary sensor platform.""" +from homeassistant.components.button import DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME +from homeassistant.helpers import entity_registry as er + +from tests.components.sleepiq.conftest import ( + BED_ID, + BED_NAME, + BED_NAME_LOWER, + setup_platform, +) + + +async def test_button_calibrate(hass, mock_asyncsleepiq): + """Test the SleepIQ calibrate button.""" + await setup_platform(hass, DOMAIN) + entity_registry = er.async_get(hass) + + state = hass.states.get(f"button.sleepnumber_{BED_NAME_LOWER}_calibrate") + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == f"SleepNumber {BED_NAME} Calibrate" + ) + + entity = entity_registry.async_get(f"button.sleepnumber_{BED_NAME_LOWER}_calibrate") + assert entity + assert entity.unique_id == f"{BED_ID}-calibrate" + + await hass.services.async_call( + DOMAIN, + "press", + {ATTR_ENTITY_ID: f"button.sleepnumber_{BED_NAME_LOWER}_calibrate"}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_asyncsleepiq.beds[BED_ID].calibrate.assert_called_once() + + +async def test_button_stop_pump(hass, mock_asyncsleepiq): + """Test the SleepIQ stop pump button.""" + await setup_platform(hass, DOMAIN) + entity_registry = er.async_get(hass) + + state = hass.states.get(f"button.sleepnumber_{BED_NAME_LOWER}_stop_pump") + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == f"SleepNumber {BED_NAME} Stop Pump" + ) + + entity = entity_registry.async_get(f"button.sleepnumber_{BED_NAME_LOWER}_stop_pump") + assert entity + assert entity.unique_id == f"{BED_ID}-stop-pump" + + await hass.services.async_call( + DOMAIN, + "press", + {ATTR_ENTITY_ID: f"button.sleepnumber_{BED_NAME_LOWER}_stop_pump"}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_asyncsleepiq.beds[BED_ID].stop_pump.assert_called_once()