mirror of
https://github.com/home-assistant/core.git
synced 2025-07-30 16:57:19 +00:00
Add action to retrieve list of programs on miele appliance (#149307)
This commit is contained in:
parent
7f9be420d2
commit
cf05f1046d
@ -105,6 +105,9 @@
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"get_programs": {
|
||||
"service": "mdi:stack-overflow"
|
||||
},
|
||||
"set_program": {
|
||||
"service": "mdi:arrow-right-circle-outline"
|
||||
}
|
||||
|
@ -7,7 +7,12 @@ import aiohttp
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_DEVICE_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
)
|
||||
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
|
||||
@ -27,6 +32,13 @@ SERVICE_SET_PROGRAM_SCHEMA = vol.Schema(
|
||||
},
|
||||
)
|
||||
|
||||
SERVICE_GET_PROGRAMS = "get_programs"
|
||||
SERVICE_GET_PROGRAMS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_DEVICE_ID): str,
|
||||
},
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -47,17 +59,12 @@ async def _extract_config_entry(service_call: ServiceCall) -> MieleConfigEntry:
|
||||
return target_entries[0]
|
||||
|
||||
|
||||
async def set_program(call: ServiceCall) -> None:
|
||||
"""Set a program on a Miele appliance."""
|
||||
async def _get_serial_number(call: ServiceCall) -> str:
|
||||
"""Extract the serial number from the device identifier."""
|
||||
|
||||
_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]
|
||||
@ -71,6 +78,18 @@ async def set_program(call: ServiceCall) -> None:
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_target",
|
||||
)
|
||||
return serial_number
|
||||
|
||||
|
||||
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)
|
||||
api = config_entry.runtime_data.api
|
||||
|
||||
serial_number = await _get_serial_number(call)
|
||||
data = {"programId": call.data[ATTR_PROGRAM_ID]}
|
||||
try:
|
||||
await api.set_program(serial_number, data)
|
||||
except aiohttp.ClientResponseError as ex:
|
||||
@ -84,9 +103,82 @@ async def set_program(call: ServiceCall) -> None:
|
||||
) from ex
|
||||
|
||||
|
||||
async def get_programs(call: ServiceCall) -> ServiceResponse:
|
||||
"""Get available programs from appliance."""
|
||||
|
||||
config_entry = await _extract_config_entry(call)
|
||||
api = config_entry.runtime_data.api
|
||||
serial_number = await _get_serial_number(call)
|
||||
|
||||
try:
|
||||
programs = await api.get_programs(serial_number)
|
||||
except aiohttp.ClientResponseError as ex:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="get_programs_error",
|
||||
translation_placeholders={
|
||||
"status": str(ex.status),
|
||||
"message": ex.message,
|
||||
},
|
||||
) from ex
|
||||
|
||||
return {
|
||||
"programs": [
|
||||
{
|
||||
"program_id": item["programId"],
|
||||
"program": item["program"],
|
||||
"parameters": (
|
||||
{
|
||||
"temperature": (
|
||||
{
|
||||
"min": item["parameters"]["temperature"]["min"],
|
||||
"max": item["parameters"]["temperature"]["max"],
|
||||
"step": item["parameters"]["temperature"]["step"],
|
||||
"mandatory": item["parameters"]["temperature"][
|
||||
"mandatory"
|
||||
],
|
||||
}
|
||||
if "temperature" in item["parameters"]
|
||||
else {}
|
||||
),
|
||||
"duration": (
|
||||
{
|
||||
"min": {
|
||||
"hours": item["parameters"]["duration"]["min"][0],
|
||||
"minutes": item["parameters"]["duration"]["min"][1],
|
||||
},
|
||||
"max": {
|
||||
"hours": item["parameters"]["duration"]["max"][0],
|
||||
"minutes": item["parameters"]["duration"]["max"][1],
|
||||
},
|
||||
"mandatory": item["parameters"]["duration"][
|
||||
"mandatory"
|
||||
],
|
||||
}
|
||||
if "duration" in item["parameters"]
|
||||
else {}
|
||||
),
|
||||
}
|
||||
if item["parameters"]
|
||||
else {}
|
||||
),
|
||||
}
|
||||
for item in programs
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_GET_PROGRAMS,
|
||||
get_programs,
|
||||
SERVICE_GET_PROGRAMS_SCHEMA,
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
@ -1,5 +1,13 @@
|
||||
# Services descriptions for Miele integration
|
||||
|
||||
get_programs:
|
||||
fields:
|
||||
device_id:
|
||||
selector:
|
||||
device:
|
||||
integration: miele
|
||||
required: true
|
||||
|
||||
set_program:
|
||||
fields:
|
||||
device_id:
|
||||
|
@ -1062,6 +1062,9 @@
|
||||
"invalid_target": {
|
||||
"message": "Invalid device targeted."
|
||||
},
|
||||
"get_programs_error": {
|
||||
"message": "'Get programs' action failed {status} / {message}."
|
||||
},
|
||||
"set_program_error": {
|
||||
"message": "'Set program' action failed {status} / {message}."
|
||||
},
|
||||
@ -1070,12 +1073,22 @@
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"get_programs": {
|
||||
"name": "Get programs",
|
||||
"description": "Returns a list of available programs.",
|
||||
"fields": {
|
||||
"device_id": {
|
||||
"description": "[%key:component::miele::services::set_program::fields::device_id::description%]",
|
||||
"name": "[%key:component::miele::services::set_program::fields::device_id::name%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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.",
|
||||
"description": "The target device for this action.",
|
||||
"name": "Device"
|
||||
},
|
||||
"program_id": {
|
||||
|
@ -20,8 +20,8 @@ from .const import CLIENT_ID, CLIENT_SECRET
|
||||
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
async_load_fixture,
|
||||
async_load_json_object_fixture,
|
||||
load_json_value_fixture,
|
||||
)
|
||||
|
||||
|
||||
@ -99,13 +99,13 @@ async def action_fixture(hass: HomeAssistant, load_action_file: str) -> MieleAct
|
||||
@pytest.fixture(scope="package")
|
||||
def load_programs_file() -> str:
|
||||
"""Fixture for loading programs file."""
|
||||
return "programs_washing_machine.json"
|
||||
return "programs.json"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def programs_fixture(hass: HomeAssistant, load_programs_file: str) -> list[dict]:
|
||||
"""Fixture for available programs."""
|
||||
return await async_load_fixture(hass, load_programs_file, DOMAIN)
|
||||
return load_json_value_fixture(load_programs_file, DOMAIN)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
34
tests/components/miele/fixtures/programs.json
Normal file
34
tests/components/miele/fixtures/programs.json
Normal file
@ -0,0 +1,34 @@
|
||||
[
|
||||
{
|
||||
"programId": 1,
|
||||
"program": "Cottons",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 146,
|
||||
"program": "QuickPowerWash",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 123,
|
||||
"program": "Dark garments / Denim",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 13,
|
||||
"program": "Fan plus",
|
||||
"parameters": {
|
||||
"temperature": {
|
||||
"min": 30,
|
||||
"max": 250,
|
||||
"step": 5,
|
||||
"mandatory": false
|
||||
},
|
||||
"duration": {
|
||||
"min": [0, 1],
|
||||
"max": [12, 0],
|
||||
"mandatory": true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
@ -1,117 +0,0 @@
|
||||
[
|
||||
{
|
||||
"programId": 146,
|
||||
"program": "QuickPowerWash",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 123,
|
||||
"program": "Dark garments / Denim",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 190,
|
||||
"program": "ECO 40-60 ",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 27,
|
||||
"program": "Proofing",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 23,
|
||||
"program": "Shirts",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 9,
|
||||
"program": "Silks ",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 8,
|
||||
"program": "Woollens ",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 4,
|
||||
"program": "Delicates",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 3,
|
||||
"program": "Minimum iron",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 1,
|
||||
"program": "Cottons",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 69,
|
||||
"program": "Cottons hygiene",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 37,
|
||||
"program": "Outerwear",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 122,
|
||||
"program": "Express 20",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 29,
|
||||
"program": "Sportswear",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 31,
|
||||
"program": "Automatic plus",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 39,
|
||||
"program": "Pillows",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 22,
|
||||
"program": "Curtains",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 129,
|
||||
"program": "Down filled items",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 53,
|
||||
"program": "First wash",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 95,
|
||||
"program": "Down duvets",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 52,
|
||||
"program": "Separate rinse / Starch",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 21,
|
||||
"program": "Drain / Spin",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"programId": 91,
|
||||
"program": "Clean machine",
|
||||
"parameters": {}
|
||||
}
|
||||
]
|
48
tests/components/miele/snapshots/test_services.ambr
Normal file
48
tests/components/miele/snapshots/test_services.ambr
Normal file
@ -0,0 +1,48 @@
|
||||
# serializer version: 1
|
||||
# name: test_services_with_response
|
||||
dict({
|
||||
'programs': list([
|
||||
dict({
|
||||
'parameters': dict({
|
||||
}),
|
||||
'program': 'Cottons',
|
||||
'program_id': 1,
|
||||
}),
|
||||
dict({
|
||||
'parameters': dict({
|
||||
}),
|
||||
'program': 'QuickPowerWash',
|
||||
'program_id': 146,
|
||||
}),
|
||||
dict({
|
||||
'parameters': dict({
|
||||
}),
|
||||
'program': 'Dark garments / Denim',
|
||||
'program_id': 123,
|
||||
}),
|
||||
dict({
|
||||
'parameters': dict({
|
||||
'duration': dict({
|
||||
'mandatory': True,
|
||||
'max': dict({
|
||||
'hours': 12,
|
||||
'minutes': 0,
|
||||
}),
|
||||
'min': dict({
|
||||
'hours': 0,
|
||||
'minutes': 1,
|
||||
}),
|
||||
}),
|
||||
'temperature': dict({
|
||||
'mandatory': False,
|
||||
'max': 250,
|
||||
'min': 30,
|
||||
'step': 5,
|
||||
}),
|
||||
}),
|
||||
'program': 'Fan plus',
|
||||
'program_id': 13,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
# ---
|
@ -4,10 +4,15 @@ from unittest.mock import MagicMock
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
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.components.miele.services import (
|
||||
ATTR_PROGRAM_ID,
|
||||
SERVICE_GET_PROGRAMS,
|
||||
SERVICE_SET_PROGRAM,
|
||||
)
|
||||
from homeassistant.const import ATTR_DEVICE_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
@ -44,6 +49,28 @@ async def test_services(
|
||||
)
|
||||
|
||||
|
||||
async def test_services_with_response(
|
||||
hass: HomeAssistant,
|
||||
device_registry: DeviceRegistry,
|
||||
mock_miele_client: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Tests that the custom services that returns a response are correct."""
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
device = device_registry.async_get_device(identifiers={(DOMAIN, TEST_APPLIANCE)})
|
||||
assert snapshot == await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_GET_PROGRAMS,
|
||||
{
|
||||
ATTR_DEVICE_ID: device.id,
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_service_api_errors(
|
||||
hass: HomeAssistant,
|
||||
device_registry: DeviceRegistry,
|
||||
@ -60,7 +87,7 @@ async def test_service_api_errors(
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_PROGRAM,
|
||||
{"device_id": device.id, ATTR_PROGRAM_ID: 1},
|
||||
{ATTR_DEVICE_ID: device.id, ATTR_PROGRAM_ID: 1},
|
||||
blocking=True,
|
||||
)
|
||||
mock_miele_client.set_program.assert_called_once_with(
|
||||
@ -68,6 +95,29 @@ async def test_service_api_errors(
|
||||
)
|
||||
|
||||
|
||||
async def test_get_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.get_programs.side_effect = ClientResponseError("TestInfo", "test")
|
||||
with pytest.raises(HomeAssistantError, match="'Get programs' action failed"):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_GET_PROGRAMS,
|
||||
{ATTR_DEVICE_ID: device.id},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
mock_miele_client.get_programs.assert_called_once()
|
||||
|
||||
|
||||
async def test_service_validation_errors(
|
||||
hass: HomeAssistant,
|
||||
device_registry: DeviceRegistry,
|
||||
|
Loading…
x
Reference in New Issue
Block a user