Refactor sleepiq as async with config flow (#64850)
Co-authored-by: J. Nick Koston <nick@koston.org>pull/66664/head
parent
dbc445c2fa
commit
0bd0b4766e
|
@ -166,6 +166,7 @@ homeassistant.components.senseme.*
|
|||
homeassistant.components.shelly.*
|
||||
homeassistant.components.simplisafe.*
|
||||
homeassistant.components.slack.*
|
||||
homeassistant.components.sleepiq.*
|
||||
homeassistant.components.smhi.*
|
||||
homeassistant.components.ssdp.*
|
||||
homeassistant.components.stookalert.*
|
||||
|
|
|
@ -850,6 +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/slide/* @ualex73
|
||||
homeassistant/components/sma/* @kellerza @rklomp
|
||||
tests/components/sma/* @kellerza @rklomp
|
||||
|
|
|
@ -1,115 +1,73 @@
|
|||
"""Support for SleepIQ from SleepNumber."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from sleepyq import Sleepyq
|
||||
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.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
|
||||
from .coordinator import SleepIQDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(DOMAIN): vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
}
|
||||
)
|
||||
DOMAIN: {
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
}
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the SleepIQ component.
|
||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
|
||||
Will automatically load sensor components to support
|
||||
devices discovered on the account.
|
||||
"""
|
||||
username = config[DOMAIN][CONF_USERNAME]
|
||||
password = config[DOMAIN][CONF_PASSWORD]
|
||||
client = Sleepyq(username, password)
|
||||
try:
|
||||
data = SleepIQData(client)
|
||||
data.update()
|
||||
except ValueError:
|
||||
message = """
|
||||
SleepIQ failed to login, double check your username and password"
|
||||
"""
|
||||
_LOGGER.error(message)
|
||||
return False
|
||||
|
||||
hass.data[DOMAIN] = data
|
||||
discovery.load_platform(hass, Platform.SENSOR, DOMAIN, {}, config)
|
||||
discovery.load_platform(hass, Platform.BINARY_SENSOR, DOMAIN, {}, config)
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up sleepiq component."""
|
||||
if DOMAIN in config:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN]
|
||||
)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class SleepIQData:
|
||||
"""Get the latest data from SleepIQ."""
|
||||
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
|
||||
|
||||
def __init__(self, client):
|
||||
"""Initialize the data object."""
|
||||
self._client = client
|
||||
self.beds = {}
|
||||
coordinator = SleepIQDataUpdateCoordinator(
|
||||
hass,
|
||||
client=client,
|
||||
username=entry.data[CONF_USERNAME],
|
||||
)
|
||||
|
||||
self.update()
|
||||
# Call the SleepIQ API to refresh data
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data from SleepIQ."""
|
||||
self._client.login()
|
||||
beds = self._client.beds_with_sleeper_status()
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
|
||||
self.beds = {bed.bed_id: bed for bed in beds}
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class SleepIQSensor(Entity):
|
||||
"""Implementation of a SleepIQ sensor."""
|
||||
|
||||
def __init__(self, sleepiq_data, bed_id, side):
|
||||
"""Initialize the sensor."""
|
||||
self._bed_id = bed_id
|
||||
self._side = side
|
||||
self.sleepiq_data = sleepiq_data
|
||||
self.side = None
|
||||
self.bed = None
|
||||
|
||||
# added by subclass
|
||||
self._name = None
|
||||
self.type = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return "SleepNumber {} {} {}".format(
|
||||
self.bed.name, self.side.sleeper.first_name, self._name
|
||||
)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID for the bed."""
|
||||
return f"{self._bed_id}-{self._side}-{self.type}"
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data from SleepIQ and updates the states."""
|
||||
# Call the API for new sleepiq data. Each sensor will re-trigger this
|
||||
# same exact call, but that's fine. We cache results for a short period
|
||||
# of time to prevent hitting API limits.
|
||||
self.sleepiq_data.update()
|
||||
|
||||
self.bed = self.sleepiq_data.beds[self._bed_id]
|
||||
self.side = getattr(self.bed, self._side)
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload the config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
|
|
|
@ -1,61 +1,49 @@
|
|||
"""Support for SleepIQ sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import SleepIQSensor
|
||||
from .const import DOMAIN, IS_IN_BED, SENSOR_TYPES, SIDES
|
||||
from .const import BED, DOMAIN, ICON_EMPTY, ICON_OCCUPIED, IS_IN_BED, SIDES
|
||||
from .coordinator import SleepIQDataUpdateCoordinator
|
||||
from .entity import SleepIQSensor
|
||||
|
||||
|
||||
def setup_platform(
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the SleepIQ sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
data = hass.data[DOMAIN]
|
||||
data.update()
|
||||
|
||||
dev = []
|
||||
for bed_id, bed in data.beds.items():
|
||||
for side in SIDES:
|
||||
if getattr(bed, side) is not None:
|
||||
dev.append(IsInBedBinarySensor(data, bed_id, side))
|
||||
add_entities(dev)
|
||||
"""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
|
||||
)
|
||||
|
||||
|
||||
class IsInBedBinarySensor(SleepIQSensor, BinarySensorEntity):
|
||||
"""Implementation of a SleepIQ presence sensor."""
|
||||
|
||||
def __init__(self, sleepiq_data, bed_id, side):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(sleepiq_data, bed_id, side)
|
||||
self._state = None
|
||||
self.type = IS_IN_BED
|
||||
self._name = SENSOR_TYPES[self.type]
|
||||
self.update()
|
||||
_attr_device_class = BinarySensorDeviceClass.OCCUPANCY
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the status of the sensor."""
|
||||
return self._state is True
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: SleepIQDataUpdateCoordinator,
|
||||
bed_id: str,
|
||||
side: str,
|
||||
) -> None:
|
||||
"""Initialize the SleepIQ bed side binary sensor."""
|
||||
super().__init__(coordinator, bed_id, side, IS_IN_BED)
|
||||
|
||||
@property
|
||||
def device_class(self) -> BinarySensorDeviceClass:
|
||||
"""Return the class of this sensor."""
|
||||
return BinarySensorDeviceClass.OCCUPANCY
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data from SleepIQ and updates the states."""
|
||||
super().update()
|
||||
self._state = self.side.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
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
"""Config flow to configure SleepIQ component."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from sleepyq import Sleepyq
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
|
||||
from .const import DOMAIN, SLEEPYQ_INVALID_CREDENTIALS_MESSAGE
|
||||
|
||||
|
||||
class SleepIQFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a SleepIQ config flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult:
|
||||
"""Import a SleepIQ account as a config entry.
|
||||
|
||||
This flow is triggered by 'async_setup' for configured accounts.
|
||||
"""
|
||||
await self.async_set_unique_id(import_config[CONF_USERNAME].lower())
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=import_config[CONF_USERNAME], data=import_config
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle a flow initialized by the user."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
# Don't allow multiple instances with the same username
|
||||
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:
|
||||
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(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_USERNAME,
|
||||
default=user_input.get(CONF_USERNAME)
|
||||
if user_input is not None
|
||||
else "",
|
||||
): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
last_step=True,
|
||||
)
|
||||
|
||||
|
||||
def try_connection(user_input: dict[str, Any]) -> str:
|
||||
"""Test if the given credentials can successfully login to SleepIQ."""
|
||||
|
||||
client = Sleepyq(user_input[CONF_USERNAME], user_input[CONF_PASSWORD])
|
||||
|
||||
try:
|
||||
client.login()
|
||||
except ValueError as error:
|
||||
return str(error)
|
||||
|
||||
return ""
|
|
@ -1,7 +1,12 @@
|
|||
"""Define constants for the SleepIQ component."""
|
||||
|
||||
DATA_SLEEPIQ = "data_sleepiq"
|
||||
DOMAIN = "sleepiq"
|
||||
SLEEPYQ_INVALID_CREDENTIALS_MESSAGE = "username or password"
|
||||
|
||||
BED = "bed"
|
||||
ICON_EMPTY = "mdi:bed-empty"
|
||||
ICON_OCCUPIED = "mdi:bed"
|
||||
IS_IN_BED = "is_in_bed"
|
||||
SLEEP_NUMBER = "sleep_number"
|
||||
SENSOR_TYPES = {SLEEP_NUMBER: "SleepNumber", IS_IN_BED: "Is In Bed"}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
"""Coordinator for SleepIQ."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from sleepyq import Sleepyq
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class SleepIQDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
|
||||
"""SleepIQ data update coordinator."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
client: Sleepyq,
|
||||
username: str,
|
||||
) -> None:
|
||||
"""Initialize coordinator."""
|
||||
super().__init__(
|
||||
hass, _LOGGER, name=f"{username}@SleepIQ", 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()
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
"""Entity for the SleepIQ integration."""
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import BED, ICON_OCCUPIED, SENSOR_TYPES
|
||||
from .coordinator import SleepIQDataUpdateCoordinator
|
||||
|
||||
|
||||
class SleepIQSensor(CoordinatorEntity):
|
||||
"""Implementation of a SleepIQ sensor."""
|
||||
|
||||
_attr_icon = ICON_OCCUPIED
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: SleepIQDataUpdateCoordinator,
|
||||
bed_id: str,
|
||||
side: str,
|
||||
name: str,
|
||||
) -> None:
|
||||
"""Initialize the SleepIQ side entity."""
|
||||
super().__init__(coordinator)
|
||||
self.bed_id = bed_id
|
||||
self.side = side
|
||||
|
||||
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}"
|
||||
)
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self._async_update_attrs()
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@callback
|
||||
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)
|
|
@ -1,9 +1,13 @@
|
|||
{
|
||||
"domain": "sleepiq",
|
||||
"name": "SleepIQ",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/sleepiq",
|
||||
"requirements": ["sleepyq==0.8.1"],
|
||||
"codeowners": [],
|
||||
"codeowners": ["@mfugate1"],
|
||||
"dhcp": [
|
||||
{"macaddress": "64DBA0*"}
|
||||
],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["sleepyq"]
|
||||
}
|
||||
|
|
|
@ -1,62 +1,43 @@
|
|||
"""Support for SleepIQ sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import SleepIQSensor
|
||||
from .const import DOMAIN, SENSOR_TYPES, SIDES, SLEEP_NUMBER
|
||||
|
||||
ICON = "mdi:bed"
|
||||
from .const import BED, DOMAIN, SIDES, SLEEP_NUMBER
|
||||
from .coordinator import SleepIQDataUpdateCoordinator
|
||||
from .entity import SleepIQSensor
|
||||
|
||||
|
||||
def setup_platform(
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the SleepIQ sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
data = hass.data[DOMAIN]
|
||||
data.update()
|
||||
|
||||
dev = []
|
||||
for bed_id, bed in data.beds.items():
|
||||
for side in SIDES:
|
||||
if getattr(bed, side) is not None:
|
||||
dev.append(SleepNumberSensor(data, bed_id, side))
|
||||
add_entities(dev)
|
||||
"""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
|
||||
)
|
||||
|
||||
|
||||
class SleepNumberSensor(SleepIQSensor, SensorEntity):
|
||||
"""Implementation of a SleepIQ sensor."""
|
||||
|
||||
def __init__(self, sleepiq_data, bed_id, side):
|
||||
"""Initialize the sensor."""
|
||||
SleepIQSensor.__init__(self, sleepiq_data, bed_id, side)
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: SleepIQDataUpdateCoordinator,
|
||||
bed_id: str,
|
||||
side: str,
|
||||
) -> None:
|
||||
"""Initialize the SleepIQ sleep number sensor."""
|
||||
super().__init__(coordinator, bed_id, side, SLEEP_NUMBER)
|
||||
|
||||
self._state = None
|
||||
self.type = SLEEP_NUMBER
|
||||
self._name = SENSOR_TYPES[self.type]
|
||||
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon to use in the frontend, if any."""
|
||||
return ICON
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data from SleepIQ and updates the states."""
|
||||
SleepIQSensor.update(self)
|
||||
self._state = self.side.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
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Account is already configured"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect",
|
||||
"invalid_auth": "Invalid authentication"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "Password",
|
||||
"username": "Username"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -286,6 +286,7 @@ FLOWS = [
|
|||
"shopping_list",
|
||||
"sia",
|
||||
"simplisafe",
|
||||
"sleepiq",
|
||||
"sma",
|
||||
"smappee",
|
||||
"smart_meter_texas",
|
||||
|
|
|
@ -88,6 +88,7 @@ DHCP: list[dict[str, str | bool]] = [
|
|||
{'domain': 'senseme', 'macaddress': '20F85E*'},
|
||||
{'domain': 'sensibo', 'hostname': 'sensibo*'},
|
||||
{'domain': 'simplisafe', 'hostname': 'simplisafe*', 'macaddress': '30AEA4*'},
|
||||
{'domain': 'sleepiq', 'macaddress': '64DBA0*'},
|
||||
{'domain': 'smartthings', 'hostname': 'st*', 'macaddress': '24FD5B*'},
|
||||
{'domain': 'smartthings', 'hostname': 'smartthings*', 'macaddress': '24FD5B*'},
|
||||
{'domain': 'smartthings', 'hostname': 'hub*', 'macaddress': '24FD5B*'},
|
||||
|
|
11
mypy.ini
11
mypy.ini
|
@ -1635,6 +1635,17 @@ no_implicit_optional = true
|
|||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.sleepiq.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
no_implicit_optional = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.smhi.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
"""Common fixtures for sleepiq tests."""
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from sleepyq import Bed, FamilyStatus, Sleeper
|
||||
|
||||
from homeassistant.components.sleepiq.const import DOMAIN
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
|
||||
from tests.common import MockConfigEntry, load_fixture
|
||||
|
||||
|
||||
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"]
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config_data():
|
||||
"""Provide configuration data for tests."""
|
||||
return {
|
||||
CONF_USERNAME: "username",
|
||||
CONF_PASSWORD: "password",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config_entry(config_data):
|
||||
"""Create a mock config entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data=config_data,
|
||||
options={},
|
||||
)
|
||||
|
||||
|
||||
@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)
|
||||
await hass.async_block_till_done()
|
||||
return {"account_type": request.param, "mock_entry": config_entry}
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"beds" : [
|
||||
{
|
||||
"bedId" : "-31",
|
||||
"rightSide" : {
|
||||
"alertId" : 0,
|
||||
"lastLink" : "00:00:00",
|
||||
"isInBed" : true,
|
||||
"sleepNumber" : 40,
|
||||
"alertDetailedMessage" : "No Alert",
|
||||
"pressure" : -16
|
||||
},
|
||||
"status" : 1,
|
||||
"leftSide" : null
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"edpLoginStatus" : 200,
|
||||
"userId" : "-42",
|
||||
"registrationState" : 13,
|
||||
"key" : "0987",
|
||||
"edpLoginMessage" : "not used"
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"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,45 +1,34 @@
|
|||
"""The tests for SleepIQ binary sensor platform."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from homeassistant.components.sleepiq import binary_sensor as sleepiq
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.components.sleepiq.test_init import mock_responses
|
||||
|
||||
CONFIG = {"username": "foo", "password": "bar"}
|
||||
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||
from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, ATTR_ICON
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
|
||||
async def test_sensor_setup(hass, requests_mock):
|
||||
"""Test for successfully setting up the SleepIQ platform."""
|
||||
mock_responses(requests_mock)
|
||||
async def test_binary_sensors(hass, setup_entry):
|
||||
"""Test the SleepIQ binary sensors."""
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
await async_setup_component(hass, "sleepiq", {"sleepiq": CONFIG})
|
||||
state = hass.states.get("binary_sensor.sleepnumber_ile_test1_is_in_bed")
|
||||
assert 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"
|
||||
|
||||
device_mock = MagicMock()
|
||||
sleepiq.setup_platform(hass, CONFIG, device_mock, MagicMock())
|
||||
devices = device_mock.call_args[0][0]
|
||||
assert len(devices) == 2
|
||||
entry = entity_registry.async_get("binary_sensor.sleepnumber_ile_test1_is_in_bed")
|
||||
assert entry
|
||||
assert entry.unique_id == "-31_Test1_is_in_bed"
|
||||
|
||||
left_side = devices[1]
|
||||
assert left_side.name == "SleepNumber ILE Test1 Is In Bed"
|
||||
assert left_side.state == "on"
|
||||
# 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
|
||||
|
||||
right_side = devices[0]
|
||||
assert right_side.name == "SleepNumber ILE Test2 Is In Bed"
|
||||
assert right_side.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"
|
||||
|
||||
|
||||
async def test_setup_single(hass, requests_mock):
|
||||
"""Test for successfully setting up the SleepIQ platform."""
|
||||
mock_responses(requests_mock, single=True)
|
||||
|
||||
await async_setup_component(hass, "sleepiq", {"sleepiq": CONFIG})
|
||||
|
||||
device_mock = MagicMock()
|
||||
sleepiq.setup_platform(hass, CONFIG, device_mock, MagicMock())
|
||||
devices = device_mock.call_args[0][0]
|
||||
assert len(devices) == 1
|
||||
|
||||
right_side = devices[0]
|
||||
assert right_side.name == "SleepNumber ILE Test1 Is In Bed"
|
||||
assert right_side.state == "on"
|
||||
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_DEVICE_CLASS) == BinarySensorDeviceClass.OCCUPANCY
|
||||
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "SleepNumber ILE Test2 Is In Bed"
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
"""Tests for the SleepIQ config flow."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow, setup
|
||||
from homeassistant.components.sleepiq.const import (
|
||||
DOMAIN,
|
||||
SLEEPYQ_INVALID_CREDENTIALS_MESSAGE,
|
||||
)
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
SLEEPIQ_CONFIG = {
|
||||
CONF_USERNAME: "username",
|
||||
CONF_PASSWORD: "password",
|
||||
}
|
||||
|
||||
|
||||
async def test_import(hass: HomeAssistant) -> None:
|
||||
"""Test that we can import a config entry."""
|
||||
with patch("sleepyq.Sleepyq.login"):
|
||||
assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: SLEEPIQ_CONFIG})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
entry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||
assert entry.data[CONF_USERNAME] == SLEEPIQ_CONFIG[CONF_USERNAME]
|
||||
assert entry.data[CONF_PASSWORD] == SLEEPIQ_CONFIG[CONF_PASSWORD]
|
||||
|
||||
|
||||
async def test_show_set_form(hass: HomeAssistant) -> None:
|
||||
"""Test that the setup form is served."""
|
||||
with patch("sleepyq.Sleepyq.login"):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
|
||||
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),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=SLEEPIQ_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
|
||||
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"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=SLEEPIQ_CONFIG
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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]
|
|
@ -1,65 +1,54 @@
|
|||
"""The tests for the SleepIQ component."""
|
||||
from http import HTTPStatus
|
||||
from unittest.mock import MagicMock, patch
|
||||
"""Tests for the SleepIQ integration."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant import setup
|
||||
import homeassistant.components.sleepiq as sleepiq
|
||||
from homeassistant.components.sleepiq.const import DOMAIN
|
||||
from homeassistant.components.sleepiq.coordinator import UPDATE_INTERVAL
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from tests.common import load_fixture
|
||||
|
||||
CONFIG = {"sleepiq": {"username": "foo", "password": "bar"}}
|
||||
from tests.common import async_fire_time_changed
|
||||
from tests.components.sleepiq.conftest import (
|
||||
mock_bed_family_status,
|
||||
mock_beds,
|
||||
mock_sleepers,
|
||||
)
|
||||
|
||||
|
||||
def mock_responses(mock, single=False):
|
||||
"""Mock responses for SleepIQ."""
|
||||
base_url = "https://prod-api.sleepiq.sleepnumber.com/rest/"
|
||||
if single:
|
||||
suffix = "-single"
|
||||
else:
|
||||
suffix = ""
|
||||
mock.put(base_url + "login", text=load_fixture("sleepiq-login.json"))
|
||||
mock.get(base_url + "bed?_k=0987", text=load_fixture(f"sleepiq-bed{suffix}.json"))
|
||||
mock.get(base_url + "sleeper?_k=0987", text=load_fixture("sleepiq-sleeper.json"))
|
||||
mock.get(
|
||||
base_url + "bed/familyStatus?_k=0987",
|
||||
text=load_fixture(f"sleepiq-familystatus{suffix}.json"),
|
||||
)
|
||||
async def test_unload_entry(hass: HomeAssistant, setup_entry) -> None:
|
||||
"""Test unloading the SleepIQ entry."""
|
||||
entry = setup_entry["mock_entry"]
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state is ConfigEntryState.NOT_LOADED
|
||||
assert not hass.data.get(DOMAIN)
|
||||
|
||||
|
||||
async def test_setup(hass, requests_mock):
|
||||
"""Test the setup."""
|
||||
mock_responses(requests_mock)
|
||||
|
||||
# We're mocking the load_platform discoveries or else the platforms
|
||||
# will be setup during tear down when blocking till done, but the mocks
|
||||
# are no longer active.
|
||||
with patch("homeassistant.helpers.discovery.load_platform", MagicMock()):
|
||||
assert sleepiq.setup(hass, CONFIG)
|
||||
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_setup_login_failed(hass, requests_mock):
|
||||
"""Test the setup if a bad username or password is given."""
|
||||
mock_responses(requests_mock)
|
||||
requests_mock.put(
|
||||
"https://prod-api.sleepiq.sleepnumber.com/rest/login",
|
||||
status_code=HTTPStatus.UNAUTHORIZED,
|
||||
json=load_fixture("sleepiq-login-failed.json"),
|
||||
)
|
||||
async def test_update_interval(hass: HomeAssistant, setup_entry) -> 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
|
||||
|
||||
response = sleepiq.setup(hass, CONFIG)
|
||||
assert not response
|
||||
async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def test_setup_component_no_login(hass):
|
||||
"""Test the setup when no login is configured."""
|
||||
conf = CONFIG.copy()
|
||||
del conf["sleepiq"]["username"]
|
||||
assert not await setup.async_setup_component(hass, sleepiq.DOMAIN, conf)
|
||||
|
||||
|
||||
async def test_setup_component_no_password(hass):
|
||||
"""Test the setup when no password is configured."""
|
||||
conf = CONFIG.copy()
|
||||
del conf["sleepiq"]["password"]
|
||||
|
||||
assert not await setup.async_setup_component(hass, sleepiq.DOMAIN, conf)
|
||||
assert beds.call_count == 1
|
||||
assert sleepers.call_count == 1
|
||||
assert bed_family_status.call_count == 1
|
||||
|
|
|
@ -1,48 +1,35 @@
|
|||
"""The tests for SleepIQ sensor platform."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import homeassistant.components.sleepiq.sensor as sleepiq
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.components.sleepiq.test_init import mock_responses
|
||||
|
||||
CONFIG = {"username": "foo", "password": "bar"}
|
||||
from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_ICON
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
|
||||
async def test_setup(hass, requests_mock):
|
||||
"""Test for successfully setting up the SleepIQ platform."""
|
||||
mock_responses(requests_mock)
|
||||
async def test_sensors(hass, setup_entry):
|
||||
"""Test the SleepIQ binary sensors for a bed with two sides."""
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
assert await async_setup_component(hass, "sleepiq", {"sleepiq": CONFIG})
|
||||
state = hass.states.get("sensor.sleepnumber_ile_test1_sleepnumber")
|
||||
assert state.state == "40"
|
||||
assert state.attributes.get(ATTR_ICON) == "mdi:bed"
|
||||
assert (
|
||||
state.attributes.get(ATTR_FRIENDLY_NAME) == "SleepNumber ILE Test1 SleepNumber"
|
||||
)
|
||||
|
||||
device_mock = MagicMock()
|
||||
sleepiq.setup_platform(hass, CONFIG, device_mock, MagicMock())
|
||||
devices = device_mock.call_args[0][0]
|
||||
assert len(devices) == 2
|
||||
entry = entity_registry.async_get("sensor.sleepnumber_ile_test1_sleepnumber")
|
||||
assert entry
|
||||
assert entry.unique_id == "-31_Test1_sleep_number"
|
||||
|
||||
left_side = devices[1]
|
||||
left_side.hass = hass
|
||||
assert left_side.name == "SleepNumber ILE Test1 SleepNumber"
|
||||
assert left_side.state == 40
|
||||
# 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
|
||||
|
||||
right_side = devices[0]
|
||||
right_side.hass = hass
|
||||
assert right_side.name == "SleepNumber ILE Test2 SleepNumber"
|
||||
assert right_side.state == 80
|
||||
state = hass.states.get("sensor.sleepnumber_ile_test2_sleepnumber")
|
||||
assert state.state == "80"
|
||||
assert state.attributes.get(ATTR_ICON) == "mdi:bed"
|
||||
assert (
|
||||
state.attributes.get(ATTR_FRIENDLY_NAME) == "SleepNumber ILE Test2 SleepNumber"
|
||||
)
|
||||
|
||||
|
||||
async def test_setup_single(hass, requests_mock):
|
||||
"""Test for successfully setting up the SleepIQ platform."""
|
||||
mock_responses(requests_mock, single=True)
|
||||
|
||||
assert await async_setup_component(hass, "sleepiq", {"sleepiq": CONFIG})
|
||||
|
||||
device_mock = MagicMock()
|
||||
sleepiq.setup_platform(hass, CONFIG, device_mock, MagicMock())
|
||||
devices = device_mock.call_args[0][0]
|
||||
assert len(devices) == 1
|
||||
|
||||
right_side = devices[0]
|
||||
right_side.hass = hass
|
||||
assert right_side.name == "SleepNumber ILE Test1 SleepNumber"
|
||||
assert right_side.state == 40
|
||||
entry = entity_registry.async_get("sensor.sleepnumber_ile_test2_sleepnumber")
|
||||
assert entry
|
||||
assert entry.unique_id == "-31_Test2_sleep_number"
|
||||
|
|
|
@ -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,28 +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 +0,0 @@
|
|||
{"Error":{"Code":401,"Message":"Authentication token of type [class org.apache.shiro.authc.UsernamePasswordToken] could not be authenticated by any configured realms. Please ensure that at least one realm can authenticate these tokens."}}
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"edpLoginStatus" : 200,
|
||||
"userId" : "-42",
|
||||
"registrationState" : 13,
|
||||
"key" : "0987",
|
||||
"edpLoginMessage" : "not used"
|
||||
}
|
|
@ -1,55 +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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Loading…
Reference in New Issue