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()