Add action for set_program_oven to miele (#149620)

This commit is contained in:
Åke Strandberg 2025-07-30 13:35:24 +02:00 committed by GitHub
parent 5930ac6425
commit 1eb6d5fe32
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 173 additions and 5 deletions

View File

@ -110,6 +110,9 @@
}, },
"set_program": { "set_program": {
"service": "mdi:arrow-right-circle-outline" "service": "mdi:arrow-right-circle-outline"
},
"set_program_oven": {
"service": "mdi:arrow-right-circle-outline"
} }
} }
} }

View File

@ -1,12 +1,13 @@
"""Services for Miele integration.""" """Services for Miele integration."""
from datetime import timedelta
import logging import logging
from typing import cast from typing import cast
import aiohttp import aiohttp
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ATTR_DEVICE_ID from homeassistant.const import ATTR_DEVICE_ID, ATTR_TEMPERATURE
from homeassistant.core import ( from homeassistant.core import (
HomeAssistant, HomeAssistant,
ServiceCall, ServiceCall,
@ -32,6 +33,19 @@ SERVICE_SET_PROGRAM_SCHEMA = vol.Schema(
}, },
) )
SERVICE_SET_PROGRAM_OVEN = "set_program_oven"
SERVICE_SET_PROGRAM_OVEN_SCHEMA = vol.Schema(
{
vol.Required(ATTR_DEVICE_ID): str,
vol.Required(ATTR_PROGRAM_ID): cv.positive_int,
vol.Optional(ATTR_TEMPERATURE): cv.positive_int,
vol.Optional(ATTR_DURATION): vol.All(
cv.time_period,
vol.Range(min=timedelta(minutes=1), max=timedelta(hours=12)),
),
},
)
SERVICE_GET_PROGRAMS = "get_programs" SERVICE_GET_PROGRAMS = "get_programs"
SERVICE_GET_PROGRAMS_SCHEMA = vol.Schema( SERVICE_GET_PROGRAMS_SCHEMA = vol.Schema(
{ {
@ -103,6 +117,36 @@ async def set_program(call: ServiceCall) -> None:
) from ex ) from ex
async def set_program_oven(call: ServiceCall) -> None:
"""Set a program on a Miele oven."""
_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]}
if call.data.get(ATTR_DURATION) is not None:
td = call.data[ATTR_DURATION]
data["duration"] = [
td.seconds // 3600, # hours
(td.seconds // 60) % 60, # minutes
]
if call.data.get(ATTR_TEMPERATURE) is not None:
data["temperature"] = call.data[ATTR_TEMPERATURE]
try:
await api.set_program(serial_number, data)
except aiohttp.ClientResponseError as ex:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="set_program_oven_error",
translation_placeholders={
"status": str(ex.status),
"message": ex.message,
},
) from ex
async def get_programs(call: ServiceCall) -> ServiceResponse: async def get_programs(call: ServiceCall) -> ServiceResponse:
"""Get available programs from appliance.""" """Get available programs from appliance."""
@ -172,7 +216,17 @@ async def async_setup_services(hass: HomeAssistant) -> None:
"""Set up services.""" """Set up services."""
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SET_PROGRAM, set_program, SERVICE_SET_PROGRAM_SCHEMA DOMAIN,
SERVICE_SET_PROGRAM,
set_program,
SERVICE_SET_PROGRAM_SCHEMA,
)
hass.services.async_register(
DOMAIN,
SERVICE_SET_PROGRAM_OVEN,
set_program_oven,
SERVICE_SET_PROGRAM_OVEN_SCHEMA,
) )
hass.services.async_register( hass.services.async_register(

View File

@ -23,3 +23,33 @@ set_program:
max: 99999 max: 99999
mode: box mode: box
example: 24 example: 24
set_program_oven:
fields:
device_id:
selector:
device:
integration: miele
required: true
program_id:
required: true
selector:
number:
min: 0
max: 99999
mode: box
example: 24
temperature:
required: false
selector:
number:
min: 30
max: 300
unit_of_measurement: "°C"
mode: box
example: 180
duration:
required: false
selector:
duration:
example: 1:15:00

View File

@ -1068,6 +1068,9 @@
"set_program_error": { "set_program_error": {
"message": "'Set program' action failed {status} / {message}." "message": "'Set program' action failed {status} / {message}."
}, },
"set_program_oven_error": {
"message": "'Set program on oven' action failed {status} / {message}."
},
"set_state_error": { "set_state_error": {
"message": "Failed to set state for {entity}." "message": "Failed to set state for {entity}."
} }
@ -1096,6 +1099,28 @@
"name": "Program ID" "name": "Program ID"
} }
} }
},
"set_program_oven": {
"name": "Set program on oven",
"description": "[%key:component::miele::services::set_program::description%]",
"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%]"
},
"program_id": {
"description": "[%key:component::miele::services::set_program::fields::program_id::description%]",
"name": "[%key:component::miele::services::set_program::fields::program_id::name%]"
},
"temperature": {
"description": "The target temperature for the oven program.",
"name": "[%key:component::sensor::entity_component::temperature::name%]"
},
"duration": {
"description": "The duration for the oven program.",
"name": "[%key:component::sensor::entity_component::duration::name%]"
}
}
} }
} }
} }

