Home Connect add FridgeFreezer switch entities (#122881)

* Home Connect add FridgeFreezer switch entities

* Fix unrelated test

* Implemented requested changes from review

* Move exist_fn check code to setup

* Assign entity_description during init

* Resolve issue with functional testing
This commit is contained in:
Robert Contreras 2024-09-05 11:52:12 -07:00 committed by GitHub
parent d2d01b337d
commit d686b877b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 269 additions and 7 deletions

View File

@ -23,6 +23,13 @@ BSH_OPERATION_STATE_FINISHED = "BSH.Common.EnumType.OperationState.Finished"
COOKING_LIGHTING = "Cooking.Common.Setting.Lighting"
COOKING_LIGHTING_BRIGHTNESS = "Cooking.Common.Setting.LightingBrightness"
REFRIGERATION_SUPERMODEFREEZER = "Refrigeration.FridgeFreezer.Setting.SuperModeFreezer"
REFRIGERATION_SUPERMODEREFRIGERATOR = (
"Refrigeration.FridgeFreezer.Setting.SuperModeRefrigerator"
)
REFRIGERATION_DISPENSER = "Refrigeration.Common.Setting.Dispenser.Enabled"
BSH_AMBIENT_LIGHT_ENABLED = "BSH.Common.Setting.AmbientLightEnabled"
BSH_AMBIENT_LIGHT_BRIGHTNESS = "BSH.Common.Setting.AmbientLightBrightness"
BSH_AMBIENT_LIGHT_COLOR = "BSH.Common.Setting.AmbientLightColor"

View File

@ -21,5 +21,15 @@
"change_setting": {
"service": "mdi:cog"
}
},
"entity": {
"switch": {
"refrigeration_dispenser": {
"default": "mdi:snowflake",
"state": {
"off": "mdi:snowflake-off"
}
}
}
}
}

View File

