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/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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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.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

View file

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

View file

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

View file

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

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."""
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"

View file

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

View file

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

View file

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