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

View file

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

View file

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

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."""
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
)

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",
"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": []
}

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": {
"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})."
}
}
}

View file

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

View file

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

View file

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

View file

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

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