@ -1,16 +1,18 @@
"""Provides a switch for Home Connect."""
from dataclasses import dataclass
import logging
from typing import Any
from homeconnect.api import HomeConnectError
from homeassistant.components.switch import SwitchEntity
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE, CONF_ENTITIES
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .api import ConfigEntryAuth
from .const import (
ATTR_VALUE,
BSH_ACTIVE_PROGRAM,
@ -19,12 +21,39 @@ from .const import (
BSH_POWER_ON,
BSH_POWER_STATE,
DOMAIN,
REFRIGERATION_DISPENSER,
REFRIGERATION_SUPERMODEFREEZER,
REFRIGERATION_SUPERMODEREFRIGERATOR,
)
from .entity import HomeConnectEntity
from .entity import HomeConnectDevice, HomeConnectEntity
_LOGGER = logging.getLogger(__name__)
@dataclass(frozen=True, kw_only=True)
class HomeConnectSwitchEntityDescription(SwitchEntityDescription):
"""Switch entity description."""
on_key: str
SWITCHES: tuple[HomeConnectSwitchEntityDescription, ...] = (
HomeConnectSwitchEntityDescription(
key="Supermode Freezer",
on_key=REFRIGERATION_SUPERMODEFREEZER,
),
HomeConnectSwitchEntityDescription(
key="Supermode Refrigerator",
on_key=REFRIGERATION_SUPERMODEREFRIGERATOR,
),
HomeConnectSwitchEntityDescription(
key="Dispenser Enabled",
on_key=REFRIGERATION_DISPENSER,
translation_key="refrigeration_dispenser",
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
@ -35,18 +64,87 @@ async def async_setup_entry(
def get_entities():
"""Get a list of entities."""
entities = []
hc_api = hass.data[DOMAIN][config_entry.entry_id]
hc_api: ConfigEntryAuth = hass.data[DOMAIN][config_entry.entry_id]
for device_dict in hc_api.devices:
entity_dicts = device_dict.get(CONF_ENTITIES, {}).get("switch", [])
entity_list = [HomeConnectProgramSwitch(**d) for d in entity_dicts]
entity_list += [HomeConnectPowerSwitch(device_dict[CONF_DEVICE])]
entity_list += [HomeConnectChildLockSwitch(device_dict[CONF_DEVICE])]
entities += entity_list
# Auto-discover entities
hc_device: HomeConnectDevice = device_dict[CONF_DEVICE]
entities.extend(
HomeConnectSwitch(device=hc_device, entity_description=description)
for description in SWITCHES
if description.on_key in hc_device.appliance.status
)
entities.extend(entity_list)
return entities
async_add_entities(await hass.async_add_executor_job(get_entities), True)
class HomeConnectSwitch(HomeConnectEntity, SwitchEntity):
"""Generic switch class for Home Connect Binary Settings."""
entity_description: HomeConnectSwitchEntityDescription
_attr_available: bool = False
def __init__(
self,
device: HomeConnectDevice,
entity_description: HomeConnectSwitchEntityDescription,
) -> None:
"""Initialize the entity."""
self.entity_description = entity_description
super().__init__(device=device, desc=entity_description.key)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on setting."""
_LOGGER.debug("Turning on %s", self.entity_description.key)
try:
await self.hass.async_add_executor_job(
self.device.appliance.set_setting, self.entity_description.on_key, True
)
except HomeConnectError as err:
_LOGGER.error("Error while trying to turn on: %s", err)
self._attr_available = False
return
self._attr_available = True
self.async_entity_update()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off setting."""
_LOGGER.debug("Turning off %s", self.entity_description.key)
try:
await self.hass.async_add_executor_job(
self.device.appliance.set_setting, self.entity_description.on_key, False
)
except HomeConnectError as err:
_LOGGER.error("Error while trying to turn off: %s", err)
self._attr_available = False
return
self._attr_available = True
self.async_entity_update()
async def async_update(self) -> None:
"""Update the switch's status."""
self._attr_is_on = self.device.appliance.status.get(
self.entity_description.on_key, {}
).get(ATTR_VALUE)
self._attr_available = True
_LOGGER.debug(
"Updated %s, new state: %s",
self.entity_description.key,
self._attr_is_on,
)
class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
"""Switch class for Home Connect."""

View File

@ -111,5 +111,35 @@
}
]
}
},
"FridgeFreezer": {
"data": {
"settings": [
{
"key": "Refrigeration.FridgeFreezer.Setting.SuperModeFreezer",
"value": false,
"type": "Boolean",
"constraints": {
"access": "readWrite"
}
},
{
"key": "Refrigeration.FridgeFreezer.Setting.SuperModeRefrigerator",
"value": false,
"type": "Boolean",
"constraints": {
"access": "readWrite"
}
},
{
"key": "Refrigeration.Common.Setting.Dispenser.Enabled",
"value": false,
"type": "Boolean",
"constraints": {
"access": "readWrite"
}
}
]
}
}
}

View File

