Add Home Connect action with recognized programs and options (#130662)

* Added recognized options to Home Connect actions

* Fix ruff

* Fix strings.json

* Fix dishwasher typo

* Improved test_bsh_key_transformations

* Add missing return types

* Added descriptions

* Remove custom options

* Fixes

* Merge the 4 services (select, start, set options for active or selected program)

And deprecate the original ones

* Delete stale snapshots

* Clean up logic after service validation

* Make deprecated actions issues fixable

And delete issue on entry unload

* Fixes and improvements

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Improvements

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Fix name and descriptions

* Add `affects_to` to strings and service.yaml

* Add missing periods at strings

* Fix

Co-authored-by: Norbert Rittel <norbert@rittel.de>

* Add tests to check if the flow removes the deprecated action issue

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Norbert Rittel <norbert@rittel.de>
This commit is contained in:
J. Diego Rodríguez Royo 2025-02-14 20:21:01 +01:00 committed by GitHub
parent d99044572a
commit 2bfe96dded
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 2331 additions and 355 deletions

View File

@ -2,11 +2,20 @@
from __future__ import annotations
from collections.abc import Awaitable
from datetime import timedelta
import logging
from typing import Any, cast
from aiohomeconnect.client import Client as HomeConnectClient
from aiohomeconnect.model import CommandKey, Option, OptionKey, ProgramKey, SettingKey
from aiohomeconnect.model import (
ArrayOfOptions,
CommandKey,
Option,
OptionKey,
ProgramKey,
SettingKey,
)
from aiohomeconnect.model.error import HomeConnectError
import voluptuous as vol
@ -19,34 +28,84 @@ from homeassistant.helpers import (
device_registry as dr,
)
from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from homeassistant.helpers.typing import ConfigType
from .api import AsyncConfigEntryAuth
from .const import (
AFFECTS_TO_ACTIVE_PROGRAM,
AFFECTS_TO_SELECTED_PROGRAM,
ATTR_AFFECTS_TO,
ATTR_KEY,
ATTR_PROGRAM,
ATTR_UNIT,
ATTR_VALUE,
DOMAIN,
OLD_NEW_UNIQUE_ID_SUFFIX_MAP,
PROGRAM_ENUM_OPTIONS,
SERVICE_OPTION_ACTIVE,
SERVICE_OPTION_SELECTED,
SERVICE_PAUSE_PROGRAM,
SERVICE_RESUME_PROGRAM,
SERVICE_SELECT_PROGRAM,
SERVICE_SET_PROGRAM_AND_OPTIONS,
SERVICE_SETTING,
SERVICE_START_PROGRAM,
SVE_TRANSLATION_PLACEHOLDER_KEY,
SVE_TRANSLATION_PLACEHOLDER_PROGRAM,
SVE_TRANSLATION_PLACEHOLDER_VALUE,
TRANSLATION_KEYS_PROGRAMS_MAP,
)
from .coordinator import HomeConnectConfigEntry, HomeConnectCoordinator
from .utils import get_dict_from_home_connect_error
from .utils import bsh_key_to_translation_key, get_dict_from_home_connect_error
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
PROGRAM_OPTIONS = {
bsh_key_to_translation_key(key): (
key,
value,
)
for key, value in {
OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_FILL_QUANTITY: int,
OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_MULTIPLE_BEVERAGES: bool,
OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_COFFEE_MILK_RATIO: int,
OptionKey.DISHCARE_DISHWASHER_INTENSIV_ZONE: bool,
OptionKey.DISHCARE_DISHWASHER_BRILLIANCE_DRY: bool,
OptionKey.DISHCARE_DISHWASHER_VARIO_SPEED_PLUS: bool,
OptionKey.DISHCARE_DISHWASHER_SILENCE_ON_DEMAND: bool,
OptionKey.DISHCARE_DISHWASHER_HALF_LOAD: bool,
OptionKey.DISHCARE_DISHWASHER_EXTRA_DRY: bool,
OptionKey.DISHCARE_DISHWASHER_HYGIENE_PLUS: bool,
OptionKey.DISHCARE_DISHWASHER_ECO_DRY: bool,
OptionKey.DISHCARE_DISHWASHER_ZEOLITE_DRY: bool,
OptionKey.COOKING_OVEN_SETPOINT_TEMPERATURE: int,
OptionKey.COOKING_OVEN_FAST_PRE_HEAT: bool,
OptionKey.LAUNDRY_CARE_WASHER_I_DOS_1_ACTIVE: bool,
OptionKey.LAUNDRY_CARE_WASHER_I_DOS_2_ACTIVE: bool,
}.items()
}
TIME_PROGRAM_OPTIONS = {
bsh_key_to_translation_key(key): (
key,
value,
)
for key, value in {
OptionKey.BSH_COMMON_START_IN_RELATIVE: cv.time_period_str,
OptionKey.BSH_COMMON_DURATION: cv.time_period_str,
OptionKey.BSH_COMMON_FINISH_IN_RELATIVE: cv.time_period_str,
}.items()
}
SERVICE_SETTING_SCHEMA = vol.Schema(
{
vol.Required(ATTR_DEVICE_ID): str,
@ -58,6 +117,7 @@ SERVICE_SETTING_SCHEMA = vol.Schema(
}
)
# DEPRECATED: Remove in 2025.9.0
SERVICE_OPTION_SCHEMA = vol.Schema(
{
vol.Required(ATTR_DEVICE_ID): str,
@ -70,6 +130,7 @@ SERVICE_OPTION_SCHEMA = vol.Schema(
}
)
# DEPRECATED: Remove in 2025.9.0
SERVICE_PROGRAM_SCHEMA = vol.Any(
{
vol.Required(ATTR_DEVICE_ID): str,
@ -93,6 +154,51 @@ SERVICE_PROGRAM_SCHEMA = vol.Any(
},
)
def _require_program_or_at_least_one_option(data: dict) -> dict:
if ATTR_PROGRAM not in data and not any(
option_key in data
for option_key in (
PROGRAM_ENUM_OPTIONS | PROGRAM_OPTIONS | TIME_PROGRAM_OPTIONS
)
):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="required_program_or_one_option_at_least",
)
return data
SERVICE_PROGRAM_AND_OPTIONS_SCHEMA = vol.All(
vol.Schema(
{
vol.Required(ATTR_DEVICE_ID): str,
vol.Required(ATTR_AFFECTS_TO): vol.In(
[AFFECTS_TO_ACTIVE_PROGRAM, AFFECTS_TO_SELECTED_PROGRAM]
),
vol.Optional(ATTR_PROGRAM): vol.In(TRANSLATION_KEYS_PROGRAMS_MAP.keys()),
}
)
.extend(
{
vol.Optional(translation_key): vol.In(allowed_values.keys())
for translation_key, (
key,
allowed_values,
) in PROGRAM_ENUM_OPTIONS.items()
}
)
.extend(
{
vol.Optional(translation_key): schema
for translation_key, (key, schema) in (
PROGRAM_OPTIONS | TIME_PROGRAM_OPTIONS
).items()
}
),
_require_program_or_at_least_one_option,
)
SERVICE_COMMAND_SCHEMA = vol.Schema({vol.Required(ATTR_DEVICE_ID): str})
PLATFORMS = [
@ -144,7 +250,7 @@ async def _get_client_and_ha_id(
return entry.runtime_data.client, ha_id
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: C901
"""Set up Home Connect component."""
async def _async_service_program(call: ServiceCall, start: bool):
@ -165,6 +271,57 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
else None
)
async_create_issue(
hass,
DOMAIN,
"deprecated_set_program_and_option_actions",
breaks_in_ha_version="2025.9.0",
is_fixable=True,
is_persistent=True,
severity=IssueSeverity.WARNING,
translation_key="deprecated_set_program_and_option_actions",
translation_placeholders={
"new_action_key": SERVICE_SET_PROGRAM_AND_OPTIONS,
"remove_release": "2025.9.0",
"deprecated_action_yaml": "\n".join(
[
"```yaml",
f"action: {DOMAIN}.{SERVICE_START_PROGRAM if start else SERVICE_SELECT_PROGRAM}",
"data:",
f" {ATTR_DEVICE_ID}: DEVICE_ID",
f" {ATTR_PROGRAM}: {program}",
*([f" {ATTR_KEY}: {options[0].key}"] if options else []),
*([f" {ATTR_VALUE}: {options[0].value}"] if options else []),
*(
[f" {ATTR_UNIT}: {options[0].unit}"]
if options and options[0].unit
else []
),
"```",
]
),
"new_action_yaml": "\n ".join(
[
"```yaml",
f"action: {DOMAIN}.{SERVICE_SET_PROGRAM_AND_OPTIONS}",
"data:",
f" {ATTR_DEVICE_ID}: DEVICE_ID",
f" {ATTR_AFFECTS_TO}: {AFFECTS_TO_ACTIVE_PROGRAM if start else AFFECTS_TO_SELECTED_PROGRAM}",
f" {ATTR_PROGRAM}: {bsh_key_to_translation_key(program.value)}",
*(
[
f" {bsh_key_to_translation_key(options[0].key)}: {options[0].value}"
]
if options
else []
),
"```",
]
),
"repo_link": "[aiohomeconnect](https://github.com/MartinHjelmare/aiohomeconnect)",
},
)
try:
if start:
await client.start_program(ha_id, program_key=program, options=options)
@ -189,6 +346,44 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
unit = call.data.get(ATTR_UNIT)
client, ha_id = await _get_client_and_ha_id(hass, call.data[ATTR_DEVICE_ID])
async_create_issue(
hass,
DOMAIN,
"deprecated_set_program_and_option_actions",
breaks_in_ha_version="2025.9.0",
is_fixable=True,
is_persistent=True,
severity=IssueSeverity.WARNING,
translation_key="deprecated_set_program_and_option_actions",
translation_placeholders={
"new_action_key": SERVICE_SET_PROGRAM_AND_OPTIONS,
"remove_release": "2025.9.0",
"deprecated_action_yaml": "\n".join(
[
"```yaml",
f"action: {DOMAIN}.{SERVICE_OPTION_ACTIVE if active else SERVICE_OPTION_SELECTED}",
"data:",
f" {ATTR_DEVICE_ID}: DEVICE_ID",
f" {ATTR_KEY}: {option_key}",
f" {ATTR_VALUE}: {value}",
*([f" {ATTR_UNIT}: {unit}"] if unit else []),
"```",
]
),
"new_action_yaml": "\n ".join(
[
"```yaml",
f"action: {DOMAIN}.{SERVICE_SET_PROGRAM_AND_OPTIONS}",
"data:",
f" {ATTR_DEVICE_ID}: DEVICE_ID",
f" {ATTR_AFFECTS_TO}: {AFFECTS_TO_ACTIVE_PROGRAM if active else AFFECTS_TO_SELECTED_PROGRAM}",
f" {bsh_key_to_translation_key(option_key)}: {value}",
"```",
]
),
"repo_link": "[aiohomeconnect](https://github.com/MartinHjelmare/aiohomeconnect)",
},
)
try:
if active:
await client.set_active_program_option(
@ -272,6 +467,82 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Service for selecting a program."""
await _async_service_program(call, False)
async def async_service_set_program_and_options(call: ServiceCall):
"""Service for setting a program and options."""
data = dict(call.data)
program = data.pop(ATTR_PROGRAM, None)
affects_to = data.pop(ATTR_AFFECTS_TO)
client, ha_id = await _get_client_and_ha_id(hass, data.pop(ATTR_DEVICE_ID))
options: list[Option] = []
for option, value in data.items():
if option in PROGRAM_ENUM_OPTIONS:
options.append(
Option(
PROGRAM_ENUM_OPTIONS[option][0],
PROGRAM_ENUM_OPTIONS[option][1][value],
)
)
elif option in PROGRAM_OPTIONS:
option_key = PROGRAM_OPTIONS[option][0]
options.append(Option(option_key, value))
elif option in TIME_PROGRAM_OPTIONS:
options.append(
Option(
TIME_PROGRAM_OPTIONS[option][0],
int(cast(timedelta, value).total_seconds()),
)
)
method_call: Awaitable[Any]
exception_translation_key: str
if program:
program = (
program
if isinstance(program, ProgramKey)
else TRANSLATION_KEYS_PROGRAMS_MAP[program]
)
if affects_to == AFFECTS_TO_ACTIVE_PROGRAM:
method_call = client.start_program(
ha_id, program_key=program, options=options
)
exception_translation_key = "start_program"
elif affects_to == AFFECTS_TO_SELECTED_PROGRAM:
method_call = client.set_selected_program(
ha_id, program_key=program, options=options
)
exception_translation_key = "select_program"
else:
array_of_options = ArrayOfOptions(options)
if affects_to == AFFECTS_TO_ACTIVE_PROGRAM:
method_call = client.set_active_program_options(
ha_id, array_of_options=array_of_options
)
exception_translation_key = "set_options_active_program"
else:
# affects_to is AFFECTS_TO_SELECTED_PROGRAM
method_call = client.set_selected_program_options(
ha_id, array_of_options=array_of_options
)
exception_translation_key = "set_options_selected_program"
try:
await method_call
except HomeConnectError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key=exception_translation_key,
translation_placeholders={
**get_dict_from_home_connect_error(err),
**(
{SVE_TRANSLATION_PLACEHOLDER_PROGRAM: program}
if program
else {}
),
},
) from err
async def async_service_start_program(call: ServiceCall):
"""Service for starting a program."""
await _async_service_program(call, True)
@ -315,6 +586,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async_service_start_program,
schema=SERVICE_PROGRAM_SCHEMA,
)
hass.services.async_register(
DOMAIN,
SERVICE_SET_PROGRAM_AND_OPTIONS,
async_service_set_program_and_options,
schema=SERVICE_PROGRAM_AND_OPTIONS_SCHEMA,
)
return True
@ -349,6 +626,7 @@ async def async_unload_entry(
hass: HomeAssistant, entry: HomeConnectConfigEntry
) -> bool:
"""Unload a config entry."""
async_delete_issue(hass, DOMAIN, "deprecated_set_program_and_option_actions")
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@ -1,6 +1,10 @@
"""Constants for the Home Connect integration."""
from aiohomeconnect.model import EventKey, SettingKey, StatusKey
from typing import cast
from aiohomeconnect.model import EventKey, OptionKey, ProgramKey, SettingKey, StatusKey
from .utils import bsh_key_to_translation_key
DOMAIN = "home_connect"
@ -52,15 +56,18 @@ SERVICE_OPTION_SELECTED = "set_option_selected"
SERVICE_PAUSE_PROGRAM = "pause_program"
SERVICE_RESUME_PROGRAM = "resume_program"
SERVICE_SELECT_PROGRAM = "select_program"
SERVICE_SET_PROGRAM_AND_OPTIONS = "set_program_and_options"
SERVICE_SETTING = "change_setting"
SERVICE_START_PROGRAM = "start_program"
ATTR_AFFECTS_TO = "affects_to"
ATTR_KEY = "key"
ATTR_PROGRAM = "program"
ATTR_UNIT = "unit"
ATTR_VALUE = "value"
AFFECTS_TO_ACTIVE_PROGRAM = "active_program"
AFFECTS_TO_SELECTED_PROGRAM = "selected_program"
SVE_TRANSLATION_KEY_SET_SETTING = "set_setting_entity"
SVE_TRANSLATION_PLACEHOLDER_APPLIANCE_NAME = "appliance_name"
@ -70,6 +77,244 @@ SVE_TRANSLATION_PLACEHOLDER_KEY = "key"
SVE_TRANSLATION_PLACEHOLDER_VALUE = "value"
TRANSLATION_KEYS_PROGRAMS_MAP = {
bsh_key_to_translation_key(program.value): cast(ProgramKey, program)
for program in ProgramKey
if program != ProgramKey.UNKNOWN
}
PROGRAMS_TRANSLATION_KEYS_MAP = {
value: key for key, value in TRANSLATION_KEYS_PROGRAMS_MAP.items()
}
REFERENCE_MAP_ID_OPTIONS = {
bsh_key_to_translation_key(option): option
for option in (
"ConsumerProducts.CleaningRobot.EnumType.AvailableMaps.TempMap",
"ConsumerProducts.CleaningRobot.EnumType.AvailableMaps.Map1",
"ConsumerProducts.CleaningRobot.EnumType.AvailableMaps.Map2",
"ConsumerProducts.CleaningRobot.EnumType.AvailableMaps.Map3",
)
}
CLEANING_MODE_OPTIONS = {
bsh_key_to_translation_key(option): option
for option in (
"ConsumerProducts.CleaningRobot.EnumType.CleaningModes.Silent",
"ConsumerProducts.CleaningRobot.EnumType.CleaningModes.Standard",
"ConsumerProducts.CleaningRobot.EnumType.CleaningModes.Power",
)
}
BEAN_AMOUNT_OPTIONS = {
bsh_key_to_translation_key(option): option
for option in (
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.VeryMild",
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.Mild",
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.MildPlus",
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.Normal",
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.NormalPlus",
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.Strong",
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.StrongPlus",
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.VeryStrong",
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.VeryStrongPlus",
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.ExtraStrong",
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.DoubleShot",
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.DoubleShotPlus",
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.DoubleShotPlusPlus",
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.TripleShot",
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.TripleShotPlus",
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.CoffeeGround",
)
}
COFFEE_TEMPERATURE_OPTIONS = {
bsh_key_to_translation_key(option): option
for option in (
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeTemperature.88C",
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeTemperature.90C",
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeTemperature.92C",
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeTemperature.94C",
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeTemperature.95C",
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeTemperature.96C",
)
}
BEAN_CONTAINER_OPTIONS = {
bsh_key_to_translation_key(option): option
for option in (
"ConsumerProducts.CoffeeMaker.EnumType.BeanContainerSelection.Right",
"ConsumerProducts.CoffeeMaker.EnumType.BeanContainerSelection.Left",
)
}
FLOW_RATE_OPTIONS = {
bsh_key_to_translation_key(option): option
for option in (
"ConsumerProducts.CoffeeMaker.EnumType.FlowRate.Normal",
"ConsumerProducts.CoffeeMaker.EnumType.FlowRate.Intense",
"ConsumerProducts.CoffeeMaker.EnumType.FlowRate.IntensePlus",
)
}
HOT_WATER_TEMPERATURE_OPTIONS = {
bsh_key_to_translation_key(option): option
for option in (
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.WhiteTea",
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.GreenTea",
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.BlackTea",
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.50C",
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.55C",
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.60C",
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.65C",
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.70C",
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.75C",
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.80C",
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.85C",
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.90C",
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.95C",
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.97C",
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.122F",
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.131F",
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.140F",
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.149F",
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.158F",
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.167F",
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.176F",
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.185F",
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.194F",
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.203F",
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.Max",
)
}
DRYING_TARGET_OPTIONS = {
bsh_key_to_translation_key(option): option
for option in (
"LaundryCare.Dryer.EnumType.DryingTarget.IronDry",
"LaundryCare.Dryer.EnumType.DryingTarget.GentleDry",
"LaundryCare.Dryer.EnumType.DryingTarget.CupboardDry",
"LaundryCare.Dryer.EnumType.DryingTarget.CupboardDryPlus",
"LaundryCare.Dryer.EnumType.DryingTarget.ExtraDry",
)
}
VENTING_LEVEL_OPTIONS = {
bsh_key_to_translation_key(option): option
for option in (
"Cooking.Hood.EnumType.Stage.FanOff",
"Cooking.Hood.EnumType.Stage.FanStage01",
"Cooking.Hood.EnumType.Stage.FanStage02",
"Cooking.Hood.EnumType.Stage.FanStage03",
"Cooking.Hood.EnumType.Stage.FanStage04",
"Cooking.Hood.EnumType.Stage.FanStage05",
)
}
INTENSIVE_LEVEL_OPTIONS = {
bsh_key_to_translation_key(option): option
for option in (
"Cooking.Hood.EnumType.IntensiveStage.IntensiveStageOff",
"Cooking.Hood.EnumType.IntensiveStage.IntensiveStage1",
"Cooking.Hood.EnumType.IntensiveStage.IntensiveStage2",
)
}
WARMING_LEVEL_OPTIONS = {
bsh_key_to_translation_key(option): option
for option in (
"Cooking.Oven.EnumType.WarmingLevel.Low",
"Cooking.Oven.EnumType.WarmingLevel.Medium",
"Cooking.Oven.EnumType.WarmingLevel.High",
)
}
TEMPERATURE_OPTIONS = {
bsh_key_to_translation_key(option): option
for option in (
"LaundryCare.Washer.EnumType.Temperature.Cold",
"LaundryCare.Washer.EnumType.Temperature.GC20",
"LaundryCare.Washer.EnumType.Temperature.GC30",
"LaundryCare.Washer.EnumType.Temperature.GC40",
"LaundryCare.Washer.EnumType.Temperature.GC50",
"LaundryCare.Washer.EnumType.Temperature.GC60",
"LaundryCare.Washer.EnumType.Temperature.GC70",
"LaundryCare.Washer.EnumType.Temperature.GC80",
"LaundryCare.Washer.EnumType.Temperature.GC90",
"LaundryCare.Washer.EnumType.Temperature.UlCold",
"LaundryCare.Washer.EnumType.Temperature.UlWarm",
"LaundryCare.Washer.EnumType.Temperature.UlHot",
"LaundryCare.Washer.EnumType.Temperature.UlExtraHot",
)
}
SPIN_SPEED_OPTIONS = {
bsh_key_to_translation_key(option): option
for option in (
"LaundryCare.Washer.EnumType.SpinSpeed.Off",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM400",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM600",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM800",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM1000",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM1200",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM1400",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM1600",
"LaundryCare.Washer.EnumType.SpinSpeed.UlOff",
"LaundryCare.Washer.EnumType.SpinSpeed.UlLow",
"LaundryCare.Washer.EnumType.SpinSpeed.UlMedium",
"LaundryCare.Washer.EnumType.SpinSpeed.UlHigh",
)
}
VARIO_PERFECT_OPTIONS = {
bsh_key_to_translation_key(option): option
for option in (
"LaundryCare.Common.EnumType.VarioPerfect.Off",
"LaundryCare.Common.EnumType.VarioPerfect.EcoPerfect",
"LaundryCare.Common.EnumType.VarioPerfect.SpeedPerfect",
)
}
PROGRAM_ENUM_OPTIONS = {
bsh_key_to_translation_key(option_key): (
option_key,
options,
)
for option_key, options in (
(
OptionKey.CONSUMER_PRODUCTS_CLEANING_ROBOT_REFERENCE_MAP_ID,
REFERENCE_MAP_ID_OPTIONS,
),
(
OptionKey.CONSUMER_PRODUCTS_CLEANING_ROBOT_CLEANING_MODE,
CLEANING_MODE_OPTIONS,
),
(OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEAN_AMOUNT, BEAN_AMOUNT_OPTIONS),
(
OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_COFFEE_TEMPERATURE,
COFFEE_TEMPERATURE_OPTIONS,
),
(
OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEAN_CONTAINER_SELECTION,
BEAN_CONTAINER_OPTIONS,
),
(OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_FLOW_RATE, FLOW_RATE_OPTIONS),
(
OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_HOT_WATER_TEMPERATURE,
HOT_WATER_TEMPERATURE_OPTIONS,
),
(OptionKey.LAUNDRY_CARE_DRYER_DRYING_TARGET, DRYING_TARGET_OPTIONS),
(OptionKey.COOKING_COMMON_HOOD_VENTING_LEVEL, VENTING_LEVEL_OPTIONS),
(OptionKey.COOKING_COMMON_HOOD_INTENSIVE_LEVEL, INTENSIVE_LEVEL_OPTIONS),
(OptionKey.COOKING_OVEN_WARMING_LEVEL, WARMING_LEVEL_OPTIONS),
(OptionKey.LAUNDRY_CARE_WASHER_TEMPERATURE, TEMPERATURE_OPTIONS),
(OptionKey.LAUNDRY_CARE_WASHER_SPIN_SPEED, SPIN_SPEED_OPTIONS),
(OptionKey.LAUNDRY_CARE_COMMON_VARIO_PERFECT, VARIO_PERFECT_OPTIONS),
)
}
OLD_NEW_UNIQUE_ID_SUFFIX_MAP = {
"ChildLock": SettingKey.BSH_COMMON_CHILD_LOCK,
"Operation State": StatusKey.BSH_COMMON_OPERATION_STATE,

View File

@ -18,6 +18,9 @@
"set_option_selected": {
"service": "mdi:gesture-tap"
},
"set_program_and_options": {
"service": "mdi:form-select"
},
"change_setting": {
"service": "mdi:cog"
}

View File

@ -3,7 +3,7 @@
"name": "Home Connect",
"codeowners": ["@DavidMStraub", "@Diegorro98", "@MartinHjelmare"],
"config_flow": true,
"dependencies": ["application_credentials"],
"dependencies": ["application_credentials", "repairs"],
"documentation": "https://www.home-assistant.io/integrations/home_connect",
"iot_class": "cloud_push",
"loggers": ["aiohomeconnect"],

View File

@ -15,24 +15,20 @@ 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, SVE_TRANSLATION_PLACEHOLDER_PROGRAM
from .const import (
APPLIANCES_WITH_PROGRAMS,
DOMAIN,
PROGRAMS_TRANSLATION_KEYS_MAP,
SVE_TRANSLATION_PLACEHOLDER_PROGRAM,
TRANSLATION_KEYS_PROGRAMS_MAP,
)
from .coordinator import (
HomeConnectApplianceData,
HomeConnectConfigEntry,
HomeConnectCoordinator,
)
from .entity import HomeConnectEntity
from .utils import bsh_key_to_translation_key, get_dict_from_home_connect_error
TRANSLATION_KEYS_PROGRAMS_MAP = {
bsh_key_to_translation_key(program.value): cast(ProgramKey, program)
for program in ProgramKey
if program != ProgramKey.UNKNOWN
}
PROGRAMS_TRANSLATION_KEYS_MAP = {
value: key for key, value in TRANSLATION_KEYS_PROGRAMS_MAP.items()
}
from .utils import get_dict_from_home_connect_error
@dataclass(frozen=True, kw_only=True)

View File

@ -46,6 +46,532 @@ select_program:
example: "seconds"
selector:
text:
set_program_and_options:
fields:
device_id:
required: true
selector:
device:
integration: home_connect
affects_to:
example: active_program
required: true
selector:
select:
translation_key: affects_to
options:
- active_program
- selected_program
program:
example: dishcare_dishwasher_program_auto2
required: true
selector:
select:
mode: dropdown
custom_value: false
translation_key: programs
options:
- consumer_products_cleaning_robot_program_cleaning_clean_all
- consumer_products_cleaning_robot_program_cleaning_clean_map
- consumer_products_cleaning_robot_program_basic_go_home
- consumer_products_coffee_maker_program_beverage_ristretto
- consumer_products_coffee_maker_program_beverage_espresso
- consumer_products_coffee_maker_program_beverage_espresso_doppio
- consumer_products_coffee_maker_program_beverage_coffee
- consumer_products_coffee_maker_program_beverage_x_l_coffee
- consumer_products_coffee_maker_program_beverage_caffe_grande
- consumer_products_coffee_maker_program_beverage_espresso_macchiato
- consumer_products_coffee_maker_program_beverage_cappuccino
- consumer_products_coffee_maker_program_beverage_latte_macchiato
- consumer_products_coffee_maker_program_beverage_caffe_latte
- consumer_products_coffee_maker_program_beverage_milk_froth
- consumer_products_coffee_maker_program_beverage_warm_milk
- consumer_products_coffee_maker_program_coffee_world_kleiner_brauner
- consumer_products_coffee_maker_program_coffee_world_grosser_brauner
- consumer_products_coffee_maker_program_coffee_world_verlaengerter
- consumer_products_coffee_maker_program_coffee_world_verlaengerter_braun
- consumer_products_coffee_maker_program_coffee_world_wiener_melange
- consumer_products_coffee_maker_program_coffee_world_flat_white
- consumer_products_coffee_maker_program_coffee_world_cortado
- consumer_products_coffee_maker_program_coffee_world_cafe_cortado
- consumer_products_coffee_maker_program_coffee_world_cafe_con_leche
- consumer_products_coffee_maker_program_coffee_world_cafe_au_lait
- consumer_products_coffee_maker_program_coffee_world_doppio
- consumer_products_coffee_maker_program_coffee_world_kaapi
- consumer_products_coffee_maker_program_coffee_world_koffie_verkeerd
- consumer_products_coffee_maker_program_coffee_world_galao
- consumer_products_coffee_maker_program_coffee_world_garoto
- consumer_products_coffee_maker_program_coffee_world_americano
- consumer_products_coffee_maker_program_coffee_world_red_eye
- consumer_products_coffee_maker_program_coffee_world_black_eye
- consumer_products_coffee_maker_program_coffee_world_dead_eye
- consumer_products_coffee_maker_program_beverage_hot_water
- dishcare_dishwasher_program_pre_rinse
- dishcare_dishwasher_program_auto_1
- dishcare_dishwasher_program_auto_2
- dishcare_dishwasher_program_auto_3
- dishcare_dishwasher_program_eco_50
- dishcare_dishwasher_program_quick_45
- dishcare_dishwasher_program_intensiv_70
- dishcare_dishwasher_program_normal_65
- dishcare_dishwasher_program_glas_40
- dishcare_dishwasher_program_glass_care
- dishcare_dishwasher_program_night_wash
- dishcare_dishwasher_program_quick_65
- dishcare_dishwasher_program_normal_45
- dishcare_dishwasher_program_intensiv_45
- dishcare_dishwasher_program_auto_half_load
- dishcare_dishwasher_program_intensiv_power
- dishcare_dishwasher_program_magic_daily
- dishcare_dishwasher_program_super_60
- dishcare_dishwasher_program_kurz_60
- dishcare_dishwasher_program_express_sparkle_65
- dishcare_dishwasher_program_machine_care
- dishcare_dishwasher_program_steam_fresh
- dishcare_dishwasher_program_maximum_cleaning
- dishcare_dishwasher_program_mixed_load
- laundry_care_dryer_program_cotton
- laundry_care_dryer_program_synthetic
- laundry_care_dryer_program_mix
- laundry_care_dryer_program_blankets
- laundry_care_dryer_program_business_shirts
- laundry_care_dryer_program_down_feathers
- laundry_care_dryer_program_hygiene
- laundry_care_dryer_program_jeans
- laundry_care_dryer_program_outdoor
- laundry_care_dryer_program_synthetic_refresh
- laundry_care_dryer_program_towels
- laundry_care_dryer_program_delicates
- laundry_care_dryer_program_super_40
- laundry_care_dryer_program_shirts_15
- laundry_care_dryer_program_pillow
- laundry_care_dryer_program_anti_shrink
- laundry_care_dryer_program_my_time_my_drying_time
- laundry_care_dryer_program_time_cold
- laundry_care_dryer_program_time_warm
- laundry_care_dryer_program_in_basket
- laundry_care_dryer_program_time_cold_fix_time_cold_20
- laundry_care_dryer_program_time_cold_fix_time_cold_30
- laundry_care_dryer_program_time_cold_fix_time_cold_60
- laundry_care_dryer_program_time_warm_fix_time_warm_30
- laundry_care_dryer_program_time_warm_fix_time_warm_40
- laundry_care_dryer_program_time_warm_fix_time_warm_60
- laundry_care_dryer_program_dessous
- cooking_common_program_hood_automatic
- cooking_common_program_hood_venting
- cooking_common_program_hood_delayed_shut_off
- cooking_oven_program_heating_mode_pre_heating
- cooking_oven_program_heating_mode_hot_air
- cooking_oven_program_heating_mode_hot_air_eco
- cooking_oven_program_heating_mode_hot_air_grilling
- cooking_oven_program_heating_mode_top_bottom_heating
- cooking_oven_program_heating_mode_top_bottom_heating_eco
- cooking_oven_program_heating_mode_bottom_heating
- cooking_oven_program_heating_mode_pizza_setting
- cooking_oven_program_heating_mode_slow_cook
- cooking_oven_program_heating_mode_intensive_heat
- cooking_oven_program_heating_mode_keep_warm
- cooking_oven_program_heating_mode_preheat_ovenware
- cooking_oven_program_heating_mode_frozen_heatup_special
- cooking_oven_program_heating_mode_desiccation
- cooking_oven_program_heating_mode_defrost
- cooking_oven_program_heating_mode_proof
- cooking_oven_program_heating_mode_hot_air_30_steam
- cooking_oven_program_heating_mode_hot_air_60_steam
- cooking_oven_program_heating_mode_hot_air_80_steam
- cooking_oven_program_heating_mode_hot_air_100_steam
- cooking_oven_program_heating_mode_sabbath_programme
- cooking_oven_program_microwave_90_watt
- cooking_oven_program_microwave_180_watt
- cooking_oven_program_microwave_360_watt
- cooking_oven_program_microwave_600_watt
- cooking_oven_program_microwave_900_watt
- cooking_oven_program_microwave_1000_watt
- cooking_oven_program_microwave_max
- cooking_oven_program_heating_mode_warming_drawer
- laundry_care_washer_program_cotton
- laundry_care_washer_program_cotton_cotton_eco
- laundry_care_washer_program_cotton_eco_4060
- laundry_care_washer_program_cotton_colour
- laundry_care_washer_program_easy_care
- laundry_care_washer_program_mix
- laundry_care_washer_program_mix_night_wash
- laundry_care_washer_program_delicates_silk
- laundry_care_washer_program_wool
- laundry_care_washer_program_sensitive
- laundry_care_washer_program_auto_30
- laundry_care_washer_program_auto_40
- laundry_care_washer_program_auto_60
- laundry_care_washer_program_chiffon
- laundry_care_washer_program_curtains
- laundry_care_washer_program_dark_wash
- laundry_care_washer_program_dessous
- laundry_care_washer_program_monsoon
- laundry_care_washer_program_outdoor
- laundry_care_washer_program_plush_toy
- laundry_care_washer_program_shirts_blouses
- laundry_care_washer_program_sport_fitness
- laundry_care_washer_program_towels
- laundry_care_washer_program_water_proof
- laundry_care_washer_program_power_speed_59
- laundry_care_washer_program_super_153045_super_15
- laundry_care_washer_program_super_153045_super_1530
- laundry_care_washer_program_down_duvet_duvet
- laundry_care_washer_program_rinse_rinse_spin_drain
- laundry_care_washer_program_drum_clean
- laundry_care_washer_dryer_program_cotton
- laundry_care_washer_dryer_program_cotton_eco_4060
- laundry_care_washer_dryer_program_mix
- laundry_care_washer_dryer_program_easy_care
- laundry_care_washer_dryer_program_wash_and_dry_60
- laundry_care_washer_dryer_program_wash_and_dry_90
cleaning_robot_options:
collapsed: true
fields:
consumer_products_cleaning_robot_option_reference_map_id:
example: consumer_products_cleaning_robot_enum_type_available_maps_map1
required: false
selector:
select:
mode: dropdown
translation_key: available_maps
options:
- consumer_products_cleaning_robot_enum_type_available_maps_temp_map
- consumer_products_cleaning_robot_enum_type_available_maps_map1
- consumer_products_cleaning_robot_enum_type_available_maps_map2
- consumer_products_cleaning_robot_enum_type_available_maps_map3
consumer_products_cleaning_robot_option_cleaning_mode:
example: consumer_products_cleaning_robot_enum_type_cleaning_modes_standard
required: false
selector:
select:
mode: dropdown
translation_key: cleaning_mode
options:
- consumer_products_cleaning_robot_enum_type_cleaning_modes_silent
- consumer_products_cleaning_robot_enum_type_cleaning_modes_standard
- consumer_products_cleaning_robot_enum_type_cleaning_modes_power
coffee_maker_options:
collapsed: true
fields:
consumer_products_coffee_maker_option_bean_amount:
example: consumer_products_coffee_maker_enum_type_bean_amount_normal
required: false
selector:
select:
mode: dropdown
translation_key: bean_amount
options:
- consumer_products_coffee_maker_enum_type_bean_amount_very_mild
- consumer_products_coffee_maker_enum_type_bean_amount_mild
- consumer_products_coffee_maker_enum_type_bean_amount_mild_plus
- consumer_products_coffee_maker_enum_type_bean_amount_normal
- consumer_products_coffee_maker_enum_type_bean_amount_normal_plus
- consumer_products_coffee_maker_enum_type_bean_amount_strong
- consumer_products_coffee_maker_enum_type_bean_amount_strong_plus
- consumer_products_coffee_maker_enum_type_bean_amount_very_strong
- consumer_products_coffee_maker_enum_type_bean_amount_very_strong_plus
- consumer_products_coffee_maker_enum_type_bean_amount_extra_strong
- consumer_products_coffee_maker_enum_type_bean_amount_double_shot
- consumer_products_coffee_maker_enum_type_bean_amount_double_shot_plus
- consumer_products_coffee_maker_enum_type_bean_amount_double_shot_plus_plus
- consumer_products_coffee_maker_enum_type_bean_amount_triple_shot
- consumer_products_coffee_maker_enum_type_bean_amount_triple_shot_plus
- consumer_products_coffee_maker_enum_type_bean_amount_coffee_ground
consumer_products_coffee_maker_option_fill_quantity:
example: 60
required: false
selector:
number:
min: 0
step: 1
mode: box
unit_of_measurement: ml
consumer_products_coffee_maker_option_coffee_temperature:
example: consumer_products_coffee_maker_enum_type_coffee_temperature_88_c
required: false
selector:
select:
mode: dropdown
translation_key: coffee_temperature
options:
- consumer_products_coffee_maker_enum_type_coffee_temperature_88_c
- consumer_products_coffee_maker_enum_type_coffee_temperature_90_c
- consumer_products_coffee_maker_enum_type_coffee_temperature_92_c
- consumer_products_coffee_maker_enum_type_coffee_temperature_94_c
- consumer_products_coffee_maker_enum_type_coffee_temperature_95_c
- consumer_products_coffee_maker_enum_type_coffee_temperature_96_c
consumer_products_coffee_maker_option_bean_container:
example: consumer_products_coffee_maker_enum_type_bean_container_selection_right
required: false
selector:
select:
mode: dropdown
translation_key: bean_container
options:
- consumer_products_coffee_maker_enum_type_bean_container_selection_right
- consumer_products_coffee_maker_enum_type_bean_container_selection_left
consumer_products_coffee_maker_option_flow_rate:
example: consumer_products_coffee_maker_enum_type_flow_rate_normal
required: false
selector:
select:
mode: dropdown
translation_key: flow_rate
options:
- consumer_products_coffee_maker_enum_type_flow_rate_normal
- consumer_products_coffee_maker_enum_type_flow_rate_intense
- consumer_products_coffee_maker_enum_type_flow_rate_intense_plus
consumer_products_coffee_maker_option_multiple_beverages:
example: false
required: false
selector:
boolean:
consumer_products_coffee_maker_option_coffee_milk_ratio:
example: 50
required: false
selector:
number:
unit_of_measurement: "%"
step: 10
min: 10
max: 90
consumer_products_coffee_maker_option_hot_water_temperature:
example: consumer_products_coffee_maker_enum_type_hot_water_temperature_50_c
required: false
selector:
select:
mode: dropdown
translation_key: hot_water_temperature
options:
- consumer_products_coffee_maker_enum_type_hot_water_temperature_white_tea
- consumer_products_coffee_maker_enum_type_hot_water_temperature_green_tea
- consumer_products_coffee_maker_enum_type_hot_water_temperature_black_tea
- consumer_products_coffee_maker_enum_type_hot_water_temperature_50_c
- consumer_products_coffee_maker_enum_type_hot_water_temperature_55_c
- consumer_products_coffee_maker_enum_type_hot_water_temperature_60_c
- consumer_products_coffee_maker_enum_type_hot_water_temperature_65_c
- consumer_products_coffee_maker_enum_type_hot_water_temperature_70_c
- consumer_products_coffee_maker_enum_type_hot_water_temperature_75_c
- consumer_products_coffee_maker_enum_type_hot_water_temperature_80_c
- consumer_products_coffee_maker_enum_type_hot_water_temperature_85_c
- consumer_products_coffee_maker_enum_type_hot_water_temperature_90_c
- consumer_products_coffee_maker_enum_type_hot_water_temperature_95_c
- consumer_products_coffee_maker_enum_type_hot_water_temperature_97_c
- consumer_products_coffee_maker_enum_type_hot_water_temperature_122_f
- consumer_products_coffee_maker_enum_type_hot_water_temperature_131_f
- consumer_products_coffee_maker_enum_type_hot_water_temperature_140_f
- consumer_products_coffee_maker_enum_type_hot_water_temperature_149_f
- consumer_products_coffee_maker_enum_type_hot_water_temperature_158_f
- consumer_products_coffee_maker_enum_type_hot_water_temperature_167_f
- consumer_products_coffee_maker_enum_type_hot_water_temperature_176_f
- consumer_products_coffee_maker_enum_type_hot_water_temperature_185_f
- consumer_products_coffee_maker_enum_type_hot_water_temperature_194_f
- consumer_products_coffee_maker_enum_type_hot_water_temperature_203_f
- consumer_products_coffee_maker_enum_type_hot_water_temperature_max
dish_washer_options:
collapsed: true
fields:
b_s_h_common_option_start_in_relative:
example: "30:00"
required: false
selector:
time:
dishcare_dishwasher_option_intensiv_zone:
example: false
required: false
selector:
boolean:
dishcare_dishwasher_option_brilliance_dry:
example: false
required: false
selector:
boolean:
dishcare_dishwasher_option_vario_speed_plus:
example: false
required: false
selector:
boolean:
dishcare_dishwasher_option_silence_on_demand:
example: false
required: false
selector:
boolean:
dishcare_dishwasher_option_half_load:
example: false
required: false
selector:
boolean:
dishcare_dishwasher_option_extra_dry:
example: false
required: false
selector:
boolean:
dishcare_dishwasher_option_hygiene_plus:
example: false
required: false
selector:
boolean:
dishcare_dishwasher_option_eco_dry:
example: false
required: false
selector:
boolean:
dishcare_dishwasher_option_zeolite_dry:
example: false
required: false
selector:
boolean:
dryer_options:
collapsed: true
fields:
laundry_care_dryer_option_drying_target:
example: laundry_care_dryer_enum_type_drying_target_iron_dry
required: false
selector:
select:
mode: dropdown
translation_key: drying_target
options:
- laundry_care_dryer_enum_type_drying_target_iron_dry
- laundry_care_dryer_enum_type_drying_target_gentle_dry
- laundry_care_dryer_enum_type_drying_target_cupboard_dry
- laundry_care_dryer_enum_type_drying_target_cupboard_dry_plus
- laundry_care_dryer_enum_type_drying_target_extra_dry
hood_options:
collapsed: true
fields:
cooking_hood_option_venting_level:
example: cooking_hood_enum_type_stage_fan_stage01
required: false
selector:
select:
mode: dropdown
translation_key: venting_level
options:
- cooking_hood_enum_type_stage_fan_off
- cooking_hood_enum_type_stage_fan_stage01
- cooking_hood_enum_type_stage_fan_stage02
- cooking_hood_enum_type_stage_fan_stage03
- cooking_hood_enum_type_stage_fan_stage04
- cooking_hood_enum_type_stage_fan_stage05
cooking_hood_option_intensive_level:
example: cooking_hood_enum_type_intensive_stage_intensive_stage1
required: false
selector:
select:
mode: dropdown
translation_key: intensive_level
options:
- cooking_hood_enum_type_intensive_stage_intensive_stage_off
- cooking_hood_enum_type_intensive_stage_intensive_stage1
- cooking_hood_enum_type_intensive_stage_intensive_stage2
oven_options:
collapsed: true
fields:
cooking_oven_option_setpoint_temperature:
example: 180
required: false
selector:
number:
min: 0
step: 1
mode: box
unit_of_measurement: °C/°F
b_s_h_common_option_duration:
example: "30:00"
required: false
selector:
time:
cooking_oven_option_fast_pre_heat:
example: false
required: false
selector:
boolean:
warming_drawer_options:
collapsed: true
fields:
cooking_oven_option_warming_level:
example: cooking_oven_enum_type_warming_level_medium
required: false
selector:
select:
mode: dropdown
translation_key: warming_level
options:
- cooking_oven_enum_type_warming_level_low
- cooking_oven_enum_type_warming_level_medium
- cooking_oven_enum_type_warming_level_high
washer_options:
collapsed: true
fields:
laundry_care_washer_option_temperature:
example: laundry_care_washer_enum_type_temperature_g_c40
required: false
selector:
select:
mode: dropdown
translation_key: washer_temperature
options:
- laundry_care_washer_enum_type_temperature_cold
- laundry_care_washer_enum_type_temperature_g_c20
- laundry_care_washer_enum_type_temperature_g_c30
- laundry_care_washer_enum_type_temperature_g_c40
- laundry_care_washer_enum_type_temperature_g_c50
- laundry_care_washer_enum_type_temperature_g_c60
- laundry_care_washer_enum_type_temperature_g_c70
- laundry_care_washer_enum_type_temperature_g_c80
- laundry_care_washer_enum_type_temperature_g_c90
- laundry_care_washer_enum_type_temperature_ul_cold
- laundry_care_washer_enum_type_temperature_ul_warm
- laundry_care_washer_enum_type_temperature_ul_hot
- laundry_care_washer_enum_type_temperature_ul_extra_hot
laundry_care_washer_option_spin_speed:
example: laundry_care_washer_enum_type_spin_speed_r_p_m800
required: false
selector:
select:
mode: dropdown
translation_key: spin_speed
options:
- laundry_care_washer_enum_type_spin_speed_off
- laundry_care_washer_enum_type_spin_speed_r_p_m400
- laundry_care_washer_enum_type_spin_speed_r_p_m600
- laundry_care_washer_enum_type_spin_speed_r_p_m800
- laundry_care_washer_enum_type_spin_speed_r_p_m1000
- laundry_care_washer_enum_type_spin_speed_r_p_m1200
- laundry_care_washer_enum_type_spin_speed_r_p_m1400
- laundry_care_washer_enum_type_spin_speed_r_p_m1600
- laundry_care_washer_enum_type_spin_speed_ul_off
- laundry_care_washer_enum_type_spin_speed_ul_low
- laundry_care_washer_enum_type_spin_speed_ul_medium
- laundry_care_washer_enum_type_spin_speed_ul_high
b_s_h_common_option_finish_in_relative:
example: "30:00"
required: false
selector:
time:
laundry_care_washer_option_i_dos1_active:
example: false
required: false
selector:
boolean:
laundry_care_washer_option_i_dos2_active:
example: false
required: false
selector:
boolean:
laundry_care_washer_option_vario_perfect:
example: laundry_care_common_enum_type_vario_perfect_eco_perfect
required: false
selector:
select:
mode: dropdown
translation_key: vario_perfect
options:
- laundry_care_common_enum_type_vario_perfect_off
- laundry_care_common_enum_type_vario_perfect_eco_perfect
- laundry_care_common_enum_type_vario_perfect_speed_perfect
pause_program:
fields:
device_id:

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,7 @@ from aiohomeconnect.client import Client as HomeConnectClient
from aiohomeconnect.model import (
ArrayOfEvents,
ArrayOfHomeAppliances,
ArrayOfOptions,
ArrayOfPrograms,
ArrayOfSettings,
ArrayOfStatus,
@ -199,13 +200,13 @@ def _get_set_program_side_effect(
return set_program_side_effect
def _get_set_key_value_side_effect(
event_queue: asyncio.Queue[list[EventMessage]], parameter_key: str
def _get_set_setting_side_effect(
event_queue: asyncio.Queue[list[EventMessage]],
):
"""Set program options side effect."""
"""Set settings side effect."""
async def set_key_value_side_effect(ha_id: str, *_, **kwargs) -> None:
event_key = EventKey(kwargs[parameter_key])
async def set_settings_side_effect(ha_id: str, *_, **kwargs) -> None:
event_key = EventKey(kwargs["setting_key"])
await event_queue.put(
[
EventMessage(
@ -227,7 +228,48 @@ def _get_set_key_value_side_effect(
]
)
return set_key_value_side_effect
return set_settings_side_effect
def _get_set_program_options_side_effect(
event_queue: asyncio.Queue[list[EventMessage]],
):
"""Set programs side effect."""
async def set_program_options_side_effect(ha_id: str, *_, **kwargs) -> None:
await event_queue.put(
[
EventMessage(
ha_id,
EventType.NOTIFY,
ArrayOfEvents(
[
Event(
key=EventKey(option.key),
raw_key=option.key.value,
timestamp=0,
level="",
handling="",
value=option.value,
)
for option in (
cast(ArrayOfOptions, kwargs["array_of_options"]).options
if "array_of_options" in kwargs
else [
Option(
kwargs["option_key"],
kwargs["value"],
unit=kwargs["unit"],
)
]
)
]
),
),
]
)
return set_program_options_side_effect
async def _get_all_programs_side_effect(ha_id: str) -> ArrayOfPrograms:
@ -319,13 +361,19 @@ def mock_client(request: pytest.FixtureRequest) -> MagicMock:
),
)
mock.set_active_program_option = AsyncMock(
side_effect=_get_set_key_value_side_effect(event_queue, "option_key"),
side_effect=_get_set_program_options_side_effect(event_queue),
)
mock.set_active_program_options = AsyncMock(
side_effect=_get_set_program_options_side_effect(event_queue),
)
mock.set_selected_program_option = AsyncMock(
side_effect=_get_set_key_value_side_effect(event_queue, "option_key"),
side_effect=_get_set_program_options_side_effect(event_queue),
)
mock.set_selected_program_options = AsyncMock(
side_effect=_get_set_program_options_side_effect(event_queue),
)
mock.set_setting = AsyncMock(
side_effect=_get_set_key_value_side_effect(event_queue, "setting_key"),
side_effect=_get_set_setting_side_effect(event_queue),
)
mock.get_settings = AsyncMock(side_effect=_get_settings_side_effect)
mock.get_setting = AsyncMock(side_effect=_get_setting_side_effect)
@ -363,7 +411,9 @@ def mock_client_with_exception(request: pytest.FixtureRequest) -> MagicMock:
mock.stop_program = AsyncMock(side_effect=exception)
mock.set_selected_program = AsyncMock(side_effect=exception)
mock.set_active_program_option = 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_options = AsyncMock(side_effect=exception)
mock.set_setting = AsyncMock(side_effect=exception)
mock.get_settings = AsyncMock(side_effect=exception)
mock.get_setting = AsyncMock(side_effect=exception)

View File

@ -0,0 +1,79 @@
# serializer version: 1
# name: test_set_program_and_options[service_call0-set_selected_program]
_Call(
tuple(
'SIEMENS-HCS03WCH1-7BC6383CF794',
),
dict({
'options': list([
dict({
'display_value': None,
'key': <OptionKey.BSH_COMMON_START_IN_RELATIVE: 'BSH.Common.Option.StartInRelative'>,
'name': None,
'unit': None,
'value': 1800,
}),
]),
'program_key': <ProgramKey.DISHCARE_DISHWASHER_ECO_50: 'Dishcare.Dishwasher.Program.Eco50'>,
}),
)
# ---
# name: test_set_program_and_options[service_call1-start_program]
_Call(
tuple(
'SIEMENS-HCS03WCH1-7BC6383CF794',
),
dict({
'options': list([
dict({
'display_value': None,
'key': <OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEAN_AMOUNT: 'ConsumerProducts.CoffeeMaker.Option.BeanAmount'>,
'name': None,
'unit': None,
'value': 'ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.Normal',
}),
]),
'program_key': <ProgramKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COFFEE: 'ConsumerProducts.CoffeeMaker.Program.Beverage.Coffee'>,
}),
)
# ---
# name: test_set_program_and_options[service_call2-set_active_program_options]
_Call(
tuple(
'SIEMENS-HCS03WCH1-7BC6383CF794',
),
dict({
'array_of_options': dict({
'options': list([
dict({
'display_value': None,
'key': <OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_COFFEE_MILK_RATIO: 'ConsumerProducts.CoffeeMaker.Option.CoffeeMilkRatio'>,
'name': None,
'unit': None,
'value': 60,
}),
]),
}),
}),
)
# ---
# name: test_set_program_and_options[service_call3-set_selected_program_options]
_Call(
tuple(
'SIEMENS-HCS03WCH1-7BC6383CF794',
),
dict({
'array_of_options': dict({
'options': list([
dict({
'display_value': None,
'key': <OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_FILL_QUANTITY: 'ConsumerProducts.CoffeeMaker.Option.FillQuantity'>,
'name': None,
'unit': None,
'value': 35,
}),
]),
}),
}),
)
# ---

View File

@ -1,6 +1,7 @@
"""Test the integration init functionality."""
from collections.abc import Awaitable, Callable
from http import HTTPStatus
from typing import Any
from unittest.mock import MagicMock, patch
@ -10,6 +11,7 @@ from aiohomeconnect.model.error import HomeConnectError, UnauthorizedError
import pytest
import requests_mock
import respx
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.home_connect.const import DOMAIN
@ -22,6 +24,7 @@ from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import device_registry as dr, entity_registry as er
import homeassistant.helpers.issue_registry as ir
from script.hassfest.translations import RE_TRANSLATION_KEY
from .conftest import (
@ -34,8 +37,9 @@ from .conftest import (
from tests.common import MockConfigEntry
from tests.test_util.aiohttp import AiohttpClientMocker
from tests.typing import ClientSessionGenerator
SERVICE_KV_CALL_PARAMS = [
DEPRECATED_SERVICE_KV_CALL_PARAMS = [
{
"domain": DOMAIN,
"service": "set_option_active",
@ -57,6 +61,10 @@ SERVICE_KV_CALL_PARAMS = [
},
"blocking": True,
},
]
SERVICE_KV_CALL_PARAMS = [
*DEPRECATED_SERVICE_KV_CALL_PARAMS,
{
"domain": DOMAIN,
"service": "change_setting",
@ -125,6 +133,62 @@ SERVICE_APPLIANCE_METHOD_MAPPING = {
"start_program": "start_program",
}
SERVICE_VALIDATION_ERROR_MAPPING = {
"set_option_active": r"Error.*setting.*options.*active.*program.*",
"set_option_selected": r"Error.*setting.*options.*selected.*program.*",
"change_setting": r"Error.*assigning.*value.*setting.*",
"pause_program": r"Error.*executing.*command.*",
"resume_program": r"Error.*executing.*command.*",
"select_program": r"Error.*selecting.*program.*",
"start_program": r"Error.*starting.*program.*",
}
SERVICES_SET_PROGRAM_AND_OPTIONS = [
{
"domain": DOMAIN,
"service": "set_program_and_options",
"service_data": {
"device_id": "DEVICE_ID",
"affects_to": "selected_program",
"program": "dishcare_dishwasher_program_eco_50",
"b_s_h_common_option_start_in_relative": "00:30:00",
},
"blocking": True,
},
{
"domain": DOMAIN,
"service": "set_program_and_options",
"service_data": {
"device_id": "DEVICE_ID",
"affects_to": "active_program",
"program": "consumer_products_coffee_maker_program_beverage_coffee",
"consumer_products_coffee_maker_option_bean_amount": "consumer_products_coffee_maker_enum_type_bean_amount_normal",
},
"blocking": True,
},
{
"domain": DOMAIN,
"service": "set_program_and_options",
"service_data": {
"device_id": "DEVICE_ID",
"affects_to": "active_program",
"consumer_products_coffee_maker_option_coffee_milk_ratio": 60,
},
"blocking": True,
},
{
"domain": DOMAIN,
"service": "set_program_and_options",
"service_data": {
"device_id": "DEVICE_ID",
"affects_to": "selected_program",
"consumer_products_coffee_maker_option_fill_quantity": 35,
},
"blocking": True,
},
]
async def test_entry_setup(
hass: HomeAssistant,
@ -244,7 +308,7 @@ async def test_client_error(
"service_call",
SERVICE_KV_CALL_PARAMS + SERVICE_COMMAND_CALL_PARAMS + SERVICE_PROGRAM_CALL_PARAMS,
)
async def test_services(
async def test_key_value_services(
service_call: dict[str, Any],
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
@ -273,11 +337,188 @@ async def test_services(
)
@pytest.mark.parametrize(
"service_call",
DEPRECATED_SERVICE_KV_CALL_PARAMS + SERVICE_PROGRAM_CALL_PARAMS,
)
async def test_programs_and_options_actions_deprecation(
service_call: dict[str, Any],
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client: MagicMock,
appliance_ha_id: str,
issue_registry: ir.IssueRegistry,
hass_client: ClientSessionGenerator,
) -> None:
"""Test deprecated service keys."""
issue_id = "deprecated_set_program_and_option_actions"
assert config_entry.state == ConfigEntryState.NOT_LOADED
assert await integration_setup(client)
assert config_entry.state == ConfigEntryState.LOADED
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
identifiers={(DOMAIN, appliance_ha_id)},
)
service_call["service_data"]["device_id"] = device_entry.id
await hass.services.async_call(**service_call)
await hass.async_block_till_done()
assert len(issue_registry.issues) == 1
issue = issue_registry.async_get_issue(DOMAIN, issue_id)
assert issue
_client = await hass_client()
resp = await _client.post(
"/api/repairs/issues/fix",
json={"handler": DOMAIN, "issue_id": issue.issue_id},
)
assert resp.status == HTTPStatus.OK
flow_id = (await resp.json())["flow_id"]
resp = await _client.post(f"/api/repairs/issues/fix/{flow_id}")
assert not issue_registry.async_get_issue(DOMAIN, issue_id)
assert len(issue_registry.issues) == 0
await hass.services.async_call(**service_call)
await hass.async_block_till_done()
assert len(issue_registry.issues) == 1
assert issue_registry.async_get_issue(DOMAIN, issue_id)
await hass.config_entries.async_unload(config_entry.entry_id)
await hass.async_block_till_done()
# Assert the issue is no longer present
assert not issue_registry.async_get_issue(DOMAIN, issue_id)
assert len(issue_registry.issues) == 0
@pytest.mark.parametrize(
("service_call", "called_method"),
zip(
SERVICES_SET_PROGRAM_AND_OPTIONS,
[
"set_selected_program",
"start_program",
"set_active_program_options",
"set_selected_program_options",
],
strict=True,
),
)
async def test_set_program_and_options(
service_call: dict[str, Any],
called_method: str,
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client: MagicMock,
appliance_ha_id: str,
snapshot: SnapshotAssertion,
) -> None:
"""Test recognized options."""
assert config_entry.state == ConfigEntryState.NOT_LOADED
assert await integration_setup(client)
assert config_entry.state == ConfigEntryState.LOADED
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
identifiers={(DOMAIN, appliance_ha_id)},
)
service_call["service_data"]["device_id"] = device_entry.id
await hass.services.async_call(**service_call)
await hass.async_block_till_done()
method_mock: MagicMock = getattr(client, called_method)
assert method_mock.call_count == 1
assert method_mock.call_args == snapshot
@pytest.mark.parametrize(
("service_call", "error_regex"),
zip(
SERVICES_SET_PROGRAM_AND_OPTIONS,
[
r"Error.*selecting.*program.*",
r"Error.*starting.*program.*",
r"Error.*setting.*options.*active.*program.*",
r"Error.*setting.*options.*selected.*program.*",
],
strict=True,
),
)
async def test_set_program_and_options_exceptions(
service_call: dict[str, Any],
error_regex: str,
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client_with_exception: MagicMock,
appliance_ha_id: str,
) -> None:
"""Test recognized options."""
assert config_entry.state == ConfigEntryState.NOT_LOADED
assert await integration_setup(client_with_exception)
assert config_entry.state == ConfigEntryState.LOADED
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
identifiers={(DOMAIN, appliance_ha_id)},
)
service_call["service_data"]["device_id"] = device_entry.id
with pytest.raises(HomeAssistantError, match=error_regex):
await hass.services.async_call(**service_call)
async def test_required_program_or_at_least_an_option(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client: MagicMock,
appliance_ha_id: str,
) -> None:
"Test that the set_program_and_options does raise an exception if no program nor options are set."
assert config_entry.state == ConfigEntryState.NOT_LOADED
assert await integration_setup(client)
assert config_entry.state == ConfigEntryState.LOADED
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
identifiers={(DOMAIN, appliance_ha_id)},
)
with pytest.raises(
ServiceValidationError,
):
await hass.services.async_call(
DOMAIN,
"set_program_and_options",
{
"device_id": device_entry.id,
"affects_to": "selected_program",
},
True,
)
@pytest.mark.parametrize(
"service_call",
SERVICE_KV_CALL_PARAMS + SERVICE_COMMAND_CALL_PARAMS + SERVICE_PROGRAM_CALL_PARAMS,
)
async def test_services_exception(
async def test_services_exception_device_id(
service_call: dict[str, Any],
hass: HomeAssistant,
config_entry: MockConfigEntry,
@ -348,6 +589,40 @@ async def test_services_appliance_not_found(
await hass.services.async_call(**service_call)
@pytest.mark.parametrize(
"service_call",
SERVICE_KV_CALL_PARAMS + SERVICE_COMMAND_CALL_PARAMS + SERVICE_PROGRAM_CALL_PARAMS,
)
async def test_services_exception(
service_call: dict[str, Any],
hass: HomeAssistant,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client_with_exception: MagicMock,
appliance_ha_id: str,
device_registry: dr.DeviceRegistry,
) -> None:
"""Raise a ValueError when device id does not match."""
assert config_entry.state == ConfigEntryState.NOT_LOADED
assert await integration_setup(client_with_exception)
assert config_entry.state == ConfigEntryState.LOADED
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
identifiers={(DOMAIN, appliance_ha_id)},
)
service_call["service_data"]["device_id"] = device_entry.id
service_name = service_call["service"]
with pytest.raises(
HomeAssistantError,
match=SERVICE_VALIDATION_ERROR_MAPPING[service_name],
):
await hass.services.async_call(**service_call)
async def test_entity_migration(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,