bump
This commit is contained in:
@@ -0,0 +1,165 @@
|
|||||||
|
"""Support for SleepIQ from SleepNumber."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from asyncsleepiq import (
|
||||||
|
AsyncSleepIQ,
|
||||||
|
SleepIQAPIException,
|
||||||
|
SleepIQLoginException,
|
||||||
|
SleepIQTimeoutException,
|
||||||
|
)
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, PRESSURE, Platform
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||||
|
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||||
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
|
from .const import DOMAIN, IS_IN_BED, SLEEP_NUMBER
|
||||||
|
from .coordinator import (
|
||||||
|
SleepIQConfigEntry,
|
||||||
|
SleepIQData,
|
||||||
|
SleepIQDataUpdateCoordinator,
|
||||||
|
SleepIQPauseUpdateCoordinator,
|
||||||
|
SleepIQSleepDataCoordinator,
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PLATFORMS = [
|
||||||
|
Platform.BINARY_SENSOR,
|
||||||
|
Platform.BUTTON,
|
||||||
|
Platform.LIGHT,
|
||||||
|
Platform.NUMBER,
|
||||||
|
Platform.SELECT,
|
||||||
|
Platform.SENSOR,
|
||||||
|
Platform.SWITCH,
|
||||||
|
]
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
DOMAIN: {
|
||||||
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extra=vol.ALLOW_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
"""Set up sleepiq component."""
|
||||||
|
if DOMAIN in config:
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: SleepIQConfigEntry) -> bool:
|
||||||
|
"""Set up the SleepIQ config entry."""
|
||||||
|
conf = entry.data
|
||||||
|
email = conf[CONF_USERNAME]
|
||||||
|
password = conf[CONF_PASSWORD]
|
||||||
|
|
||||||
|
client_session = async_create_clientsession(hass)
|
||||||
|
|
||||||
|
gateway = AsyncSleepIQ(client_session=client_session)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await gateway.login(email, password)
|
||||||
|
except SleepIQLoginException as err:
|
||||||
|
_LOGGER.error("Could not authenticate with SleepIQ server")
|
||||||
|
raise ConfigEntryAuthFailed(err) from err
|
||||||
|
except SleepIQTimeoutException as err:
|
||||||
|
raise ConfigEntryNotReady(
|
||||||
|
str(err) or "Timed out during authentication"
|
||||||
|
) from err
|
||||||
|
|
||||||
|
try:
|
||||||
|
await gateway.init_beds()
|
||||||
|
except SleepIQTimeoutException as err:
|
||||||
|
raise ConfigEntryNotReady(
|
||||||
|
str(err) or "Timed out during initialization"
|
||||||
|
) from err
|
||||||
|
except SleepIQAPIException as err:
|
||||||
|
raise ConfigEntryNotReady(str(err) or "Error reading from SleepIQ API") from err
|
||||||
|
|
||||||
|
await _async_migrate_unique_ids(hass, entry, gateway)
|
||||||
|
|
||||||
|
coordinator = SleepIQDataUpdateCoordinator(hass, entry, gateway)
|
||||||
|
pause_coordinator = SleepIQPauseUpdateCoordinator(hass, entry, gateway)
|
||||||
|
sleep_data_coordinator = SleepIQSleepDataCoordinator(hass, entry, gateway)
|
||||||
|
|
||||||
|
# Call the SleepIQ API to refresh data
|
||||||
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
await pause_coordinator.async_config_entry_first_refresh()
|
||||||
|
await sleep_data_coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
|
entry.runtime_data = SleepIQData(
|
||||||
|
data_coordinator=coordinator,
|
||||||
|
pause_coordinator=pause_coordinator,
|
||||||
|
sleep_data_coordinator=sleep_data_coordinator,
|
||||||
|
client=gateway,
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: SleepIQConfigEntry) -> bool:
|
||||||
|
"""Unload the config entry."""
|
||||||
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_migrate_unique_ids(
|
||||||
|
hass: HomeAssistant, entry: ConfigEntry, gateway: AsyncSleepIQ
|
||||||
|
) -> None:
|
||||||
|
"""Migrate old unique ids."""
|
||||||
|
names_to_ids = {
|
||||||
|
sleeper.name: sleeper.sleeper_id
|
||||||
|
for bed in gateway.beds.values()
|
||||||
|
for sleeper in bed.sleepers
|
||||||
|
}
|
||||||
|
|
||||||
|
bed_ids = {bed.id for bed in gateway.beds.values()}
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_migrator(entity_entry: er.RegistryEntry) -> dict[str, Any] | None:
|
||||||
|
# Old format for sleeper entities was {bed_id}_{sleeper.name}_{sensor_type}.....
|
||||||
|
# New format is {sleeper.sleeper_id}_{sensor_type}....
|
||||||
|
sensor_types = [IS_IN_BED, PRESSURE, SLEEP_NUMBER]
|
||||||
|
|
||||||
|
old_unique_id = entity_entry.unique_id
|
||||||
|
parts = old_unique_id.split("_")
|
||||||
|
|
||||||
|
# If it doesn't begin with a bed id or end with one of the sensor types,
|
||||||
|
# it doesn't need to be migrated
|
||||||
|
if parts[0] not in bed_ids or not old_unique_id.endswith(tuple(sensor_types)):
|
||||||
|
return None
|
||||||
|
|
||||||
|
sensor_type = next(filter(old_unique_id.endswith, sensor_types))
|
||||||
|
sleeper_name = "_".join(parts[1:]).removesuffix(f"_{sensor_type}")
|
||||||
|
sleeper_id = names_to_ids.get(sleeper_name)
|
||||||
|
|
||||||
|
if not sleeper_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
new_unique_id = f"{sleeper_id}_{sensor_type}"
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Migrating unique_id from [%s] to [%s]",
|
||||||
|
old_unique_id,
|
||||||
|
new_unique_id,
|
||||||
|
)
|
||||||
|
return {"new_unique_id": new_unique_id}
|
||||||
|
|
||||||
|
await er.async_migrate_entries(hass, entry.entry_id, _async_migrator)
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""Support for SleepIQ sensors."""
|
||||||
|
|
||||||
|
from typing import override
|
||||||
|
|
||||||
|
from asyncsleepiq import SleepIQBed, SleepIQSleeper
|
||||||
|
|
||||||
|
from homeassistant.components.binary_sensor import (
|
||||||
|
BinarySensorDeviceClass,
|
||||||
|
BinarySensorEntity,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from .const import ICON_EMPTY, ICON_OCCUPIED, IS_IN_BED
|
||||||
|
from .coordinator import SleepIQConfigEntry, SleepIQDataUpdateCoordinator
|
||||||
|
from .entity import SleepIQSleeperEntity
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: SleepIQConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the SleepIQ bed binary sensors."""
|
||||||
|
data = entry.runtime_data
|
||||||
|
async_add_entities(
|
||||||
|
IsInBedBinarySensor(data.data_coordinator, bed, sleeper)
|
||||||
|
for bed in data.client.beds.values()
|
||||||
|
for sleeper in bed.sleepers
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class IsInBedBinarySensor(
|
||||||
|
SleepIQSleeperEntity[SleepIQDataUpdateCoordinator], BinarySensorEntity
|
||||||
|
):
|
||||||
|
"""Implementation of a SleepIQ presence sensor."""
|
||||||
|
|
||||||
|
_attr_device_class = BinarySensorDeviceClass.OCCUPANCY
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: SleepIQDataUpdateCoordinator,
|
||||||
|
bed: SleepIQBed,
|
||||||
|
sleeper: SleepIQSleeper,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
super().__init__(coordinator, bed, sleeper, IS_IN_BED)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
@override
|
||||||
|
def _async_update_attrs(self) -> None:
|
||||||
|
"""Update sensor attributes."""
|
||||||
|
self._attr_is_on = self.sleeper.in_bed
|
||||||
|
self._attr_icon = ICON_OCCUPIED if self.sleeper.in_bed else ICON_EMPTY
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
"""Support for SleepIQ buttons."""
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, override
|
||||||
|
|
||||||
|
from asyncsleepiq import SleepIQBed
|
||||||
|
|
||||||
|
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from .coordinator import SleepIQConfigEntry
|
||||||
|
from .entity import SleepIQEntity
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class SleepIQButtonEntityDescription(ButtonEntityDescription):
|
||||||
|
"""Class to describe a Button entity."""
|
||||||
|
|
||||||
|
press_action: Callable[[SleepIQBed], Any]
|
||||||
|
|
||||||
|
|
||||||
|
ENTITY_DESCRIPTIONS = [
|
||||||
|
SleepIQButtonEntityDescription(
|
||||||
|
key="calibrate",
|
||||||
|
name="Calibrate",
|
||||||
|
press_action=lambda client: client.calibrate(),
|
||||||
|
icon="mdi:target",
|
||||||
|
),
|
||||||
|
SleepIQButtonEntityDescription(
|
||||||
|
key="stop-pump",
|
||||||
|
name="Stop Pump",
|
||||||
|
press_action=lambda client: client.stop_pump(),
|
||||||
|
icon="mdi:stop",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: SleepIQConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the sleep number buttons."""
|
||||||
|
data = entry.runtime_data
|
||||||
|
|
||||||
|
async_add_entities(
|
||||||
|
SleepNumberButton(bed, ed)
|
||||||
|
for bed in data.client.beds.values()
|
||||||
|
for ed in ENTITY_DESCRIPTIONS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SleepNumberButton(SleepIQEntity, ButtonEntity):
|
||||||
|
"""Representation of an SleepIQ button."""
|
||||||
|
|
||||||
|
entity_description: SleepIQButtonEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, bed: SleepIQBed, entity_description: SleepIQButtonEntityDescription
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the Button."""
|
||||||
|
super().__init__(bed)
|
||||||
|
self._attr_name = f"SleepNumber {bed.name} {entity_description.name}"
|
||||||
|
self._attr_unique_id = f"{bed.id}-{entity_description.key}"
|
||||||
|
self.entity_description = entity_description
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def async_press(self) -> None:
|
||||||
|
"""Press the button."""
|
||||||
|
await self.entity_description.press_action(self.bed)
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
"""Config flow to configure SleepIQ component."""
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
import logging
|
||||||
|
from typing import Any, override
|
||||||
|
|
||||||
|
from asyncsleepiq import AsyncSleepIQ, SleepIQLoginException, SleepIQTimeoutException
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SleepIQFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a SleepIQ config flow."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
|
||||||
|
"""Import a SleepIQ account as a config entry.
|
||||||
|
|
||||||
|
This flow is triggered by 'async_setup' for configured accounts.
|
||||||
|
"""
|
||||||
|
await self.async_set_unique_id(import_data[CONF_USERNAME].lower())
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
if error := await try_connection(self.hass, import_data):
|
||||||
|
_LOGGER.error("Could not authenticate with SleepIQ server: %s", error)
|
||||||
|
return self.async_abort(reason=error)
|
||||||
|
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=import_data[CONF_USERNAME], data=import_data
|
||||||
|
)
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle a flow initialized by the user."""
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
# Don't allow multiple instances with the same username
|
||||||
|
await self.async_set_unique_id(user_input[CONF_USERNAME].lower())
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
if error := await try_connection(self.hass, user_input):
|
||||||
|
errors["base"] = error
|
||||||
|
else:
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=user_input[CONF_USERNAME], data=user_input
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
user_input = {}
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(
|
||||||
|
CONF_USERNAME,
|
||||||
|
default=user_input.get(CONF_USERNAME),
|
||||||
|
): str,
|
||||||
|
vol.Required(CONF_PASSWORD): str,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
errors=errors,
|
||||||
|
last_step=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_reauth(
|
||||||
|
self, entry_data: Mapping[str, Any]
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Perform reauth upon an API authentication error."""
|
||||||
|
return await self.async_step_reauth_confirm()
|
||||||
|
|
||||||
|
async def async_step_reauth_confirm(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Confirm reauth."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
|
reauth_entry = self._get_reauth_entry()
|
||||||
|
if user_input is not None:
|
||||||
|
data = {
|
||||||
|
CONF_USERNAME: reauth_entry.data[CONF_USERNAME],
|
||||||
|
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||||
|
}
|
||||||
|
|
||||||
|
if not (error := await try_connection(self.hass, data)):
|
||||||
|
return self.async_update_reload_and_abort(reauth_entry, data=data)
|
||||||
|
errors["base"] = error
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="reauth_confirm",
|
||||||
|
data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}),
|
||||||
|
errors=errors,
|
||||||
|
description_placeholders={
|
||||||
|
CONF_USERNAME: reauth_entry.data[CONF_USERNAME],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def try_connection(hass: HomeAssistant, user_input: dict[str, Any]) -> str | None:
|
||||||
|
"""Test if the given credentials can successfully login to SleepIQ."""
|
||||||
|
|
||||||
|
client_session = async_get_clientsession(hass)
|
||||||
|
|
||||||
|
gateway = AsyncSleepIQ(client_session=client_session)
|
||||||
|
try:
|
||||||
|
await gateway.login(user_input[CONF_USERNAME], user_input[CONF_PASSWORD])
|
||||||
|
except SleepIQLoginException:
|
||||||
|
return "invalid_auth"
|
||||||
|
except SleepIQTimeoutException:
|
||||||
|
return "cannot_connect"
|
||||||
|
|
||||||
|
return None
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"""Define constants for the SleepIQ component."""
|
||||||
|
|
||||||
|
from homeassistant.const import PRESSURE
|
||||||
|
|
||||||
|
DATA_SLEEPIQ = "data_sleepiq"
|
||||||
|
DOMAIN = "sleepiq"
|
||||||
|
|
||||||
|
ACTUATOR = "actuator"
|
||||||
|
CORE_CLIMATE_TIMER = "core_climate_timer"
|
||||||
|
CORE_CLIMATE = "core_climate"
|
||||||
|
BED = "bed"
|
||||||
|
FIRMNESS = "firmness"
|
||||||
|
ICON_EMPTY = "mdi:bed-empty"
|
||||||
|
ICON_OCCUPIED = "mdi:bed"
|
||||||
|
IS_IN_BED = "is_in_bed"
|
||||||
|
SLEEP_NUMBER = "sleep_number"
|
||||||
|
FOOT_WARMING_TIMER = "foot_warming_timer"
|
||||||
|
FOOT_WARMER = "foot_warmer"
|
||||||
|
SLEEP_SCORE = "sleep_score"
|
||||||
|
SLEEP_DURATION = "sleep_duration"
|
||||||
|
HEART_RATE = "heart_rate"
|
||||||
|
RESPIRATORY_RATE = "respiratory_rate"
|
||||||
|
HRV = "hrv"
|
||||||
|
ENTITY_TYPES = {
|
||||||
|
ACTUATOR: "Position",
|
||||||
|
CORE_CLIMATE_TIMER: "Core Climate Timer",
|
||||||
|
CORE_CLIMATE: "Core Climate",
|
||||||
|
FIRMNESS: "Firmness",
|
||||||
|
PRESSURE: "Pressure",
|
||||||
|
IS_IN_BED: "Is In Bed",
|
||||||
|
SLEEP_NUMBER: "SleepNumber",
|
||||||
|
FOOT_WARMING_TIMER: "Foot Warming Timer",
|
||||||
|
FOOT_WARMER: "Foot Warmer",
|
||||||
|
SLEEP_SCORE: "Sleep Score",
|
||||||
|
SLEEP_DURATION: "Sleep Duration",
|
||||||
|
HEART_RATE: "Heart Rate Average",
|
||||||
|
RESPIRATORY_RATE: "Respiratory Rate Average",
|
||||||
|
HRV: "Heart Rate Variability",
|
||||||
|
}
|
||||||
|
|
||||||
|
LEFT = "left"
|
||||||
|
RIGHT = "right"
|
||||||
|
SIDES = [LEFT, RIGHT]
|
||||||
|
|
||||||
|
SLEEPIQ_DATA = "sleepiq_data"
|
||||||
|
SLEEPIQ_STATUS_COORDINATOR = "sleepiq_status"
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
"""Coordinator for SleepIQ."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
from typing import override
|
||||||
|
|
||||||
|
from asyncsleepiq import AsyncSleepIQ, SleepIQAPIException, SleepIQTimeoutException
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_USERNAME
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
UPDATE_INTERVAL = timedelta(seconds=60)
|
||||||
|
LONGER_UPDATE_INTERVAL = timedelta(minutes=5)
|
||||||
|
SLEEP_DATA_UPDATE_INTERVAL = timedelta(hours=1) # Sleep data doesn't change frequently
|
||||||
|
|
||||||
|
type SleepIQConfigEntry = ConfigEntry[SleepIQData]
|
||||||
|
|
||||||
|
|
||||||
|
class SleepIQDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||||
|
"""SleepIQ data update coordinator."""
|
||||||
|
|
||||||
|
config_entry: SleepIQConfigEntry
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: SleepIQConfigEntry,
|
||||||
|
client: AsyncSleepIQ,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize coordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
config_entry=config_entry,
|
||||||
|
name=f"{config_entry.data[CONF_USERNAME]}@SleepIQ",
|
||||||
|
update_interval=UPDATE_INTERVAL,
|
||||||
|
)
|
||||||
|
self.client = client
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def _async_update_data(self) -> None:
|
||||||
|
tasks = [self.client.fetch_bed_statuses()] + [
|
||||||
|
bed.foundation.update_foundation_status()
|
||||||
|
for bed in self.client.beds.values()
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
await asyncio.gather(*tasks)
|
||||||
|
except SleepIQTimeoutException as err:
|
||||||
|
raise UpdateFailed(f"Timed out fetching SleepIQ data: {err}") from err
|
||||||
|
except SleepIQAPIException as err:
|
||||||
|
raise UpdateFailed(f"Failed to fetch SleepIQ data: {err}") from err
|
||||||
|
|
||||||
|
|
||||||
|
class SleepIQPauseUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||||
|
"""SleepIQ pause update coordinator."""
|
||||||
|
|
||||||
|
config_entry: SleepIQConfigEntry
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: SleepIQConfigEntry,
|
||||||
|
client: AsyncSleepIQ,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize coordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
config_entry=config_entry,
|
||||||
|
name=f"{config_entry.data[CONF_USERNAME]}@SleepIQPause",
|
||||||
|
update_interval=LONGER_UPDATE_INTERVAL,
|
||||||
|
)
|
||||||
|
self.client = client
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def _async_update_data(self) -> None:
|
||||||
|
try:
|
||||||
|
await asyncio.gather(
|
||||||
|
*[bed.fetch_pause_mode() for bed in self.client.beds.values()]
|
||||||
|
)
|
||||||
|
except SleepIQTimeoutException as err:
|
||||||
|
raise UpdateFailed(f"Timed out fetching SleepIQ pause data: {err}") from err
|
||||||
|
except SleepIQAPIException as err:
|
||||||
|
raise UpdateFailed(f"Failed to fetch SleepIQ pause data: {err}") from err
|
||||||
|
|
||||||
|
|
||||||
|
class SleepIQSleepDataCoordinator(DataUpdateCoordinator[None]):
|
||||||
|
"""SleepIQ sleep health data coordinator."""
|
||||||
|
|
||||||
|
config_entry: SleepIQConfigEntry
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: SleepIQConfigEntry,
|
||||||
|
client: AsyncSleepIQ,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize coordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
config_entry=config_entry,
|
||||||
|
name=f"{config_entry.data[CONF_USERNAME]}@SleepIQSleepData",
|
||||||
|
update_interval=SLEEP_DATA_UPDATE_INTERVAL,
|
||||||
|
)
|
||||||
|
self.client = client
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def _async_update_data(self) -> None:
|
||||||
|
"""Fetch sleep health data from API via asyncsleepiq library."""
|
||||||
|
try:
|
||||||
|
await asyncio.gather(
|
||||||
|
*[
|
||||||
|
sleeper.fetch_sleep_data()
|
||||||
|
for bed in self.client.beds.values()
|
||||||
|
for sleeper in bed.sleepers
|
||||||
|
]
|
||||||
|
)
|
||||||
|
except SleepIQTimeoutException as err:
|
||||||
|
raise UpdateFailed(f"Timed out fetching SleepIQ sleep data: {err}") from err
|
||||||
|
except SleepIQAPIException as err:
|
||||||
|
raise UpdateFailed(f"Failed to fetch SleepIQ sleep data: {err}") from err
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SleepIQData:
|
||||||
|
"""Data for the sleepiq integration."""
|
||||||
|
|
||||||
|
data_coordinator: SleepIQDataUpdateCoordinator
|
||||||
|
pause_coordinator: SleepIQPauseUpdateCoordinator
|
||||||
|
sleep_data_coordinator: SleepIQSleepDataCoordinator
|
||||||
|
client: AsyncSleepIQ
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
"""Entity for the SleepIQ integration."""
|
||||||
|
|
||||||
|
from abc import abstractmethod
|
||||||
|
from typing import override
|
||||||
|
|
||||||
|
from asyncsleepiq import SleepIQBed, SleepIQSleeper
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from .const import ENTITY_TYPES, ICON_OCCUPIED
|
||||||
|
from .coordinator import (
|
||||||
|
SleepIQDataUpdateCoordinator,
|
||||||
|
SleepIQPauseUpdateCoordinator,
|
||||||
|
SleepIQSleepDataCoordinator,
|
||||||
|
)
|
||||||
|
|
||||||
|
type _DataCoordinatorType = (
|
||||||
|
SleepIQDataUpdateCoordinator
|
||||||
|
| SleepIQPauseUpdateCoordinator
|
||||||
|
| SleepIQSleepDataCoordinator
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def device_from_bed(bed: SleepIQBed) -> DeviceInfo:
|
||||||
|
"""Create a device given a bed."""
|
||||||
|
return DeviceInfo(
|
||||||
|
connections={(dr.CONNECTION_NETWORK_MAC, bed.mac_addr)},
|
||||||
|
manufacturer="SleepNumber",
|
||||||
|
name=bed.name,
|
||||||
|
model=bed.model,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sleeper_for_side(bed: SleepIQBed, side: str) -> SleepIQSleeper:
|
||||||
|
"""Find the sleeper for a side or the first sleeper."""
|
||||||
|
for sleeper in bed.sleepers:
|
||||||
|
if sleeper.side == side:
|
||||||
|
return sleeper
|
||||||
|
return bed.sleepers[0]
|
||||||
|
|
||||||
|
|
||||||
|
class SleepIQEntity(Entity):
|
||||||
|
"""Implementation of a SleepIQ entity."""
|
||||||
|
|
||||||
|
def __init__(self, bed: SleepIQBed) -> None:
|
||||||
|
"""Initialize the SleepIQ entity."""
|
||||||
|
self.bed = bed
|
||||||
|
self._attr_device_info = device_from_bed(bed)
|
||||||
|
|
||||||
|
|
||||||
|
class SleepIQBedEntity[_SleepIQCoordinatorT: _DataCoordinatorType](
|
||||||
|
CoordinatorEntity[_SleepIQCoordinatorT]
|
||||||
|
):
|
||||||
|
"""Implementation of a SleepIQ sensor."""
|
||||||
|
|
||||||
|
_attr_icon = ICON_OCCUPIED
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: _SleepIQCoordinatorT,
|
||||||
|
bed: SleepIQBed,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the SleepIQ sensor entity."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self.bed = bed
|
||||||
|
self._attr_device_info = device_from_bed(bed)
|
||||||
|
self._async_update_attrs()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
@override
|
||||||
|
def _handle_coordinator_update(self) -> None:
|
||||||
|
"""Handle updated data from the coordinator."""
|
||||||
|
self._async_update_attrs()
|
||||||
|
super()._handle_coordinator_update()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
@abstractmethod
|
||||||
|
def _async_update_attrs(self) -> None:
|
||||||
|
"""Update sensor attributes."""
|
||||||
|
|
||||||
|
|
||||||
|
class SleepIQSleeperEntity[_SleepIQCoordinatorT: _DataCoordinatorType](
|
||||||
|
SleepIQBedEntity[_SleepIQCoordinatorT]
|
||||||
|
):
|
||||||
|
"""Implementation of a SleepIQ sensor."""
|
||||||
|
|
||||||
|
_attr_icon = ICON_OCCUPIED
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: _SleepIQCoordinatorT,
|
||||||
|
bed: SleepIQBed,
|
||||||
|
sleeper: SleepIQSleeper,
|
||||||
|
name: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the SleepIQ sensor entity."""
|
||||||
|
self.sleeper = sleeper
|
||||||
|
super().__init__(coordinator, bed)
|
||||||
|
|
||||||
|
self._attr_name = f"SleepNumber {bed.name} {sleeper.name} {ENTITY_TYPES[name]}"
|
||||||
|
self._attr_unique_id = f"{sleeper.sleeper_id}_{name}"
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"heart_rate_avg": {
|
||||||
|
"default": "mdi:heart-pulse"
|
||||||
|
},
|
||||||
|
"hrv": {
|
||||||
|
"default": "mdi:heart-flash"
|
||||||
|
},
|
||||||
|
"respiratory_rate_avg": {
|
||||||
|
"default": "mdi:lungs"
|
||||||
|
},
|
||||||
|
"sleep_duration": {
|
||||||
|
"default": "mdi:sleep"
|
||||||
|
},
|
||||||
|
"sleep_score": {
|
||||||
|
"default": "mdi:sleep"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
"""Support for SleepIQ outlet lights."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any, override
|
||||||
|
|
||||||
|
from asyncsleepiq import SleepIQBed, SleepIQLight
|
||||||
|
|
||||||
|
from homeassistant.components.light import ColorMode, LightEntity
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from .coordinator import SleepIQConfigEntry, SleepIQDataUpdateCoordinator
|
||||||
|
from .entity import SleepIQBedEntity
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: SleepIQConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the SleepIQ bed lights."""
|
||||||
|
data = entry.runtime_data
|
||||||
|
async_add_entities(
|
||||||
|
SleepIQLightEntity(data.data_coordinator, bed, light)
|
||||||
|
for bed in data.client.beds.values()
|
||||||
|
for light in bed.foundation.lights
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SleepIQLightEntity(SleepIQBedEntity[SleepIQDataUpdateCoordinator], LightEntity):
|
||||||
|
"""Representation of a light."""
|
||||||
|
|
||||||
|
_attr_color_mode = ColorMode.ONOFF
|
||||||
|
_attr_supported_color_modes = {ColorMode.ONOFF}
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: SleepIQDataUpdateCoordinator,
|
||||||
|
bed: SleepIQBed,
|
||||||
|
light: SleepIQLight,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the light."""
|
||||||
|
self.light = light
|
||||||
|
super().__init__(coordinator, bed)
|
||||||
|
self._attr_name = f"SleepNumber {bed.name} Light {light.outlet_id}"
|
||||||
|
self._attr_unique_id = f"{bed.id}-light-{light.outlet_id}" # pylint: disable=home-assistant-entity-unique-id-redundant-platform
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn on light."""
|
||||||
|
await self.light.turn_on()
|
||||||
|
self._handle_coordinator_update()
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn off light."""
|
||||||
|
await self.light.turn_off()
|
||||||
|
self._handle_coordinator_update()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
@override
|
||||||
|
def _async_update_attrs(self) -> None:
|
||||||
|
"""Update light attributes."""
|
||||||
|
self._attr_is_on = self.light.is_on
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"domain": "kitty_sleepiq",
|
||||||
|
"name": "Kitty SleepIQ",
|
||||||
|
"issue_tracker": "https://winslowthehedgehog.com/issues",
|
||||||
|
"version": "2026.06.30",
|
||||||
|
"codeowners": ["@nuggets_home_maryishan"],
|
||||||
|
"config_flow": true,
|
||||||
|
"dhcp": [
|
||||||
|
{
|
||||||
|
"macaddress": "64DBA0*"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/sleepiq",
|
||||||
|
"integration_type": "hub",
|
||||||
|
"iot_class": "cloud_polling",
|
||||||
|
"loggers": ["asyncsleepiq"],
|
||||||
|
"requirements": ["asyncsleepiq==1.7.1"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
"""Support for SleepIQ SleepNumber firmness number entities."""
|
||||||
|
|
||||||
|
from collections.abc import Callable, Coroutine
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, cast, override
|
||||||
|
|
||||||
|
from asyncsleepiq import (
|
||||||
|
CoreTemps,
|
||||||
|
FootWarmingTemps,
|
||||||
|
SleepIQActuator,
|
||||||
|
SleepIQBed,
|
||||||
|
SleepIQCoreClimate,
|
||||||
|
SleepIQFootWarmer,
|
||||||
|
SleepIQSleeper,
|
||||||
|
)
|
||||||
|
|
||||||
|
from homeassistant.components.number import (
|
||||||
|
NumberDeviceClass,
|
||||||
|
NumberEntity,
|
||||||
|
NumberEntityDescription,
|
||||||
|
)
|
||||||
|
from homeassistant.const import UnitOfTime
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
ACTUATOR,
|
||||||
|
CORE_CLIMATE_TIMER,
|
||||||
|
ENTITY_TYPES,
|
||||||
|
FIRMNESS,
|
||||||
|
FOOT_WARMING_TIMER,
|
||||||
|
ICON_OCCUPIED,
|
||||||
|
)
|
||||||
|
from .coordinator import SleepIQConfigEntry, SleepIQDataUpdateCoordinator
|
||||||
|
from .entity import SleepIQBedEntity, sleeper_for_side
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class SleepIQNumberEntityDescription(NumberEntityDescription):
|
||||||
|
"""Class to describe a SleepIQ number entity."""
|
||||||
|
|
||||||
|
value_fn: Callable[[Any], float]
|
||||||
|
set_value_fn: Callable[[Any, int], Coroutine[None, None, None]]
|
||||||
|
get_name_fn: Callable[[SleepIQBed, Any], str]
|
||||||
|
get_unique_id_fn: Callable[[SleepIQBed, Any], str]
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_set_firmness(sleeper: SleepIQSleeper, firmness: int) -> None:
|
||||||
|
await sleeper.set_sleepnumber(firmness)
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_set_actuator_position(
|
||||||
|
actuator: SleepIQActuator, position: int
|
||||||
|
) -> None:
|
||||||
|
await actuator.set_position(position)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_actuator_name(bed: SleepIQBed, actuator: SleepIQActuator) -> str:
|
||||||
|
if actuator.side:
|
||||||
|
return (
|
||||||
|
"SleepNumber"
|
||||||
|
f" {bed.name} {actuator.side_full}"
|
||||||
|
f" {actuator.actuator_full}"
|
||||||
|
f" {ENTITY_TYPES[ACTUATOR]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return f"SleepNumber {bed.name} {actuator.actuator_full} {ENTITY_TYPES[ACTUATOR]}"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_actuator_unique_id(bed: SleepIQBed, actuator: SleepIQActuator) -> str:
|
||||||
|
if actuator.side:
|
||||||
|
return f"{bed.id}_{actuator.side.value}_{actuator.actuator}"
|
||||||
|
|
||||||
|
return f"{bed.id}_{actuator.actuator}"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_sleeper_name(bed: SleepIQBed, sleeper: SleepIQSleeper) -> str:
|
||||||
|
return f"SleepNumber {bed.name} {sleeper.name} {ENTITY_TYPES[FIRMNESS]}"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_sleeper_unique_id(bed: SleepIQBed, sleeper: SleepIQSleeper) -> str:
|
||||||
|
return f"{sleeper.sleeper_id}_{FIRMNESS}"
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_set_foot_warmer_time(
|
||||||
|
foot_warmer: SleepIQFootWarmer, time: int
|
||||||
|
) -> None:
|
||||||
|
temperature = FootWarmingTemps(foot_warmer.temperature)
|
||||||
|
if temperature != FootWarmingTemps.OFF:
|
||||||
|
await foot_warmer.turn_on(temperature, time)
|
||||||
|
|
||||||
|
foot_warmer.timer = time
|
||||||
|
|
||||||
|
|
||||||
|
def _get_foot_warming_name(bed: SleepIQBed, foot_warmer: SleepIQFootWarmer) -> str:
|
||||||
|
sleeper = sleeper_for_side(bed, foot_warmer.side)
|
||||||
|
return f"SleepNumber {bed.name} {sleeper.name} {ENTITY_TYPES[FOOT_WARMING_TIMER]}"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_foot_warming_unique_id(bed: SleepIQBed, foot_warmer: SleepIQFootWarmer) -> str:
|
||||||
|
return f"{bed.id}_{foot_warmer.side.value}_{FOOT_WARMING_TIMER}"
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_set_core_climate_time(
|
||||||
|
core_climate: SleepIQCoreClimate, time: int
|
||||||
|
) -> None:
|
||||||
|
temperature = CoreTemps(core_climate.temperature)
|
||||||
|
if temperature != CoreTemps.OFF:
|
||||||
|
await core_climate.turn_on(temperature, time)
|
||||||
|
|
||||||
|
core_climate.timer = time
|
||||||
|
|
||||||
|
|
||||||
|
def _get_core_climate_name(bed: SleepIQBed, core_climate: SleepIQCoreClimate) -> str:
|
||||||
|
sleeper = sleeper_for_side(bed, core_climate.side)
|
||||||
|
return f"SleepNumber {bed.name} {sleeper.name} {ENTITY_TYPES[CORE_CLIMATE_TIMER]}"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_core_climate_unique_id(
|
||||||
|
bed: SleepIQBed, core_climate: SleepIQCoreClimate
|
||||||
|
) -> str:
|
||||||
|
return f"{bed.id}_{core_climate.side.value}_{CORE_CLIMATE_TIMER}"
|
||||||
|
|
||||||
|
|
||||||
|
NUMBER_DESCRIPTIONS: dict[str, SleepIQNumberEntityDescription] = {
|
||||||
|
FIRMNESS: SleepIQNumberEntityDescription(
|
||||||
|
key=FIRMNESS,
|
||||||
|
native_min_value=5,
|
||||||
|
native_max_value=100,
|
||||||
|
native_step=5,
|
||||||
|
name=ENTITY_TYPES[FIRMNESS],
|
||||||
|
icon=ICON_OCCUPIED,
|
||||||
|
value_fn=lambda sleeper: cast(float, sleeper.sleep_number),
|
||||||
|
set_value_fn=_async_set_firmness,
|
||||||
|
get_name_fn=_get_sleeper_name,
|
||||||
|
get_unique_id_fn=_get_sleeper_unique_id,
|
||||||
|
),
|
||||||
|
ACTUATOR: SleepIQNumberEntityDescription(
|
||||||
|
key=ACTUATOR,
|
||||||
|
native_min_value=0,
|
||||||
|
native_max_value=100,
|
||||||
|
native_step=1,
|
||||||
|
name=ENTITY_TYPES[ACTUATOR],
|
||||||
|
icon=ICON_OCCUPIED,
|
||||||
|
value_fn=lambda actuator: cast(float, actuator.position),
|
||||||
|
set_value_fn=_async_set_actuator_position,
|
||||||
|
get_name_fn=_get_actuator_name,
|
||||||
|
get_unique_id_fn=_get_actuator_unique_id,
|
||||||
|
),
|
||||||
|
FOOT_WARMING_TIMER: SleepIQNumberEntityDescription(
|
||||||
|
key=FOOT_WARMING_TIMER,
|
||||||
|
native_min_value=30,
|
||||||
|
native_max_value=360,
|
||||||
|
native_step=30,
|
||||||
|
name=ENTITY_TYPES[FOOT_WARMING_TIMER],
|
||||||
|
icon="mdi:timer",
|
||||||
|
value_fn=lambda foot_warmer: foot_warmer.timer,
|
||||||
|
set_value_fn=_async_set_foot_warmer_time,
|
||||||
|
get_name_fn=_get_foot_warming_name,
|
||||||
|
get_unique_id_fn=_get_foot_warming_unique_id,
|
||||||
|
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||||
|
device_class=NumberDeviceClass.DURATION,
|
||||||
|
),
|
||||||
|
CORE_CLIMATE_TIMER: SleepIQNumberEntityDescription(
|
||||||
|
key=CORE_CLIMATE_TIMER,
|
||||||
|
native_min_value=0,
|
||||||
|
native_max_value=SleepIQCoreClimate.max_core_climate_time,
|
||||||
|
native_step=30,
|
||||||
|
name=ENTITY_TYPES[CORE_CLIMATE_TIMER],
|
||||||
|
icon="mdi:timer",
|
||||||
|
value_fn=lambda core_climate: core_climate.timer,
|
||||||
|
set_value_fn=_async_set_core_climate_time,
|
||||||
|
get_name_fn=_get_core_climate_name,
|
||||||
|
get_unique_id_fn=_get_core_climate_unique_id,
|
||||||
|
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||||
|
device_class=NumberDeviceClass.DURATION,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: SleepIQConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the SleepIQ bed sensors."""
|
||||||
|
data = entry.runtime_data
|
||||||
|
|
||||||
|
entities: list[SleepIQNumberEntity] = []
|
||||||
|
for bed in data.client.beds.values():
|
||||||
|
entities.extend(
|
||||||
|
SleepIQNumberEntity(
|
||||||
|
data.data_coordinator,
|
||||||
|
bed,
|
||||||
|
sleeper,
|
||||||
|
NUMBER_DESCRIPTIONS[FIRMNESS],
|
||||||
|
)
|
||||||
|
for sleeper in bed.sleepers
|
||||||
|
)
|
||||||
|
entities.extend(
|
||||||
|
SleepIQNumberEntity(
|
||||||
|
data.data_coordinator,
|
||||||
|
bed,
|
||||||
|
actuator,
|
||||||
|
NUMBER_DESCRIPTIONS[ACTUATOR],
|
||||||
|
)
|
||||||
|
for actuator in bed.foundation.actuators
|
||||||
|
)
|
||||||
|
entities.extend(
|
||||||
|
SleepIQNumberEntity(
|
||||||
|
data.data_coordinator,
|
||||||
|
bed,
|
||||||
|
foot_warmer,
|
||||||
|
NUMBER_DESCRIPTIONS[FOOT_WARMING_TIMER],
|
||||||
|
)
|
||||||
|
for foot_warmer in bed.foundation.foot_warmers
|
||||||
|
)
|
||||||
|
entities.extend(
|
||||||
|
SleepIQNumberEntity(
|
||||||
|
data.data_coordinator,
|
||||||
|
bed,
|
||||||
|
core_climate,
|
||||||
|
NUMBER_DESCRIPTIONS[CORE_CLIMATE_TIMER],
|
||||||
|
)
|
||||||
|
for core_climate in bed.foundation.core_climates
|
||||||
|
)
|
||||||
|
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
|
class SleepIQNumberEntity(SleepIQBedEntity[SleepIQDataUpdateCoordinator], NumberEntity):
|
||||||
|
"""Representation of a SleepIQ number entity."""
|
||||||
|
|
||||||
|
entity_description: SleepIQNumberEntityDescription
|
||||||
|
_attr_icon = "mdi:bed"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: SleepIQDataUpdateCoordinator,
|
||||||
|
bed: SleepIQBed,
|
||||||
|
device: Any,
|
||||||
|
description: SleepIQNumberEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the number."""
|
||||||
|
self.entity_description = description
|
||||||
|
self.device = device
|
||||||
|
|
||||||
|
self._attr_name = description.get_name_fn(bed, device)
|
||||||
|
self._attr_unique_id = description.get_unique_id_fn(bed, device)
|
||||||
|
if description.icon:
|
||||||
|
self._attr_icon = description.icon
|
||||||
|
|
||||||
|
super().__init__(coordinator, bed)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
@override
|
||||||
|
def _async_update_attrs(self) -> None:
|
||||||
|
"""Update number attributes."""
|
||||||
|
self._attr_native_value = float(self.entity_description.value_fn(self.device))
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def async_set_native_value(self, value: float) -> None:
|
||||||
|
"""Set the number value."""
|
||||||
|
await self.entity_description.set_value_fn(self.device, int(value))
|
||||||
|
self._attr_native_value = value
|
||||||
|
self.async_write_ha_state()
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
"""Support for SleepIQ foundation preset selection."""
|
||||||
|
|
||||||
|
from typing import override
|
||||||
|
|
||||||
|
from asyncsleepiq import (
|
||||||
|
CoreTemps,
|
||||||
|
FootWarmingTemps,
|
||||||
|
Side,
|
||||||
|
SleepIQBed,
|
||||||
|
SleepIQCoreClimate,
|
||||||
|
SleepIQFootWarmer,
|
||||||
|
SleepIQPreset,
|
||||||
|
)
|
||||||
|
|
||||||
|
from homeassistant.components.select import SelectEntity
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from .const import CORE_CLIMATE, FOOT_WARMER
|
||||||
|
from .coordinator import SleepIQConfigEntry, SleepIQDataUpdateCoordinator
|
||||||
|
from .entity import SleepIQBedEntity, SleepIQSleeperEntity, sleeper_for_side
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: SleepIQConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the SleepIQ foundation preset select entities."""
|
||||||
|
data = entry.runtime_data
|
||||||
|
entities: list[SleepIQBedEntity] = []
|
||||||
|
for bed in data.client.beds.values():
|
||||||
|
entities.extend(
|
||||||
|
SleepIQSelectEntity(data.data_coordinator, bed, preset)
|
||||||
|
for preset in bed.foundation.presets
|
||||||
|
)
|
||||||
|
entities.extend(
|
||||||
|
SleepIQFootWarmingTempSelectEntity(data.data_coordinator, bed, foot_warmer)
|
||||||
|
for foot_warmer in bed.foundation.foot_warmers
|
||||||
|
)
|
||||||
|
entities.extend(
|
||||||
|
SleepIQCoreTempSelectEntity(data.data_coordinator, bed, core_climate)
|
||||||
|
for core_climate in bed.foundation.core_climates
|
||||||
|
)
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
|
class SleepIQSelectEntity(SleepIQBedEntity[SleepIQDataUpdateCoordinator], SelectEntity):
|
||||||
|
"""Representation of a SleepIQ select entity."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: SleepIQDataUpdateCoordinator,
|
||||||
|
bed: SleepIQBed,
|
||||||
|
preset: SleepIQPreset,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the select entity."""
|
||||||
|
self.preset = preset
|
||||||
|
|
||||||
|
self._attr_name = f"SleepNumber {bed.name} Foundation Preset"
|
||||||
|
self._attr_unique_id = f"{bed.id}_preset"
|
||||||
|
if preset.side != Side.NONE:
|
||||||
|
self._attr_name += f" {preset.side_full}"
|
||||||
|
self._attr_unique_id += f"_{preset.side.value}"
|
||||||
|
self._attr_options = preset.options
|
||||||
|
|
||||||
|
super().__init__(coordinator, bed)
|
||||||
|
self._async_update_attrs()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
@override
|
||||||
|
def _async_update_attrs(self) -> None:
|
||||||
|
"""Update entity attributes."""
|
||||||
|
self._attr_current_option = self.preset.preset
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def async_select_option(self, option: str) -> None:
|
||||||
|
"""Change the current preset."""
|
||||||
|
await self.preset.set_preset(option)
|
||||||
|
self._attr_current_option = option
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
|
||||||
|
class SleepIQFootWarmingTempSelectEntity(
|
||||||
|
SleepIQSleeperEntity[SleepIQDataUpdateCoordinator], SelectEntity
|
||||||
|
):
|
||||||
|
"""Representation of a SleepIQ foot warming temperature select entity."""
|
||||||
|
|
||||||
|
_attr_icon = "mdi:heat-wave"
|
||||||
|
_attr_options = [e.name.lower() for e in FootWarmingTemps]
|
||||||
|
_attr_translation_key = "foot_warmer_temp"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: SleepIQDataUpdateCoordinator,
|
||||||
|
bed: SleepIQBed,
|
||||||
|
foot_warmer: SleepIQFootWarmer,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the select entity."""
|
||||||
|
self.foot_warmer = foot_warmer
|
||||||
|
sleeper = sleeper_for_side(bed, foot_warmer.side)
|
||||||
|
super().__init__(coordinator, bed, sleeper, FOOT_WARMER)
|
||||||
|
self._async_update_attrs()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
@override
|
||||||
|
def _async_update_attrs(self) -> None:
|
||||||
|
"""Update entity attributes."""
|
||||||
|
self._attr_current_option = FootWarmingTemps(
|
||||||
|
self.foot_warmer.temperature
|
||||||
|
).name.lower()
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def async_select_option(self, option: str) -> None:
|
||||||
|
"""Change the current preset."""
|
||||||
|
temperature = FootWarmingTemps[option.upper()]
|
||||||
|
timer = self.foot_warmer.timer or 120
|
||||||
|
|
||||||
|
if temperature == 0:
|
||||||
|
await self.foot_warmer.turn_off()
|
||||||
|
else:
|
||||||
|
await self.foot_warmer.turn_on(temperature, timer)
|
||||||
|
|
||||||
|
self._attr_current_option = option
|
||||||
|
await self.coordinator.async_request_refresh()
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
|
||||||
|
class SleepIQCoreTempSelectEntity(
|
||||||
|
SleepIQSleeperEntity[SleepIQDataUpdateCoordinator], SelectEntity
|
||||||
|
):
|
||||||
|
"""Representation of a SleepIQ core climate temperature select entity."""
|
||||||
|
|
||||||
|
# Maps to translate between asyncsleepiq and HA's naming preference
|
||||||
|
SLEEPIQ_TO_HA_CORE_TEMP_MAP = {
|
||||||
|
CoreTemps.OFF: "off",
|
||||||
|
CoreTemps.HEATING_PUSH_LOW: "heating_low",
|
||||||
|
CoreTemps.HEATING_PUSH_MED: "heating_medium",
|
||||||
|
CoreTemps.HEATING_PUSH_HIGH: "heating_high",
|
||||||
|
CoreTemps.COOLING_PULL_LOW: "cooling_low",
|
||||||
|
CoreTemps.COOLING_PULL_MED: "cooling_medium",
|
||||||
|
CoreTemps.COOLING_PULL_HIGH: "cooling_high",
|
||||||
|
}
|
||||||
|
HA_TO_SLEEPIQ_CORE_TEMP_MAP = {v: k for k, v in SLEEPIQ_TO_HA_CORE_TEMP_MAP.items()}
|
||||||
|
|
||||||
|
_attr_icon = "mdi:heat-wave"
|
||||||
|
_attr_options = list(SLEEPIQ_TO_HA_CORE_TEMP_MAP.values())
|
||||||
|
_attr_translation_key = "core_temps"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: SleepIQDataUpdateCoordinator,
|
||||||
|
bed: SleepIQBed,
|
||||||
|
core_climate: SleepIQCoreClimate,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the select entity."""
|
||||||
|
self.core_climate = core_climate
|
||||||
|
sleeper = sleeper_for_side(bed, core_climate.side)
|
||||||
|
super().__init__(coordinator, bed, sleeper, CORE_CLIMATE)
|
||||||
|
self._async_update_attrs()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
@override
|
||||||
|
def _async_update_attrs(self) -> None:
|
||||||
|
"""Update entity attributes."""
|
||||||
|
sleepiq_option = CoreTemps(self.core_climate.temperature)
|
||||||
|
self._attr_current_option = self.SLEEPIQ_TO_HA_CORE_TEMP_MAP[sleepiq_option]
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def async_select_option(self, option: str) -> None:
|
||||||
|
"""Change the current preset."""
|
||||||
|
temperature = self.HA_TO_SLEEPIQ_CORE_TEMP_MAP[option]
|
||||||
|
timer = self.core_climate.timer or 240
|
||||||
|
|
||||||
|
if temperature == CoreTemps.OFF:
|
||||||
|
await self.core_climate.turn_off()
|
||||||
|
else:
|
||||||
|
await self.core_climate.turn_on(temperature, timer)
|
||||||
|
|
||||||
|
self._attr_current_option = option
|
||||||
|
await self.coordinator.async_request_refresh()
|
||||||
|
self.async_write_ha_state()
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
"""Support for SleepIQ sensors."""
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import override
|
||||||
|
|
||||||
|
from asyncsleepiq import SleepIQBed, SleepIQSleeper
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import (
|
||||||
|
SensorDeviceClass,
|
||||||
|
SensorEntity,
|
||||||
|
SensorEntityDescription,
|
||||||
|
SensorStateClass,
|
||||||
|
)
|
||||||
|
from homeassistant.const import PRESSURE, UnitOfTime
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
HEART_RATE,
|
||||||
|
HRV,
|
||||||
|
RESPIRATORY_RATE,
|
||||||
|
SLEEP_DURATION,
|
||||||
|
SLEEP_NUMBER,
|
||||||
|
SLEEP_SCORE,
|
||||||
|
)
|
||||||
|
from .coordinator import (
|
||||||
|
SleepIQConfigEntry,
|
||||||
|
SleepIQDataUpdateCoordinator,
|
||||||
|
SleepIQSleepDataCoordinator,
|
||||||
|
)
|
||||||
|
from .entity import SleepIQSleeperEntity
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class SleepIQSensorEntityDescription(SensorEntityDescription):
|
||||||
|
"""Describes SleepIQ sensor entity."""
|
||||||
|
|
||||||
|
value_fn: Callable[[SleepIQSleeper], float | int | None]
|
||||||
|
|
||||||
|
|
||||||
|
BED_SENSORS: tuple[SleepIQSensorEntityDescription, ...] = (
|
||||||
|
SleepIQSensorEntityDescription(
|
||||||
|
key=PRESSURE,
|
||||||
|
translation_key="pressure",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
value_fn=lambda sleeper: sleeper.pressure,
|
||||||
|
),
|
||||||
|
SleepIQSensorEntityDescription(
|
||||||
|
key=SLEEP_NUMBER,
|
||||||
|
translation_key="sleep_number",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
value_fn=lambda sleeper: sleeper.sleep_number,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
SLEEP_HEALTH_SENSORS: tuple[SleepIQSensorEntityDescription, ...] = (
|
||||||
|
SleepIQSensorEntityDescription(
|
||||||
|
key=SLEEP_SCORE,
|
||||||
|
translation_key="sleep_score",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement="score",
|
||||||
|
value_fn=lambda sleeper: (
|
||||||
|
sleeper.sleep_data.sleep_score if sleeper.sleep_data else None
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SleepIQSensorEntityDescription(
|
||||||
|
key=SLEEP_DURATION,
|
||||||
|
translation_key="sleep_duration",
|
||||||
|
device_class=SensorDeviceClass.DURATION,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfTime.HOURS,
|
||||||
|
suggested_display_precision=1,
|
||||||
|
value_fn=lambda sleeper: (
|
||||||
|
round(sleeper.sleep_data.duration / 3600, 1)
|
||||||
|
if sleeper.sleep_data and sleeper.sleep_data.duration
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SleepIQSensorEntityDescription(
|
||||||
|
key=HEART_RATE,
|
||||||
|
translation_key="heart_rate_avg",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement="bpm",
|
||||||
|
value_fn=lambda sleeper: (
|
||||||
|
sleeper.sleep_data.heart_rate if sleeper.sleep_data else None
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SleepIQSensorEntityDescription(
|
||||||
|
key=RESPIRATORY_RATE,
|
||||||
|
translation_key="respiratory_rate_avg",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement="brpm",
|
||||||
|
value_fn=lambda sleeper: (
|
||||||
|
sleeper.sleep_data.respiratory_rate if sleeper.sleep_data else None
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SleepIQSensorEntityDescription(
|
||||||
|
key=HRV,
|
||||||
|
translation_key="hrv",
|
||||||
|
device_class=SensorDeviceClass.DURATION,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfTime.MILLISECONDS,
|
||||||
|
value_fn=lambda sleeper: sleeper.sleep_data.hrv if sleeper.sleep_data else None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: SleepIQConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the SleepIQ bed sensors."""
|
||||||
|
data = entry.runtime_data
|
||||||
|
|
||||||
|
entities: list[SensorEntity] = []
|
||||||
|
|
||||||
|
entities.extend(
|
||||||
|
SleepIQSensorEntity(data.data_coordinator, bed, sleeper, description)
|
||||||
|
for bed in data.client.beds.values()
|
||||||
|
for sleeper in bed.sleepers
|
||||||
|
for description in BED_SENSORS
|
||||||
|
)
|
||||||
|
|
||||||
|
entities.extend(
|
||||||
|
SleepIQSensorEntity(data.sleep_data_coordinator, bed, sleeper, description)
|
||||||
|
for bed in data.client.beds.values()
|
||||||
|
for sleeper in bed.sleepers
|
||||||
|
for description in SLEEP_HEALTH_SENSORS
|
||||||
|
)
|
||||||
|
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
|
class SleepIQSensorEntity(
|
||||||
|
SleepIQSleeperEntity[SleepIQDataUpdateCoordinator | SleepIQSleepDataCoordinator],
|
||||||
|
SensorEntity,
|
||||||
|
):
|
||||||
|
"""Representation of a SleepIQ sensor."""
|
||||||
|
|
||||||
|
entity_description: SleepIQSensorEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: SleepIQDataUpdateCoordinator | SleepIQSleepDataCoordinator,
|
||||||
|
bed: SleepIQBed,
|
||||||
|
sleeper: SleepIQSleeper,
|
||||||
|
description: SleepIQSensorEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
self.entity_description = description
|
||||||
|
super().__init__(coordinator, bed, sleeper, description.key)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
@override
|
||||||
|
def _async_update_attrs(self) -> None:
|
||||||
|
"""Update sensor attributes."""
|
||||||
|
self._attr_native_value = self.entity_description.value_fn(self.sleeper)
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||||
|
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"reauth_confirm": {
|
||||||
|
"data": {
|
||||||
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
|
},
|
||||||
|
"description": "The SleepIQ integration needs to re-authenticate your account {username}.",
|
||||||
|
"title": "[%key:common::config_flow::title::reauth%]"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
|
"username": "[%key:common::config_flow::data::username%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"select": {
|
||||||
|
"core_temps": {
|
||||||
|
"state": {
|
||||||
|
"cooling_high": "Cooling high",
|
||||||
|
"cooling_low": "Cooling low",
|
||||||
|
"cooling_medium": "Cooling medium",
|
||||||
|
"heating_high": "Heating high",
|
||||||
|
"heating_low": "Heating low",
|
||||||
|
"heating_medium": "Heating medium",
|
||||||
|
"off": "[%key:common::state::off%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foot_warmer_temp": {
|
||||||
|
"state": {
|
||||||
|
"high": "[%key:common::state::high%]",
|
||||||
|
"low": "[%key:common::state::low%]",
|
||||||
|
"medium": "[%key:common::state::medium%]",
|
||||||
|
"off": "[%key:common::state::off%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
"""Support for SleepIQ switches."""
|
||||||
|
|
||||||
|
from typing import Any, override
|
||||||
|
|
||||||
|
from asyncsleepiq import SleepIQBed
|
||||||
|
|
||||||
|
from homeassistant.components.switch import SwitchEntity
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from .coordinator import SleepIQConfigEntry, SleepIQPauseUpdateCoordinator
|
||||||
|
from .entity import SleepIQBedEntity
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: SleepIQConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the sleep number switches."""
|
||||||
|
data = entry.runtime_data
|
||||||
|
async_add_entities(
|
||||||
|
SleepNumberPrivateSwitch(data.pause_coordinator, bed)
|
||||||
|
for bed in data.client.beds.values()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SleepNumberPrivateSwitch(
|
||||||
|
SleepIQBedEntity[SleepIQPauseUpdateCoordinator], SwitchEntity
|
||||||
|
):
|
||||||
|
"""Representation of SleepIQ privacy mode."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, coordinator: SleepIQPauseUpdateCoordinator, bed: SleepIQBed
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the switch."""
|
||||||
|
super().__init__(coordinator, bed)
|
||||||
|
self._attr_name = f"SleepNumber {bed.name} Pause Mode"
|
||||||
|
self._attr_unique_id = f"{bed.id}-pause-mode"
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn on switch."""
|
||||||
|
await self.bed.set_pause_mode(True)
|
||||||
|
self._handle_coordinator_update()
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn off switch."""
|
||||||
|
await self.bed.set_pause_mode(False)
|
||||||
|
self._handle_coordinator_update()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
@override
|
||||||
|
def _async_update_attrs(self) -> None:
|
||||||
|
"""Update switch attributes."""
|
||||||
|
self._attr_is_on = self.bed.paused
|
||||||
Reference in New Issue
Block a user