Home connect number platform with temperature set points entities (#126145)

pull/126157/head
J. Diego Rodríguez Royo 2024-10-26 14:04:52 +02:00 committed by GitHub
parent 65ee4e1916
commit 2acad4a78c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 359 additions and 0 deletions

View File

@ -82,6 +82,7 @@ SERVICE_COMMAND_SCHEMA = vol.Schema({vol.Required(ATTR_DEVICE_ID): str})
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.LIGHT,
Platform.NUMBER,
Platform.SENSOR,
Platform.SWITCH,
Platform.TIME,

View File

@ -95,14 +95,17 @@ SERVICE_SELECT_PROGRAM = "select_program"
SERVICE_SETTING = "change_setting"
SERVICE_START_PROGRAM = "start_program"
ATTR_ALLOWED_VALUES = "allowedvalues"
ATTR_AMBIENT = "ambient"
ATTR_BSH_KEY = "bsh_key"
ATTR_CONSTRAINTS = "constraints"
ATTR_DESC = "desc"
ATTR_DEVICE = "device"
ATTR_KEY = "key"
ATTR_PROGRAM = "program"
ATTR_SENSOR_TYPE = "sensor_type"
ATTR_SIGN = "sign"
ATTR_STEPSIZE = "stepsize"
ATTR_UNIT = "unit"
ATTR_VALUE = "value"

View File

@ -0,0 +1,153 @@
"""Provides number enties for Home Connect."""
import logging
from homeconnect.api import HomeConnectError
from homeassistant.components.number import (
ATTR_MAX,
ATTR_MIN,
NumberDeviceClass,
NumberEntity,
NumberEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .api import ConfigEntryAuth
from .const import ATTR_CONSTRAINTS, ATTR_STEPSIZE, ATTR_UNIT, ATTR_VALUE, DOMAIN
from .entity import HomeConnectEntity
_LOGGER = logging.getLogger(__name__)
NUMBERS = (
NumberEntityDescription(
key="Refrigeration.FridgeFreezer.Setting.SetpointTemperatureRefrigerator",
device_class=NumberDeviceClass.TEMPERATURE,
translation_key="refrigerator_setpoint_temperature",
),
NumberEntityDescription(
key="Refrigeration.FridgeFreezer.Setting.SetpointTemperatureFreezer",
device_class=NumberDeviceClass.TEMPERATURE,
translation_key="freezer_setpoint_temperature",
),
NumberEntityDescription(
key="Refrigeration.Common.Setting.BottleCooler.SetpointTemperature",
device_class=NumberDeviceClass.TEMPERATURE,
translation_key="bottle_cooler_setpoint_temperature",
),
NumberEntityDescription(
key="Refrigeration.Common.Setting.ChillerLeft.SetpointTemperature",
device_class=NumberDeviceClass.TEMPERATURE,
translation_key="chiller_left_setpoint_temperature",
),
NumberEntityDescription(
key="Refrigeration.Common.Setting.ChillerCommon.SetpointTemperature",
device_class=NumberDeviceClass.TEMPERATURE,
translation_key="chiller_setpoint_temperature",
),
NumberEntityDescription(
key="Refrigeration.Common.Setting.ChillerRight.SetpointTemperature",
device_class=NumberDeviceClass.TEMPERATURE,
translation_key="chiller_right_setpoint_temperature",
),
NumberEntityDescription(
key="Refrigeration.Common.Setting.WineCompartment.SetpointTemperature",
device_class=NumberDeviceClass.TEMPERATURE,
translation_key="wine_compartment_setpoint_temperature",
),
NumberEntityDescription(
key="Refrigeration.Common.Setting.WineCompartment2.SetpointTemperature",
device_class=NumberDeviceClass.TEMPERATURE,
translation_key="wine_compartment_2_setpoint_temperature",
),
NumberEntityDescription(
key="Refrigeration.Common.Setting.WineCompartment3.SetpointTemperature",
device_class=NumberDeviceClass.TEMPERATURE,
translation_key="wine_compartment_3_setpoint_temperature",
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Home Connect number."""
def get_entities() -> list[HomeConnectNumberEntity]:
"""Get a list of entities."""
hc_api: ConfigEntryAuth = hass.data[DOMAIN][config_entry.entry_id]
return [
HomeConnectNumberEntity(device, description)
for description in NUMBERS
for device in hc_api.devices
if description.key in device.appliance.status
]
async_add_entities(await hass.async_add_executor_job(get_entities), True)
class HomeConnectNumberEntity(HomeConnectEntity, NumberEntity):
"""Number setting class for Home Connect."""
async def async_set_native_value(self, value: float) -> None:
"""Set the native value of the entity."""
_LOGGER.debug(
"Tried to set value %s to %s for %s",
value,
self.bsh_key,
self.entity_id,
)
try:
await self.hass.async_add_executor_job(
self.device.appliance.set_setting,
self.bsh_key,
value,
)
except HomeConnectError as err:
_LOGGER.error(
"Error setting value %s to %s for %s: %s",
value,
self.bsh_key,
self.entity_id,
err,
)
async def async_fetch_constraints(self) -> None:
"""Fetch the max and min values and step for the number entity."""
try:
data = await self.hass.async_add_executor_job(
self.device.appliance.get, f"/settings/{self.bsh_key}"
)
except HomeConnectError as err:
_LOGGER.error("An error occurred: %s", err)
return
if not data or not (constraints := data.get(ATTR_CONSTRAINTS)):
return
self._attr_native_max_value = constraints.get(ATTR_MAX)
self._attr_native_min_value = constraints.get(ATTR_MIN)
self._attr_native_step = constraints.get(ATTR_STEPSIZE)
self._attr_native_unit_of_measurement = data.get(ATTR_UNIT)
async def async_update(self) -> None:
"""Update the number setting status."""
if not (data := self.device.appliance.status.get(self.bsh_key)):
_LOGGER.error("No value for %s", self.bsh_key)
self._attr_native_value = None
return
self._attr_native_value = data.get(ATTR_VALUE, None)
_LOGGER.debug("Updated, new value: %s", self._attr_native_value)
if (
not hasattr(self, "_attr_native_min_value")
or self._attr_native_min_value is None
or not hasattr(self, "_attr_native_max_value")
or self._attr_native_max_value is None
or not hasattr(self, "_attr_native_step")
or self._attr_native_step is None
):
await self.async_fetch_constraints()

View File

@ -188,6 +188,35 @@
"name": "Internal light"
}
},
"number": {
"refrigerator_setpoint_temperature": {
"name": "Refrigerator temperature"
},
"freezer_setpoint_temperature": {
"name": "Freezer temperature"
},
"bottle_cooler_setpoint_temperature": {
"name": "Bottle cooler temperature"
},
"chiller_left_setpoint_temperature": {
"name": "Chiller left temperature"
},
"chiller_setpoint_temperature": {
"name": "Chiller temperature"
},
"chiller_right_setpoint_temperature": {
"name": "Chiller right temperature"
},
"wine_compartment_setpoint_temperature": {
"name": "Wine compartment temperature"
},
"wine_compartment_2_setpoint_temperature": {
"name": "Wine compartment 2 temperature"
},
"wine_compartment_3_setpoint_temperature": {
"name": "Wine compartment 3 temperature"
}
},
"sensor": {
"program_progress": {
"name": "Program progress"

View File

@ -178,6 +178,7 @@ def mock_problematic_appliance(request: pytest.FixtureRequest) -> Mock:
)
mock.name = app
type(mock).status = PropertyMock(return_value={})
mock.get.side_effect = HomeConnectError
mock.get_programs_active.side_effect = HomeConnectError
mock.get_programs_available.side_effect = HomeConnectError
mock.start_program.side_effect = HomeConnectError

View File

@ -0,0 +1,172 @@
"""Tests for home_connect number entities."""
from collections.abc import Awaitable, Callable, Generator
import random
from unittest.mock import MagicMock, Mock
from homeconnect.api import HomeConnectError
import pytest
from homeassistant.components.home_connect.const import (
ATTR_CONSTRAINTS,
ATTR_STEPSIZE,
ATTR_UNIT,
ATTR_VALUE,
)
from homeassistant.components.number import (
ATTR_MAX,
ATTR_MIN,
ATTR_VALUE as SERVICE_ATTR_VALUE,
DEFAULT_MIN_VALUE,
DOMAIN as NUMBER_DOMAIN,
SERVICE_SET_VALUE,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from .conftest import get_all_appliances
from tests.common import MockConfigEntry
@pytest.fixture
def platforms() -> list[str]:
"""Fixture to specify platforms to test."""
return [Platform.NUMBER]
async def test_number(
bypass_throttle: Generator[None],
hass: HomeAssistant,
config_entry: MockConfigEntry,
integration_setup: Callable[[], Awaitable[bool]],
setup_credentials: None,
get_appliances: Mock,
) -> None:
"""Test number entity."""
get_appliances.side_effect = get_all_appliances
assert config_entry.state is ConfigEntryState.NOT_LOADED
assert await integration_setup()
assert config_entry.state is ConfigEntryState.LOADED
@pytest.mark.parametrize("appliance", ["Refrigerator"], indirect=True)
@pytest.mark.parametrize(
(
"entity_id",
"setting_key",
"min_value",
"max_value",
"step_size",
"unit_of_measurement",
),
[
(
f"{NUMBER_DOMAIN.lower()}.refrigerator_refrigerator_temperature",
"Refrigeration.FridgeFreezer.Setting.SetpointTemperatureRefrigerator",
7,
15,
0.1,
"°C",
),
],
)
@pytest.mark.usefixtures("bypass_throttle")
async def test_number_entity_functionality(
appliance: Mock,
entity_id: str,
setting_key: str,
bypass_throttle: Generator[None],
min_value: int,
max_value: int,
step_size: float,
unit_of_measurement: str,
hass: HomeAssistant,
config_entry: MockConfigEntry,
integration_setup: Callable[[], Awaitable[bool]],
setup_credentials: None,
get_appliances: MagicMock,
) -> None:
"""Test number entity functionality."""
appliance.get.side_effect = [
{
ATTR_CONSTRAINTS: {
ATTR_MIN: min_value,
ATTR_MAX: max_value,
ATTR_STEPSIZE: step_size,
},
ATTR_UNIT: unit_of_measurement,
}
]
get_appliances.return_value = [appliance]
current_value = min_value
appliance.status.update({setting_key: {ATTR_VALUE: current_value}})
assert config_entry.state is ConfigEntryState.NOT_LOADED
assert await integration_setup()
assert config_entry.state is ConfigEntryState.LOADED
assert hass.states.is_state(entity_id, str(current_value))
state = hass.states.get(entity_id)
assert state.attributes["min"] == min_value
assert state.attributes["max"] == max_value
assert state.attributes["step"] == step_size
assert state.attributes["unit_of_measurement"] == unit_of_measurement
new_value = random.randint(min_value + 1, max_value)
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: entity_id,
SERVICE_ATTR_VALUE: new_value,
},
blocking=True,
)
appliance.set_setting.assert_called_once_with(setting_key, new_value)
@pytest.mark.parametrize("problematic_appliance", ["Refrigerator"], indirect=True)
@pytest.mark.parametrize(
("entity_id", "setting_key", "mock_attr"),
[
(
f"{NUMBER_DOMAIN.lower()}.refrigerator_refrigerator_temperature",
"Refrigeration.FridgeFreezer.Setting.SetpointTemperatureRefrigerator",
"set_setting",
),
],
)
@pytest.mark.usefixtures("bypass_throttle")
async def test_number_entity_error(
problematic_appliance: Mock,
entity_id: str,
setting_key: str,
mock_attr: str,
hass: HomeAssistant,
config_entry: MockConfigEntry,
integration_setup: Callable[[], Awaitable[bool]],
setup_credentials: None,
get_appliances: MagicMock,
) -> None:
"""Test number entity error."""
get_appliances.return_value = [problematic_appliance]
assert config_entry.state is ConfigEntryState.NOT_LOADED
problematic_appliance.status.update({setting_key: {}})
assert await integration_setup()
assert config_entry.state is ConfigEntryState.LOADED
with pytest.raises(HomeConnectError):
getattr(problematic_appliance, mock_attr)()
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: entity_id,
SERVICE_ATTR_VALUE: DEFAULT_MIN_VALUE,
},
blocking=True,
)
assert getattr(problematic_appliance, mock_attr).call_count == 2