Remove eight_sleep integration (#102669)

This commit is contained in:
Raman Gupta 2023-10-25 00:13:10 -04:00 committed by GitHub
parent 626123acc0
commit aa36229519
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 83 additions and 856 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -121,7 +121,6 @@ FLOWS = {
"ecowitt", "ecowitt",
"edl21", "edl21",
"efergy", "efergy",
"eight_sleep",
"electrasmart", "electrasmart",
"electric_kiwi", "electric_kiwi",
"elgato", "elgato",

View file

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

View file

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

View file

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

View file

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

View file

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

View 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