mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 19:27:45 +00:00
Modernize Sleepiq and add new entities (#66336)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
beb30a1ff1
commit
a367d2be40
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
|
||||||
|
@ -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 ""
|
|
||||||
|
@ -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"
|
||||||
|
@ -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()
|
|
||||||
}
|
|
||||||
|
@ -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)
|
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"edpLoginStatus" : 200,
|
|
||||||
"userId" : "-42",
|
|
||||||
"registrationState" : 13,
|
|
||||||
"key" : "0987",
|
|
||||||
"edpLoginMessage" : "not used"
|
|
||||||
}
|
|
@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -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"
|
||||||
|
@ -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}
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=SLEEPIQ_CONFIG
|
)
|
||||||
)
|
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
|
||||||
|
@ -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)
|
||||||
|
@ -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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user