mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 23:57:06 +00:00
Add set_program service to Miele (#143442)
This commit is contained in:
parent
e79d42ecfc
commit
49807c9fbe
@ -7,16 +7,18 @@ from aiohttp import ClientError, ClientResponseError
|
|||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||||
OAuth2Session,
|
OAuth2Session,
|
||||||
async_get_config_entry_implementation,
|
async_get_config_entry_implementation,
|
||||||
)
|
)
|
||||||
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .api import AsyncConfigEntryAuth
|
from .api import AsyncConfigEntryAuth
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import MieleConfigEntry, MieleDataUpdateCoordinator
|
from .coordinator import MieleConfigEntry, MieleDataUpdateCoordinator
|
||||||
|
from .services import async_setup_services
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [
|
PLATFORMS: list[Platform] = [
|
||||||
Platform.BINARY_SENSOR,
|
Platform.BINARY_SENSOR,
|
||||||
@ -29,6 +31,15 @@ PLATFORMS: list[Platform] = [
|
|||||||
Platform.VACUUM,
|
Platform.VACUUM,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
"""Set up service actions."""
|
||||||
|
await async_setup_services(hass)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: MieleConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: MieleConfigEntry) -> bool:
|
||||||
"""Set up Miele from a config entry."""
|
"""Set up Miele from a config entry."""
|
||||||
|
@ -103,5 +103,10 @@
|
|||||||
"default": "mdi:snowflake"
|
"default": "mdi:snowflake"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"set_program": {
|
||||||
|
"service": "mdi:arrow-right-circle-outline"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
92
homeassistant/components/miele/services.py
Normal file
92
homeassistant/components/miele/services.py
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
"""Services for Miele integration."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.const import ATTR_DEVICE_ID
|
||||||
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
|
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||||
|
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||||
|
from homeassistant.helpers.service import async_extract_config_entry_ids
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import MieleConfigEntry
|
||||||
|
|
||||||
|
ATTR_PROGRAM_ID = "program_id"
|
||||||
|
ATTR_DURATION = "duration"
|
||||||
|
|
||||||
|
|
||||||
|
SERVICE_SET_PROGRAM = "set_program"
|
||||||
|
SERVICE_SET_PROGRAM_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(ATTR_DEVICE_ID): str,
|
||||||
|
vol.Required(ATTR_PROGRAM_ID): cv.positive_int,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def _extract_config_entry(service_call: ServiceCall) -> MieleConfigEntry:
|
||||||
|
"""Extract config entry from the service call."""
|
||||||
|
hass = service_call.hass
|
||||||
|
target_entry_ids = await async_extract_config_entry_ids(hass, service_call)
|
||||||
|
target_entries: list[MieleConfigEntry] = [
|
||||||
|
loaded_entry
|
||||||
|
for loaded_entry in hass.config_entries.async_loaded_entries(DOMAIN)
|
||||||
|
if loaded_entry.entry_id in target_entry_ids
|
||||||
|
]
|
||||||
|
if not target_entries:
|
||||||
|
raise ServiceValidationError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="invalid_target",
|
||||||
|
)
|
||||||
|
return target_entries[0]
|
||||||
|
|
||||||
|
|
||||||
|
async def set_program(call: ServiceCall) -> None:
|
||||||
|
"""Set a program on a Miele appliance."""
|
||||||
|
|
||||||
|
_LOGGER.debug("Set program call: %s", call)
|
||||||
|
config_entry = await _extract_config_entry(call)
|
||||||
|
device_reg = dr.async_get(call.hass)
|
||||||
|
api = config_entry.runtime_data.api
|
||||||
|
device = call.data[ATTR_DEVICE_ID]
|
||||||
|
device_entry = device_reg.async_get(device)
|
||||||
|
|
||||||
|
data = {"programId": call.data[ATTR_PROGRAM_ID]}
|
||||||
|
serial_number = next(
|
||||||
|
(
|
||||||
|
identifier[1]
|
||||||
|
for identifier in cast(dr.DeviceEntry, device_entry).identifiers
|
||||||
|
if identifier[0] == DOMAIN
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if serial_number is None:
|
||||||
|
raise ServiceValidationError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="invalid_target",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await api.set_program(serial_number, data)
|
||||||
|
except aiohttp.ClientResponseError as ex:
|
||||||
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="set_program_error",
|
||||||
|
translation_placeholders={
|
||||||
|
"status": str(ex.status),
|
||||||
|
"message": ex.message,
|
||||||
|
},
|
||||||
|
) from ex
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_services(hass: HomeAssistant) -> None:
|
||||||
|
"""Set up services."""
|
||||||
|
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN, SERVICE_SET_PROGRAM, set_program, SERVICE_SET_PROGRAM_SCHEMA
|
||||||
|
)
|
17
homeassistant/components/miele/services.yaml
Normal file
17
homeassistant/components/miele/services.yaml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Services descriptions for Miele integration
|
||||||
|
|
||||||
|
set_program:
|
||||||
|
fields:
|
||||||
|
device_id:
|
||||||
|
selector:
|
||||||
|
device:
|
||||||
|
integration: miele
|
||||||
|
required: true
|
||||||
|
program_id:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 0
|
||||||
|
max: 99999
|
||||||
|
mode: box
|
||||||
|
example: 24
|
@ -1059,8 +1059,30 @@
|
|||||||
"config_entry_not_ready": {
|
"config_entry_not_ready": {
|
||||||
"message": "Error while loading the integration."
|
"message": "Error while loading the integration."
|
||||||
},
|
},
|
||||||
|
"invalid_target": {
|
||||||
|
"message": "Invalid device targeted."
|
||||||
|
},
|
||||||
|
"set_program_error": {
|
||||||
|
"message": "'Set program' action failed {status} / {message}."
|
||||||
|
},
|
||||||
"set_state_error": {
|
"set_state_error": {
|
||||||
"message": "Failed to set state for {entity}."
|
"message": "Failed to set state for {entity}."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"set_program": {
|
||||||
|
"name": "Set program",
|
||||||
|
"description": "Sets and starts a program on the appliance.",
|
||||||
|
"fields": {
|
||||||
|
"device_id": {
|
||||||
|
"description": "The device to set the program on.",
|
||||||
|
"name": "Device"
|
||||||
|
},
|
||||||
|
"program_id": {
|
||||||
|
"description": "The ID of the program to set.",
|
||||||
|
"name": "Program ID"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -125,6 +125,7 @@ def mock_miele_client(
|
|||||||
client.get_devices.return_value = device_fixture
|
client.get_devices.return_value = device_fixture
|
||||||
client.get_actions.return_value = action_fixture
|
client.get_actions.return_value = action_fixture
|
||||||
client.get_programs.return_value = programs_fixture
|
client.get_programs.return_value = programs_fixture
|
||||||
|
client.set_program.return_value = None
|
||||||
|
|
||||||
yield client
|
yield client
|
||||||
|
|
||||||
|
110
tests/components/miele/test_services.py
Normal file
110
tests/components/miele/test_services.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
"""Tests the services provided by the miele integration."""
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from aiohttp import ClientResponseError
|
||||||
|
import pytest
|
||||||
|
from voluptuous import MultipleInvalid
|
||||||
|
|
||||||
|
from homeassistant.components.miele.const import DOMAIN
|
||||||
|
from homeassistant.components.miele.services import ATTR_PROGRAM_ID, SERVICE_SET_PROGRAM
|
||||||
|
from homeassistant.const import ATTR_DEVICE_ID
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||||
|
from homeassistant.helpers.device_registry import DeviceRegistry
|
||||||
|
|
||||||
|
from . import setup_integration
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
TEST_APPLIANCE = "Dummy_Appliance_1"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_services(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
device_registry: DeviceRegistry,
|
||||||
|
mock_miele_client: MagicMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Tests that the custom services are correct."""
|
||||||
|
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
device = device_registry.async_get_device(identifiers={(DOMAIN, TEST_APPLIANCE)})
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_PROGRAM,
|
||||||
|
{
|
||||||
|
ATTR_DEVICE_ID: device.id,
|
||||||
|
ATTR_PROGRAM_ID: 24,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
mock_miele_client.set_program.assert_called_once_with(
|
||||||
|
TEST_APPLIANCE, {"programId": 24}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_service_api_errors(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
device_registry: DeviceRegistry,
|
||||||
|
mock_miele_client: MagicMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test service api errors."""
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
device = device_registry.async_get_device(identifiers={(DOMAIN, TEST_APPLIANCE)})
|
||||||
|
|
||||||
|
# Test http error
|
||||||
|
mock_miele_client.set_program.side_effect = ClientResponseError("TestInfo", "test")
|
||||||
|
with pytest.raises(HomeAssistantError, match="'Set program' action failed"):
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_PROGRAM,
|
||||||
|
{"device_id": device.id, ATTR_PROGRAM_ID: 1},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
mock_miele_client.set_program.assert_called_once_with(
|
||||||
|
TEST_APPLIANCE, {"programId": 1}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_service_validation_errors(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
device_registry: DeviceRegistry,
|
||||||
|
mock_miele_client: MagicMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Tests that the custom services handle bad data."""
|
||||||
|
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
device = device_registry.async_get_device(identifiers={(DOMAIN, TEST_APPLIANCE)})
|
||||||
|
|
||||||
|
# Test missing program_id
|
||||||
|
with pytest.raises(MultipleInvalid, match="required key not provided"):
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_PROGRAM,
|
||||||
|
{"device_id": device.id},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
mock_miele_client.set_program.assert_not_called()
|
||||||
|
|
||||||
|
# Test invalid program_id
|
||||||
|
with pytest.raises(MultipleInvalid, match="expected int for dictionary value"):
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_PROGRAM,
|
||||||
|
{"device_id": device.id, ATTR_PROGRAM_ID: "invalid"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
mock_miele_client.set_program.assert_not_called()
|
||||||
|
|
||||||
|
# Test invalid device
|
||||||
|
with pytest.raises(ServiceValidationError, match="Invalid device targeted"):
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_PROGRAM,
|
||||||
|
{"device_id": "invalid_device", ATTR_PROGRAM_ID: 1},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
mock_miele_client.set_program.assert_not_called()
|
Loading…
x
Reference in New Issue
Block a user