diff --git a/.coveragerc b/.coveragerc index 9634ca2edb8..eaae941c70d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -286,9 +286,6 @@ omit = homeassistant/components/edl21/__init__.py homeassistant/components/edl21/sensor.py 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/api.py homeassistant/components/electric_kiwi/oauth2.py diff --git a/CODEOWNERS b/CODEOWNERS index 5441fea97d1..62dccee04c7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -319,8 +319,6 @@ build.json @home-assistant/supervisor /homeassistant/components/efergy/ @tkdrob /tests/components/efergy/ @tkdrob /homeassistant/components/egardia/ @jeroenterheerdt -/homeassistant/components/eight_sleep/ @mezz64 @raman325 -/tests/components/eight_sleep/ @mezz64 @raman325 /homeassistant/components/electrasmart/ @jafar-atili /tests/components/electrasmart/ @jafar-atili /homeassistant/components/electric_kiwi/ @mikey0000 diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index b8066f2eb31..ab5eff3b60f 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -1,222 +1,37 @@ -"""Support for Eight smart mattress covers and mattresses.""" +"""The Eight Sleep integration.""" from __future__ import annotations -from dataclasses import dataclass -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.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError -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 homeassistant.helpers import issue_registry as ir -from .const import DOMAIN, NAME_MAP - -_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, -) +DOMAIN = "eight_sleep" -@dataclass -class EightSleepConfigEntryData: - """Data used for all entities for a given config entry.""" - - 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( +async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool: + """Set up Eight Sleep from a config entry.""" + ir.async_create_issue( hass, - _LOGGER, - name=f"{DOMAIN}_heat", - update_interval=HEAT_SCAN_INTERVAL, - update_method=eight.update_device_data, + DOMAIN, + DOMAIN, + is_fixable=False, + 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 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - # stop the API before unloading everything - config_entry_data: EightSleepConfigEntryData = hass.data[DOMAIN][entry.entry_id] - await config_entry_data.api.stop() - hass.data[DOMAIN].pop(entry.entry_id) - if not hass.data[DOMAIN]: - hass.data.pop(DOMAIN) + if all( + config_entry.state is ConfigEntryState.NOT_LOADED + for config_entry in hass.config_entries.async_entries(DOMAIN) + if config_entry.entry_id != entry.entry_id + ): + ir.async_delete_issue(hass, DOMAIN, DOMAIN) - return unload_ok - - -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() + return True diff --git a/homeassistant/components/eight_sleep/binary_sensor.py b/homeassistant/components/eight_sleep/binary_sensor.py deleted file mode 100644 index 7ad1b882008..00000000000 --- a/homeassistant/components/eight_sleep/binary_sensor.py +++ /dev/null @@ -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) diff --git a/homeassistant/components/eight_sleep/config_flow.py b/homeassistant/components/eight_sleep/config_flow.py index 504fbeb2817..8839cdf4719 100644 --- a/homeassistant/components/eight_sleep/config_flow.py +++ b/homeassistant/components/eight_sleep/config_flow.py @@ -1,90 +1,11 @@ -"""Config flow for Eight Sleep integration.""" -from __future__ import annotations +"""The Eight Sleep integration config flow.""" -import logging -from typing import Any +from homeassistant.config_entries import ConfigFlow -from pyeight.eight import EightSleep -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) - ), - } -) +from . import DOMAIN -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class EightSleepConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Eight Sleep.""" 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 - ) diff --git a/homeassistant/components/eight_sleep/const.py b/homeassistant/components/eight_sleep/const.py deleted file mode 100644 index 23689066665..00000000000 --- a/homeassistant/components/eight_sleep/const.py +++ /dev/null @@ -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" diff --git a/homeassistant/components/eight_sleep/manifest.json b/homeassistant/components/eight_sleep/manifest.json index 71e01f75d46..a4f7482c920 100644 --- a/homeassistant/components/eight_sleep/manifest.json +++ b/homeassistant/components/eight_sleep/manifest.json @@ -1,10 +1,9 @@ { "domain": "eight_sleep", "name": "Eight Sleep", - "codeowners": ["@mezz64", "@raman325"], - "config_flow": true, + "codeowners": [], "documentation": "https://www.home-assistant.io/integrations/eight_sleep", + "integration_type": "system", "iot_class": "cloud_polling", - "loggers": ["pyeight"], - "requirements": ["pyEight==0.3.2"] + "requirements": [] } diff --git a/homeassistant/components/eight_sleep/sensor.py b/homeassistant/components/eight_sleep/sensor.py deleted file mode 100644 index e546318a4dd..00000000000 --- a/homeassistant/components/eight_sleep/sensor.py +++ /dev/null @@ -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 diff --git a/homeassistant/components/eight_sleep/services.yaml b/homeassistant/components/eight_sleep/services.yaml deleted file mode 100644 index b191187bb0a..00000000000 --- a/homeassistant/components/eight_sleep/services.yaml +++ /dev/null @@ -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: "°" diff --git a/homeassistant/components/eight_sleep/strings.json b/homeassistant/components/eight_sleep/strings.json index b2fb73cc020..15773084462 100644 --- a/homeassistant/components/eight_sleep/strings.json +++ b/homeassistant/components/eight_sleep/strings.json @@ -1,35 +1,8 @@ { - "config": { - "step": { - "user": { - "data": { - "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." - } - } + "issues": { + "integration_removed": { + "title": "The Eight Sleep integration has been removed", + "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})." } } } diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 64806d8fb86..88706ed4c94 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -121,7 +121,6 @@ FLOWS = { "ecowitt", "edl21", "efergy", - "eight_sleep", "electrasmart", "electric_kiwi", "elgato", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index b89139d7447..36ef89216e2 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1375,12 +1375,6 @@ "config_flow": false, "iot_class": "local_polling" }, - "eight_sleep": { - "name": "Eight Sleep", - "integration_type": "hub", - "config_flow": true, - "iot_class": "cloud_polling" - }, "electrasmart": { "name": "Electra Smart", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index 65d5ca29ca5..a4c682fc5c8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1551,9 +1551,6 @@ pyControl4==1.1.0 # homeassistant.components.duotecno pyDuotecno==2023.10.1 -# homeassistant.components.eight_sleep -pyEight==0.3.2 - # homeassistant.components.electrasmart pyElectra==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b89c7579d32..30e153543c6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1184,9 +1184,6 @@ pyControl4==1.1.0 # homeassistant.components.duotecno pyDuotecno==2023.10.1 -# homeassistant.components.eight_sleep -pyEight==0.3.2 - # homeassistant.components.electrasmart pyElectra==1.2.0 diff --git a/tests/components/eight_sleep/conftest.py b/tests/components/eight_sleep/conftest.py deleted file mode 100644 index 753fe1e30d5..00000000000 --- a/tests/components/eight_sleep/conftest.py +++ /dev/null @@ -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 diff --git a/tests/components/eight_sleep/test_config_flow.py b/tests/components/eight_sleep/test_config_flow.py deleted file mode 100644 index 6a64f6a5731..00000000000 --- a/tests/components/eight_sleep/test_config_flow.py +++ /dev/null @@ -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" diff --git a/tests/components/eight_sleep/test_init.py b/tests/components/eight_sleep/test_init.py new file mode 100644 index 00000000000..6b94ff31139 --- /dev/null +++ b/tests/components/eight_sleep/test_init.py @@ -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