mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 21:57:51 +00:00
SleepIQ add core climate for SleepNumber Climate 360 beds (#134718)
This commit is contained in:
parent
875219ccb5
commit
2d86fa079e
@ -4,6 +4,8 @@ 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"
|
||||
@ -15,6 +17,8 @@ FOOT_WARMING_TIMER = "foot_warming_timer"
|
||||
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",
|
||||
|
@ -7,20 +7,28 @@ from dataclasses import dataclass
|
||||
from typing import Any, cast
|
||||
|
||||
from asyncsleepiq import (
|
||||
CoreTemps,
|
||||
FootWarmingTemps,
|
||||
SleepIQActuator,
|
||||
SleepIQBed,
|
||||
SleepIQCoreClimate,
|
||||
SleepIQFootWarmer,
|
||||
SleepIQSleeper,
|
||||
)
|
||||
|
||||
from homeassistant.components.number import NumberEntity, NumberEntityDescription
|
||||
from homeassistant.components.number import (
|
||||
NumberDeviceClass,
|
||||
NumberEntity,
|
||||
NumberEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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,
|
||||
DOMAIN,
|
||||
ENTITY_TYPES,
|
||||
FIRMNESS,
|
||||
@ -95,6 +103,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 +161,20 @@ NUMBER_DESCRIPTIONS: dict[str, SleepIQNumberEntityDescription] = {
|
||||
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,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@ -172,6 +215,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)
|
||||
|
||||
|
@ -3,9 +3,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncsleepiq import (
|
||||
CoreTemps,
|
||||
FootWarmingTemps,
|
||||
Side,
|
||||
SleepIQBed,
|
||||
SleepIQCoreClimate,
|
||||
SleepIQFootWarmer,
|
||||
SleepIQPreset,
|
||||
)
|
||||
@ -15,7 +17,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
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,57 @@ class SleepIQFootWarmingTempSelectEntity(
|
||||
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
|
||||
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]
|
||||
|
||||
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()
|
||||
|
@ -33,6 +33,17 @@
|
||||
"medium": "[%key:common::state::medium%]",
|
||||
"high": "[%key:common::state::high%]"
|
||||
}
|
||||
},
|
||||
"core_temps": {
|
||||
"state": {
|
||||
"off": "[%key:common::state::off%]",
|
||||
"heating_low": "Heating low",
|
||||
"heating_medium": "Heating medium",
|
||||
"heating_high": "Heating high",
|
||||
"cooling_low": "Cooling low",
|
||||
"cooling_medium": "Cooling medium",
|
||||
"cooling_high": "Cooling high"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,10 +7,12 @@ from unittest.mock import AsyncMock, MagicMock, create_autospec, patch
|
||||
|
||||
from asyncsleepiq import (
|
||||
BED_PRESETS,
|
||||
CoreTemps,
|
||||
FootWarmingTemps,
|
||||
Side,
|
||||
SleepIQActuator,
|
||||
SleepIQBed,
|
||||
SleepIQCoreClimate,
|
||||
SleepIQFootWarmer,
|
||||
SleepIQFoundation,
|
||||
SleepIQLight,
|
||||
@ -29,6 +31,7 @@ from tests.common import MockConfigEntry
|
||||
BED_ID = "123456"
|
||||
BED_NAME = "Test Bed"
|
||||
BED_NAME_LOWER = BED_NAME.lower().replace(" ", "_")
|
||||
CORE_CLIMATE_TIME = 240
|
||||
SLEEPER_L_ID = "98765"
|
||||
SLEEPER_R_ID = "43219"
|
||||
SLEEPER_L_NAME = "SleeperL"
|
||||
@ -91,6 +94,7 @@ def mock_bed() -> MagicMock:
|
||||
bed.foundation.lights = [light_1, light_2]
|
||||
|
||||
bed.foundation.foot_warmers = []
|
||||
bed.foundation.core_climates = []
|
||||
return bed
|
||||
|
||||
|
||||
@ -127,6 +131,7 @@ def mock_asyncsleepiq_single_foundation(
|
||||
preset.options = BED_PRESETS
|
||||
|
||||
mock_bed.foundation.foot_warmers = []
|
||||
mock_bed.foundation.core_climates = []
|
||||
yield client
|
||||
|
||||
|
||||
@ -185,6 +190,18 @@ def mock_asyncsleepiq(mock_bed: MagicMock) -> Generator[MagicMock]:
|
||||
foot_warmer_r.timer = FOOT_WARM_TIME
|
||||
foot_warmer_r.temperature = FootWarmingTemps.OFF
|
||||
|
||||
core_climate_l = create_autospec(SleepIQCoreClimate)
|
||||
core_climate_r = create_autospec(SleepIQCoreClimate)
|
||||
mock_bed.foundation.core_climates = [core_climate_l, core_climate_r]
|
||||
|
||||
core_climate_l.side = Side.LEFT
|
||||
core_climate_l.timer = CORE_CLIMATE_TIME
|
||||
core_climate_l.temperature = CoreTemps.COOLING_PULL_MED
|
||||
|
||||
core_climate_r.side = Side.RIGHT
|
||||
core_climate_r.timer = CORE_CLIMATE_TIME
|
||||
core_climate_r.temperature = CoreTemps.OFF
|
||||
|
||||
yield client
|
||||
|
||||
|
||||
|
@ -198,3 +198,42 @@ async def test_foot_warmer_timer(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_asyncsleepiq.beds[BED_ID].foundation.foot_warmers[0].timer == 300
|
||||
|
||||
|
||||
async def test_core_climate_timer(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry, mock_asyncsleepiq
|
||||
) -> None:
|
||||
"""Test the SleepIQ core climate number values for a bed with two sides."""
|
||||
entry = await setup_platform(hass, NUMBER_DOMAIN)
|
||||
|
||||
state = hass.states.get(
|
||||
f"number.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_L_NAME_LOWER}_core_climate_timer"
|
||||
)
|
||||
assert state.state == "240.0"
|
||||
assert state.attributes.get(ATTR_ICON) == "mdi:timer"
|
||||
assert state.attributes.get(ATTR_MIN) == 0
|
||||
assert state.attributes.get(ATTR_MAX) == 600
|
||||
assert state.attributes.get(ATTR_STEP) == 30
|
||||
assert (
|
||||
state.attributes.get(ATTR_FRIENDLY_NAME)
|
||||
== f"SleepNumber {BED_NAME} {SLEEPER_L_NAME} Core Climate Timer"
|
||||
)
|
||||
|
||||
entry = entity_registry.async_get(
|
||||
f"number.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_L_NAME_LOWER}_core_climate_timer"
|
||||
)
|
||||
assert entry
|
||||
assert entry.unique_id == f"{BED_ID}_L_core_climate_timer"
|
||||
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: f"number.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_L_NAME_LOWER}_core_climate_timer",
|
||||
ATTR_VALUE: 420,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_asyncsleepiq.beds[BED_ID].foundation.core_climates[0].timer == 420
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from asyncsleepiq import FootWarmingTemps
|
||||
from asyncsleepiq import CoreTemps, FootWarmingTemps
|
||||
|
||||
from homeassistant.components.select import (
|
||||
DOMAIN as SELECT_DOMAIN,
|
||||
@ -21,6 +21,7 @@ from .conftest import (
|
||||
BED_ID,
|
||||
BED_NAME,
|
||||
BED_NAME_LOWER,
|
||||
CORE_CLIMATE_TIME,
|
||||
FOOT_WARM_TIME,
|
||||
PRESET_L_STATE,
|
||||
PRESET_R_STATE,
|
||||
@ -204,3 +205,77 @@ async def test_foot_warmer(
|
||||
mock_asyncsleepiq.beds[BED_ID].foundation.foot_warmers[
|
||||
1
|
||||
].turn_on.assert_called_with(FootWarmingTemps.HIGH, FOOT_WARM_TIME)
|
||||
|
||||
|
||||
async def test_core_climate(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_asyncsleepiq: MagicMock,
|
||||
) -> None:
|
||||
"""Test the SleepIQ select entity for core climate."""
|
||||
entry = await setup_platform(hass, SELECT_DOMAIN)
|
||||
|
||||
state = hass.states.get(
|
||||
f"select.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_L_NAME_LOWER}_core_climate"
|
||||
)
|
||||
assert state.state == "cooling_medium"
|
||||
assert state.attributes.get(ATTR_ICON) == "mdi:heat-wave"
|
||||
assert (
|
||||
state.attributes.get(ATTR_FRIENDLY_NAME)
|
||||
== f"SleepNumber {BED_NAME} {SLEEPER_L_NAME} Core Climate"
|
||||
)
|
||||
|
||||
entry = entity_registry.async_get(
|
||||
f"select.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_L_NAME_LOWER}_core_climate"
|
||||
)
|
||||
assert entry
|
||||
assert entry.unique_id == f"{SLEEPER_L_ID}_core_climate"
|
||||
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{
|
||||
ATTR_ENTITY_ID: f"select.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_L_NAME_LOWER}_core_climate",
|
||||
ATTR_OPTION: "off",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_asyncsleepiq.beds[BED_ID].foundation.core_climates[
|
||||
0
|
||||
].turn_off.assert_called_once()
|
||||
|
||||
state = hass.states.get(
|
||||
f"select.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_core_climate"
|
||||
)
|
||||
assert state.state == CoreTemps.OFF.name.lower()
|
||||
assert state.attributes.get(ATTR_ICON) == "mdi:heat-wave"
|
||||
assert (
|
||||
state.attributes.get(ATTR_FRIENDLY_NAME)
|
||||
== f"SleepNumber {BED_NAME} {SLEEPER_R_NAME} Core Climate"
|
||||
)
|
||||
|
||||
entry = entity_registry.async_get(
|
||||
f"select.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_core_climate"
|
||||
)
|
||||
assert entry
|
||||
assert entry.unique_id == f"{SLEEPER_R_ID}_core_climate"
|
||||
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{
|
||||
ATTR_ENTITY_ID: f"select.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_core_climate",
|
||||
ATTR_OPTION: "heating_high",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_asyncsleepiq.beds[BED_ID].foundation.core_climates[
|
||||
1
|
||||
].turn_on.assert_called_once()
|
||||
mock_asyncsleepiq.beds[BED_ID].foundation.core_climates[
|
||||
1
|
||||
].turn_on.assert_called_with(CoreTemps.HEATING_PUSH_HIGH, CORE_CLIMATE_TIME)
|
||||
|
Loading…
x
Reference in New Issue
Block a user