mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 18:57:06 +00:00
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:
parent
d2d01b337d
commit
d686b877b1
@ -23,6 +23,13 @@ BSH_OPERATION_STATE_FINISHED = "BSH.Common.EnumType.OperationState.Finished"
|
|||||||
COOKING_LIGHTING = "Cooking.Common.Setting.Lighting"
|
COOKING_LIGHTING = "Cooking.Common.Setting.Lighting"
|
||||||
COOKING_LIGHTING_BRIGHTNESS = "Cooking.Common.Setting.LightingBrightness"
|
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_ENABLED = "BSH.Common.Setting.AmbientLightEnabled"
|
||||||
BSH_AMBIENT_LIGHT_BRIGHTNESS = "BSH.Common.Setting.AmbientLightBrightness"
|
BSH_AMBIENT_LIGHT_BRIGHTNESS = "BSH.Common.Setting.AmbientLightBrightness"
|
||||||
BSH_AMBIENT_LIGHT_COLOR = "BSH.Common.Setting.AmbientLightColor"
|
BSH_AMBIENT_LIGHT_COLOR = "BSH.Common.Setting.AmbientLightColor"
|
||||||
|
@ -21,5 +21,15 @@
|
|||||||
"change_setting": {
|
"change_setting": {
|
||||||
"service": "mdi:cog"
|
"service": "mdi:cog"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"switch": {
|
||||||
|
"refrigeration_dispenser": {
|
||||||
|
"default": "mdi:snowflake",
|
||||||
|
"state": {
|
||||||
|
"off": "mdi:snowflake-off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
"""Provides a switch for Home Connect."""
|
"""Provides a switch for Home Connect."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from homeconnect.api import HomeConnectError
|
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.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_DEVICE, CONF_ENTITIES
|
from homeassistant.const import CONF_DEVICE, CONF_ENTITIES
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from .api import ConfigEntryAuth
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_VALUE,
|
ATTR_VALUE,
|
||||||
BSH_ACTIVE_PROGRAM,
|
BSH_ACTIVE_PROGRAM,
|
||||||
@ -19,12 +21,39 @@ from .const import (
|
|||||||
BSH_POWER_ON,
|
BSH_POWER_ON,
|
||||||
BSH_POWER_STATE,
|
BSH_POWER_STATE,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
REFRIGERATION_DISPENSER,
|
||||||
|
REFRIGERATION_SUPERMODEFREEZER,
|
||||||
|
REFRIGERATION_SUPERMODEREFRIGERATOR,
|
||||||
)
|
)
|
||||||
from .entity import HomeConnectEntity
|
from .entity import HomeConnectDevice, HomeConnectEntity
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: ConfigEntry,
|
||||||
@ -35,18 +64,87 @@ async def async_setup_entry(
|
|||||||
def get_entities():
|
def get_entities():
|
||||||
"""Get a list of entities."""
|
"""Get a list of entities."""
|
||||||
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:
|
for device_dict in hc_api.devices:
|
||||||
entity_dicts = device_dict.get(CONF_ENTITIES, {}).get("switch", [])
|
entity_dicts = device_dict.get(CONF_ENTITIES, {}).get("switch", [])
|
||||||
entity_list = [HomeConnectProgramSwitch(**d) for d in entity_dicts]
|
entity_list = [HomeConnectProgramSwitch(**d) for d in entity_dicts]
|
||||||
entity_list += [HomeConnectPowerSwitch(device_dict[CONF_DEVICE])]
|
entity_list += [HomeConnectPowerSwitch(device_dict[CONF_DEVICE])]
|
||||||
entity_list += [HomeConnectChildLockSwitch(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
|
return entities
|
||||||
|
|
||||||
async_add_entities(await hass.async_add_executor_job(get_entities), True)
|
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):
|
class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
|
||||||
"""Switch class for Home Connect."""
|
"""Switch class for Home Connect."""
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import pytest
|
|||||||
from requests import HTTPError
|
from requests import HTTPError
|
||||||
import requests_mock
|
import requests_mock
|
||||||
|
|
||||||
|
from homeassistant.components.home_connect import SCAN_INTERVAL
|
||||||
from homeassistant.components.home_connect.const import DOMAIN, OAUTH2_TOKEN
|
from homeassistant.components.home_connect.const import DOMAIN, OAUTH2_TOKEN
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@ -155,14 +156,14 @@ async def test_update_throttle(
|
|||||||
# First re-load after 1 minute is not blocked.
|
# First re-load after 1 minute is not blocked.
|
||||||
assert await hass.config_entries.async_unload(config_entry.entry_id)
|
assert await hass.config_entries.async_unload(config_entry.entry_id)
|
||||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
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 await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
assert get_appliances.call_count == get_appliances_call_count + 1
|
assert get_appliances.call_count == get_appliances_call_count + 1
|
||||||
|
|
||||||
# Second re-load is blocked by Throttle.
|
# Second re-load is blocked by Throttle.
|
||||||
assert await hass.config_entries.async_unload(config_entry.entry_id)
|
assert await hass.config_entries.async_unload(config_entry.entry_id)
|
||||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
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 await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
assert get_appliances.call_count == get_appliances_call_count + 1
|
assert get_appliances.call_count == get_appliances_call_count + 1
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
from collections.abc import Awaitable, Callable, Generator
|
from collections.abc import Awaitable, Callable, Generator
|
||||||
from unittest.mock import MagicMock, Mock
|
from unittest.mock import MagicMock, Mock
|
||||||
|
|
||||||
from homeconnect.api import HomeConnectError
|
from homeconnect.api import HomeConnectAppliance, HomeConnectError
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.home_connect.const import (
|
from homeassistant.components.home_connect.const import (
|
||||||
@ -13,10 +13,12 @@ from homeassistant.components.home_connect.const import (
|
|||||||
BSH_POWER_OFF,
|
BSH_POWER_OFF,
|
||||||
BSH_POWER_ON,
|
BSH_POWER_ON,
|
||||||
BSH_POWER_STATE,
|
BSH_POWER_STATE,
|
||||||
|
REFRIGERATION_SUPERMODEFREEZER,
|
||||||
)
|
)
|
||||||
from homeassistant.components.switch import DOMAIN
|
from homeassistant.components.switch import DOMAIN
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
SERVICE_TURN_OFF,
|
SERVICE_TURN_OFF,
|
||||||
SERVICE_TURN_ON,
|
SERVICE_TURN_ON,
|
||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
@ -214,3 +216,117 @@ async def test_switch_exception_handling(
|
|||||||
DOMAIN, service, {"entity_id": entity_id}, blocking=True
|
DOMAIN, service, {"entity_id": entity_id}, blocking=True
|
||||||
)
|
)
|
||||||
assert getattr(problematic_appliance, mock_attr).call_count == 2
|
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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user