diff --git a/CODEOWNERS b/CODEOWNERS index e27c9488f0a..dabc32c0e11 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -850,8 +850,8 @@ homeassistant/components/sisyphus/* @jkeljo homeassistant/components/sky_hub/* @rogerselwyn homeassistant/components/slack/* @bachya tests/components/slack/* @bachya -homeassistant/components/sleepiq/* @mfugate1 -tests/components/sleepiq/* @mfugate1 +homeassistant/components/sleepiq/* @mfugate1 @kbickar +tests/components/sleepiq/* @mfugate1 @kbickar homeassistant/components/slide/* @ualex73 homeassistant/components/sma/* @kellerza @rklomp tests/components/sma/* @kellerza @rklomp diff --git a/homeassistant/components/sleepiq/__init__.py b/homeassistant/components/sleepiq/__init__.py index 5a69cfacd11..2fc0c52c706 100644 --- a/homeassistant/components/sleepiq/__init__.py +++ b/homeassistant/components/sleepiq/__init__.py @@ -1,12 +1,19 @@ """Support for SleepIQ from SleepNumber.""" import logging -from sleepyq import Sleepyq +from asyncsleepiq import ( + AsyncSleepIQ, + SleepIQAPIException, + SleepIQLoginException, + SleepIQTimeoutException, +) import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -15,6 +22,8 @@ from .coordinator import SleepIQDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] + CONFIG_SCHEMA = vol.Schema( { DOMAIN: { @@ -43,18 +52,34 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the SleepIQ config entry.""" - client = Sleepyq(entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD]) - try: - await hass.async_add_executor_job(client.login) - except ValueError: - _LOGGER.error("SleepIQ login failed, double check your username and password") - return False + conf = entry.data + email = conf[CONF_USERNAME] + password = conf[CONF_PASSWORD] - coordinator = SleepIQDataUpdateCoordinator( - hass, - client=client, - username=entry.data[CONF_USERNAME], - ) + client_session = async_get_clientsession(hass) + + gateway = AsyncSleepIQ(client_session=client_session) + + try: + await gateway.login(email, password) + except SleepIQLoginException: + _LOGGER.error("Could not authenticate with SleepIQ server") + return False + except SleepIQTimeoutException as err: + raise ConfigEntryNotReady( + str(err) or "Timed out during authentication" + ) from err + + try: + await gateway.init_beds() + except SleepIQTimeoutException as err: + raise ConfigEntryNotReady( + str(err) or "Timed out during initialization" + ) from err + except SleepIQAPIException as err: + raise ConfigEntryNotReady(str(err) or "Error reading from SleepIQ API") from err + + coordinator = SleepIQDataUpdateCoordinator(hass, gateway, email) # Call the SleepIQ API to refresh data await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/sleepiq/binary_sensor.py b/homeassistant/components/sleepiq/binary_sensor.py index 890cd6711a6..d2aeae06e8a 100644 --- a/homeassistant/components/sleepiq/binary_sensor.py +++ b/homeassistant/components/sleepiq/binary_sensor.py @@ -1,4 +1,6 @@ """Support for SleepIQ sensors.""" +from asyncsleepiq import SleepIQBed, SleepIQSleeper + from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -6,8 +8,9 @@ from homeassistant.components.binary_sensor import ( 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 BED, DOMAIN, ICON_EMPTY, ICON_OCCUPIED, IS_IN_BED, SIDES +from .const import DOMAIN, ICON_EMPTY, ICON_OCCUPIED, IS_IN_BED from .coordinator import SleepIQDataUpdateCoordinator from .entity import SleepIQSensor @@ -20,10 +23,9 @@ async def async_setup_entry( """Set up the SleepIQ bed binary sensors.""" coordinator: SleepIQDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( - IsInBedBinarySensor(coordinator, bed_id, side) - for side in SIDES - for bed_id in coordinator.data - if getattr(coordinator.data[bed_id][BED], side) is not None + IsInBedBinarySensor(coordinator, bed, sleeper) + for bed in coordinator.client.beds.values() + for sleeper in bed.sleepers ) @@ -34,16 +36,15 @@ class IsInBedBinarySensor(SleepIQSensor, BinarySensorEntity): def __init__( self, - coordinator: SleepIQDataUpdateCoordinator, - bed_id: str, - side: str, + coordinator: DataUpdateCoordinator, + bed: SleepIQBed, + sleeper: SleepIQSleeper, ) -> None: - """Initialize the SleepIQ bed side binary sensor.""" - super().__init__(coordinator, bed_id, side, IS_IN_BED) + """Initialize the sensor.""" + super().__init__(coordinator, bed, sleeper, IS_IN_BED) @callback def _async_update_attrs(self) -> None: """Update sensor attributes.""" - super()._async_update_attrs() - self._attr_is_on = getattr(self.side_data, IS_IN_BED) - self._attr_icon = ICON_OCCUPIED if self.is_on else ICON_EMPTY + self._attr_is_on = self.sleeper.in_bed + self._attr_icon = ICON_OCCUPIED if self.sleeper.in_bed else ICON_EMPTY diff --git a/homeassistant/components/sleepiq/config_flow.py b/homeassistant/components/sleepiq/config_flow.py index aff4d7e8dc7..dffb30f39d7 100644 --- a/homeassistant/components/sleepiq/config_flow.py +++ b/homeassistant/components/sleepiq/config_flow.py @@ -3,14 +3,16 @@ from __future__ import annotations from typing import Any -from sleepyq import Sleepyq +from asyncsleepiq import AsyncSleepIQ, SleepIQLoginException, SleepIQTimeoutException import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN, SLEEPYQ_INVALID_CREDENTIALS_MESSAGE +from .const import DOMAIN class SleepIQFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -41,19 +43,17 @@ class SleepIQFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(user_input[CONF_USERNAME].lower()) self._abort_if_unique_id_configured() - login_error = await self.hass.async_add_executor_job( - try_connection, user_input - ) - if not login_error: + try: + await try_connection(self.hass, user_input) + except SleepIQLoginException: + errors["base"] = "invalid_auth" + except SleepIQTimeoutException: + errors["base"] = "cannot_connect" + else: return self.async_create_entry( title=user_input[CONF_USERNAME], data=user_input ) - if SLEEPYQ_INVALID_CREDENTIALS_MESSAGE in login_error: - errors["base"] = "invalid_auth" - else: - errors["base"] = "cannot_connect" - return self.async_show_form( step_id="user", data_schema=vol.Schema( @@ -72,14 +72,10 @@ class SleepIQFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -def try_connection(user_input: dict[str, Any]) -> str: +async def try_connection(hass: HomeAssistant, user_input: dict[str, Any]) -> None: """Test if the given credentials can successfully login to SleepIQ.""" - client = Sleepyq(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) + client_session = async_get_clientsession(hass) - try: - client.login() - except ValueError as error: - return str(error) - - return "" + gateway = AsyncSleepIQ(client_session=client_session) + await gateway.login(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) diff --git a/homeassistant/components/sleepiq/const.py b/homeassistant/components/sleepiq/const.py index 3fc0ae999fd..63e86270925 100644 --- a/homeassistant/components/sleepiq/const.py +++ b/homeassistant/components/sleepiq/const.py @@ -14,3 +14,6 @@ SENSOR_TYPES = {SLEEP_NUMBER: "SleepNumber", IS_IN_BED: "Is In Bed"} LEFT = "left" RIGHT = "right" SIDES = [LEFT, RIGHT] + +SLEEPIQ_DATA = "sleepiq_data" +SLEEPIQ_STATUS_COORDINATOR = "sleepiq_status" diff --git a/homeassistant/components/sleepiq/coordinator.py b/homeassistant/components/sleepiq/coordinator.py index 467238e907e..ca664f99426 100644 --- a/homeassistant/components/sleepiq/coordinator.py +++ b/homeassistant/components/sleepiq/coordinator.py @@ -2,13 +2,11 @@ from datetime import timedelta import logging -from sleepyq import Sleepyq +from asyncsleepiq import AsyncSleepIQ from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import BED - _LOGGER = logging.getLogger(__name__) UPDATE_INTERVAL = timedelta(seconds=60) @@ -20,21 +18,15 @@ class SleepIQDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): def __init__( self, hass: HomeAssistant, - *, - client: Sleepyq, + client: AsyncSleepIQ, username: str, ) -> None: """Initialize coordinator.""" super().__init__( - hass, _LOGGER, name=f"{username}@SleepIQ", update_interval=UPDATE_INTERVAL + hass, + _LOGGER, + name=f"{username}@SleepIQ", + update_method=client.fetch_bed_statuses, + update_interval=UPDATE_INTERVAL, ) self.client = client - - async def _async_update_data(self) -> dict[str, dict]: - return await self.hass.async_add_executor_job(self.update_data) - - def update_data(self) -> dict[str, dict]: - """Get latest data from the client.""" - return { - bed.bed_id: {BED: bed} for bed in self.client.beds_with_sleeper_status() - } diff --git a/homeassistant/components/sleepiq/entity.py b/homeassistant/components/sleepiq/entity.py index 350435573f1..42458472057 100644 --- a/homeassistant/components/sleepiq/entity.py +++ b/homeassistant/components/sleepiq/entity.py @@ -1,9 +1,15 @@ """Entity for the SleepIQ integration.""" -from homeassistant.core import callback -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from abc import abstractmethod -from .const import BED, ICON_OCCUPIED, SENSOR_TYPES -from .coordinator import SleepIQDataUpdateCoordinator +from asyncsleepiq import SleepIQBed, SleepIQSleeper + +from homeassistant.core import callback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import ICON_OCCUPIED, SENSOR_TYPES class SleepIQSensor(CoordinatorEntity): @@ -13,22 +19,19 @@ class SleepIQSensor(CoordinatorEntity): def __init__( self, - coordinator: SleepIQDataUpdateCoordinator, - bed_id: str, - side: str, + coordinator: DataUpdateCoordinator, + bed: SleepIQBed, + sleeper: SleepIQSleeper, name: str, ) -> None: """Initialize the SleepIQ side entity.""" super().__init__(coordinator) - self.bed_id = bed_id - self.side = side - + self.bed = bed + self.sleeper = sleeper self._async_update_attrs() - self._attr_name = f"SleepNumber {self.bed_data.name} {self.side_data.sleeper.first_name} {SENSOR_TYPES[name]}" - self._attr_unique_id = ( - f"{self.bed_id}_{self.side_data.sleeper.first_name}_{name}" - ) + self._attr_name = f"SleepNumber {bed.name} {sleeper.name} {SENSOR_TYPES[name]}" + self._attr_unique_id = f"{bed.id}_{sleeper.name}_{name}" @callback def _handle_coordinator_update(self) -> None: @@ -37,7 +40,6 @@ class SleepIQSensor(CoordinatorEntity): super()._handle_coordinator_update() @callback + @abstractmethod def _async_update_attrs(self) -> None: """Update sensor attributes.""" - self.bed_data = self.coordinator.data[self.bed_id][BED] - self.side_data = getattr(self.bed_data, self.side) diff --git a/homeassistant/components/sleepiq/manifest.json b/homeassistant/components/sleepiq/manifest.json index a516bd7545b..48ada7b14a2 100644 --- a/homeassistant/components/sleepiq/manifest.json +++ b/homeassistant/components/sleepiq/manifest.json @@ -3,11 +3,13 @@ "name": "SleepIQ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sleepiq", - "requirements": ["sleepyq==0.8.1"], - "codeowners": ["@mfugate1"], + "requirements": ["asyncsleepiq==1.0.0"], + "codeowners": ["@mfugate1", "@kbickar"], "dhcp": [ - {"macaddress": "64DBA0*"} + { + "macaddress": "64DBA0*" + } ], "iot_class": "cloud_polling", - "loggers": ["sleepyq"] + "loggers": ["asyncsleepiq"] } diff --git a/homeassistant/components/sleepiq/sensor.py b/homeassistant/components/sleepiq/sensor.py index 52ded76762d..dd7fdabcfb3 100644 --- a/homeassistant/components/sleepiq/sensor.py +++ b/homeassistant/components/sleepiq/sensor.py @@ -1,10 +1,15 @@ -"""Support for SleepIQ sensors.""" +"""Support for SleepIQ Sensor.""" +from __future__ import annotations + +from asyncsleepiq import SleepIQBed, SleepIQSleeper + from homeassistant.components.sensor import SensorEntity 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 BED, DOMAIN, SIDES, SLEEP_NUMBER +from .const import DOMAIN, SLEEP_NUMBER from .coordinator import SleepIQDataUpdateCoordinator from .entity import SleepIQSensor @@ -17,27 +22,27 @@ async def async_setup_entry( """Set up the SleepIQ bed sensors.""" coordinator: SleepIQDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( - SleepNumberSensor(coordinator, bed_id, side) - for side in SIDES - for bed_id in coordinator.data - if getattr(coordinator.data[bed_id][BED], side) is not None + SleepNumberSensorEntity(coordinator, bed, sleeper) + for bed in coordinator.client.beds.values() + for sleeper in bed.sleepers ) -class SleepNumberSensor(SleepIQSensor, SensorEntity): - """Implementation of a SleepIQ sensor.""" +class SleepNumberSensorEntity(SleepIQSensor, SensorEntity): + """Representation of an SleepIQ Entity with CoordinatorEntity.""" + + _attr_icon = "mdi:bed" def __init__( self, - coordinator: SleepIQDataUpdateCoordinator, - bed_id: str, - side: str, + coordinator: DataUpdateCoordinator, + bed: SleepIQBed, + sleeper: SleepIQSleeper, ) -> None: - """Initialize the SleepIQ sleep number sensor.""" - super().__init__(coordinator, bed_id, side, SLEEP_NUMBER) + """Initialize the sensor.""" + super().__init__(coordinator, bed, sleeper, SLEEP_NUMBER) @callback def _async_update_attrs(self) -> None: """Update sensor attributes.""" - super()._async_update_attrs() - self._attr_native_value = self.side_data.sleep_number + self._attr_native_value = self.sleeper.sleep_number diff --git a/requirements_all.txt b/requirements_all.txt index 33aed1f238c..9f41bd500eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -349,6 +349,9 @@ async-upnp-client==0.23.5 # homeassistant.components.supla asyncpysupla==0.0.5 +# homeassistant.components.sleepiq +asyncsleepiq==1.0.0 + # homeassistant.components.aten_pe atenpdu==0.3.2 @@ -2201,9 +2204,6 @@ skybellpy==0.6.3 # homeassistant.components.slack slackclient==2.5.0 -# homeassistant.components.sleepiq -sleepyq==0.8.1 - # homeassistant.components.xmpp slixmpp==1.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8dad410b49c..4a93ab379f6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -254,6 +254,9 @@ arcam-fmj==0.12.0 # homeassistant.components.yeelight async-upnp-client==0.23.5 +# homeassistant.components.sleepiq +asyncsleepiq==1.0.0 + # homeassistant.components.aurora auroranoaa==0.0.2 @@ -1354,9 +1357,6 @@ simplisafe-python==2022.02.1 # homeassistant.components.slack slackclient==2.5.0 -# homeassistant.components.sleepiq -sleepyq==0.8.1 - # homeassistant.components.smart_meter_texas smart-meter-texas==0.4.7 diff --git a/tests/components/sleepiq/conftest.py b/tests/components/sleepiq/conftest.py index 707fc436c15..b694928a042 100644 --- a/tests/components/sleepiq/conftest.py +++ b/tests/components/sleepiq/conftest.py @@ -1,75 +1,67 @@ -"""Common fixtures for sleepiq tests.""" -import json -from unittest.mock import patch +"""Common methods for SleepIQ.""" +from unittest.mock import MagicMock, patch import pytest -from sleepyq import Bed, FamilyStatus, Sleeper -from homeassistant.components.sleepiq.const import DOMAIN +from homeassistant.components.sleepiq import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, load_fixture +from tests.common import MockConfigEntry - -def mock_beds(account_type): - """Mock sleepnumber bed data.""" - return [ - Bed(bed) - for bed in json.loads(load_fixture(f"bed{account_type}.json", "sleepiq"))[ - "beds" - ] - ] - - -def mock_sleepers(): - """Mock sleeper data.""" - return [ - Sleeper(sleeper) - for sleeper in json.loads(load_fixture("sleeper.json", "sleepiq"))["sleepers"] - ] - - -def mock_bed_family_status(account_type): - """Mock family status data.""" - return [ - FamilyStatus(status) - for status in json.loads( - load_fixture(f"familystatus{account_type}.json", "sleepiq") - )["beds"] - ] +BED_ID = "123456" +BED_NAME = "Test Bed" +BED_NAME_LOWER = BED_NAME.lower().replace(" ", "_") +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(" ", "_") @pytest.fixture -def config_data(): - """Provide configuration data for tests.""" - return { - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - } +def mock_asyncsleepiq(): + """Mock an AsyncSleepIQ object.""" + with patch("homeassistant.components.sleepiq.AsyncSleepIQ", autospec=True) as mock: + client = mock.return_value + bed = MagicMock() + 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() + 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_r.side = "R" + sleeper_r.name = SLEEPER_R_NAME + sleeper_r.in_bed = False + sleeper_r.sleep_number = 80 + + yield client -@pytest.fixture -def config_entry(config_data): - """Create a mock config entry.""" - return MockConfigEntry( +async def setup_platform(hass: HomeAssistant, platform) -> MockConfigEntry: + """Set up the SleepIQ platform.""" + mock_entry = MockConfigEntry( domain=DOMAIN, - data=config_data, - options={}, + data={ + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + }, ) + mock_entry.add_to_hass(hass) - -@pytest.fixture(params=["-single", ""]) -async def setup_entry(hass, request, config_entry): - """Initialize the config entry.""" - with patch("sleepyq.Sleepyq.beds", return_value=mock_beds(request.param)), patch( - "sleepyq.Sleepyq.sleepers", return_value=mock_sleepers() - ), patch( - "sleepyq.Sleepyq.bed_family_status", - return_value=mock_bed_family_status(request.param), - ), patch( - "sleepyq.Sleepyq.login" - ): - config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) + if platform: + with patch("homeassistant.components.sleepiq.PLATFORMS", [platform]): + assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() - return {"account_type": request.param, "mock_entry": config_entry} + + return mock_entry diff --git a/tests/components/sleepiq/fixtures/bed-single.json b/tests/components/sleepiq/fixtures/bed-single.json deleted file mode 100644 index f1e59f5ad2d..00000000000 --- a/tests/components/sleepiq/fixtures/bed-single.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "beds" : [ - { - "dualSleep" : false, - "base" : "FlexFit", - "sku" : "AILE", - "model" : "ILE", - "size" : "KING", - "isKidsBed" : false, - "sleeperRightId" : "-80", - "accountId" : "-32", - "bedId" : "-31", - "registrationDate" : "2016-07-22T14:00:58Z", - "serial" : null, - "reference" : "95000794555-1", - "macAddress" : "CD13A384BA51", - "version" : null, - "purchaseDate" : "2016-06-22T00:00:00Z", - "sleeperLeftId" : "0", - "zipcode" : "12345", - "returnRequestStatus" : 0, - "name" : "ILE", - "status" : 1, - "timezone" : "US/Eastern" - } - ] - } \ No newline at end of file diff --git a/tests/components/sleepiq/fixtures/bed.json b/tests/components/sleepiq/fixtures/bed.json deleted file mode 100644 index 5fb12da0507..00000000000 --- a/tests/components/sleepiq/fixtures/bed.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "beds" : [ - { - "dualSleep" : true, - "base" : "FlexFit", - "sku" : "AILE", - "model" : "ILE", - "size" : "KING", - "isKidsBed" : false, - "sleeperRightId" : "-80", - "accountId" : "-32", - "bedId" : "-31", - "registrationDate" : "2016-07-22T14:00:58Z", - "serial" : null, - "reference" : "95000794555-1", - "macAddress" : "CD13A384BA51", - "version" : null, - "purchaseDate" : "2016-06-22T00:00:00Z", - "sleeperLeftId" : "-92", - "zipcode" : "12345", - "returnRequestStatus" : 0, - "name" : "ILE", - "status" : 1, - "timezone" : "US/Eastern" - } - ] - } diff --git a/tests/components/sleepiq/fixtures/familystatus-single.json b/tests/components/sleepiq/fixtures/familystatus-single.json deleted file mode 100644 index 1d5c0d89943..00000000000 --- a/tests/components/sleepiq/fixtures/familystatus-single.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "beds" : [ - { - "bedId" : "-31", - "rightSide" : { - "alertId" : 0, - "lastLink" : "00:00:00", - "isInBed" : true, - "sleepNumber" : 40, - "alertDetailedMessage" : "No Alert", - "pressure" : -16 - }, - "status" : 1, - "leftSide" : null - } - ] - } \ No newline at end of file diff --git a/tests/components/sleepiq/fixtures/familystatus.json b/tests/components/sleepiq/fixtures/familystatus.json deleted file mode 100644 index c9b60824115..00000000000 --- a/tests/components/sleepiq/fixtures/familystatus.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "beds" : [ - { - "bedId" : "-31", - "rightSide" : { - "alertId" : 0, - "lastLink" : "00:00:00", - "isInBed" : true, - "sleepNumber" : 40, - "alertDetailedMessage" : "No Alert", - "pressure" : -16 - }, - "status" : 1, - "leftSide" : { - "alertId" : 0, - "lastLink" : "00:00:00", - "sleepNumber" : 80, - "alertDetailedMessage" : "No Alert", - "isInBed" : false, - "pressure" : 2191 - } - } - ] - } \ No newline at end of file diff --git a/tests/components/sleepiq/fixtures/login.json b/tests/components/sleepiq/fixtures/login.json deleted file mode 100644 index a665db7de29..00000000000 --- a/tests/components/sleepiq/fixtures/login.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "edpLoginStatus" : 200, - "userId" : "-42", - "registrationState" : 13, - "key" : "0987", - "edpLoginMessage" : "not used" - } \ No newline at end of file diff --git a/tests/components/sleepiq/fixtures/sleeper.json b/tests/components/sleepiq/fixtures/sleeper.json deleted file mode 100644 index c009e684220..00000000000 --- a/tests/components/sleepiq/fixtures/sleeper.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "sleepers" : [ - { - "timezone" : "US/Eastern", - "firstName" : "Test1", - "weight" : 150, - "birthMonth" : 12, - "birthYear" : "1990", - "active" : true, - "lastLogin" : "2016-08-26 21:43:27 CDT", - "side" : 1, - "accountId" : "-32", - "height" : 60, - "bedId" : "-31", - "username" : "test1@example.com", - "sleeperId" : "-80", - "avatar" : "", - "emailValidated" : true, - "licenseVersion" : 6, - "duration" : null, - "email" : "test1@example.com", - "isAccountOwner" : true, - "sleepGoal" : 480, - "zipCode" : "12345", - "isChild" : false, - "isMale" : true - }, - { - "email" : "test2@example.com", - "duration" : null, - "emailValidated" : true, - "licenseVersion" : 5, - "isChild" : false, - "isMale" : false, - "zipCode" : "12345", - "isAccountOwner" : false, - "sleepGoal" : 480, - "side" : 0, - "lastLogin" : "2016-07-17 15:37:30 CDT", - "birthMonth" : 1, - "birthYear" : "1991", - "active" : true, - "weight" : 151, - "firstName" : "Test2", - "timezone" : "US/Eastern", - "avatar" : "", - "username" : "test2@example.com", - "sleeperId" : "-92", - "bedId" : "-31", - "height" : 65, - "accountId" : "-32" - } - ] - } diff --git a/tests/components/sleepiq/test_binary_sensor.py b/tests/components/sleepiq/test_binary_sensor.py index ca9bf3c84fc..2b265e19626 100644 --- a/tests/components/sleepiq/test_binary_sensor.py +++ b/tests/components/sleepiq/test_binary_sensor.py @@ -1,34 +1,61 @@ """The tests for SleepIQ binary sensor platform.""" -from homeassistant.components.binary_sensor import BinarySensorDeviceClass -from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, ATTR_ICON +from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDeviceClass +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + ATTR_ICON, + STATE_OFF, + STATE_ON, +) from homeassistant.helpers import entity_registry as er +from tests.components.sleepiq.conftest import ( + BED_ID, + BED_NAME, + BED_NAME_LOWER, + SLEEPER_L_NAME, + SLEEPER_L_NAME_LOWER, + SLEEPER_R_NAME, + SLEEPER_R_NAME_LOWER, + setup_platform, +) -async def test_binary_sensors(hass, setup_entry): + +async def test_binary_sensors(hass, mock_asyncsleepiq): """Test the SleepIQ binary sensors.""" + await setup_platform(hass, DOMAIN) entity_registry = er.async_get(hass) - state = hass.states.get("binary_sensor.sleepnumber_ile_test1_is_in_bed") - assert state.state == "on" + state = hass.states.get( + f"binary_sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_L_NAME_LOWER}_is_in_bed" + ) + assert state.state == STATE_ON assert state.attributes.get(ATTR_ICON) == "mdi:bed" assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.OCCUPANCY - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "SleepNumber ILE Test1 Is In Bed" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == f"SleepNumber {BED_NAME} {SLEEPER_L_NAME} Is In Bed" + ) - entry = entity_registry.async_get("binary_sensor.sleepnumber_ile_test1_is_in_bed") - assert entry - assert entry.unique_id == "-31_Test1_is_in_bed" + entity = entity_registry.async_get( + f"binary_sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_L_NAME_LOWER}_is_in_bed" + ) + assert entity + assert entity.unique_id == f"{BED_ID}_{SLEEPER_L_NAME}_is_in_bed" - # If account type is set, only a single bed account was created and there will - # not be a second entity - if setup_entry["account_type"]: - return - - entry = entity_registry.async_get("binary_sensor.sleepnumber_ile_test2_is_in_bed") - assert entry - assert entry.unique_id == "-31_Test2_is_in_bed" - - state = hass.states.get("binary_sensor.sleepnumber_ile_test2_is_in_bed") - assert state.state == "off" + state = hass.states.get( + f"binary_sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_is_in_bed" + ) + assert state.state == STATE_OFF assert state.attributes.get(ATTR_ICON) == "mdi:bed-empty" assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.OCCUPANCY - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "SleepNumber ILE Test2 Is In Bed" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == f"SleepNumber {BED_NAME} {SLEEPER_R_NAME} Is In Bed" + ) + + entity = entity_registry.async_get( + f"binary_sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_is_in_bed" + ) + assert entity + assert entity.unique_id == f"{BED_ID}_{SLEEPER_R_NAME}_is_in_bed" diff --git a/tests/components/sleepiq/test_config_flow.py b/tests/components/sleepiq/test_config_flow.py index e4a422c888f..b2554ea968e 100644 --- a/tests/components/sleepiq/test_config_flow.py +++ b/tests/components/sleepiq/test_config_flow.py @@ -1,11 +1,10 @@ """Tests for the SleepIQ config flow.""" from unittest.mock import patch +from asyncsleepiq import SleepIQLoginException, SleepIQTimeoutException + from homeassistant import config_entries, data_entry_flow, setup -from homeassistant.components.sleepiq.const import ( - DOMAIN, - SLEEPYQ_INVALID_CREDENTIALS_MESSAGE, -) +from homeassistant.components.sleepiq.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant @@ -17,7 +16,7 @@ SLEEPIQ_CONFIG = { async def test_import(hass: HomeAssistant) -> None: """Test that we can import a config entry.""" - with patch("sleepyq.Sleepyq.login"): + with patch("asyncsleepiq.AsyncSleepIQ.login"): assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: SLEEPIQ_CONFIG}) await hass.async_block_till_done() @@ -29,7 +28,7 @@ async def test_import(hass: HomeAssistant) -> None: async def test_show_set_form(hass: HomeAssistant) -> None: """Test that the setup form is served.""" - with patch("sleepyq.Sleepyq.login"): + with patch("asyncsleepiq.AsyncSleepIQ.login"): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) @@ -41,8 +40,8 @@ async def test_show_set_form(hass: HomeAssistant) -> None: async def test_login_invalid_auth(hass: HomeAssistant) -> None: """Test we show user form with appropriate error on login failure.""" with patch( - "sleepyq.Sleepyq.login", - side_effect=ValueError(SLEEPYQ_INVALID_CREDENTIALS_MESSAGE), + "asyncsleepiq.AsyncSleepIQ.login", + side_effect=SleepIQLoginException, ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=SLEEPIQ_CONFIG @@ -56,8 +55,8 @@ async def test_login_invalid_auth(hass: HomeAssistant) -> None: async def test_login_cannot_connect(hass: HomeAssistant) -> None: """Test we show user form with appropriate error on login failure.""" with patch( - "sleepyq.Sleepyq.login", - side_effect=ValueError("Unexpected response code"), + "asyncsleepiq.AsyncSleepIQ.login", + side_effect=SleepIQTimeoutException, ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=SLEEPIQ_CONFIG @@ -70,11 +69,23 @@ async def test_login_cannot_connect(hass: HomeAssistant) -> None: async def test_success(hass: HomeAssistant) -> None: """Test successful flow provides entry creation data.""" - with patch("sleepyq.Sleepyq.login"): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=SLEEPIQ_CONFIG - ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"][CONF_USERNAME] == SLEEPIQ_CONFIG[CONF_USERNAME] - assert result["data"][CONF_PASSWORD] == SLEEPIQ_CONFIG[CONF_PASSWORD] + with patch("asyncsleepiq.AsyncSleepIQ.login", return_value=True), patch( + "homeassistant.components.sleepiq.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], SLEEPIQ_CONFIG + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["data"][CONF_USERNAME] == SLEEPIQ_CONFIG[CONF_USERNAME] + assert result2["data"][CONF_PASSWORD] == SLEEPIQ_CONFIG[CONF_PASSWORD] + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/sleepiq/test_init.py b/tests/components/sleepiq/test_init.py index 15af03e14ce..0aed23c4c50 100644 --- a/tests/components/sleepiq/test_init.py +++ b/tests/components/sleepiq/test_init.py @@ -1,5 +1,9 @@ """Tests for the SleepIQ integration.""" -from unittest.mock import patch +from asyncsleepiq import ( + SleepIQAPIException, + SleepIQLoginException, + SleepIQTimeoutException, +) from homeassistant.components.sleepiq.const import DOMAIN from homeassistant.components.sleepiq.coordinator import UPDATE_INTERVAL @@ -8,16 +12,12 @@ from homeassistant.core import HomeAssistant from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed -from tests.components.sleepiq.conftest import ( - mock_bed_family_status, - mock_beds, - mock_sleepers, -) +from tests.components.sleepiq.conftest import setup_platform -async def test_unload_entry(hass: HomeAssistant, setup_entry) -> None: +async def test_unload_entry(hass: HomeAssistant, mock_asyncsleepiq) -> None: """Test unloading the SleepIQ entry.""" - entry = setup_entry["mock_entry"] + entry = await setup_platform(hass, "sensor") assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() @@ -25,30 +25,42 @@ async def test_unload_entry(hass: HomeAssistant, setup_entry) -> None: assert not hass.data.get(DOMAIN) -async def test_entry_setup_login_error(hass: HomeAssistant, config_entry) -> None: - """Test when sleepyq client is unable to login.""" - with patch("sleepyq.Sleepyq.login", side_effect=ValueError): - config_entry.add_to_hass(hass) - assert not await hass.config_entries.async_setup(config_entry.entry_id) +async def test_entry_setup_login_error(hass: HomeAssistant, mock_asyncsleepiq) -> None: + """Test when sleepiq client is unable to login.""" + mock_asyncsleepiq.login.side_effect = SleepIQLoginException + entry = await setup_platform(hass, None) + assert not await hass.config_entries.async_setup(entry.entry_id) -async def test_update_interval(hass: HomeAssistant, setup_entry) -> None: +async def test_entry_setup_timeout_error( + hass: HomeAssistant, mock_asyncsleepiq +) -> None: + """Test when sleepiq client timeout.""" + mock_asyncsleepiq.login.side_effect = SleepIQTimeoutException + entry = await setup_platform(hass, None) + assert not await hass.config_entries.async_setup(entry.entry_id) + + +async def test_update_interval(hass: HomeAssistant, mock_asyncsleepiq) -> None: """Test update interval.""" - with patch("sleepyq.Sleepyq.beds", return_value=mock_beds("")) as beds, patch( - "sleepyq.Sleepyq.sleepers", return_value=mock_sleepers() - ) as sleepers, patch( - "sleepyq.Sleepyq.bed_family_status", - return_value=mock_bed_family_status(""), - ) as bed_family_status, patch( - "sleepyq.Sleepyq.login", return_value=True - ): - assert beds.call_count == 0 - assert sleepers.call_count == 0 - assert bed_family_status.call_count == 0 + await setup_platform(hass, "sensor") + assert mock_asyncsleepiq.fetch_bed_statuses.call_count == 1 - async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL) - await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL) + await hass.async_block_till_done() - assert beds.call_count == 1 - assert sleepers.call_count == 1 - assert bed_family_status.call_count == 1 + assert mock_asyncsleepiq.fetch_bed_statuses.call_count == 2 + + +async def test_api_error(hass: HomeAssistant, mock_asyncsleepiq) -> None: + """Test when sleepiq client is unable to login.""" + mock_asyncsleepiq.init_beds.side_effect = SleepIQAPIException + entry = await setup_platform(hass, None) + assert not await hass.config_entries.async_setup(entry.entry_id) + + +async def test_api_timeout(hass: HomeAssistant, mock_asyncsleepiq) -> None: + """Test when sleepiq client timeout.""" + mock_asyncsleepiq.init_beds.side_effect = SleepIQTimeoutException + entry = await setup_platform(hass, None) + assert not await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/sleepiq/test_sensor.py b/tests/components/sleepiq/test_sensor.py index baa8732365b..26ddc9aa485 100644 --- a/tests/components/sleepiq/test_sensor.py +++ b/tests/components/sleepiq/test_sensor.py @@ -1,35 +1,53 @@ """The tests for SleepIQ sensor platform.""" +from homeassistant.components.sensor import DOMAIN from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_ICON from homeassistant.helpers import entity_registry as er +from tests.components.sleepiq.conftest import ( + BED_ID, + BED_NAME, + BED_NAME_LOWER, + SLEEPER_L_NAME, + SLEEPER_L_NAME_LOWER, + SLEEPER_R_NAME, + SLEEPER_R_NAME_LOWER, + setup_platform, +) -async def test_sensors(hass, setup_entry): + +async def test_sensors(hass, mock_asyncsleepiq): """Test the SleepIQ binary sensors for a bed with two sides.""" + entry = await setup_platform(hass, DOMAIN) entity_registry = er.async_get(hass) - state = hass.states.get("sensor.sleepnumber_ile_test1_sleepnumber") + state = hass.states.get( + f"sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_L_NAME_LOWER}_sleepnumber" + ) assert state.state == "40" assert state.attributes.get(ATTR_ICON) == "mdi:bed" assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "SleepNumber ILE Test1 SleepNumber" + state.attributes.get(ATTR_FRIENDLY_NAME) + == f"SleepNumber {BED_NAME} {SLEEPER_L_NAME} SleepNumber" ) - entry = entity_registry.async_get("sensor.sleepnumber_ile_test1_sleepnumber") + entry = entity_registry.async_get( + f"sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_L_NAME_LOWER}_sleepnumber" + ) assert entry - assert entry.unique_id == "-31_Test1_sleep_number" + assert entry.unique_id == f"{BED_ID}_{SLEEPER_L_NAME}_sleep_number" - # If account type is set, only a single bed account was created and there will - # not be a second entity - if setup_entry["account_type"]: - return - - state = hass.states.get("sensor.sleepnumber_ile_test2_sleepnumber") + state = hass.states.get( + f"sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_sleepnumber" + ) assert state.state == "80" assert state.attributes.get(ATTR_ICON) == "mdi:bed" assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "SleepNumber ILE Test2 SleepNumber" + state.attributes.get(ATTR_FRIENDLY_NAME) + == f"SleepNumber {BED_NAME} {SLEEPER_R_NAME} SleepNumber" ) - entry = entity_registry.async_get("sensor.sleepnumber_ile_test2_sleepnumber") + entry = entity_registry.async_get( + f"sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_sleepnumber" + ) assert entry - assert entry.unique_id == "-31_Test2_sleep_number" + assert entry.unique_id == f"{BED_ID}_{SLEEPER_R_NAME}_sleep_number"