mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Add number entities to control SleepIQ actuator positions (#67770)
This commit is contained in:
parent
8e3454e46a
commit
6831be67f4
@ -3,6 +3,7 @@
|
||||
DATA_SLEEPIQ = "data_sleepiq"
|
||||
DOMAIN = "sleepiq"
|
||||
|
||||
ACTUATOR = "actuator"
|
||||
BED = "bed"
|
||||
FIRMNESS = "firmness"
|
||||
ICON_EMPTY = "mdi:bed-empty"
|
||||
@ -10,7 +11,8 @@ ICON_OCCUPIED = "mdi:bed"
|
||||
IS_IN_BED = "is_in_bed"
|
||||
PRESSURE = "pressure"
|
||||
SLEEP_NUMBER = "sleep_number"
|
||||
SENSOR_TYPES = {
|
||||
ENTITY_TYPES = {
|
||||
ACTUATOR: "Position",
|
||||
FIRMNESS: "Firmness",
|
||||
PRESSURE: "Pressure",
|
||||
IS_IN_BED: "Is In Bed",
|
||||
|
@ -34,9 +34,11 @@ class SleepIQDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
self.client = client
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
tasks = [self.client.fetch_bed_statuses()] + [
|
||||
bed.foundation.update_lights() for bed in self.client.beds.values()
|
||||
]
|
||||
tasks = (
|
||||
[self.client.fetch_bed_statuses()]
|
||||
+ [bed.foundation.update_lights() for bed in self.client.beds.values()]
|
||||
+ [bed.foundation.update_actuators() for bed in self.client.beds.values()]
|
||||
)
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
|
||||
|
@ -11,7 +11,7 @@ from homeassistant.helpers.update_coordinator import (
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
|
||||
from .const import ICON_OCCUPIED, SENSOR_TYPES
|
||||
from .const import ENTITY_TYPES, ICON_OCCUPIED
|
||||
|
||||
|
||||
def device_from_bed(bed: SleepIQBed) -> DeviceInfo:
|
||||
@ -77,5 +77,5 @@ class SleepIQSleeperEntity(SleepIQBedEntity):
|
||||
self.sleeper = sleeper
|
||||
super().__init__(coordinator, bed)
|
||||
|
||||
self._attr_name = f"SleepNumber {bed.name} {sleeper.name} {SENSOR_TYPES[name]}"
|
||||
self._attr_name = f"SleepNumber {bed.name} {sleeper.name} {ENTITY_TYPES[name]}"
|
||||
self._attr_unique_id = f"{bed.id}_{sleeper.name}_{name}"
|
||||
|
@ -1,15 +1,98 @@
|
||||
"""Support for SleepIQ SleepNumber firmness number entities."""
|
||||
from asyncsleepiq import SleepIQBed, SleepIQSleeper
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.number import NumberEntity
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, cast
|
||||
|
||||
from asyncsleepiq import SleepIQActuator, SleepIQBed, SleepIQSleeper
|
||||
|
||||
from homeassistant.components.number import NumberEntity, NumberEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import DOMAIN, FIRMNESS
|
||||
from .const import ACTUATOR, DOMAIN, ENTITY_TYPES, FIRMNESS, ICON_OCCUPIED
|
||||
from .coordinator import SleepIQData
|
||||
from .entity import SleepIQSleeperEntity
|
||||
from .entity import SleepIQBedEntity
|
||||
|
||||
|
||||
@dataclass
|
||||
class SleepIQNumberEntityDescriptionMixin:
|
||||
"""Mixin 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]
|
||||
|
||||
|
||||
@dataclass
|
||||
class SleepIQNumberEntityDescription(
|
||||
NumberEntityDescription, SleepIQNumberEntityDescriptionMixin
|
||||
):
|
||||
"""Class to describe a SleepIQ number entity."""
|
||||
|
||||
|
||||
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 f"SleepNumber {bed.name} {actuator.side_full} {actuator.actuator_full} {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}_{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}"
|
||||
|
||||
|
||||
NUMBER_DESCRIPTIONS: dict[str, SleepIQNumberEntityDescription] = {
|
||||
FIRMNESS: SleepIQNumberEntityDescription(
|
||||
key=FIRMNESS,
|
||||
min_value=5,
|
||||
max_value=100,
|
||||
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,
|
||||
min_value=0,
|
||||
max_value=100,
|
||||
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,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@ -19,37 +102,59 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the SleepIQ bed sensors."""
|
||||
data: SleepIQData = hass.data[DOMAIN][entry.entry_id]
|
||||
async_add_entities(
|
||||
SleepNumberFirmnessEntity(data.data_coordinator, bed, sleeper)
|
||||
for bed in data.client.beds.values()
|
||||
for sleeper in bed.sleepers
|
||||
)
|
||||
|
||||
entities = []
|
||||
for bed in data.client.beds.values():
|
||||
for sleeper in bed.sleepers:
|
||||
entities.append(
|
||||
SleepIQNumberEntity(
|
||||
data.data_coordinator,
|
||||
bed,
|
||||
sleeper,
|
||||
NUMBER_DESCRIPTIONS[FIRMNESS],
|
||||
)
|
||||
)
|
||||
for actuator in bed.foundation.actuators:
|
||||
entities.append(
|
||||
SleepIQNumberEntity(
|
||||
data.data_coordinator,
|
||||
bed,
|
||||
actuator,
|
||||
NUMBER_DESCRIPTIONS[ACTUATOR],
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class SleepNumberFirmnessEntity(SleepIQSleeperEntity, NumberEntity):
|
||||
"""Representation of an SleepIQ Entity with CoordinatorEntity."""
|
||||
class SleepIQNumberEntity(SleepIQBedEntity, NumberEntity):
|
||||
"""Representation of a SleepIQ number entity."""
|
||||
|
||||
_attr_icon = "mdi:bed"
|
||||
_attr_max_value: float = 100
|
||||
_attr_min_value: float = 5
|
||||
_attr_step: float = 5
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
bed: SleepIQBed,
|
||||
sleeper: SleepIQSleeper,
|
||||
device: Any,
|
||||
description: SleepIQNumberEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator, bed, sleeper, FIRMNESS)
|
||||
"""Initialize the number."""
|
||||
self.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)
|
||||
|
||||
super().__init__(coordinator, bed)
|
||||
|
||||
@callback
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update sensor attributes."""
|
||||
self._attr_value = float(self.sleeper.sleep_number)
|
||||
"""Update number attributes."""
|
||||
self._attr_value = float(self.description.value_fn(self.device))
|
||||
|
||||
async def async_set_value(self, value: float) -> None:
|
||||
"""Set the firmness value."""
|
||||
await self.sleeper.set_sleepnumber(int(value))
|
||||
"""Set the number value."""
|
||||
await self.description.set_value_fn(self.device, int(value))
|
||||
self._attr_value = value
|
||||
self.async_write_ha_state()
|
||||
|
@ -3,7 +3,13 @@ from __future__ import annotations
|
||||
|
||||
from unittest.mock import create_autospec, patch
|
||||
|
||||
from asyncsleepiq import SleepIQBed, SleepIQFoundation, SleepIQLight, SleepIQSleeper
|
||||
from asyncsleepiq import (
|
||||
SleepIQActuator,
|
||||
SleepIQBed,
|
||||
SleepIQFoundation,
|
||||
SleepIQLight,
|
||||
SleepIQSleeper,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.sleepiq import DOMAIN
|
||||
@ -16,12 +22,13 @@ from tests.common import MockConfigEntry
|
||||
BED_ID = "123456"
|
||||
BED_NAME = "Test Bed"
|
||||
BED_NAME_LOWER = BED_NAME.lower().replace(" ", "_")
|
||||
SLEEPER_L_ID = "98765"
|
||||
SLEEPER_R_ID = "43219"
|
||||
SLEEPER_L_NAME = "SleeperL"
|
||||
SLEEPER_R_NAME = "Sleeper R"
|
||||
SLEEPER_L_NAME_LOWER = SLEEPER_L_NAME.lower().replace(" ", "_")
|
||||
SLEEPER_R_NAME_LOWER = SLEEPER_R_NAME.lower().replace(" ", "_")
|
||||
|
||||
|
||||
SLEEPIQ_CONFIG = {
|
||||
CONF_USERNAME: "user@email.com",
|
||||
CONF_PASSWORD: "password",
|
||||
@ -49,12 +56,14 @@ def mock_asyncsleepiq():
|
||||
sleeper_l.in_bed = True
|
||||
sleeper_l.sleep_number = 40
|
||||
sleeper_l.pressure = 1000
|
||||
sleeper_l.sleeper_id = SLEEPER_L_ID
|
||||
|
||||
sleeper_r.side = "R"
|
||||
sleeper_r.name = SLEEPER_R_NAME
|
||||
sleeper_r.in_bed = False
|
||||
sleeper_r.sleep_number = 80
|
||||
sleeper_r.pressure = 1400
|
||||
sleeper_r.sleeper_id = SLEEPER_R_ID
|
||||
|
||||
bed.foundation = create_autospec(SleepIQFoundation)
|
||||
light_1 = create_autospec(SleepIQLight)
|
||||
@ -65,6 +74,28 @@ def mock_asyncsleepiq():
|
||||
light_2.is_on = False
|
||||
bed.foundation.lights = [light_1, light_2]
|
||||
|
||||
actuator_h_r = create_autospec(SleepIQActuator)
|
||||
actuator_h_l = create_autospec(SleepIQActuator)
|
||||
actuator_f = create_autospec(SleepIQActuator)
|
||||
bed.foundation.actuators = [actuator_h_r, actuator_h_l, actuator_f]
|
||||
|
||||
actuator_h_r.side = "R"
|
||||
actuator_h_r.side_full = "Right"
|
||||
actuator_h_r.actuator = "H"
|
||||
actuator_h_r.actuator_full = "Head"
|
||||
actuator_h_r.position = 60
|
||||
|
||||
actuator_h_l.side = "L"
|
||||
actuator_h_l.side_full = "Left"
|
||||
actuator_h_l.actuator = "H"
|
||||
actuator_h_l.actuator_full = "Head"
|
||||
actuator_h_l.position = 50
|
||||
|
||||
actuator_f.side = None
|
||||
actuator_f.actuator = "F"
|
||||
actuator_f.actuator_full = "Foot"
|
||||
actuator_f.position = 10
|
||||
|
||||
yield client
|
||||
|
||||
|
||||
|
@ -8,8 +8,10 @@ from tests.components.sleepiq.conftest import (
|
||||
BED_ID,
|
||||
BED_NAME,
|
||||
BED_NAME_LOWER,
|
||||
SLEEPER_L_ID,
|
||||
SLEEPER_L_NAME,
|
||||
SLEEPER_L_NAME_LOWER,
|
||||
SLEEPER_R_ID,
|
||||
SLEEPER_R_NAME,
|
||||
SLEEPER_R_NAME_LOWER,
|
||||
setup_platform,
|
||||
@ -35,7 +37,7 @@ async def test_firmness(hass, mock_asyncsleepiq):
|
||||
f"number.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_L_NAME_LOWER}_firmness"
|
||||
)
|
||||
assert entry
|
||||
assert entry.unique_id == f"{BED_ID}_{SLEEPER_L_NAME}_firmness"
|
||||
assert entry.unique_id == f"{SLEEPER_L_ID}_firmness"
|
||||
|
||||
state = hass.states.get(
|
||||
f"number.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_firmness"
|
||||
@ -51,7 +53,7 @@ async def test_firmness(hass, mock_asyncsleepiq):
|
||||
f"number.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_firmness"
|
||||
)
|
||||
assert entry
|
||||
assert entry.unique_id == f"{BED_ID}_{SLEEPER_R_NAME}_firmness"
|
||||
assert entry.unique_id == f"{SLEEPER_R_ID}_firmness"
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
@ -66,3 +68,69 @@ async def test_firmness(hass, mock_asyncsleepiq):
|
||||
|
||||
mock_asyncsleepiq.beds[BED_ID].sleepers[0].set_sleepnumber.assert_called_once()
|
||||
mock_asyncsleepiq.beds[BED_ID].sleepers[0].set_sleepnumber.assert_called_with(42)
|
||||
|
||||
|
||||
async def test_actuators(hass, mock_asyncsleepiq):
|
||||
"""Test the SleepIQ actuator position values for a bed with adjustable head and foot."""
|
||||
entry = await setup_platform(hass, DOMAIN)
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
state = hass.states.get(f"number.sleepnumber_{BED_NAME_LOWER}_right_head_position")
|
||||
assert state.state == "60.0"
|
||||
assert state.attributes.get(ATTR_ICON) == "mdi:bed"
|
||||
assert (
|
||||
state.attributes.get(ATTR_FRIENDLY_NAME)
|
||||
== f"SleepNumber {BED_NAME} Right Head Position"
|
||||
)
|
||||
|
||||
entry = entity_registry.async_get(
|
||||
f"number.sleepnumber_{BED_NAME_LOWER}_right_head_position"
|
||||
)
|
||||
assert entry
|
||||
assert entry.unique_id == f"{BED_ID}_R_H"
|
||||
|
||||
state = hass.states.get(f"number.sleepnumber_{BED_NAME_LOWER}_left_head_position")
|
||||
assert state.state == "50.0"
|
||||
assert state.attributes.get(ATTR_ICON) == "mdi:bed"
|
||||
assert (
|
||||
state.attributes.get(ATTR_FRIENDLY_NAME)
|
||||
== f"SleepNumber {BED_NAME} Left Head Position"
|
||||
)
|
||||
|
||||
entry = entity_registry.async_get(
|
||||
f"number.sleepnumber_{BED_NAME_LOWER}_left_head_position"
|
||||
)
|
||||
assert entry
|
||||
assert entry.unique_id == f"{BED_ID}_L_H"
|
||||
|
||||
state = hass.states.get(f"number.sleepnumber_{BED_NAME_LOWER}_foot_position")
|
||||
assert state.state == "10.0"
|
||||
assert state.attributes.get(ATTR_ICON) == "mdi:bed"
|
||||
assert (
|
||||
state.attributes.get(ATTR_FRIENDLY_NAME)
|
||||
== f"SleepNumber {BED_NAME} Foot Position"
|
||||
)
|
||||
|
||||
entry = entity_registry.async_get(
|
||||
f"number.sleepnumber_{BED_NAME_LOWER}_foot_position"
|
||||
)
|
||||
assert entry
|
||||
assert entry.unique_id == f"{BED_ID}_F"
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: f"number.sleepnumber_{BED_NAME_LOWER}_right_head_position",
|
||||
ATTR_VALUE: 42,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_asyncsleepiq.beds[BED_ID].foundation.actuators[
|
||||
0
|
||||
].set_position.assert_called_once()
|
||||
mock_asyncsleepiq.beds[BED_ID].foundation.actuators[
|
||||
0
|
||||
].set_position.assert_called_with(42)
|
||||
|
Loading…
x
Reference in New Issue
Block a user