mirror of
https://github.com/home-assistant/core.git
synced 2025-12-25 05:26:47 +00:00
Ensure that Home Connect program update value event is a string when updating options (#156416)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
committed by
GitHub
parent
09a105d9ad
commit
34c1d45ee0
@@ -7,7 +7,7 @@ from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
from typing import Any
|
||||
|
||||
from aiohomeconnect.client import Client as HomeConnectClient
|
||||
from aiohomeconnect.model import (
|
||||
@@ -247,14 +247,15 @@ class HomeConnectCoordinator(
|
||||
value=event.value,
|
||||
)
|
||||
else:
|
||||
event_value = event.value
|
||||
if event_key in (
|
||||
EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
|
||||
EventKey.BSH_COMMON_ROOT_SELECTED_PROGRAM,
|
||||
):
|
||||
) and isinstance(event_value, str):
|
||||
await self.update_options(
|
||||
event_message_ha_id,
|
||||
event_key,
|
||||
ProgramKey(cast(str, event.value)),
|
||||
ProgramKey(event_value),
|
||||
)
|
||||
events[event_key] = event
|
||||
self._call_event_listener(event_message)
|
||||
|
||||
@@ -14,7 +14,6 @@ from aiohomeconnect.model.error import (
|
||||
TooManyRequestsError,
|
||||
)
|
||||
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -62,10 +61,8 @@ class HomeConnectEntity(CoordinatorEntity[HomeConnectCoordinator]):
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self.update_native_value()
|
||||
available = self._attr_available = self.appliance.info.connected
|
||||
self.async_write_ha_state()
|
||||
state = STATE_UNAVAILABLE if not available else self.state
|
||||
_LOGGER.debug("Updated %s, new state: %s", self.entity_id, state)
|
||||
_LOGGER.debug("Updated %s", self)
|
||||
|
||||
@property
|
||||
def bsh_key(self) -> str:
|
||||
@@ -80,7 +77,7 @@ class HomeConnectEntity(CoordinatorEntity[HomeConnectCoordinator]):
|
||||
as event updates should take precedence over the coordinator
|
||||
refresh.
|
||||
"""
|
||||
return self._attr_available
|
||||
return self.appliance.info.connected and self._attr_available
|
||||
|
||||
|
||||
class HomeConnectOptionEntity(HomeConnectEntity):
|
||||
|
||||
@@ -190,7 +190,7 @@ async def test_connected_devices(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("appliance", ["FridgeFreezer"], indirect=True)
|
||||
@pytest.mark.parametrize("appliance", ["Oven"], indirect=True)
|
||||
async def test_number_entity_availability(
|
||||
hass: HomeAssistant,
|
||||
client: MagicMock,
|
||||
@@ -200,8 +200,19 @@ async def test_number_entity_availability(
|
||||
) -> None:
|
||||
"""Test if number entities availability are based on the appliance connection state."""
|
||||
entity_ids = [
|
||||
f"{NUMBER_DOMAIN.lower()}.fridgefreezer_refrigerator_temperature",
|
||||
f"{NUMBER_DOMAIN.lower()}.oven_alarm_clock",
|
||||
f"{NUMBER_DOMAIN.lower()}.oven_setpoint_temperature",
|
||||
]
|
||||
client.get_available_program = AsyncMock(
|
||||
return_value=ProgramDefinition(
|
||||
ProgramKey.UNKNOWN,
|
||||
options=[
|
||||
ProgramDefinitionOption(
|
||||
OptionKey.COOKING_OVEN_SETPOINT_TEMPERATURE, "Boolean"
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
client.get_setting.side_effect = None
|
||||
# Setting constrains are not needed for this test
|
||||
@@ -616,3 +627,133 @@ async def test_options_functionality(
|
||||
"value": 80,
|
||||
}
|
||||
assert hass.states.is_state(entity_id, "80.0")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("appliance", ["Oven"], indirect=True)
|
||||
async def test_options_unavailable_when_option_is_missing(
|
||||
hass: HomeAssistant,
|
||||
client: MagicMock,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
appliance: HomeAppliance,
|
||||
) -> None:
|
||||
"""Test that option entities become unavailable when the option is missing."""
|
||||
entity_id = "number.oven_setpoint_temperature"
|
||||
client.get_available_program = AsyncMock(
|
||||
return_value=ProgramDefinition(
|
||||
ProgramKey.UNKNOWN,
|
||||
options=[
|
||||
ProgramDefinitionOption(
|
||||
OptionKey.COOKING_OVEN_SETPOINT_TEMPERATURE, "Double"
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
client.get_available_program = AsyncMock(
|
||||
return_value=ProgramDefinition(
|
||||
ProgramKey.COOKING_OVEN_HEATING_MODE_INTENSIVE_HEAT,
|
||||
options=[],
|
||||
)
|
||||
)
|
||||
await client.add_events(
|
||||
[
|
||||
EventMessage(
|
||||
appliance.ha_id,
|
||||
EventType.NOTIFY,
|
||||
data=ArrayOfEvents(
|
||||
[
|
||||
Event(
|
||||
EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
|
||||
EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM.value,
|
||||
0,
|
||||
level="info",
|
||||
handling="auto",
|
||||
value=ProgramKey.COOKING_OVEN_HEATING_MODE_INTENSIVE_HEAT,
|
||||
)
|
||||
]
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
@pytest.mark.parametrize("appliance", ["Oven"], indirect=True)
|
||||
@pytest.mark.parametrize(
|
||||
"event_key",
|
||||
[
|
||||
EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
|
||||
EventKey.BSH_COMMON_ROOT_SELECTED_PROGRAM,
|
||||
],
|
||||
)
|
||||
async def test_options_available_when_program_is_null(
|
||||
hass: HomeAssistant,
|
||||
client: MagicMock,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
appliance: HomeAppliance,
|
||||
event_key: EventKey,
|
||||
) -> None:
|
||||
"""Test that option entities still available when the active program becomes null.
|
||||
|
||||
This can happen when the appliance starts or finish the program; the appliance first
|
||||
updates the non-null program, and then the null program value.
|
||||
This test ensures that the options defined by the non-null program are not removed
|
||||
from the coordinator and therefore, the entities remain available.
|
||||
"""
|
||||
entity_id = "number.oven_setpoint_temperature"
|
||||
client.get_available_program = AsyncMock(
|
||||
return_value=ProgramDefinition(
|
||||
ProgramKey.UNKNOWN,
|
||||
options=[
|
||||
ProgramDefinitionOption(
|
||||
OptionKey.COOKING_OVEN_SETPOINT_TEMPERATURE, "Double"
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
await client.add_events(
|
||||
[
|
||||
EventMessage(
|
||||
appliance.ha_id,
|
||||
EventType.NOTIFY,
|
||||
data=ArrayOfEvents(
|
||||
[
|
||||
Event(
|
||||
event_key,
|
||||
event_key.value,
|
||||
0,
|
||||
level="info",
|
||||
handling="auto",
|
||||
value=None,
|
||||
)
|
||||
]
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
@@ -215,9 +215,17 @@ async def test_select_entity_availability(
|
||||
appliance: HomeAppliance,
|
||||
) -> None:
|
||||
"""Test if select entities availability are based on the appliance connection state."""
|
||||
entity_ids = [
|
||||
"select.washer_active_program",
|
||||
]
|
||||
entity_ids = ["select.washer_active_program", "select.washer_temperature"]
|
||||
client.get_available_program = AsyncMock(
|
||||
return_value=ProgramDefinition(
|
||||
ProgramKey.UNKNOWN,
|
||||
options=[
|
||||
ProgramDefinitionOption(
|
||||
OptionKey.LAUNDRY_CARE_WASHER_TEMPERATURE, "Boolean"
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
@@ -967,3 +975,133 @@ async def test_options_functionality(
|
||||
assert hass.states.is_state(
|
||||
entity_id, "laundry_care_washer_enum_type_temperature_ul_warm"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("appliance", ["Washer"], indirect=True)
|
||||
async def test_options_unavailable_when_option_is_missing(
|
||||
hass: HomeAssistant,
|
||||
client: MagicMock,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
appliance: HomeAppliance,
|
||||
) -> None:
|
||||
"""Test that option entities become unavailable when the option is missing."""
|
||||
entity_id = "select.washer_temperature"
|
||||
client.get_available_program = AsyncMock(
|
||||
return_value=ProgramDefinition(
|
||||
ProgramKey.UNKNOWN,
|
||||
options=[
|
||||
ProgramDefinitionOption(
|
||||
OptionKey.LAUNDRY_CARE_WASHER_TEMPERATURE, "Boolean"
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
client.get_available_program = AsyncMock(
|
||||
return_value=ProgramDefinition(
|
||||
ProgramKey.LAUNDRY_CARE_WASHER_AUTO_30,
|
||||
options=[],
|
||||
)
|
||||
)
|
||||
await client.add_events(
|
||||
[
|
||||
EventMessage(
|
||||
appliance.ha_id,
|
||||
EventType.NOTIFY,
|
||||
data=ArrayOfEvents(
|
||||
[
|
||||
Event(
|
||||
EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
|
||||
EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM.value,
|
||||
0,
|
||||
level="info",
|
||||
handling="auto",
|
||||
value=ProgramKey.LAUNDRY_CARE_WASHER_AUTO_30,
|
||||
)
|
||||
]
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
@pytest.mark.parametrize("appliance", ["Washer"], indirect=True)
|
||||
@pytest.mark.parametrize(
|
||||
"event_key",
|
||||
[
|
||||
EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
|
||||
EventKey.BSH_COMMON_ROOT_SELECTED_PROGRAM,
|
||||
],
|
||||
)
|
||||
async def test_options_available_when_program_is_null(
|
||||
hass: HomeAssistant,
|
||||
client: MagicMock,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
appliance: HomeAppliance,
|
||||
event_key: EventKey,
|
||||
) -> None:
|
||||
"""Test that option entities still available when the active program becomes null.
|
||||
|
||||
This can happen when the appliance starts or finish the program; the appliance first
|
||||
updates the non-null program, and then the null program value.
|
||||
This test ensures that the options defined by the non-null program are not removed
|
||||
from the coordinator and therefore, the entities remain available.
|
||||
"""
|
||||
entity_id = "select.washer_temperature"
|
||||
client.get_available_program = AsyncMock(
|
||||
return_value=ProgramDefinition(
|
||||
ProgramKey.UNKNOWN,
|
||||
options=[
|
||||
ProgramDefinitionOption(
|
||||
OptionKey.LAUNDRY_CARE_WASHER_TEMPERATURE, "Enumeration"
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
await client.add_events(
|
||||
[
|
||||
EventMessage(
|
||||
appliance.ha_id,
|
||||
EventType.NOTIFY,
|
||||
data=ArrayOfEvents(
|
||||
[
|
||||
Event(
|
||||
event_key,
|
||||
event_key.value,
|
||||
0,
|
||||
level="info",
|
||||
handling="auto",
|
||||
value=None,
|
||||
)
|
||||
]
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
@@ -7,6 +7,7 @@ from unittest.mock import AsyncMock, MagicMock
|
||||
from aiohomeconnect.model import (
|
||||
ArrayOfEvents,
|
||||
ArrayOfSettings,
|
||||
Event,
|
||||
EventMessage,
|
||||
EventType,
|
||||
GetSetting,
|
||||
@@ -31,6 +32,7 @@ from homeassistant.components.home_connect.const import (
|
||||
BSH_POWER_ON,
|
||||
BSH_POWER_STANDBY,
|
||||
DOMAIN,
|
||||
EventKey,
|
||||
)
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
@@ -191,7 +193,18 @@ async def test_switch_entity_availability(
|
||||
entity_ids = [
|
||||
"switch.dishwasher_power",
|
||||
"switch.dishwasher_child_lock",
|
||||
"switch.dishwasher_half_load",
|
||||
]
|
||||
client.get_available_program = AsyncMock(
|
||||
return_value=ProgramDefinition(
|
||||
ProgramKey.UNKNOWN,
|
||||
options=[
|
||||
ProgramDefinitionOption(
|
||||
OptionKey.DISHCARE_DISHWASHER_HALF_LOAD, "Boolean"
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
@@ -735,3 +748,133 @@ async def test_options_functionality(
|
||||
"value": True,
|
||||
}
|
||||
assert hass.states.is_state(entity_id, STATE_ON)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("appliance", ["Dishwasher"], indirect=True)
|
||||
async def test_options_unavailable_when_option_is_missing(
|
||||
hass: HomeAssistant,
|
||||
client: MagicMock,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
appliance: HomeAppliance,
|
||||
) -> None:
|
||||
"""Test that option entities become unavailable when the option is missing."""
|
||||
entity_id = "switch.dishwasher_half_load"
|
||||
client.get_available_program = AsyncMock(
|
||||
return_value=ProgramDefinition(
|
||||
ProgramKey.UNKNOWN,
|
||||
options=[
|
||||
ProgramDefinitionOption(
|
||||
OptionKey.DISHCARE_DISHWASHER_HALF_LOAD, "Boolean"
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
client.get_available_program = AsyncMock(
|
||||
return_value=ProgramDefinition(
|
||||
ProgramKey.DISHCARE_DISHWASHER_AUTO_1,
|
||||
options=[],
|
||||
)
|
||||
)
|
||||
await client.add_events(
|
||||
[
|
||||
EventMessage(
|
||||
appliance.ha_id,
|
||||
EventType.NOTIFY,
|
||||
data=ArrayOfEvents(
|
||||
[
|
||||
Event(
|
||||
EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
|
||||
EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM.value,
|
||||
0,
|
||||
level="info",
|
||||
handling="auto",
|
||||
value=ProgramKey.DISHCARE_DISHWASHER_AUTO_1,
|
||||
)
|
||||
]
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
@pytest.mark.parametrize("appliance", ["Dishwasher"], indirect=True)
|
||||
@pytest.mark.parametrize(
|
||||
"event_key",
|
||||
[
|
||||
EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
|
||||
EventKey.BSH_COMMON_ROOT_SELECTED_PROGRAM,
|
||||
],
|
||||
)
|
||||
async def test_options_available_when_program_is_null(
|
||||
hass: HomeAssistant,
|
||||
client: MagicMock,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
appliance: HomeAppliance,
|
||||
event_key: EventKey,
|
||||
) -> None:
|
||||
"""Test that option entities still available when the active program becomes null.
|
||||
|
||||
This can happen when the appliance starts or finish the program; the appliance first
|
||||
updates the non-null program, and then the null program value.
|
||||
This test ensures that the options defined by the non-null program are not removed
|
||||
from the coordinator and therefore, the entities remain available.
|
||||
"""
|
||||
entity_id = "switch.dishwasher_half_load"
|
||||
client.get_available_program = AsyncMock(
|
||||
return_value=ProgramDefinition(
|
||||
ProgramKey.UNKNOWN,
|
||||
options=[
|
||||
ProgramDefinitionOption(
|
||||
OptionKey.DISHCARE_DISHWASHER_HALF_LOAD, "Boolean"
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
await client.add_events(
|
||||
[
|
||||
EventMessage(
|
||||
appliance.ha_id,
|
||||
EventType.NOTIFY,
|
||||
data=ArrayOfEvents(
|
||||
[
|
||||
Event(
|
||||
event_key,
|
||||
event_key.value,
|
||||
0,
|
||||
level="info",
|
||||
handling="auto",
|
||||
value=None,
|
||||
)
|
||||
]
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
Reference in New Issue
Block a user