From e88a4e1cf8516e98de8a9f246ba99f8d33b63005 Mon Sep 17 00:00:00 2001 From: David Ferguson Date: Sat, 4 Jan 2025 20:39:44 +0000 Subject: [PATCH 1/2] Add Core Climate capabilities to the SleepIQ integration * The upstream asyncsleepiq library has been updated to support the new climate capabilities of the SleepIQ API. This PR adds the new climate capabilities to the SleepIQ integration in Home Assistant. * LOW/MEDIUM/HIGH for both COOL and HEAT are available. * Default timer is 4 hours to match the SleepIQ app (Max is 10 hours). --- homeassistant/components/sleepiq/const.py | 4 ++ homeassistant/components/sleepiq/number.py | 45 ++++++++++++++++ homeassistant/components/sleepiq/select.py | 51 ++++++++++++++++++- homeassistant/components/sleepiq/strings.json | 11 ++++ 4 files changed, 110 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sleepiq/const.py b/homeassistant/components/sleepiq/const.py index 4243684cd52f59..7a9415bac20f13 100644 --- a/homeassistant/components/sleepiq/const.py +++ b/homeassistant/components/sleepiq/const.py @@ -4,6 +4,8 @@ DOMAIN = "sleepiq" ACTUATOR = "actuator" +CORE_CLIMATE_TIMER = "core_climate_timer" +CORE_CLIMATE = "core_climate" BED = "bed" FIRMNESS = "firmness" ICON_EMPTY = "mdi:bed-empty" @@ -15,6 +17,8 @@ FOOT_WARMER = "foot_warmer" ENTITY_TYPES = { ACTUATOR: "Position", + CORE_CLIMATE_TIMER: "Core Climate Timer", + CORE_CLIMATE: "Core Climate", FIRMNESS: "Firmness", PRESSURE: "Pressure", IS_IN_BED: "Is In Bed", diff --git a/homeassistant/components/sleepiq/number.py b/homeassistant/components/sleepiq/number.py index 905ceab18bdce7..2c634b30bc85e5 100644 --- a/homeassistant/components/sleepiq/number.py +++ b/homeassistant/components/sleepiq/number.py @@ -7,9 +7,11 @@ from typing import Any, cast from asyncsleepiq import ( + CoreTemps, FootWarmingTemps, SleepIQActuator, SleepIQBed, + SleepIQCoreClimate, SleepIQFootWarmer, SleepIQSleeper, ) @@ -21,6 +23,7 @@ from .const import ( ACTUATOR, + CORE_CLIMATE_TIMER, DOMAIN, ENTITY_TYPES, FIRMNESS, @@ -95,6 +98,27 @@ def _get_foot_warming_unique_id(bed: SleepIQBed, foot_warmer: SleepIQFootWarmer) 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, @@ -132,6 +156,18 @@ def _get_foot_warming_unique_id(bed: SleepIQBed, foot_warmer: SleepIQFootWarmer) get_name_fn=_get_foot_warming_name, get_unique_id_fn=_get_foot_warming_unique_id, ), + CORE_CLIMATE_TIMER: SleepIQNumberEntityDescription( + key=CORE_CLIMATE_TIMER, + native_min_value=0, + native_max_value=600, + 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, + ), } @@ -172,6 +208,15 @@ async def async_setup_entry( ) 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) diff --git a/homeassistant/components/sleepiq/select.py b/homeassistant/components/sleepiq/select.py index 0a09aa4d6578a8..025f32e6060700 100644 --- a/homeassistant/components/sleepiq/select.py +++ b/homeassistant/components/sleepiq/select.py @@ -3,9 +3,11 @@ from __future__ import annotations from asyncsleepiq import ( + CoreTemps, FootWarmingTemps, Side, SleepIQBed, + SleepIQCoreClimate, SleepIQFootWarmer, SleepIQPreset, ) @@ -15,7 +17,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, FOOT_WARMER +from .const import CORE_CLIMATE, DOMAIN, FOOT_WARMER from .coordinator import SleepIQData, SleepIQDataUpdateCoordinator from .entity import SleepIQBedEntity, SleepIQSleeperEntity, sleeper_for_side @@ -37,6 +39,10 @@ async def async_setup_entry( 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) @@ -115,3 +121,46 @@ async def async_select_option(self, option: str) -> None: 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.""" + + _attr_icon = "mdi:heat-wave" + _attr_options = [e.name.lower() for e in CoreTemps] + _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 + def _async_update_attrs(self) -> None: + """Update entity attributes.""" + self._attr_current_option = CoreTemps( + self.core_climate.temperature + ).name.lower() + + async def async_select_option(self, option: str) -> None: + """Change the current preset.""" + temperature = CoreTemps[option.upper()] + timer = self.core_climate.timer or 240 + + if temperature == 0: + 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() diff --git a/homeassistant/components/sleepiq/strings.json b/homeassistant/components/sleepiq/strings.json index bdafbfb6c77285..b479eeeb201f78 100644 --- a/homeassistant/components/sleepiq/strings.json +++ b/homeassistant/components/sleepiq/strings.json @@ -33,6 +33,17 @@ "medium": "Medium", "high": "High" } + }, + "core_temps": { + "state": { + "off": "[%key:common::state::off%]", + "heating_push_low": "Heating low", + "heating_push_med": "Heating medium", + "heating_push_high": "Heating high", + "cooling_pull_low": "Cooling low", + "cooling_pull_med": "Cooling medium", + "cooling_pull_high": "Cooling high" + } } } } From 7d734ebc12acdab06bc871b859c5f07c4cab0faf Mon Sep 17 00:00:00 2001 From: David Ferguson Date: Sat, 4 Jan 2025 21:02:20 +0000 Subject: [PATCH 2/2] Avoid hardcoded max core climate timeout * Bump asyncsleepiq to 1.5.3 * Use the exposed SleepIQCoreClimate.max_core_climate_time --- homeassistant/components/sleepiq/manifest.json | 2 +- homeassistant/components/sleepiq/number.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sleepiq/manifest.json b/homeassistant/components/sleepiq/manifest.json index db29e5ab5867a2..5082e2313dfd50 100644 --- a/homeassistant/components/sleepiq/manifest.json +++ b/homeassistant/components/sleepiq/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/sleepiq", "iot_class": "cloud_polling", "loggers": ["asyncsleepiq"], - "requirements": ["asyncsleepiq==1.5.2"] + "requirements": ["asyncsleepiq==1.5.3"] } diff --git a/homeassistant/components/sleepiq/number.py b/homeassistant/components/sleepiq/number.py index 2c634b30bc85e5..2d3e9efeeb0eec 100644 --- a/homeassistant/components/sleepiq/number.py +++ b/homeassistant/components/sleepiq/number.py @@ -159,7 +159,7 @@ def _get_core_climate_unique_id( CORE_CLIMATE_TIMER: SleepIQNumberEntityDescription( key=CORE_CLIMATE_TIMER, native_min_value=0, - native_max_value=600, + native_max_value=SleepIQCoreClimate.max_core_climate_time, native_step=30, name=ENTITY_TYPES[CORE_CLIMATE_TIMER], icon="mdi:timer", diff --git a/requirements_all.txt b/requirements_all.txt index bfa166541b1699..4480a06f803ca8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -517,7 +517,7 @@ asyncinotify==4.0.2 asyncpysupla==0.0.5 # homeassistant.components.sleepiq -asyncsleepiq==1.5.2 +asyncsleepiq==1.5.3 # homeassistant.components.aten_pe # atenpdu==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3977b191d72095..03f85cff99cfec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -475,7 +475,7 @@ async-upnp-client==0.42.0 asyncarve==0.1.1 # homeassistant.components.sleepiq -asyncsleepiq==1.5.2 +asyncsleepiq==1.5.3 # homeassistant.components.aurora auroranoaa==0.0.5