Add number entities to control SleepIQ actuator positions (#67770)

This commit is contained in:
Mike Fugate 2022-03-12 14:58:03 -05:00 committed by GitHub
parent 8e3454e46a
commit 6831be67f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 239 additions and 31 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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