View File

@ -1,5 +1,6 @@
"""Tests the services provided by the miele integration.""" """Tests the services provided by the miele integration."""
from datetime import timedelta
from unittest.mock import MagicMock from unittest.mock import MagicMock
from aiohttp import ClientResponseError from aiohttp import ClientResponseError
@ -9,11 +10,13 @@ from voluptuous import MultipleInvalid
from homeassistant.components.miele.const import DOMAIN from homeassistant.components.miele.const import DOMAIN
from homeassistant.components.miele.services import ( from homeassistant.components.miele.services import (
ATTR_DURATION,
ATTR_PROGRAM_ID, ATTR_PROGRAM_ID,
SERVICE_GET_PROGRAMS, SERVICE_GET_PROGRAMS,
SERVICE_SET_PROGRAM, SERVICE_SET_PROGRAM,
SERVICE_SET_PROGRAM_OVEN,
) )
from homeassistant.const import ATTR_DEVICE_ID from homeassistant.const import ATTR_DEVICE_ID, ATTR_TEMPERATURE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers.device_registry import DeviceRegistry from homeassistant.helpers.device_registry import DeviceRegistry
@ -49,6 +52,50 @@ async def test_services(
) )
@pytest.mark.parametrize(
("call_arguments", "miele_arguments"),
[
(
{ATTR_PROGRAM_ID: 24},
{"programId": 24},
),
(
{ATTR_PROGRAM_ID: 25, ATTR_DURATION: timedelta(minutes=75)},
{"programId": 25, "duration": [1, 15]},
),
(
{
ATTR_PROGRAM_ID: 26,
ATTR_DURATION: timedelta(minutes=135),
ATTR_TEMPERATURE: 180,
},
{"programId": 26, "duration": [2, 15], "temperature": 180},
),
],
)
async def test_services_oven(
hass: HomeAssistant,
device_registry: DeviceRegistry,
mock_miele_client: MagicMock,
mock_config_entry: MockConfigEntry,
call_arguments: dict,
miele_arguments: dict,
) -> None:
"""Tests that the custom services are correct for ovens."""
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_OVEN,
{ATTR_DEVICE_ID: device.id, **call_arguments},
blocking=True,
)
mock_miele_client.set_program.assert_called_once_with(
TEST_APPLIANCE, miele_arguments
)
async def test_services_with_response( async def test_services_with_response(
hass: HomeAssistant, hass: HomeAssistant,
device_registry: DeviceRegistry, device_registry: DeviceRegistry,
@ -71,11 +118,20 @@ async def test_services_with_response(
) )
@pytest.mark.parametrize(
("service", "error"),
[
(SERVICE_SET_PROGRAM, "'Set program' action failed"),
(SERVICE_SET_PROGRAM_OVEN, "'Set program on oven' action failed"),
],
)
async def test_service_api_errors( async def test_service_api_errors(
hass: HomeAssistant, hass: HomeAssistant,
device_registry: DeviceRegistry, device_registry: DeviceRegistry,
mock_miele_client: MagicMock, mock_miele_client: MagicMock,
mock_config_entry: MockConfigEntry, mock_config_entry: MockConfigEntry,
service: str,
error: str,
) -> None: ) -> None:
"""Test service api errors.""" """Test service api errors."""
await setup_integration(hass, mock_config_entry) await setup_integration(hass, mock_config_entry)
@ -83,10 +139,10 @@ async def test_service_api_errors(
# Test http error # Test http error
mock_miele_client.set_program.side_effect = ClientResponseError("TestInfo", "test") mock_miele_client.set_program.side_effect = ClientResponseError("TestInfo", "test")
with pytest.raises(HomeAssistantError, match="'Set program' action failed"): with pytest.raises(HomeAssistantError, match=error):
await hass.services.async_call( await hass.services.async_call(
DOMAIN, DOMAIN,
SERVICE_SET_PROGRAM, service,
{ATTR_DEVICE_ID: device.id, ATTR_PROGRAM_ID: 1}, {ATTR_DEVICE_ID: device.id, ATTR_PROGRAM_ID: 1},
blocking=True, blocking=True,
) )