@ -9,6 +9,7 @@ import pytest
from requests import HTTPError
import requests_mock
from homeassistant.components.home_connect import SCAN_INTERVAL
from homeassistant.components.home_connect.const import DOMAIN, OAUTH2_TOKEN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
@ -155,14 +156,14 @@ async def test_update_throttle(
# First re-load after 1 minute is not blocked.
assert await hass.config_entries.async_unload(config_entry.entry_id)
assert config_entry.state == ConfigEntryState.NOT_LOADED
freezer.tick(60)
freezer.tick(SCAN_INTERVAL.seconds + 0.1)
assert await hass.config_entries.async_setup(config_entry.entry_id)
assert get_appliances.call_count == get_appliances_call_count + 1
# Second re-load is blocked by Throttle.
assert await hass.config_entries.async_unload(config_entry.entry_id)
assert config_entry.state == ConfigEntryState.NOT_LOADED
freezer.tick(59)
freezer.tick(SCAN_INTERVAL.seconds - 0.1)
assert await hass.config_entries.async_setup(config_entry.entry_id)
assert get_appliances.call_count == get_appliances_call_count + 1

View File

@ -3,7 +3,7 @@
from collections.abc import Awaitable, Callable, Generator
from unittest.mock import MagicMock, Mock
from homeconnect.api import HomeConnectError
from homeconnect.api import HomeConnectAppliance, HomeConnectError
import pytest
from homeassistant.components.home_connect.const import (
@ -13,10 +13,12 @@ from homeassistant.components.home_connect.const import (
BSH_POWER_OFF,
BSH_POWER_ON,
BSH_POWER_STATE,
REFRIGERATION_SUPERMODEFREEZER,
)
from homeassistant.components.switch import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_OFF,
@ -214,3 +216,117 @@ async def test_switch_exception_handling(
DOMAIN, service, {"entity_id": entity_id}, blocking=True
)
assert getattr(problematic_appliance, mock_attr).call_count == 2
@pytest.mark.parametrize(
("entity_id", "status", "service", "state", "appliance"),
[
(
"switch.fridgefreezer_supermode_freezer",
{REFRIGERATION_SUPERMODEFREEZER: {"value": True}},
SERVICE_TURN_ON,
STATE_ON,
"FridgeFreezer",
),
(
"switch.fridgefreezer_supermode_freezer",
{REFRIGERATION_SUPERMODEFREEZER: {"value": False}},
SERVICE_TURN_OFF,
STATE_OFF,
"FridgeFreezer",
),
],
indirect=["appliance"],
)
async def test_ent_desc_switch_functionality(
entity_id: str,
status: dict,
service: str,
state: str,
bypass_throttle: Generator[None],
hass: HomeAssistant,
config_entry: MockConfigEntry,
integration_setup: Callable[[], Awaitable[bool]],
setup_credentials: None,
appliance: Mock,
get_appliances: MagicMock,
) -> None:
"""Test switch functionality - entity description setup."""
appliance.status.update(
HomeConnectAppliance.json2dict(
load_json_object_fixture("home_connect/settings.json")
.get(appliance.name)
.get("data")
.get("settings")
)
)
get_appliances.return_value = [appliance]
assert config_entry.state == ConfigEntryState.NOT_LOADED
assert await integration_setup()
assert config_entry.state == ConfigEntryState.LOADED
appliance.status.update(status)
await hass.services.async_call(
DOMAIN, service, {ATTR_ENTITY_ID: entity_id}, blocking=True
)
assert hass.states.is_state(entity_id, state)
@pytest.mark.parametrize(
("entity_id", "status", "service", "mock_attr", "problematic_appliance"),
[
(
"switch.fridgefreezer_supermode_freezer",
{REFRIGERATION_SUPERMODEFREEZER: {"value": ""}},
SERVICE_TURN_ON,
"set_setting",
"FridgeFreezer",
),
(
"switch.fridgefreezer_supermode_freezer",
{REFRIGERATION_SUPERMODEFREEZER: {"value": ""}},
SERVICE_TURN_OFF,
"set_setting",
"FridgeFreezer",
),
],
indirect=["problematic_appliance"],
)
async def test_ent_desc_switch_exception_handling(
entity_id: str,
status: dict,
service: str,
mock_attr: str,
bypass_throttle: Generator[None],
hass: HomeAssistant,
integration_setup: Callable[[], Awaitable[bool]],
config_entry: MockConfigEntry,
setup_credentials: None,
problematic_appliance: Mock,
get_appliances: MagicMock,
) -> None:
"""Test switch exception handling - entity description setup."""
problematic_appliance.status.update(
HomeConnectAppliance.json2dict(
load_json_object_fixture("home_connect/settings.json")
.get(problematic_appliance.name)
.get("data")
.get("settings")
)
)
get_appliances.return_value = [problematic_appliance]
assert config_entry.state == ConfigEntryState.NOT_LOADED
assert await integration_setup()
assert config_entry.state == ConfigEntryState.LOADED
# Assert that an exception is called.
with pytest.raises(HomeConnectError):
getattr(problematic_appliance, mock_attr)()
problematic_appliance.status.update(status)
await hass.services.async_call(
DOMAIN, service, {ATTR_ENTITY_ID: entity_id}, blocking=True
)
assert getattr(problematic_appliance, mock_attr).call_count == 2