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" DATA_SLEEPIQ = "data_sleepiq"
DOMAIN = "sleepiq" DOMAIN = "sleepiq"
ACTUATOR = "actuator"
BED = "bed" BED = "bed"
FIRMNESS = "firmness" FIRMNESS = "firmness"
ICON_EMPTY = "mdi:bed-empty" ICON_EMPTY = "mdi:bed-empty"
@ -10,7 +11,8 @@ ICON_OCCUPIED = "mdi:bed"
IS_IN_BED = "is_in_bed" IS_IN_BED = "is_in_bed"
PRESSURE = "pressure" PRESSURE = "pressure"
SLEEP_NUMBER = "sleep_number" SLEEP_NUMBER = "sleep_number"
SENSOR_TYPES = { ENTITY_TYPES = {
ACTUATOR: "Position",
FIRMNESS: "Firmness", FIRMNESS: "Firmness",
PRESSURE: "Pressure", PRESSURE: "Pressure",
IS_IN_BED: "Is In Bed", IS_IN_BED: "Is In Bed",

View File

@ -34,9 +34,11 @@ class SleepIQDataUpdateCoordinator(DataUpdateCoordinator[None]):
self.client = client self.client = client
async def _async_update_data(self) -> None: async def _async_update_data(self) -> None:
tasks = [self.client.fetch_bed_statuses()] + [ tasks = (
bed.foundation.update_lights() for bed in self.client.beds.values() [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) await asyncio.gather(*tasks)

View File

@ -11,7 +11,7 @@ from homeassistant.helpers.update_coordinator import (
DataUpdateCoordinator, DataUpdateCoordinator,
) )
from .const import ICON_OCCUPIED, SENSOR_TYPES from .const import ENTITY_TYPES, ICON_OCCUPIED
def device_from_bed(bed: SleepIQBed) -> DeviceInfo: def device_from_bed(bed: SleepIQBed) -> DeviceInfo:
@ -77,5 +77,5 @@ class SleepIQSleeperEntity(SleepIQBedEntity):
self.sleeper = sleeper self.sleeper = sleeper
super().__init__(coordinator, bed) 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}" self._attr_unique_id = f"{bed.id}_{sleeper.name}_{name}"

View File

@ -1,15 +1,98 @@
"""Support for SleepIQ SleepNumber firmness number entities.""" """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.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator 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 .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( async def async_setup_entry(
@ -19,37 +102,59 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up the SleepIQ bed sensors.""" """Set up the SleepIQ bed sensors."""
data: SleepIQData = hass.data[DOMAIN][entry.entry_id] data: SleepIQData = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
SleepNumberFirmnessEntity(data.data_coordinator, bed, sleeper) entities = []
for bed in data.client.beds.values() for bed in data.client.beds.values():
for sleeper in bed.sleepers 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): class SleepIQNumberEntity(SleepIQBedEntity, NumberEntity):
"""Representation of an SleepIQ Entity with CoordinatorEntity.""" """Representation of a SleepIQ number entity."""
_attr_icon = "mdi:bed" _attr_icon = "mdi:bed"
_attr_max_value: float = 100
_attr_min_value: float = 5
_attr_step: float = 5
def __init__( def __init__(
self, self,
coordinator: DataUpdateCoordinator, coordinator: DataUpdateCoordinator,
bed: SleepIQBed, bed: SleepIQBed,
sleeper: SleepIQSleeper, device: Any,
description: SleepIQNumberEntityDescription,
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the number."""
super().__init__(coordinator, bed, sleeper, FIRMNESS) 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 @callback
def _async_update_attrs(self) -> None: def _async_update_attrs(self) -> None:
"""Update sensor attributes.""" """Update number attributes."""
self._attr_value = float(self.sleeper.sleep_number) self._attr_value = float(self.description.value_fn(self.device))
async def async_set_value(self, value: float) -> None: async def async_set_value(self, value: float) -> None:
"""Set the firmness value.""" """Set the number value."""
await self.sleeper.set_sleepnumber(int(value)) await self.description.set_value_fn(self.device, int(value))
self._attr_value = value self._attr_value = value
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -3,7 +3,13 @@ from __future__ import annotations
from unittest.mock import create_autospec, patch from unittest.mock import create_autospec, patch
from asyncsleepiq import SleepIQBed, SleepIQFoundation, SleepIQLight, SleepIQSleeper from asyncsleepiq import (
SleepIQActuator,
SleepIQBed,
SleepIQFoundation,
SleepIQLight,
SleepIQSleeper,
)
import pytest import pytest
from homeassistant.components.sleepiq import DOMAIN from homeassistant.components.sleepiq import DOMAIN
@ -16,12 +22,13 @@ from tests.common import MockConfigEntry
BED_ID = "123456" BED_ID = "123456"
BED_NAME = "Test Bed" BED_NAME = "Test Bed"
BED_NAME_LOWER = BED_NAME.lower().replace(" ", "_") BED_NAME_LOWER = BED_NAME.lower().replace(" ", "_")
SLEEPER_L_ID = "98765"
SLEEPER_R_ID = "43219"
SLEEPER_L_NAME = "SleeperL" SLEEPER_L_NAME = "SleeperL"
SLEEPER_R_NAME = "Sleeper R" SLEEPER_R_NAME = "Sleeper R"
SLEEPER_L_NAME_LOWER = SLEEPER_L_NAME.lower().replace(" ", "_") SLEEPER_L_NAME_LOWER = SLEEPER_L_NAME.lower().replace(" ", "_")
SLEEPER_R_NAME_LOWER = SLEEPER_R_NAME.lower().replace(" ", "_") SLEEPER_R_NAME_LOWER = SLEEPER_R_NAME.lower().replace(" ", "_")
SLEEPIQ_CONFIG = { SLEEPIQ_CONFIG = {
CONF_USERNAME: "user@email.com", CONF_USERNAME: "user@email.com",
CONF_PASSWORD: "password", CONF_PASSWORD: "password",
@ -49,12 +56,14 @@ def mock_asyncsleepiq():
sleeper_l.in_bed = True sleeper_l.in_bed = True
sleeper_l.sleep_number = 40 sleeper_l.sleep_number = 40
sleeper_l.pressure = 1000 sleeper_l.pressure = 1000
sleeper_l.sleeper_id = SLEEPER_L_ID
sleeper_r.side = "R" sleeper_r.side = "R"
sleeper_r.name = SLEEPER_R_NAME sleeper_r.name = SLEEPER_R_NAME
sleeper_r.in_bed = False sleeper_r.in_bed = False
sleeper_r.sleep_number = 80 sleeper_r.sleep_number = 80
sleeper_r.pressure = 1400 sleeper_r.pressure = 1400
sleeper_r.sleeper_id = SLEEPER_R_ID
bed.foundation = create_autospec(SleepIQFoundation) bed.foundation = create_autospec(SleepIQFoundation)
light_1 = create_autospec(SleepIQLight) light_1 = create_autospec(SleepIQLight)
@ -65,6 +74,28 @@ def mock_asyncsleepiq():
light_2.is_on = False light_2.is_on = False
bed.foundation.lights = [light_1, light_2] 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 yield client

View File

@ -8,8 +8,10 @@ from tests.components.sleepiq.conftest import (
BED_ID, BED_ID,
BED_NAME, BED_NAME,
BED_NAME_LOWER, BED_NAME_LOWER,
SLEEPER_L_ID,
SLEEPER_L_NAME, SLEEPER_L_NAME,
SLEEPER_L_NAME_LOWER, SLEEPER_L_NAME_LOWER,
SLEEPER_R_ID,
SLEEPER_R_NAME, SLEEPER_R_NAME,
SLEEPER_R_NAME_LOWER, SLEEPER_R_NAME_LOWER,
setup_platform, setup_platform,
@ -35,7 +37,7 @@ async def test_firmness(hass, mock_asyncsleepiq):
f"number.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_L_NAME_LOWER}_firmness" f"number.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_L_NAME_LOWER}_firmness"
) )
assert entry 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( state = hass.states.get(
f"number.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_firmness" 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" f"number.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_firmness"
) )
assert entry 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( await hass.services.async_call(
DOMAIN, 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_once()
mock_asyncsleepiq.beds[BED_ID].sleepers[0].set_sleepnumber.assert_called_with(42) 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)