Add buttons to Home Connect (#138792)

* Add buttons

* Fix stale documentation
This commit is contained in:
J. Diego Rodríguez Royo 2025-02-22 21:14:11 +01:00 committed by GitHub
parent 92788a04ff
commit 98c6a578b7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 667 additions and 0 deletions

View File

@ -187,6 +187,7 @@ SERVICE_COMMAND_SCHEMA = vol.Schema({vol.Required(ATTR_DEVICE_ID): str})
PLATFORMS = [ PLATFORMS = [
Platform.BINARY_SENSOR, Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.LIGHT, Platform.LIGHT,
Platform.NUMBER, Platform.NUMBER,
Platform.SELECT, Platform.SELECT,

View File

@ -0,0 +1,160 @@
"""Provides button entities for Home Connect."""
from aiohomeconnect.model import CommandKey, EventKey
from aiohomeconnect.model.error import HomeConnectError
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .common import setup_home_connect_entry
from .const import APPLIANCES_WITH_PROGRAMS, DOMAIN
from .coordinator import (
HomeConnectApplianceData,
HomeConnectConfigEntry,
HomeConnectCoordinator,
)
from .entity import HomeConnectEntity
from .utils import get_dict_from_home_connect_error
class HomeConnectCommandButtonEntityDescription(ButtonEntityDescription):
"""Describes Home Connect button entity."""
key: CommandKey
COMMAND_BUTTONS = (
HomeConnectCommandButtonEntityDescription(
key=CommandKey.BSH_COMMON_OPEN_DOOR,
translation_key="open_door",
),
HomeConnectCommandButtonEntityDescription(
key=CommandKey.BSH_COMMON_PARTLY_OPEN_DOOR,
translation_key="partly_open_door",
),
HomeConnectCommandButtonEntityDescription(
key=CommandKey.BSH_COMMON_PAUSE_PROGRAM,
translation_key="pause_program",
),
HomeConnectCommandButtonEntityDescription(
key=CommandKey.BSH_COMMON_RESUME_PROGRAM,
translation_key="resume_program",
),
)
def _get_entities_for_appliance(
entry: HomeConnectConfigEntry,
appliance: HomeConnectApplianceData,
) -> list[HomeConnectEntity]:
"""Get a list of entities."""
entities: list[HomeConnectEntity] = []
entities.extend(
HomeConnectCommandButtonEntity(entry.runtime_data, appliance, description)
for description in COMMAND_BUTTONS
if description.key in appliance.commands
)
if appliance.info.type in APPLIANCES_WITH_PROGRAMS:
entities.append(
HomeConnectStopProgramButtonEntity(entry.runtime_data, appliance)
)
return entities
async def async_setup_entry(
hass: HomeAssistant,
entry: HomeConnectConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Home Connect button entities."""
setup_home_connect_entry(
entry,
_get_entities_for_appliance,
async_add_entities,
)
class HomeConnectButtonEntity(HomeConnectEntity, ButtonEntity):
"""Describes Home Connect button entity."""
entity_description: ButtonEntityDescription
def __init__(
self,
coordinator: HomeConnectCoordinator,
appliance: HomeConnectApplianceData,
desc: ButtonEntityDescription,
) -> None:
"""Initialize the entity."""
super().__init__(
coordinator,
appliance,
# The entity is subscribed to the appliance connected event,
# but it will receive also the disconnected event
ButtonEntityDescription(
key=EventKey.BSH_COMMON_APPLIANCE_CONNECTED,
),
)
self.entity_description = desc
self.appliance = appliance
self.unique_id = f"{appliance.info.ha_id}-{desc.key}"
def update_native_value(self) -> None:
"""Set the value of the entity."""
class HomeConnectCommandButtonEntity(HomeConnectButtonEntity):
"""Button entity for Home Connect commands."""
entity_description: HomeConnectCommandButtonEntityDescription
async def async_press(self) -> None:
"""Press the button."""
try:
await self.coordinator.client.put_command(
self.appliance.info.ha_id,
command_key=self.entity_description.key,
value=True,
)
except HomeConnectError as error:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="execute_command",
translation_placeholders={
**get_dict_from_home_connect_error(error),
"command": self.entity_description.key,
},
) from error
class HomeConnectStopProgramButtonEntity(HomeConnectButtonEntity):
"""Button entity for stopping a program."""
def __init__(
self,
coordinator: HomeConnectCoordinator,
appliance: HomeConnectApplianceData,
) -> None:
"""Initialize the entity."""
super().__init__(
coordinator,
appliance,
ButtonEntityDescription(
key="StopProgram",
translation_key="stop_program",
),
)
async def async_press(self) -> None:
"""Press the button."""
try:
await self.coordinator.client.stop_program(self.appliance.info.ha_id)
except HomeConnectError as error:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="stop_program",
translation_placeholders=get_dict_from_home_connect_error(error),
) from error

View File

@ -11,6 +11,7 @@ from typing import Any, cast
from aiohomeconnect.client import Client as HomeConnectClient from aiohomeconnect.client import Client as HomeConnectClient
from aiohomeconnect.model import ( from aiohomeconnect.model import (
CommandKey,
Event, Event,
EventKey, EventKey,
EventMessage, EventMessage,
@ -53,6 +54,7 @@ EVENT_STREAM_RECONNECT_DELAY = 30
class HomeConnectApplianceData: class HomeConnectApplianceData:
"""Class to hold Home Connect appliance data.""" """Class to hold Home Connect appliance data."""
commands: set[CommandKey]
events: dict[EventKey, Event] events: dict[EventKey, Event]
info: HomeAppliance info: HomeAppliance
options: dict[OptionKey, ProgramDefinitionOption] options: dict[OptionKey, ProgramDefinitionOption]
@ -62,6 +64,7 @@ class HomeConnectApplianceData:
def update(self, other: HomeConnectApplianceData) -> None: def update(self, other: HomeConnectApplianceData) -> None:
"""Update data with data from other instance.""" """Update data with data from other instance."""
self.commands.update(other.commands)
self.events.update(other.events) self.events.update(other.events)
self.info.connected = other.info.connected self.info.connected = other.info.connected
self.options.clear() self.options.clear()
@ -408,7 +411,18 @@ class HomeConnectCoordinator(
unit=option.unit, unit=option.unit,
) )
try:
commands = {
command.key
for command in (
await self.client.get_available_commands(appliance.ha_id)
).commands
}
except HomeConnectError:
commands = set()
appliance_data = HomeConnectApplianceData( appliance_data = HomeConnectApplianceData(
commands=commands,
events=events, events=events,
info=appliance, info=appliance,
options=options, options=options,

View File

@ -815,6 +815,23 @@
"name": "Wine compartment door" "name": "Wine compartment door"
} }
}, },
"button": {
"open_door": {
"name": "Open door"
},
"partly_open_door": {
"name": "Partly open door"
},
"pause_program": {
"name": "Pause program"
},
"resume_program": {
"name": "Resume program"
},
"stop_program": {
"name": "Stop program"
}
},
"light": { "light": {
"cooking_lighting": { "cooking_lighting": {
"name": "Functional light" "name": "Functional light"

View File

@ -9,6 +9,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
from aiohomeconnect.client import Client as HomeConnectClient from aiohomeconnect.client import Client as HomeConnectClient
from aiohomeconnect.model import ( from aiohomeconnect.model import (
ArrayOfCommands,
ArrayOfEvents, ArrayOfEvents,
ArrayOfHomeAppliances, ArrayOfHomeAppliances,
ArrayOfOptions, ArrayOfOptions,
@ -50,6 +51,9 @@ MOCK_SETTINGS: dict[str, Any] = load_json_object_fixture("home_connect/settings.
MOCK_STATUS = ArrayOfStatus.from_dict( MOCK_STATUS = ArrayOfStatus.from_dict(
load_json_object_fixture("home_connect/status.json")["data"] load_json_object_fixture("home_connect/status.json")["data"]
) )
MOCK_AVAILABLE_COMMANDS: dict[str, Any] = load_json_object_fixture(
"home_connect/available_commands.json"
)
CLIENT_ID = "1234" CLIENT_ID = "1234"
@ -326,6 +330,14 @@ async def _get_setting_side_effect(ha_id: str, setting_key: SettingKey):
raise HomeConnectApiError("error.key", "error description") raise HomeConnectApiError("error.key", "error description")
async def _get_available_commands_side_effect(ha_id: str) -> ArrayOfCommands:
"""Get available commands."""
for appliance in MOCK_APPLIANCES.homeappliances:
if appliance.ha_id == ha_id and appliance.type in MOCK_AVAILABLE_COMMANDS:
return ArrayOfCommands.from_dict(MOCK_AVAILABLE_COMMANDS[appliance.type])
raise HomeConnectApiError("error.key", "error description")
@pytest.fixture(name="client") @pytest.fixture(name="client")
def mock_client(request: pytest.FixtureRequest) -> MagicMock: def mock_client(request: pytest.FixtureRequest) -> MagicMock:
"""Fixture to mock Client from HomeConnect.""" """Fixture to mock Client from HomeConnect."""
@ -385,6 +397,7 @@ def mock_client(request: pytest.FixtureRequest) -> MagicMock:
event_queue, EventKey.BSH_COMMON_ROOT_SELECTED_PROGRAM event_queue, EventKey.BSH_COMMON_ROOT_SELECTED_PROGRAM
), ),
) )
mock.stop_program = AsyncMock()
mock.set_active_program_option = AsyncMock( mock.set_active_program_option = AsyncMock(
side_effect=_get_set_program_options_side_effect(event_queue), side_effect=_get_set_program_options_side_effect(event_queue),
) )
@ -404,6 +417,9 @@ def mock_client(request: pytest.FixtureRequest) -> MagicMock:
mock.get_setting = AsyncMock(side_effect=_get_setting_side_effect) mock.get_setting = AsyncMock(side_effect=_get_setting_side_effect)
mock.get_status = AsyncMock(return_value=copy.deepcopy(MOCK_STATUS)) mock.get_status = AsyncMock(return_value=copy.deepcopy(MOCK_STATUS))
mock.get_all_programs = AsyncMock(side_effect=_get_all_programs_side_effect) mock.get_all_programs = AsyncMock(side_effect=_get_all_programs_side_effect)
mock.get_available_commands = AsyncMock(
side_effect=_get_available_commands_side_effect
)
mock.put_command = AsyncMock() mock.put_command = AsyncMock()
mock.get_available_program = AsyncMock( mock.get_available_program = AsyncMock(
return_value=ProgramDefinition(ProgramKey.UNKNOWN, options=[]) return_value=ProgramDefinition(ProgramKey.UNKNOWN, options=[])
@ -446,6 +462,7 @@ def mock_client_with_exception(request: pytest.FixtureRequest) -> MagicMock:
mock.start_program = AsyncMock(side_effect=exception) mock.start_program = AsyncMock(side_effect=exception)
mock.stop_program = AsyncMock(side_effect=exception) mock.stop_program = AsyncMock(side_effect=exception)
mock.set_selected_program = AsyncMock(side_effect=exception) mock.set_selected_program = AsyncMock(side_effect=exception)
mock.stop_program = AsyncMock(side_effect=exception)
mock.set_active_program_option = AsyncMock(side_effect=exception) mock.set_active_program_option = AsyncMock(side_effect=exception)
mock.set_active_program_options = AsyncMock(side_effect=exception) mock.set_active_program_options = AsyncMock(side_effect=exception)
mock.set_selected_program_option = AsyncMock(side_effect=exception) mock.set_selected_program_option = AsyncMock(side_effect=exception)
@ -455,6 +472,7 @@ def mock_client_with_exception(request: pytest.FixtureRequest) -> MagicMock:
mock.get_setting = AsyncMock(side_effect=exception) mock.get_setting = AsyncMock(side_effect=exception)
mock.get_status = AsyncMock(side_effect=exception) mock.get_status = AsyncMock(side_effect=exception)
mock.get_all_programs = AsyncMock(side_effect=exception) mock.get_all_programs = AsyncMock(side_effect=exception)
mock.get_available_commands = AsyncMock(side_effect=exception)
mock.put_command = AsyncMock(side_effect=exception) mock.put_command = AsyncMock(side_effect=exception)
mock.get_available_program = AsyncMock(side_effect=exception) mock.get_available_program = AsyncMock(side_effect=exception)
mock.get_active_program_options = AsyncMock(side_effect=exception) mock.get_active_program_options = AsyncMock(side_effect=exception)

View File

@ -0,0 +1,142 @@
{
"Cooktop": {
"commands": [
{
"key": "BSH.Common.Command.AcknowledgeEvent",
"name": "Acknowledge event"
}
]
},
"Hood": {
"commands": [
{
"key": "BSH.Common.Command.AcknowledgeEvent",
"name": "Acknowledge event"
}
]
},
"Oven": {
"commands": [
{
"key": "BSH.Common.Command.PauseProgram",
"name": "Stop program"
},
{
"key": "BSH.Common.Command.ResumeProgram",
"name": "Continue program"
},
{
"key": "BSH.Common.Command.OpenDoor",
"name": "Open door"
},
{
"key": "BSH.Common.Command.PartlyOpenDoor",
"name": "Partly open door"
},
{
"key": "BSH.Common.Command.AcknowledgeEvent",
"name": "Acknowledge event"
}
]
},
"CleaningRobot": {
"commands": [
{
"key": "BSH.Common.Command.PauseProgram",
"name": "Stop program"
},
{
"key": "BSH.Common.Command.ResumeProgram",
"name": "Continue program"
},
{
"key": "BSH.Common.Command.AcknowledgeEvent",
"name": "Acknowledge event"
}
]
},
"Dishwasher": {
"commands": [
{
"key": "BSH.Common.Command.ResumeProgram",
"name": "Continue program"
},
{
"key": "BSH.Common.Command.AcknowledgeEvent",
"name": "Acknowledge event"
}
]
},
"Dryer": {
"commands": [
{
"key": "BSH.Common.Command.PauseProgram",
"name": "Stop program"
},
{
"key": "BSH.Common.Command.ResumeProgram",
"name": "Continue program"
},
{
"key": "BSH.Common.Command.AcknowledgeEvent",
"name": "Acknowledge event"
}
]
},
"Washer": {
"commands": [
{
"key": "BSH.Common.Command.PauseProgram",
"name": "Stop program"
},
{
"key": "BSH.Common.Command.ResumeProgram",
"name": "Continue program"
},
{
"key": "BSH.Common.Command.AcknowledgeEvent",
"name": "Acknowledge event"
}
]
},
"WasherDryer": {
"commands": [
{
"key": "BSH.Common.Command.PauseProgram",
"name": "Stop program"
},
{
"key": "BSH.Common.Command.ResumeProgram",
"name": "Continue program"
},
{
"key": "BSH.Common.Command.AcknowledgeEvent",
"name": "Acknowledge event"
}
]
},
"Freezer": {
"commands": [
{
"key": "BSH.Common.Command.OpenDoor",
"name": "Open door"
}
]
},
"FridgeFreezer": {
"commands": [
{
"key": "BSH.Common.Command.OpenDoor",
"name": "Open door"
}
]
},
"Refrigerator": {
"commands": [
{
"key": "BSH.Common.Command.OpenDoor",
"name": "Open door"
}
]
}
}

View File

@ -0,0 +1,315 @@
"""Tests for home_connect button entities."""
from collections.abc import Awaitable, Callable
from typing import Any
from unittest.mock import AsyncMock, MagicMock
from aiohomeconnect.model import ArrayOfCommands, CommandKey, EventMessage
from aiohomeconnect.model.command import Command
from aiohomeconnect.model.error import HomeConnectApiError
from aiohomeconnect.model.event import ArrayOfEvents, EventType
import pytest
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
from homeassistant.components.home_connect.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er
from tests.common import MockConfigEntry
@pytest.fixture
def platforms() -> list[str]:
"""Fixture to specify platforms to test."""
return [Platform.BUTTON]
async def test_buttons(
hass: HomeAssistant,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client: MagicMock,
) -> None:
"""Test button entities."""
assert config_entry.state == ConfigEntryState.NOT_LOADED
assert await integration_setup(client)
assert config_entry.state == ConfigEntryState.LOADED
async def test_paired_depaired_devices_flow(
appliance_ha_id: str,
hass: HomeAssistant,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client: MagicMock,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that removed devices are correctly removed from and added to hass on API events."""
assert config_entry.state == ConfigEntryState.NOT_LOADED
assert await integration_setup(client)
assert config_entry.state == ConfigEntryState.LOADED
device = device_registry.async_get_device(identifiers={(DOMAIN, appliance_ha_id)})
assert device
entity_entries = entity_registry.entities.get_entries_for_device_id(device.id)
assert entity_entries
await client.add_events(
[
EventMessage(
appliance_ha_id,
EventType.DEPAIRED,
data=ArrayOfEvents([]),
)
]
)
await hass.async_block_till_done()
device = device_registry.async_get_device(identifiers={(DOMAIN, appliance_ha_id)})
assert not device
for entity_entry in entity_entries:
assert not entity_registry.async_get(entity_entry.entity_id)
# Now that all everything related to the device is removed, pair it again
await client.add_events(
[
EventMessage(
appliance_ha_id,
EventType.PAIRED,
data=ArrayOfEvents([]),
)
]
)
await hass.async_block_till_done()
assert device_registry.async_get_device(identifiers={(DOMAIN, appliance_ha_id)})
for entity_entry in entity_entries:
assert entity_registry.async_get(entity_entry.entity_id)
async def test_connected_devices(
appliance_ha_id: str,
hass: HomeAssistant,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client: MagicMock,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that devices reconnected.
Specifically those devices whose settings, status, etc. could
not be obtained while disconnected and once connected, the entities are added.
"""
get_available_commands_original_mock = client.get_available_commands
get_available_programs_mock = client.get_available_programs
async def get_available_commands_side_effect(ha_id: str):
if ha_id == appliance_ha_id:
raise HomeConnectApiError(
"SDK.Error.HomeAppliance.Connection.Initialization.Failed"
)
return await get_available_commands_original_mock.side_effect(ha_id)
async def get_available_programs_side_effect(ha_id: str):
if ha_id == appliance_ha_id:
raise HomeConnectApiError(
"SDK.Error.HomeAppliance.Connection.Initialization.Failed"
)
return await get_available_programs_mock.side_effect(ha_id)
client.get_available_commands = AsyncMock(
side_effect=get_available_commands_side_effect
)
client.get_available_programs = AsyncMock(
side_effect=get_available_programs_side_effect
)
assert config_entry.state == ConfigEntryState.NOT_LOADED
assert await integration_setup(client)
assert config_entry.state == ConfigEntryState.LOADED
client.get_available_commands = get_available_commands_original_mock
client.get_available_programs = get_available_programs_mock
device = device_registry.async_get_device(identifiers={(DOMAIN, appliance_ha_id)})
assert device
entity_entries = entity_registry.entities.get_entries_for_device_id(device.id)
await client.add_events(
[
EventMessage(
appliance_ha_id,
EventType.CONNECTED,
data=ArrayOfEvents([]),
)
]
)
await hass.async_block_till_done()
device = device_registry.async_get_device(identifiers={(DOMAIN, appliance_ha_id)})
assert device
new_entity_entries = entity_registry.entities.get_entries_for_device_id(device.id)
assert len(new_entity_entries) > len(entity_entries)
async def test_button_entity_availabilty(
hass: HomeAssistant,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client: MagicMock,
appliance_ha_id: str,
) -> None:
"""Test if button entities availability are based on the appliance connection state."""
entity_ids = [
"button.washer_pause_program",
"button.washer_stop_program",
]
assert config_entry.state == ConfigEntryState.NOT_LOADED
assert await integration_setup(client)
assert config_entry.state == ConfigEntryState.LOADED
for entity_id in entity_ids:
state = hass.states.get(entity_id)
assert state
assert state.state != STATE_UNAVAILABLE
await client.add_events(
[
EventMessage(
appliance_ha_id,
EventType.DISCONNECTED,
ArrayOfEvents([]),
)
]
)
await hass.async_block_till_done()
for entity_id in entity_ids:
assert hass.states.is_state(entity_id, STATE_UNAVAILABLE)
await client.add_events(
[
EventMessage(
appliance_ha_id,
EventType.CONNECTED,
ArrayOfEvents([]),
)
]
)
await hass.async_block_till_done()
for entity_id in entity_ids:
state = hass.states.get(entity_id)
assert state
assert state.state != STATE_UNAVAILABLE
@pytest.mark.parametrize(
("entity_id", "method_call", "expected_kwargs"),
[
(
"button.washer_pause_program",
"put_command",
{"command_key": CommandKey.BSH_COMMON_PAUSE_PROGRAM, "value": True},
),
("button.washer_stop_program", "stop_program", {}),
],
)
async def test_button_functionality(
hass: HomeAssistant,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client: MagicMock,
entity_id: str,
method_call: str,
expected_kwargs: dict[str, Any],
appliance_ha_id: str,
) -> None:
"""Test if button entities availability are based on the appliance connection state."""
assert config_entry.state == ConfigEntryState.NOT_LOADED
assert await integration_setup(client)
assert config_entry.state == ConfigEntryState.LOADED
entity = hass.states.get(entity_id)
assert entity
assert entity.state != STATE_UNAVAILABLE
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
getattr(client, method_call).assert_called_with(appliance_ha_id, **expected_kwargs)
async def test_command_button_exception(
hass: HomeAssistant,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client_with_exception: MagicMock,
) -> None:
"""Test if button entities availability are based on the appliance connection state."""
entity_id = "button.washer_pause_program"
client_with_exception.get_available_commands = AsyncMock(
return_value=ArrayOfCommands(
[
Command(
CommandKey.BSH_COMMON_PAUSE_PROGRAM,
"Pause Program",
)
]
)
)
assert config_entry.state == ConfigEntryState.NOT_LOADED
assert await integration_setup(client_with_exception)
assert config_entry.state == ConfigEntryState.LOADED
entity = hass.states.get(entity_id)
assert entity
assert entity.state != STATE_UNAVAILABLE
with pytest.raises(HomeAssistantError, match=r"Error.*executing.*command"):
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
async def test_stop_program_button_exception(
hass: HomeAssistant,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client_with_exception: MagicMock,
) -> None:
"""Test if button entities availability are based on the appliance connection state."""
entity_id = "button.washer_stop_program"
assert config_entry.state == ConfigEntryState.NOT_LOADED
assert await integration_setup(client_with_exception)
assert config_entry.state == ConfigEntryState.LOADED
entity = hass.states.get(entity_id)
assert entity
assert entity.state != STATE_UNAVAILABLE
with pytest.raises(HomeAssistantError, match=r"Error.*stop.*program"):
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)