Remove eight_sleep integration (#102669)
This commit is contained in:
parent
626123acc0
commit
aa36229519
17 changed files with 83 additions and 856 deletions
|
@ -286,9 +286,6 @@ omit =
|
||||||
homeassistant/components/edl21/__init__.py
|
homeassistant/components/edl21/__init__.py
|
||||||
homeassistant/components/edl21/sensor.py
|
homeassistant/components/edl21/sensor.py
|
||||||
homeassistant/components/egardia/*
|
homeassistant/components/egardia/*
|
||||||
homeassistant/components/eight_sleep/__init__.py
|
|
||||||
homeassistant/components/eight_sleep/binary_sensor.py
|
|
||||||
homeassistant/components/eight_sleep/sensor.py
|
|
||||||
homeassistant/components/electric_kiwi/__init__.py
|
homeassistant/components/electric_kiwi/__init__.py
|
||||||
homeassistant/components/electric_kiwi/api.py
|
homeassistant/components/electric_kiwi/api.py
|
||||||
homeassistant/components/electric_kiwi/oauth2.py
|
homeassistant/components/electric_kiwi/oauth2.py
|
||||||
|
|
|
@ -319,8 +319,6 @@ build.json @home-assistant/supervisor
|
||||||
/homeassistant/components/efergy/ @tkdrob
|
/homeassistant/components/efergy/ @tkdrob
|
||||||
/tests/components/efergy/ @tkdrob
|
/tests/components/efergy/ @tkdrob
|
||||||
/homeassistant/components/egardia/ @jeroenterheerdt
|
/homeassistant/components/egardia/ @jeroenterheerdt
|
||||||
/homeassistant/components/eight_sleep/ @mezz64 @raman325
|
|
||||||
/tests/components/eight_sleep/ @mezz64 @raman325
|
|
||||||
/homeassistant/components/electrasmart/ @jafar-atili
|
/homeassistant/components/electrasmart/ @jafar-atili
|
||||||
/tests/components/electrasmart/ @jafar-atili
|
/tests/components/electrasmart/ @jafar-atili
|
||||||
/homeassistant/components/electric_kiwi/ @mikey0000
|
/homeassistant/components/electric_kiwi/ @mikey0000
|
||||||
|
|
|
@ -1,222 +1,37 @@
|
||||||
"""Support for Eight smart mattress covers and mattresses."""
|
"""The Eight Sleep integration."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||||
from datetime import timedelta
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from pyeight.eight import EightSleep
|
|
||||||
from pyeight.exceptions import RequestError
|
|
||||||
from pyeight.user import EightUser
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
|
||||||
from homeassistant.const import (
|
|
||||||
ATTR_HW_VERSION,
|
|
||||||
ATTR_MANUFACTURER,
|
|
||||||
ATTR_MODEL,
|
|
||||||
ATTR_SW_VERSION,
|
|
||||||
CONF_PASSWORD,
|
|
||||||
CONF_USERNAME,
|
|
||||||
Platform,
|
|
||||||
)
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
from homeassistant.helpers import issue_registry as ir
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo, async_get
|
|
||||||
from homeassistant.helpers.typing import UNDEFINED, ConfigType
|
|
||||||
from homeassistant.helpers.update_coordinator import (
|
|
||||||
CoordinatorEntity,
|
|
||||||
DataUpdateCoordinator,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .const import DOMAIN, NAME_MAP
|
DOMAIN = "eight_sleep"
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
|
||||||
|
|
||||||
HEAT_SCAN_INTERVAL = timedelta(seconds=60)
|
|
||||||
USER_SCAN_INTERVAL = timedelta(seconds=300)
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema(
|
|
||||||
{
|
|
||||||
DOMAIN: vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_USERNAME): cv.string,
|
|
||||||
vol.Required(CONF_PASSWORD): cv.string,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
},
|
|
||||||
extra=vol.ALLOW_EXTRA,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool:
|
||||||
class EightSleepConfigEntryData:
|
"""Set up Eight Sleep from a config entry."""
|
||||||
"""Data used for all entities for a given config entry."""
|
ir.async_create_issue(
|
||||||
|
|
||||||
api: EightSleep
|
|
||||||
heat_coordinator: DataUpdateCoordinator
|
|
||||||
user_coordinator: DataUpdateCoordinator
|
|
||||||
|
|
||||||
|
|
||||||
def _get_device_unique_id(eight: EightSleep, user_obj: EightUser | None = None) -> str:
|
|
||||||
"""Get the device's unique ID."""
|
|
||||||
unique_id = eight.device_id
|
|
||||||
assert unique_id
|
|
||||||
if user_obj:
|
|
||||||
unique_id = f"{unique_id}.{user_obj.user_id}.{user_obj.side}"
|
|
||||||
return unique_id
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|
||||||
"""Old set up method for the Eight Sleep component."""
|
|
||||||
if DOMAIN in config:
|
|
||||||
_LOGGER.warning(
|
|
||||||
"Your Eight Sleep configuration has been imported into the UI; "
|
|
||||||
"please remove it from configuration.yaml as support for it "
|
|
||||||
"will be removed in a future release"
|
|
||||||
)
|
|
||||||
hass.async_create_task(
|
|
||||||
hass.config_entries.flow.async_init(
|
|
||||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
||||||
"""Set up the Eight Sleep config entry."""
|
|
||||||
eight = EightSleep(
|
|
||||||
entry.data[CONF_USERNAME],
|
|
||||||
entry.data[CONF_PASSWORD],
|
|
||||||
hass.config.time_zone,
|
|
||||||
client_session=async_get_clientsession(hass),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Authenticate, build sensors
|
|
||||||
try:
|
|
||||||
success = await eight.start()
|
|
||||||
except RequestError as err:
|
|
||||||
raise ConfigEntryNotReady from err
|
|
||||||
if not success:
|
|
||||||
# Authentication failed, cannot continue
|
|
||||||
return False
|
|
||||||
|
|
||||||
heat_coordinator: DataUpdateCoordinator = DataUpdateCoordinator(
|
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
DOMAIN,
|
||||||
name=f"{DOMAIN}_heat",
|
DOMAIN,
|
||||||
update_interval=HEAT_SCAN_INTERVAL,
|
is_fixable=False,
|
||||||
update_method=eight.update_device_data,
|
severity=ir.IssueSeverity.ERROR,
|
||||||
|
translation_key="integration_removed",
|
||||||
|
translation_placeholders={
|
||||||
|
"entries": "/config/integrations/integration/eight_sleep"
|
||||||
|
},
|
||||||
)
|
)
|
||||||
user_coordinator: DataUpdateCoordinator = DataUpdateCoordinator(
|
|
||||||
hass,
|
|
||||||
_LOGGER,
|
|
||||||
name=f"{DOMAIN}_user",
|
|
||||||
update_interval=USER_SCAN_INTERVAL,
|
|
||||||
update_method=eight.update_user_data,
|
|
||||||
)
|
|
||||||
await heat_coordinator.async_config_entry_first_refresh()
|
|
||||||
await user_coordinator.async_config_entry_first_refresh()
|
|
||||||
|
|
||||||
if not eight.users:
|
|
||||||
# No users, cannot continue
|
|
||||||
return False
|
|
||||||
|
|
||||||
dev_reg = async_get(hass)
|
|
||||||
assert eight.device_data
|
|
||||||
device_data = {
|
|
||||||
ATTR_MANUFACTURER: "Eight Sleep",
|
|
||||||
ATTR_MODEL: eight.device_data.get("modelString", UNDEFINED),
|
|
||||||
ATTR_HW_VERSION: eight.device_data.get("sensorInfo", {}).get(
|
|
||||||
"hwRevision", UNDEFINED
|
|
||||||
),
|
|
||||||
ATTR_SW_VERSION: eight.device_data.get("firmwareVersion", UNDEFINED),
|
|
||||||
}
|
|
||||||
dev_reg.async_get_or_create(
|
|
||||||
config_entry_id=entry.entry_id,
|
|
||||||
identifiers={(DOMAIN, _get_device_unique_id(eight))},
|
|
||||||
name=f"{entry.data[CONF_USERNAME]}'s Eight Sleep",
|
|
||||||
**device_data,
|
|
||||||
)
|
|
||||||
for user in eight.users.values():
|
|
||||||
assert user.user_profile
|
|
||||||
dev_reg.async_get_or_create(
|
|
||||||
config_entry_id=entry.entry_id,
|
|
||||||
identifiers={(DOMAIN, _get_device_unique_id(eight, user))},
|
|
||||||
name=f"{user.user_profile['firstName']}'s Eight Sleep Side",
|
|
||||||
via_device=(DOMAIN, _get_device_unique_id(eight)),
|
|
||||||
**device_data,
|
|
||||||
)
|
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = EightSleepConfigEntryData(
|
|
||||||
eight, heat_coordinator, user_coordinator
|
|
||||||
)
|
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
if all(
|
||||||
# stop the API before unloading everything
|
config_entry.state is ConfigEntryState.NOT_LOADED
|
||||||
config_entry_data: EightSleepConfigEntryData = hass.data[DOMAIN][entry.entry_id]
|
for config_entry in hass.config_entries.async_entries(DOMAIN)
|
||||||
await config_entry_data.api.stop()
|
if config_entry.entry_id != entry.entry_id
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
):
|
||||||
if not hass.data[DOMAIN]:
|
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
|
||||||
hass.data.pop(DOMAIN)
|
|
||||||
|
|
||||||
return unload_ok
|
return True
|
||||||
|
|
||||||
|
|
||||||
class EightSleepBaseEntity(CoordinatorEntity[DataUpdateCoordinator]):
|
|
||||||
"""The base Eight Sleep entity class."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
entry: ConfigEntry,
|
|
||||||
coordinator: DataUpdateCoordinator,
|
|
||||||
eight: EightSleep,
|
|
||||||
user_id: str | None,
|
|
||||||
sensor: str,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the data object."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self._config_entry = entry
|
|
||||||
self._eight = eight
|
|
||||||
self._user_id = user_id
|
|
||||||
self._sensor = sensor
|
|
||||||
self._user_obj: EightUser | None = None
|
|
||||||
if user_id:
|
|
||||||
self._user_obj = self._eight.users[user_id]
|
|
||||||
|
|
||||||
mapped_name = NAME_MAP.get(sensor, sensor.replace("_", " ").title())
|
|
||||||
if self._user_obj is not None:
|
|
||||||
assert self._user_obj.user_profile
|
|
||||||
name = f"{self._user_obj.user_profile['firstName']}'s {mapped_name}"
|
|
||||||
self._attr_name = name
|
|
||||||
else:
|
|
||||||
self._attr_name = f"Eight Sleep {mapped_name}"
|
|
||||||
unique_id = f"{_get_device_unique_id(eight, self._user_obj)}.{sensor}"
|
|
||||||
self._attr_unique_id = unique_id
|
|
||||||
identifiers = {(DOMAIN, _get_device_unique_id(eight, self._user_obj))}
|
|
||||||
self._attr_device_info = DeviceInfo(identifiers=identifiers)
|
|
||||||
|
|
||||||
async def async_heat_set(self, target: int, duration: int) -> None:
|
|
||||||
"""Handle eight sleep service calls."""
|
|
||||||
if self._user_obj is None:
|
|
||||||
raise HomeAssistantError(
|
|
||||||
"This entity does not support the heat set service."
|
|
||||||
)
|
|
||||||
|
|
||||||
await self._user_obj.set_heating_level(target, duration)
|
|
||||||
config_entry_data: EightSleepConfigEntryData = self.hass.data[DOMAIN][
|
|
||||||
self._config_entry.entry_id
|
|
||||||
]
|
|
||||||
await config_entry_data.heat_coordinator.async_request_refresh()
|
|
||||||
|
|
|
@ -1,65 +0,0 @@
|
||||||
"""Support for Eight Sleep binary sensors."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from pyeight.eight import EightSleep
|
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
|
||||||
BinarySensorDeviceClass,
|
|
||||||
BinarySensorEntity,
|
|
||||||
)
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
|
||||||
|
|
||||||
from . import EightSleepBaseEntity, EightSleepConfigEntryData
|
|
||||||
from .const import DOMAIN
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
BINARY_SENSORS = ["bed_presence"]
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
|
||||||
) -> None:
|
|
||||||
"""Set up the eight sleep binary sensor."""
|
|
||||||
config_entry_data: EightSleepConfigEntryData = hass.data[DOMAIN][entry.entry_id]
|
|
||||||
eight = config_entry_data.api
|
|
||||||
heat_coordinator = config_entry_data.heat_coordinator
|
|
||||||
async_add_entities(
|
|
||||||
EightHeatSensor(entry, heat_coordinator, eight, user.user_id, binary_sensor)
|
|
||||||
for user in eight.users.values()
|
|
||||||
for binary_sensor in BINARY_SENSORS
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EightHeatSensor(EightSleepBaseEntity, BinarySensorEntity):
|
|
||||||
"""Representation of a Eight Sleep heat-based sensor."""
|
|
||||||
|
|
||||||
_attr_device_class = BinarySensorDeviceClass.OCCUPANCY
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
entry: ConfigEntry,
|
|
||||||
coordinator: DataUpdateCoordinator,
|
|
||||||
eight: EightSleep,
|
|
||||||
user_id: str | None,
|
|
||||||
sensor: str,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the sensor."""
|
|
||||||
super().__init__(entry, coordinator, eight, user_id, sensor)
|
|
||||||
assert self._user_obj
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Presence Sensor: %s, Side: %s, User: %s",
|
|
||||||
sensor,
|
|
||||||
self._user_obj.side,
|
|
||||||
user_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_on(self) -> bool:
|
|
||||||
"""Return true if the binary sensor is on."""
|
|
||||||
assert self._user_obj
|
|
||||||
return bool(self._user_obj.bed_presence)
|
|
|
@ -1,90 +1,11 @@
|
||||||
"""Config flow for Eight Sleep integration."""
|
"""The Eight Sleep integration config flow."""
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
from homeassistant.config_entries import ConfigFlow
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from pyeight.eight import EightSleep
|
from . import DOMAIN
|
||||||
from pyeight.exceptions import RequestError
|
|
||||||
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 homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
||||||
from homeassistant.helpers.selector import (
|
|
||||||
TextSelector,
|
|
||||||
TextSelectorConfig,
|
|
||||||
TextSelectorType,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_USERNAME): TextSelector(
|
|
||||||
TextSelectorConfig(type=TextSelectorType.EMAIL)
|
|
||||||
),
|
|
||||||
vol.Required(CONF_PASSWORD): TextSelector(
|
|
||||||
TextSelectorConfig(type=TextSelectorType.PASSWORD)
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
class EightSleepConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a config flow for Eight Sleep."""
|
"""Handle a config flow for Eight Sleep."""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
|
|
||||||
async def _validate_data(self, config: dict[str, str]) -> str | None:
|
|
||||||
"""Validate input data and return any error."""
|
|
||||||
await self.async_set_unique_id(config[CONF_USERNAME].lower())
|
|
||||||
self._abort_if_unique_id_configured()
|
|
||||||
|
|
||||||
eight = EightSleep(
|
|
||||||
config[CONF_USERNAME],
|
|
||||||
config[CONF_PASSWORD],
|
|
||||||
self.hass.config.time_zone,
|
|
||||||
client_session=async_get_clientsession(self.hass),
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
await eight.fetch_token()
|
|
||||||
except RequestError as err:
|
|
||||||
return str(err)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def async_step_user(
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> FlowResult:
|
|
||||||
"""Handle the initial step."""
|
|
||||||
if user_input is None:
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
|
|
||||||
)
|
|
||||||
|
|
||||||
if (err := await self._validate_data(user_input)) is not None:
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="user",
|
|
||||||
data_schema=STEP_USER_DATA_SCHEMA,
|
|
||||||
errors={"base": "cannot_connect"},
|
|
||||||
description_placeholders={"error": err},
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.async_create_entry(title=user_input[CONF_USERNAME], data=user_input)
|
|
||||||
|
|
||||||
async def async_step_import(self, import_config: dict) -> FlowResult:
|
|
||||||
"""Handle import."""
|
|
||||||
if (err := await self._validate_data(import_config)) is not None:
|
|
||||||
_LOGGER.error("Unable to import configuration.yaml configuration: %s", err)
|
|
||||||
return self.async_abort(
|
|
||||||
reason="cannot_connect", description_placeholders={"error": err}
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.async_create_entry(
|
|
||||||
title=import_config[CONF_USERNAME], data=import_config
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
"""Eight Sleep constants."""
|
|
||||||
DOMAIN = "eight_sleep"
|
|
||||||
|
|
||||||
HEAT_ENTITY = "heat"
|
|
||||||
USER_ENTITY = "user"
|
|
||||||
|
|
||||||
NAME_MAP = {
|
|
||||||
"current_sleep": "Sleep Session",
|
|
||||||
"current_sleep_fitness": "Sleep Fitness",
|
|
||||||
"last_sleep": "Previous Sleep Session",
|
|
||||||
}
|
|
||||||
|
|
||||||
SERVICE_HEAT_SET = "heat_set"
|
|
||||||
|
|
||||||
ATTR_TARGET = "target"
|
|
||||||
ATTR_DURATION = "duration"
|
|
|
@ -1,10 +1,9 @@
|
||||||
{
|
{
|
||||||
"domain": "eight_sleep",
|
"domain": "eight_sleep",
|
||||||
"name": "Eight Sleep",
|
"name": "Eight Sleep",
|
||||||
"codeowners": ["@mezz64", "@raman325"],
|
"codeowners": [],
|
||||||
"config_flow": true,
|
|
||||||
"documentation": "https://www.home-assistant.io/integrations/eight_sleep",
|
"documentation": "https://www.home-assistant.io/integrations/eight_sleep",
|
||||||
|
"integration_type": "system",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["pyeight"],
|
"requirements": []
|
||||||
"requirements": ["pyEight==0.3.2"]
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,301 +0,0 @@
|
||||||
"""Support for Eight Sleep sensors."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from pyeight.eight import EightSleep
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
|
||||||
SensorDeviceClass,
|
|
||||||
SensorEntity,
|
|
||||||
SensorStateClass,
|
|
||||||
)
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import PERCENTAGE, UnitOfTemperature
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.entity_platform import (
|
|
||||||
AddEntitiesCallback,
|
|
||||||
async_get_current_platform,
|
|
||||||
)
|
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
|
||||||
|
|
||||||
from . import EightSleepBaseEntity, EightSleepConfigEntryData
|
|
||||||
from .const import ATTR_DURATION, ATTR_TARGET, DOMAIN, SERVICE_HEAT_SET
|
|
||||||
|
|
||||||
ATTR_ROOM_TEMP = "Room Temperature"
|
|
||||||
ATTR_AVG_ROOM_TEMP = "Average Room Temperature"
|
|
||||||
ATTR_BED_TEMP = "Bed Temperature"
|
|
||||||
ATTR_AVG_BED_TEMP = "Average Bed Temperature"
|
|
||||||
ATTR_RESP_RATE = "Respiratory Rate"
|
|
||||||
ATTR_AVG_RESP_RATE = "Average Respiratory Rate"
|
|
||||||
ATTR_HEART_RATE = "Heart Rate"
|
|
||||||
ATTR_AVG_HEART_RATE = "Average Heart Rate"
|
|
||||||
ATTR_SLEEP_DUR = "Time Slept"
|
|
||||||
ATTR_LIGHT_PERC = f"Light Sleep {PERCENTAGE}"
|
|
||||||
ATTR_DEEP_PERC = f"Deep Sleep {PERCENTAGE}"
|
|
||||||
ATTR_REM_PERC = f"REM Sleep {PERCENTAGE}"
|
|
||||||
ATTR_TNT = "Tosses & Turns"
|
|
||||||
ATTR_SLEEP_STAGE = "Sleep Stage"
|
|
||||||
ATTR_TARGET_HEAT = "Target Heating Level"
|
|
||||||
ATTR_ACTIVE_HEAT = "Heating Active"
|
|
||||||
ATTR_DURATION_HEAT = "Heating Time Remaining"
|
|
||||||
ATTR_PROCESSING = "Processing"
|
|
||||||
ATTR_SESSION_START = "Session Start"
|
|
||||||
ATTR_FIT_DATE = "Fitness Date"
|
|
||||||
ATTR_FIT_DURATION_SCORE = "Fitness Duration Score"
|
|
||||||
ATTR_FIT_ASLEEP_SCORE = "Fitness Asleep Score"
|
|
||||||
ATTR_FIT_OUT_SCORE = "Fitness Out-of-Bed Score"
|
|
||||||
ATTR_FIT_WAKEUP_SCORE = "Fitness Wakeup Score"
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
EIGHT_USER_SENSORS = [
|
|
||||||
"current_sleep",
|
|
||||||
"current_sleep_fitness",
|
|
||||||
"last_sleep",
|
|
||||||
"bed_temperature",
|
|
||||||
"sleep_stage",
|
|
||||||
]
|
|
||||||
EIGHT_HEAT_SENSORS = ["bed_state"]
|
|
||||||
EIGHT_ROOM_SENSORS = ["room_temperature"]
|
|
||||||
|
|
||||||
VALID_TARGET_HEAT = vol.All(vol.Coerce(int), vol.Clamp(min=-100, max=100))
|
|
||||||
VALID_DURATION = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=28800))
|
|
||||||
|
|
||||||
SERVICE_EIGHT_SCHEMA = {
|
|
||||||
ATTR_TARGET: VALID_TARGET_HEAT,
|
|
||||||
ATTR_DURATION: VALID_DURATION,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
|
||||||
) -> None:
|
|
||||||
"""Set up the eight sleep sensors."""
|
|
||||||
config_entry_data: EightSleepConfigEntryData = hass.data[DOMAIN][entry.entry_id]
|
|
||||||
eight = config_entry_data.api
|
|
||||||
heat_coordinator = config_entry_data.heat_coordinator
|
|
||||||
user_coordinator = config_entry_data.user_coordinator
|
|
||||||
|
|
||||||
all_sensors: list[SensorEntity] = []
|
|
||||||
|
|
||||||
for obj in eight.users.values():
|
|
||||||
all_sensors.extend(
|
|
||||||
EightUserSensor(entry, user_coordinator, eight, obj.user_id, sensor)
|
|
||||||
for sensor in EIGHT_USER_SENSORS
|
|
||||||
)
|
|
||||||
all_sensors.extend(
|
|
||||||
EightHeatSensor(entry, heat_coordinator, eight, obj.user_id, sensor)
|
|
||||||
for sensor in EIGHT_HEAT_SENSORS
|
|
||||||
)
|
|
||||||
|
|
||||||
all_sensors.extend(
|
|
||||||
EightRoomSensor(entry, user_coordinator, eight, sensor)
|
|
||||||
for sensor in EIGHT_ROOM_SENSORS
|
|
||||||
)
|
|
||||||
|
|
||||||
async_add_entities(all_sensors)
|
|
||||||
|
|
||||||
platform = async_get_current_platform()
|
|
||||||
platform.async_register_entity_service(
|
|
||||||
SERVICE_HEAT_SET,
|
|
||||||
SERVICE_EIGHT_SCHEMA,
|
|
||||||
"async_heat_set",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EightHeatSensor(EightSleepBaseEntity, SensorEntity):
|
|
||||||
"""Representation of an eight sleep heat-based sensor."""
|
|
||||||
|
|
||||||
_attr_native_unit_of_measurement = PERCENTAGE
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
entry: ConfigEntry,
|
|
||||||
coordinator: DataUpdateCoordinator,
|
|
||||||
eight: EightSleep,
|
|
||||||
user_id: str,
|
|
||||||
sensor: str,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the sensor."""
|
|
||||||
super().__init__(entry, coordinator, eight, user_id, sensor)
|
|
||||||
assert self._user_obj
|
|
||||||
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Heat Sensor: %s, Side: %s, User: %s",
|
|
||||||
self._sensor,
|
|
||||||
self._user_obj.side,
|
|
||||||
self._user_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def native_value(self) -> int | None:
|
|
||||||
"""Return the state of the sensor."""
|
|
||||||
assert self._user_obj
|
|
||||||
return self._user_obj.heating_level
|
|
||||||
|
|
||||||
@property
|
|
||||||
def extra_state_attributes(self) -> dict[str, Any]:
|
|
||||||
"""Return device state attributes."""
|
|
||||||
assert self._user_obj
|
|
||||||
return {
|
|
||||||
ATTR_TARGET_HEAT: self._user_obj.target_heating_level,
|
|
||||||
ATTR_ACTIVE_HEAT: self._user_obj.now_heating,
|
|
||||||
ATTR_DURATION_HEAT: self._user_obj.heating_remaining,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _get_breakdown_percent(
|
|
||||||
attr: dict[str, Any], key: str, denominator: int | float
|
|
||||||
) -> int | float:
|
|
||||||
"""Get a breakdown percent."""
|
|
||||||
try:
|
|
||||||
return round((attr["breakdown"][key] / denominator) * 100, 2)
|
|
||||||
except (ZeroDivisionError, KeyError):
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def _get_rounded_value(attr: dict[str, Any], key: str) -> int | float | None:
|
|
||||||
"""Get rounded value for given key."""
|
|
||||||
if (val := attr.get(key)) is None:
|
|
||||||
return None
|
|
||||||
return round(val, 2)
|
|
||||||
|
|
||||||
|
|
||||||
class EightUserSensor(EightSleepBaseEntity, SensorEntity):
|
|
||||||
"""Representation of an eight sleep user-based sensor."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
entry: ConfigEntry,
|
|
||||||
coordinator: DataUpdateCoordinator,
|
|
||||||
eight: EightSleep,
|
|
||||||
user_id: str,
|
|
||||||
sensor: str,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the sensor."""
|
|
||||||
super().__init__(entry, coordinator, eight, user_id, sensor)
|
|
||||||
assert self._user_obj
|
|
||||||
|
|
||||||
if self._sensor == "bed_temperature":
|
|
||||||
self._attr_icon = "mdi:thermometer"
|
|
||||||
self._attr_device_class = SensorDeviceClass.TEMPERATURE
|
|
||||||
self._attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
|
|
||||||
elif self._sensor in ("current_sleep", "last_sleep", "current_sleep_fitness"):
|
|
||||||
self._attr_native_unit_of_measurement = "Score"
|
|
||||||
|
|
||||||
if self._sensor != "sleep_stage":
|
|
||||||
self._attr_state_class = SensorStateClass.MEASUREMENT
|
|
||||||
|
|
||||||
_LOGGER.debug(
|
|
||||||
"User Sensor: %s, Side: %s, User: %s",
|
|
||||||
self._sensor,
|
|
||||||
self._user_obj.side,
|
|
||||||
self._user_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def native_value(self) -> str | int | float | None:
|
|
||||||
"""Return the state of the sensor."""
|
|
||||||
if not self._user_obj:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if "current" in self._sensor:
|
|
||||||
if "fitness" in self._sensor:
|
|
||||||
return self._user_obj.current_sleep_fitness_score
|
|
||||||
return self._user_obj.current_sleep_score
|
|
||||||
|
|
||||||
if "last" in self._sensor:
|
|
||||||
return self._user_obj.last_sleep_score
|
|
||||||
|
|
||||||
if self._sensor == "bed_temperature":
|
|
||||||
return self._user_obj.current_values["bed_temp"]
|
|
||||||
|
|
||||||
if self._sensor == "sleep_stage":
|
|
||||||
return self._user_obj.current_values["stage"]
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def extra_state_attributes(self) -> dict[str, Any] | None:
|
|
||||||
"""Return device state attributes."""
|
|
||||||
attr = None
|
|
||||||
if "current" in self._sensor and self._user_obj:
|
|
||||||
if "fitness" in self._sensor:
|
|
||||||
attr = self._user_obj.current_fitness_values
|
|
||||||
else:
|
|
||||||
attr = self._user_obj.current_values
|
|
||||||
elif "last" in self._sensor and self._user_obj:
|
|
||||||
attr = self._user_obj.last_values
|
|
||||||
|
|
||||||
if attr is None:
|
|
||||||
# Skip attributes if sensor type doesn't support
|
|
||||||
return None
|
|
||||||
|
|
||||||
if "fitness" in self._sensor:
|
|
||||||
state_attr = {
|
|
||||||
ATTR_FIT_DATE: attr["date"],
|
|
||||||
ATTR_FIT_DURATION_SCORE: attr["duration"],
|
|
||||||
ATTR_FIT_ASLEEP_SCORE: attr["asleep"],
|
|
||||||
ATTR_FIT_OUT_SCORE: attr["out"],
|
|
||||||
ATTR_FIT_WAKEUP_SCORE: attr["wakeup"],
|
|
||||||
}
|
|
||||||
return state_attr
|
|
||||||
|
|
||||||
state_attr = {ATTR_SESSION_START: attr["date"]}
|
|
||||||
state_attr[ATTR_TNT] = attr["tnt"]
|
|
||||||
state_attr[ATTR_PROCESSING] = attr["processing"]
|
|
||||||
|
|
||||||
if attr.get("breakdown") is not None:
|
|
||||||
sleep_time = sum(attr["breakdown"].values()) - attr["breakdown"]["awake"]
|
|
||||||
state_attr[ATTR_SLEEP_DUR] = sleep_time
|
|
||||||
state_attr[ATTR_LIGHT_PERC] = _get_breakdown_percent(
|
|
||||||
attr, "light", sleep_time
|
|
||||||
)
|
|
||||||
state_attr[ATTR_DEEP_PERC] = _get_breakdown_percent(
|
|
||||||
attr, "deep", sleep_time
|
|
||||||
)
|
|
||||||
state_attr[ATTR_REM_PERC] = _get_breakdown_percent(attr, "rem", sleep_time)
|
|
||||||
|
|
||||||
room_temp = _get_rounded_value(attr, "room_temp")
|
|
||||||
bed_temp = _get_rounded_value(attr, "bed_temp")
|
|
||||||
|
|
||||||
if "current" in self._sensor:
|
|
||||||
state_attr[ATTR_RESP_RATE] = _get_rounded_value(attr, "resp_rate")
|
|
||||||
state_attr[ATTR_HEART_RATE] = _get_rounded_value(attr, "heart_rate")
|
|
||||||
state_attr[ATTR_SLEEP_STAGE] = attr["stage"]
|
|
||||||
state_attr[ATTR_ROOM_TEMP] = room_temp
|
|
||||||
state_attr[ATTR_BED_TEMP] = bed_temp
|
|
||||||
elif "last" in self._sensor:
|
|
||||||
state_attr[ATTR_AVG_RESP_RATE] = _get_rounded_value(attr, "resp_rate")
|
|
||||||
state_attr[ATTR_AVG_HEART_RATE] = _get_rounded_value(attr, "heart_rate")
|
|
||||||
state_attr[ATTR_AVG_ROOM_TEMP] = room_temp
|
|
||||||
state_attr[ATTR_AVG_BED_TEMP] = bed_temp
|
|
||||||
|
|
||||||
return state_attr
|
|
||||||
|
|
||||||
|
|
||||||
class EightRoomSensor(EightSleepBaseEntity, SensorEntity):
|
|
||||||
"""Representation of an eight sleep room sensor."""
|
|
||||||
|
|
||||||
_attr_icon = "mdi:thermometer"
|
|
||||||
_attr_device_class = SensorDeviceClass.TEMPERATURE
|
|
||||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
|
||||||
_attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
entry,
|
|
||||||
coordinator: DataUpdateCoordinator,
|
|
||||||
eight: EightSleep,
|
|
||||||
sensor: str,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the sensor."""
|
|
||||||
super().__init__(entry, coordinator, eight, None, sensor)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def native_value(self) -> int | float | None:
|
|
||||||
"""Return the state of the sensor."""
|
|
||||||
return self._eight.room_temperature
|
|
|
@ -1,20 +0,0 @@
|
||||||
heat_set:
|
|
||||||
target:
|
|
||||||
entity:
|
|
||||||
integration: eight_sleep
|
|
||||||
domain: sensor
|
|
||||||
fields:
|
|
||||||
duration:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
number:
|
|
||||||
min: 0
|
|
||||||
max: 28800
|
|
||||||
unit_of_measurement: seconds
|
|
||||||
target:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
number:
|
|
||||||
min: -100
|
|
||||||
max: 100
|
|
||||||
unit_of_measurement: "°"
|
|
|
@ -1,35 +1,8 @@
|
||||||
{
|
{
|
||||||
"config": {
|
"issues": {
|
||||||
"step": {
|
"integration_removed": {
|
||||||
"user": {
|
"title": "The Eight Sleep integration has been removed",
|
||||||
"data": {
|
"description": "The Eight Sleep integration has been removed from Home Assistant.\n\nThe Eight Sleep API has changed and now requires a unique secret which is inaccessible outside of their apps.\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing Eight Sleep integration entries]({entries})."
|
||||||
"username": "[%key:common::config_flow::data::username%]",
|
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"cannot_connect": "Cannot connect to Eight Sleep cloud: {error}"
|
|
||||||
},
|
|
||||||
"abort": {
|
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
|
||||||
"cannot_connect": "[%key:component::eight_sleep::config::error::cannot_connect%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"services": {
|
|
||||||
"heat_set": {
|
|
||||||
"name": "Heat set",
|
|
||||||
"description": "Sets heating/cooling level for eight sleep.",
|
|
||||||
"fields": {
|
|
||||||
"duration": {
|
|
||||||
"name": "Duration",
|
|
||||||
"description": "Duration to heat/cool at the target level in seconds."
|
|
||||||
},
|
|
||||||
"target": {
|
|
||||||
"name": "Target",
|
|
||||||
"description": "Target cooling/heating level from -100 to 100."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -121,7 +121,6 @@ FLOWS = {
|
||||||
"ecowitt",
|
"ecowitt",
|
||||||
"edl21",
|
"edl21",
|
||||||
"efergy",
|
"efergy",
|
||||||
"eight_sleep",
|
|
||||||
"electrasmart",
|
"electrasmart",
|
||||||
"electric_kiwi",
|
"electric_kiwi",
|
||||||
"elgato",
|
"elgato",
|
||||||
|
|
|
@ -1375,12 +1375,6 @@
|
||||||
"config_flow": false,
|
"config_flow": false,
|
||||||
"iot_class": "local_polling"
|
"iot_class": "local_polling"
|
||||||
},
|
},
|
||||||
"eight_sleep": {
|
|
||||||
"name": "Eight Sleep",
|
|
||||||
"integration_type": "hub",
|
|
||||||
"config_flow": true,
|
|
||||||
"iot_class": "cloud_polling"
|
|
||||||
},
|
|
||||||
"electrasmart": {
|
"electrasmart": {
|
||||||
"name": "Electra Smart",
|
"name": "Electra Smart",
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
|
|
|
@ -1551,9 +1551,6 @@ pyControl4==1.1.0
|
||||||
# homeassistant.components.duotecno
|
# homeassistant.components.duotecno
|
||||||
pyDuotecno==2023.10.1
|
pyDuotecno==2023.10.1
|
||||||
|
|
||||||
# homeassistant.components.eight_sleep
|
|
||||||
pyEight==0.3.2
|
|
||||||
|
|
||||||
# homeassistant.components.electrasmart
|
# homeassistant.components.electrasmart
|
||||||
pyElectra==1.2.0
|
pyElectra==1.2.0
|
||||||
|
|
||||||
|
|
|
@ -1184,9 +1184,6 @@ pyControl4==1.1.0
|
||||||
# homeassistant.components.duotecno
|
# homeassistant.components.duotecno
|
||||||
pyDuotecno==2023.10.1
|
pyDuotecno==2023.10.1
|
||||||
|
|
||||||
# homeassistant.components.eight_sleep
|
|
||||||
pyEight==0.3.2
|
|
||||||
|
|
||||||
# homeassistant.components.electrasmart
|
# homeassistant.components.electrasmart
|
||||||
pyElectra==1.2.0
|
pyElectra==1.2.0
|
||||||
|
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
"""Fixtures for Eight Sleep."""
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
from pyeight.exceptions import RequestError
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="bypass", autouse=True)
|
|
||||||
def bypass_fixture():
|
|
||||||
"""Bypasses things that slow te tests down or block them from testing the behavior."""
|
|
||||||
with patch(
|
|
||||||
"homeassistant.components.eight_sleep.config_flow.EightSleep.fetch_token",
|
|
||||||
), patch(
|
|
||||||
"homeassistant.components.eight_sleep.config_flow.EightSleep.at_exit",
|
|
||||||
), patch(
|
|
||||||
"homeassistant.components.eight_sleep.async_setup_entry",
|
|
||||||
return_value=True,
|
|
||||||
):
|
|
||||||
yield
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="token_error")
|
|
||||||
def token_error_fixture():
|
|
||||||
"""Simulate error when fetching token."""
|
|
||||||
with patch(
|
|
||||||
"homeassistant.components.eight_sleep.config_flow.EightSleep.fetch_token",
|
|
||||||
side_effect=RequestError,
|
|
||||||
):
|
|
||||||
yield
|
|
|
@ -1,82 +0,0 @@
|
||||||
"""Test the Eight Sleep config flow."""
|
|
||||||
from homeassistant import config_entries
|
|
||||||
from homeassistant.components.eight_sleep.const import DOMAIN
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
|
||||||
|
|
||||||
|
|
||||||
async def test_form(hass: HomeAssistant) -> None:
|
|
||||||
"""Test we get the form."""
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
||||||
)
|
|
||||||
assert result["type"] == FlowResultType.FORM
|
|
||||||
assert result["errors"] is None
|
|
||||||
|
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
|
||||||
result["flow_id"],
|
|
||||||
{
|
|
||||||
"username": "test-username",
|
|
||||||
"password": "test-password",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
|
||||||
assert result2["title"] == "test-username"
|
|
||||||
assert result2["data"] == {
|
|
||||||
"username": "test-username",
|
|
||||||
"password": "test-password",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def test_form_invalid_auth(hass: HomeAssistant, token_error) -> None:
|
|
||||||
"""Test we handle invalid auth."""
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
||||||
)
|
|
||||||
assert result["type"] == FlowResultType.FORM
|
|
||||||
assert result["errors"] is None
|
|
||||||
|
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
|
||||||
result["flow_id"],
|
|
||||||
{
|
|
||||||
"username": "bad-username",
|
|
||||||
"password": "bad-password",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result2["type"] == FlowResultType.FORM
|
|
||||||
assert result2["errors"] == {"base": "cannot_connect"}
|
|
||||||
|
|
||||||
|
|
||||||
async def test_import(hass: HomeAssistant) -> None:
|
|
||||||
"""Test import works."""
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
|
||||||
DOMAIN,
|
|
||||||
context={"source": config_entries.SOURCE_IMPORT},
|
|
||||||
data={
|
|
||||||
"username": "test-username",
|
|
||||||
"password": "test-password",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
|
||||||
assert result["title"] == "test-username"
|
|
||||||
assert result["data"] == {
|
|
||||||
"username": "test-username",
|
|
||||||
"password": "test-password",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def test_import_invalid_auth(hass: HomeAssistant, token_error) -> None:
|
|
||||||
"""Test we handle invalid auth on import."""
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
|
||||||
DOMAIN,
|
|
||||||
context={"source": config_entries.SOURCE_IMPORT},
|
|
||||||
data={
|
|
||||||
"username": "bad-username",
|
|
||||||
"password": "bad-password",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assert result["type"] == FlowResultType.ABORT
|
|
||||||
assert result["reason"] == "cannot_connect"
|
|
50
tests/components/eight_sleep/test_init.py
Normal file
50
tests/components/eight_sleep/test_init.py
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
"""Tests for the Eight Sleep integration."""
|
||||||
|
|
||||||
|
from homeassistant.components.eight_sleep import DOMAIN
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import issue_registry as ir
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_mazda_repair_issue(
|
||||||
|
hass: HomeAssistant, issue_registry: ir.IssueRegistry
|
||||||
|
) -> None:
|
||||||
|
"""Test the Eight Sleep configuration entry loading/unloading handles the repair."""
|
||||||
|
config_entry_1 = MockConfigEntry(
|
||||||
|
title="Example 1",
|
||||||
|
domain=DOMAIN,
|
||||||
|
)
|
||||||
|
config_entry_1.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(config_entry_1.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert config_entry_1.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
# Add a second one
|
||||||
|
config_entry_2 = MockConfigEntry(
|
||||||
|
title="Example 2",
|
||||||
|
domain=DOMAIN,
|
||||||
|
)
|
||||||
|
config_entry_2.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(config_entry_2.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert config_entry_2.state is ConfigEntryState.LOADED
|
||||||
|
assert issue_registry.async_get_issue(DOMAIN, DOMAIN)
|
||||||
|
|
||||||
|
# Remove the first one
|
||||||
|
await hass.config_entries.async_remove(config_entry_1.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert config_entry_1.state is ConfigEntryState.NOT_LOADED
|
||||||
|
assert config_entry_2.state is ConfigEntryState.LOADED
|
||||||
|
assert issue_registry.async_get_issue(DOMAIN, DOMAIN)
|
||||||
|
|
||||||
|
# Remove the second one
|
||||||
|
await hass.config_entries.async_remove(config_entry_2.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert config_entry_1.state is ConfigEntryState.NOT_LOADED
|
||||||
|
assert config_entry_2.state is ConfigEntryState.NOT_LOADED
|
||||||
|
assert issue_registry.async_get_issue(DOMAIN, DOMAIN) is None
|
Loading…
Add table
Reference in a new issue