Modernize Sleepiq and add new entities (#66336)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Keilin Bickar 2022-02-18 13:50:44 -05:00 committed by GitHub
parent beb30a1ff1
commit a367d2be40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 330 additions and 400 deletions

View File

@ -850,8 +850,8 @@ homeassistant/components/sisyphus/* @jkeljo
homeassistant/components/sky_hub/* @rogerselwyn homeassistant/components/sky_hub/* @rogerselwyn
homeassistant/components/slack/* @bachya homeassistant/components/slack/* @bachya
tests/components/slack/* @bachya tests/components/slack/* @bachya
homeassistant/components/sleepiq/* @mfugate1 homeassistant/components/sleepiq/* @mfugate1 @kbickar
tests/components/sleepiq/* @mfugate1 tests/components/sleepiq/* @mfugate1 @kbickar
homeassistant/components/slide/* @ualex73 homeassistant/components/slide/* @ualex73
homeassistant/components/sma/* @kellerza @rklomp homeassistant/components/sma/* @kellerza @rklomp
tests/components/sma/* @kellerza @rklomp tests/components/sma/* @kellerza @rklomp

View File

@ -1,12 +1,19 @@
"""Support for SleepIQ from SleepNumber.""" """Support for SleepIQ from SleepNumber."""
import logging import logging
from sleepyq import Sleepyq from asyncsleepiq import (
AsyncSleepIQ,
SleepIQAPIException,
SleepIQLoginException,
SleepIQTimeoutException,
)
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant 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 import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
@ -15,6 +22,8 @@ from .coordinator import SleepIQDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: { DOMAIN: {
@ -43,18 +52,34 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the SleepIQ config entry.""" """Set up the SleepIQ config entry."""
client = Sleepyq(entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD]) conf = entry.data
try: email = conf[CONF_USERNAME]
await hass.async_add_executor_job(client.login) password = conf[CONF_PASSWORD]
except ValueError:
_LOGGER.error("SleepIQ login failed, double check your username and password")
return False
coordinator = SleepIQDataUpdateCoordinator( client_session = async_get_clientsession(hass)
hass,
client=client, gateway = AsyncSleepIQ(client_session=client_session)
username=entry.data[CONF_USERNAME],
) 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 # Call the SleepIQ API to refresh data
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()

View File

@ -1,4 +1,6 @@
"""Support for SleepIQ sensors.""" """Support for SleepIQ sensors."""
from asyncsleepiq import SleepIQBed, SleepIQSleeper
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass, BinarySensorDeviceClass,
BinarySensorEntity, BinarySensorEntity,
@ -6,8 +8,9 @@ from homeassistant.components.binary_sensor import (
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback 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 .coordinator import SleepIQDataUpdateCoordinator
from .entity import SleepIQSensor from .entity import SleepIQSensor
@ -20,10 +23,9 @@ async def async_setup_entry(
"""Set up the SleepIQ bed binary sensors.""" """Set up the SleepIQ bed binary sensors."""
coordinator: SleepIQDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator: SleepIQDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities( async_add_entities(
IsInBedBinarySensor(coordinator, bed_id, side) IsInBedBinarySensor(coordinator, bed, sleeper)
for side in SIDES for bed in coordinator.client.beds.values()
for bed_id in coordinator.data for sleeper in bed.sleepers
if getattr(coordinator.data[bed_id][BED], side) is not None
) )
@ -34,16 +36,15 @@ class IsInBedBinarySensor(SleepIQSensor, BinarySensorEntity):
def __init__( def __init__(
self, self,
coordinator: SleepIQDataUpdateCoordinator, coordinator: DataUpdateCoordinator,
bed_id: str, bed: SleepIQBed,
side: str, sleeper: SleepIQSleeper,
) -> None: ) -> None:
"""Initialize the SleepIQ bed side binary sensor.""" """Initialize the sensor."""
super().__init__(coordinator, bed_id, side, IS_IN_BED) super().__init__(coordinator, bed, sleeper, IS_IN_BED)
@callback @callback
def _async_update_attrs(self) -> None: def _async_update_attrs(self) -> None:
"""Update sensor attributes.""" """Update sensor attributes."""
super()._async_update_attrs() self._attr_is_on = self.sleeper.in_bed
self._attr_is_on = getattr(self.side_data, IS_IN_BED) self._attr_icon = ICON_OCCUPIED if self.sleeper.in_bed else ICON_EMPTY
self._attr_icon = ICON_OCCUPIED if self.is_on else ICON_EMPTY

View File

@ -3,14 +3,16 @@ from __future__ import annotations
from typing import Any from typing import Any
from sleepyq import Sleepyq from asyncsleepiq import AsyncSleepIQ, SleepIQLoginException, SleepIQTimeoutException
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult 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): 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()) await self.async_set_unique_id(user_input[CONF_USERNAME].lower())
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
login_error = await self.hass.async_add_executor_job( try:
try_connection, user_input await try_connection(self.hass, user_input)
) except SleepIQLoginException:
if not login_error: errors["base"] = "invalid_auth"
except SleepIQTimeoutException:
errors["base"] = "cannot_connect"
else:
return self.async_create_entry( return self.async_create_entry(
title=user_input[CONF_USERNAME], data=user_input 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( return self.async_show_form(
step_id="user", step_id="user",
data_schema=vol.Schema( 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.""" """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: gateway = AsyncSleepIQ(client_session=client_session)
client.login() await gateway.login(user_input[CONF_USERNAME], user_input[CONF_PASSWORD])
except ValueError as error:
return str(error)
return ""

View File

@ -14,3 +14,6 @@ SENSOR_TYPES = {SLEEP_NUMBER: "SleepNumber", IS_IN_BED: "Is In Bed"}
LEFT = "left" LEFT = "left"
RIGHT = "right" RIGHT = "right"
SIDES = [LEFT, RIGHT] SIDES = [LEFT, RIGHT]
SLEEPIQ_DATA = "sleepiq_data"
SLEEPIQ_STATUS_COORDINATOR = "sleepiq_status"

View File

@ -2,13 +2,11 @@
from datetime import timedelta from datetime import timedelta
import logging import logging
from sleepyq import Sleepyq from asyncsleepiq import AsyncSleepIQ
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import BED
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
UPDATE_INTERVAL = timedelta(seconds=60) UPDATE_INTERVAL = timedelta(seconds=60)
@ -20,21 +18,15 @@ class SleepIQDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
def __init__( def __init__(
self, self,
hass: HomeAssistant, hass: HomeAssistant,
*, client: AsyncSleepIQ,
client: Sleepyq,
username: str, username: str,
) -> None: ) -> None:
"""Initialize coordinator.""" """Initialize coordinator."""
super().__init__( 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 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()
}

View File

@ -1,9 +1,15 @@
"""Entity for the SleepIQ integration.""" """Entity for the SleepIQ integration."""
from homeassistant.core import callback from abc import abstractmethod
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import BED, ICON_OCCUPIED, SENSOR_TYPES from asyncsleepiq import SleepIQBed, SleepIQSleeper
from .coordinator import SleepIQDataUpdateCoordinator
from homeassistant.core import callback
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from .const import ICON_OCCUPIED, SENSOR_TYPES
class SleepIQSensor(CoordinatorEntity): class SleepIQSensor(CoordinatorEntity):
@ -13,22 +19,19 @@ class SleepIQSensor(CoordinatorEntity):
def __init__( def __init__(
self, self,
coordinator: SleepIQDataUpdateCoordinator, coordinator: DataUpdateCoordinator,
bed_id: str, bed: SleepIQBed,
side: str, sleeper: SleepIQSleeper,
name: str, name: str,
) -> None: ) -> None:
"""Initialize the SleepIQ side entity.""" """Initialize the SleepIQ side entity."""
super().__init__(coordinator) super().__init__(coordinator)
self.bed_id = bed_id self.bed = bed
self.side = side self.sleeper = sleeper
self._async_update_attrs() self._async_update_attrs()
self._attr_name = f"SleepNumber {self.bed_data.name} {self.side_data.sleeper.first_name} {SENSOR_TYPES[name]}" self._attr_name = f"SleepNumber {bed.name} {sleeper.name} {SENSOR_TYPES[name]}"
self._attr_unique_id = ( self._attr_unique_id = f"{bed.id}_{sleeper.name}_{name}"
f"{self.bed_id}_{self.side_data.sleeper.first_name}_{name}"
)
@callback @callback
def _handle_coordinator_update(self) -> None: def _handle_coordinator_update(self) -> None:
@ -37,7 +40,6 @@ class SleepIQSensor(CoordinatorEntity):
super()._handle_coordinator_update() super()._handle_coordinator_update()
@callback @callback
@abstractmethod
def _async_update_attrs(self) -> None: def _async_update_attrs(self) -> None:
"""Update sensor attributes.""" """Update sensor attributes."""
self.bed_data = self.coordinator.data[self.bed_id][BED]
self.side_data = getattr(self.bed_data, self.side)

View File

@ -3,11 +3,13 @@
"name": "SleepIQ", "name": "SleepIQ",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sleepiq", "documentation": "https://www.home-assistant.io/integrations/sleepiq",
"requirements": ["sleepyq==0.8.1"], "requirements": ["asyncsleepiq==1.0.0"],
"codeowners": ["@mfugate1"], "codeowners": ["@mfugate1", "@kbickar"],
"dhcp": [ "dhcp": [
{"macaddress": "64DBA0*"} {
"macaddress": "64DBA0*"
}
], ],
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["sleepyq"] "loggers": ["asyncsleepiq"]
} }

View File

@ -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.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback 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 .coordinator import SleepIQDataUpdateCoordinator
from .entity import SleepIQSensor from .entity import SleepIQSensor
@ -17,27 +22,27 @@ async def async_setup_entry(
"""Set up the SleepIQ bed sensors.""" """Set up the SleepIQ bed sensors."""
coordinator: SleepIQDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator: SleepIQDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities( async_add_entities(
SleepNumberSensor(coordinator, bed_id, side) SleepNumberSensorEntity(coordinator, bed, sleeper)
for side in SIDES for bed in coordinator.client.beds.values()
for bed_id in coordinator.data for sleeper in bed.sleepers
if getattr(coordinator.data[bed_id][BED], side) is not None
) )
class SleepNumberSensor(SleepIQSensor, SensorEntity): class SleepNumberSensorEntity(SleepIQSensor, SensorEntity):
"""Implementation of a SleepIQ sensor.""" """Representation of an SleepIQ Entity with CoordinatorEntity."""
_attr_icon = "mdi:bed"
def __init__( def __init__(
self, self,
coordinator: SleepIQDataUpdateCoordinator, coordinator: DataUpdateCoordinator,
bed_id: str, bed: SleepIQBed,
side: str, sleeper: SleepIQSleeper,
) -> None: ) -> None:
"""Initialize the SleepIQ sleep number sensor.""" """Initialize the sensor."""
super().__init__(coordinator, bed_id, side, SLEEP_NUMBER) super().__init__(coordinator, bed, sleeper, SLEEP_NUMBER)
@callback @callback
def _async_update_attrs(self) -> None: def _async_update_attrs(self) -> None:
"""Update sensor attributes.""" """Update sensor attributes."""
super()._async_update_attrs() self._attr_native_value = self.sleeper.sleep_number
self._attr_native_value = self.side_data.sleep_number

View File

@ -349,6 +349,9 @@ async-upnp-client==0.23.5
# homeassistant.components.supla # homeassistant.components.supla
asyncpysupla==0.0.5 asyncpysupla==0.0.5
# homeassistant.components.sleepiq
asyncsleepiq==1.0.0
# homeassistant.components.aten_pe # homeassistant.components.aten_pe
atenpdu==0.3.2 atenpdu==0.3.2
@ -2201,9 +2204,6 @@ skybellpy==0.6.3
# homeassistant.components.slack # homeassistant.components.slack
slackclient==2.5.0 slackclient==2.5.0
# homeassistant.components.sleepiq
sleepyq==0.8.1
# homeassistant.components.xmpp # homeassistant.components.xmpp
slixmpp==1.7.1 slixmpp==1.7.1

View File

@ -254,6 +254,9 @@ arcam-fmj==0.12.0
# homeassistant.components.yeelight # homeassistant.components.yeelight
async-upnp-client==0.23.5 async-upnp-client==0.23.5
# homeassistant.components.sleepiq
asyncsleepiq==1.0.0
# homeassistant.components.aurora # homeassistant.components.aurora
auroranoaa==0.0.2 auroranoaa==0.0.2
@ -1354,9 +1357,6 @@ simplisafe-python==2022.02.1
# homeassistant.components.slack # homeassistant.components.slack
slackclient==2.5.0 slackclient==2.5.0
# homeassistant.components.sleepiq
sleepyq==0.8.1
# homeassistant.components.smart_meter_texas # homeassistant.components.smart_meter_texas
smart-meter-texas==0.4.7 smart-meter-texas==0.4.7

View File

@ -1,75 +1,67 @@
"""Common fixtures for sleepiq tests.""" """Common methods for SleepIQ."""
import json from unittest.mock import MagicMock, patch
from unittest.mock import patch
import pytest 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.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
BED_ID = "123456"
def mock_beds(account_type): BED_NAME = "Test Bed"
"""Mock sleepnumber bed data.""" BED_NAME_LOWER = BED_NAME.lower().replace(" ", "_")
return [ SLEEPER_L_NAME = "SleeperL"
Bed(bed) SLEEPER_R_NAME = "Sleeper R"
for bed in json.loads(load_fixture(f"bed{account_type}.json", "sleepiq"))[ SLEEPER_L_NAME_LOWER = SLEEPER_L_NAME.lower().replace(" ", "_")
"beds" SLEEPER_R_NAME_LOWER = SLEEPER_R_NAME.lower().replace(" ", "_")
]
]
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"]
]
@pytest.fixture @pytest.fixture
def config_data(): def mock_asyncsleepiq():
"""Provide configuration data for tests.""" """Mock an AsyncSleepIQ object."""
return { with patch("homeassistant.components.sleepiq.AsyncSleepIQ", autospec=True) as mock:
CONF_USERNAME: "username", client = mock.return_value
CONF_PASSWORD: "password", 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 async def setup_platform(hass: HomeAssistant, platform) -> MockConfigEntry:
def config_entry(config_data): """Set up the SleepIQ platform."""
"""Create a mock config entry.""" mock_entry = MockConfigEntry(
return MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
data=config_data, data={
options={}, CONF_USERNAME: "user@email.com",
CONF_PASSWORD: "password",
},
) )
mock_entry.add_to_hass(hass)
if platform:
@pytest.fixture(params=["-single", ""]) with patch("homeassistant.components.sleepiq.PLATFORMS", [platform]):
async def setup_entry(hass, request, config_entry): assert await async_setup_component(hass, DOMAIN, {})
"""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)
await hass.async_block_till_done() await hass.async_block_till_done()
return {"account_type": request.param, "mock_entry": config_entry}
return mock_entry

View File

@ -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"
}
]
}

View File

@ -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"
}
]
}

View File

@ -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
}
]
}

View File

@ -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
}
}
]
}

View File

@ -1,7 +0,0 @@
{
"edpLoginStatus" : 200,
"userId" : "-42",
"registrationState" : 13,
"key" : "0987",
"edpLoginMessage" : "not used"
}

View File

@ -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"
}
]
}

View File

@ -1,34 +1,61 @@
"""The tests for SleepIQ binary sensor platform.""" """The tests for SleepIQ binary sensor platform."""
from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDeviceClass
from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, ATTR_ICON 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 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.""" """Test the SleepIQ binary sensors."""
await setup_platform(hass, DOMAIN)
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
state = hass.states.get("binary_sensor.sleepnumber_ile_test1_is_in_bed") state = hass.states.get(
assert state.state == "on" 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_ICON) == "mdi:bed"
assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.OCCUPANCY 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") entity = entity_registry.async_get(
assert entry f"binary_sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_L_NAME_LOWER}_is_in_bed"
assert entry.unique_id == "-31_Test1_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 state = hass.states.get(
# not be a second entity f"binary_sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_is_in_bed"
if setup_entry["account_type"]: )
return assert state.state == STATE_OFF
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"
assert state.attributes.get(ATTR_ICON) == "mdi:bed-empty" assert state.attributes.get(ATTR_ICON) == "mdi:bed-empty"
assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.OCCUPANCY 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"

View File

@ -1,11 +1,10 @@
"""Tests for the SleepIQ config flow.""" """Tests for the SleepIQ config flow."""
from unittest.mock import patch from unittest.mock import patch
from asyncsleepiq import SleepIQLoginException, SleepIQTimeoutException
from homeassistant import config_entries, data_entry_flow, setup from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components.sleepiq.const import ( from homeassistant.components.sleepiq.const import DOMAIN
DOMAIN,
SLEEPYQ_INVALID_CREDENTIALS_MESSAGE,
)
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -17,7 +16,7 @@ SLEEPIQ_CONFIG = {
async def test_import(hass: HomeAssistant) -> None: async def test_import(hass: HomeAssistant) -> None:
"""Test that we can import a config entry.""" """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}) assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: SLEEPIQ_CONFIG})
await hass.async_block_till_done() 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: async def test_show_set_form(hass: HomeAssistant) -> None:
"""Test that the setup form is served.""" """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( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None 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: async def test_login_invalid_auth(hass: HomeAssistant) -> None:
"""Test we show user form with appropriate error on login failure.""" """Test we show user form with appropriate error on login failure."""
with patch( with patch(
"sleepyq.Sleepyq.login", "asyncsleepiq.AsyncSleepIQ.login",
side_effect=ValueError(SLEEPYQ_INVALID_CREDENTIALS_MESSAGE), side_effect=SleepIQLoginException,
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=SLEEPIQ_CONFIG 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: async def test_login_cannot_connect(hass: HomeAssistant) -> None:
"""Test we show user form with appropriate error on login failure.""" """Test we show user form with appropriate error on login failure."""
with patch( with patch(
"sleepyq.Sleepyq.login", "asyncsleepiq.AsyncSleepIQ.login",
side_effect=ValueError("Unexpected response code"), side_effect=SleepIQTimeoutException,
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=SLEEPIQ_CONFIG 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: async def test_success(hass: HomeAssistant) -> None:
"""Test successful flow provides entry creation data.""" """Test successful flow provides entry creation data."""
with patch("sleepyq.Sleepyq.login"):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=SLEEPIQ_CONFIG DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
assert result["type"] == "form"
assert result["errors"] == {}
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY with patch("asyncsleepiq.AsyncSleepIQ.login", return_value=True), patch(
assert result["data"][CONF_USERNAME] == SLEEPIQ_CONFIG[CONF_USERNAME] "homeassistant.components.sleepiq.async_setup_entry",
assert result["data"][CONF_PASSWORD] == SLEEPIQ_CONFIG[CONF_PASSWORD] 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

View File

@ -1,5 +1,9 @@
"""Tests for the SleepIQ integration.""" """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.const import DOMAIN
from homeassistant.components.sleepiq.coordinator import UPDATE_INTERVAL from homeassistant.components.sleepiq.coordinator import UPDATE_INTERVAL
@ -8,16 +12,12 @@ from homeassistant.core import HomeAssistant
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from tests.common import async_fire_time_changed from tests.common import async_fire_time_changed
from tests.components.sleepiq.conftest import ( from tests.components.sleepiq.conftest import setup_platform
mock_bed_family_status,
mock_beds,
mock_sleepers,
)
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.""" """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) assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done() 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) assert not hass.data.get(DOMAIN)
async def test_entry_setup_login_error(hass: HomeAssistant, config_entry) -> None: async def test_entry_setup_login_error(hass: HomeAssistant, mock_asyncsleepiq) -> None:
"""Test when sleepyq client is unable to login.""" """Test when sleepiq client is unable to login."""
with patch("sleepyq.Sleepyq.login", side_effect=ValueError): mock_asyncsleepiq.login.side_effect = SleepIQLoginException
config_entry.add_to_hass(hass) entry = await setup_platform(hass, None)
assert not await hass.config_entries.async_setup(config_entry.entry_id) 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.""" """Test update interval."""
with patch("sleepyq.Sleepyq.beds", return_value=mock_beds("")) as beds, patch( await setup_platform(hass, "sensor")
"sleepyq.Sleepyq.sleepers", return_value=mock_sleepers() assert mock_asyncsleepiq.fetch_bed_statuses.call_count == 1
) 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
async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL) async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL)
await hass.async_block_till_done() await hass.async_block_till_done()
assert beds.call_count == 1 assert mock_asyncsleepiq.fetch_bed_statuses.call_count == 2
assert sleepers.call_count == 1
assert bed_family_status.call_count == 1
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)

View File

@ -1,35 +1,53 @@
"""The tests for SleepIQ sensor platform.""" """The tests for SleepIQ sensor platform."""
from homeassistant.components.sensor import DOMAIN
from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_ICON from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_ICON
from homeassistant.helpers import entity_registry as er 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.""" """Test the SleepIQ binary sensors for a bed with two sides."""
entry = await setup_platform(hass, DOMAIN)
entity_registry = er.async_get(hass) 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.state == "40"
assert state.attributes.get(ATTR_ICON) == "mdi:bed" assert state.attributes.get(ATTR_ICON) == "mdi:bed"
assert ( 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
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 state = hass.states.get(
# not be a second entity f"sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_sleepnumber"
if setup_entry["account_type"]: )
return
state = hass.states.get("sensor.sleepnumber_ile_test2_sleepnumber")
assert state.state == "80" assert state.state == "80"
assert state.attributes.get(ATTR_ICON) == "mdi:bed" assert state.attributes.get(ATTR_ICON) == "mdi:bed"
assert ( 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
assert entry.unique_id == "-31_Test2_sleep_number" assert entry.unique_id == f"{BED_ID}_{SLEEPER_R_NAME}_sleep_number"