diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py index 345eeeddaaa..f57c7aeb8af 100644 --- a/homeassistant/components/home_connect/__init__.py +++ b/homeassistant/components/home_connect/__init__.py @@ -1,4 +1,5 @@ """Support for BSH Home Connect appliances.""" +from __future__ import annotations from datetime import timedelta import logging @@ -11,14 +12,39 @@ from homeassistant.components.application_credentials import ( async_import_client_credential, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, Platform +from homeassistant.const import ( + ATTR_DEVICE_ID, + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_DEVICE, + Platform, +) from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv +from homeassistant.helpers import ( + config_entry_oauth2_flow, + config_validation as cv, + device_registry as dr, +) from homeassistant.helpers.typing import ConfigType from homeassistant.util import Throttle from . import api -from .const import DOMAIN +from .const import ( + ATTR_KEY, + ATTR_PROGRAM, + ATTR_UNIT, + ATTR_VALUE, + BSH_PAUSE, + BSH_RESUME, + DOMAIN, + SERVICE_OPTION_ACTIVE, + SERVICE_OPTION_SELECTED, + SERVICE_PAUSE_PROGRAM, + SERVICE_RESUME_PROGRAM, + SERVICE_SELECT_PROGRAM, + SERVICE_SETTING, + SERVICE_START_PROGRAM, +) _LOGGER = logging.getLogger(__name__) @@ -39,9 +65,55 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +SERVICE_SETTING_SCHEMA = vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): str, + vol.Required(ATTR_KEY): str, + vol.Required(ATTR_VALUE): vol.Any(str, int, bool), + } +) + +SERVICE_OPTION_SCHEMA = vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): str, + vol.Required(ATTR_KEY): str, + vol.Required(ATTR_VALUE): vol.Any(str, int, bool), + vol.Optional(ATTR_UNIT): str, + } +) + +SERVICE_PROGRAM_SCHEMA = vol.Any( + { + vol.Required(ATTR_DEVICE_ID): str, + vol.Required(ATTR_PROGRAM): str, + vol.Required(ATTR_KEY): str, + vol.Required(ATTR_VALUE): vol.Any(int, str), + vol.Optional(ATTR_UNIT): str, + }, + { + vol.Required(ATTR_DEVICE_ID): str, + vol.Required(ATTR_PROGRAM): str, + }, +) + +SERVICE_COMMAND_SCHEMA = vol.Schema({vol.Required(ATTR_DEVICE_ID): str}) + PLATFORMS = [Platform.BINARY_SENSOR, Platform.LIGHT, Platform.SENSOR, Platform.SWITCH] +def _get_appliance_by_device_id( + hass: HomeAssistant, device_id: str +) -> api.HomeConnectDevice | None: + """Return a Home Connect appliance instance given an device_id.""" + for hc_api in hass.data[DOMAIN].values(): + for dev_dict in hc_api.devices: + device = dev_dict[CONF_DEVICE] + if device.device_id == device_id: + return device.appliance + _LOGGER.error("Appliance for device id %s not found", device_id) + return None + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Home Connect component.""" hass.data[DOMAIN] = {} @@ -65,6 +137,121 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: "configuration.yaml file" ) + async def _async_service_program(call, method): + """Execute calls to services taking a program.""" + program = call.data[ATTR_PROGRAM] + device_id = call.data[ATTR_DEVICE_ID] + options = { + ATTR_KEY: call.data.get(ATTR_KEY), + ATTR_VALUE: call.data.get(ATTR_VALUE), + ATTR_UNIT: call.data.get(ATTR_UNIT), + } + + appliance = _get_appliance_by_device_id(hass, device_id) + if appliance is not None: + await hass.async_add_executor_job( + getattr(appliance, method), program, options + ) + + async def _async_service_command(call, command): + """Execute calls to services executing a command.""" + device_id = call.data[ATTR_DEVICE_ID] + + appliance = _get_appliance_by_device_id(hass, device_id) + if appliance is not None: + await hass.async_add_executor_job(appliance.execute_command, command) + + async def _async_service_key_value(call, method): + """Execute calls to services taking a key and value.""" + key = call.data[ATTR_KEY] + value = call.data[ATTR_VALUE] + unit = call.data.get(ATTR_UNIT) + device_id = call.data[ATTR_DEVICE_ID] + + appliance = _get_appliance_by_device_id(hass, device_id) + if appliance is not None: + if unit is not None: + await hass.async_add_executor_job( + getattr(appliance, method), + key, + value, + unit, + ) + else: + await hass.async_add_executor_job( + getattr(appliance, method), + key, + value, + ) + + async def async_service_option_active(call): + """Service for setting an option for an active program.""" + await _async_service_key_value(call, "set_options_active_program") + + async def async_service_option_selected(call): + """Service for setting an option for a selected program.""" + await _async_service_key_value(call, "set_options_selected_program") + + async def async_service_setting(call): + """Service for changing a setting.""" + await _async_service_key_value(call, "set_setting") + + async def async_service_pause_program(call): + """Service for pausing a program.""" + await _async_service_command(call, BSH_PAUSE) + + async def async_service_resume_program(call): + """Service for resuming a paused program.""" + await _async_service_command(call, BSH_RESUME) + + async def async_service_select_program(call): + """Service for selecting a program.""" + await _async_service_program(call, "select_program") + + async def async_service_start_program(call): + """Service for starting a program.""" + await _async_service_program(call, "start_program") + + hass.services.async_register( + DOMAIN, + SERVICE_OPTION_ACTIVE, + async_service_option_active, + schema=SERVICE_OPTION_SCHEMA, + ) + hass.services.async_register( + DOMAIN, + SERVICE_OPTION_SELECTED, + async_service_option_selected, + schema=SERVICE_OPTION_SCHEMA, + ) + hass.services.async_register( + DOMAIN, SERVICE_SETTING, async_service_setting, schema=SERVICE_SETTING_SCHEMA + ) + hass.services.async_register( + DOMAIN, + SERVICE_PAUSE_PROGRAM, + async_service_pause_program, + schema=SERVICE_COMMAND_SCHEMA, + ) + hass.services.async_register( + DOMAIN, + SERVICE_RESUME_PROGRAM, + async_service_resume_program, + schema=SERVICE_COMMAND_SCHEMA, + ) + hass.services.async_register( + DOMAIN, + SERVICE_SELECT_PROGRAM, + async_service_select_program, + schema=SERVICE_PROGRAM_SCHEMA, + ) + hass.services.async_register( + DOMAIN, + SERVICE_START_PROGRAM, + async_service_start_program, + schema=SERVICE_PROGRAM_SCHEMA, + ) + return True @@ -101,9 +288,23 @@ async def update_all_devices(hass, entry): """Update all the devices.""" data = hass.data[DOMAIN] hc_api = data[entry.entry_id] + + device_registry = dr.async_get(hass) try: await hass.async_add_executor_job(hc_api.get_devices) for device_dict in hc_api.devices: - await hass.async_add_executor_job(device_dict["device"].initialize) + device = device_dict["device"] + + device_entry = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, device.appliance.haId)}, + name=device.appliance.name, + manufacturer=device.appliance.brand, + model=device.appliance.vib, + ) + + device.device_id = device_entry.id + + await hass.async_add_executor_job(device.initialize) except HTTPError as err: _LOGGER.warning("Cannot update devices: %s", err.response.status_code) diff --git a/homeassistant/components/home_connect/api.py b/homeassistant/components/home_connect/api.py index f3c98e618b8..00d759b47d5 100644 --- a/homeassistant/components/home_connect/api.py +++ b/homeassistant/components/home_connect/api.py @@ -113,6 +113,7 @@ class HomeConnectDevice: """Initialize the device class.""" self.hass = hass self.appliance = appliance + self.entities = [] def initialize(self): """Fetch the info needed to initialize the device.""" diff --git a/homeassistant/components/home_connect/const.py b/homeassistant/components/home_connect/const.py index 438ee5ace16..9eabc9b5d43 100644 --- a/homeassistant/components/home_connect/const.py +++ b/homeassistant/components/home_connect/const.py @@ -30,12 +30,24 @@ BSH_DOOR_STATE_CLOSED = "BSH.Common.EnumType.DoorState.Closed" BSH_DOOR_STATE_LOCKED = "BSH.Common.EnumType.DoorState.Locked" BSH_DOOR_STATE_OPEN = "BSH.Common.EnumType.DoorState.Open" +BSH_PAUSE = "BSH.Common.Command.PauseProgram" +BSH_RESUME = "BSH.Common.Command.ResumeProgram" + SIGNAL_UPDATE_ENTITIES = "home_connect.update_entities" +SERVICE_OPTION_ACTIVE = "set_option_active" +SERVICE_OPTION_SELECTED = "set_option_selected" +SERVICE_PAUSE_PROGRAM = "pause_program" +SERVICE_RESUME_PROGRAM = "resume_program" +SERVICE_SELECT_PROGRAM = "select_program" +SERVICE_SETTING = "change_setting" +SERVICE_START_PROGRAM = "start_program" + ATTR_AMBIENT = "ambient" ATTR_DESC = "desc" ATTR_DEVICE = "device" ATTR_KEY = "key" +ATTR_PROGRAM = "program" ATTR_SENSOR_TYPE = "sensor_type" ATTR_SIGN = "sign" ATTR_UNIT = "unit" diff --git a/homeassistant/components/home_connect/entity.py b/homeassistant/components/home_connect/entity.py index b27988f997d..60a0c3974cd 100644 --- a/homeassistant/components/home_connect/entity.py +++ b/homeassistant/components/home_connect/entity.py @@ -20,6 +20,7 @@ class HomeConnectEntity(Entity): self.device = device self.desc = desc self._name = f"{self.device.appliance.name} {desc}" + self.device.entities.append(self) async def async_added_to_hass(self): """Register callbacks.""" diff --git a/homeassistant/components/home_connect/services.yaml b/homeassistant/components/home_connect/services.yaml new file mode 100644 index 00000000000..06a646dd481 --- /dev/null +++ b/homeassistant/components/home_connect/services.yaml @@ -0,0 +1,169 @@ +start_program: + name: Start program + description: Selects a program and starts it. + fields: + device_id: + name: Device ID + description: Id of the device. + required: true + selector: + device: + integration: home_connect + program: + name: Program + description: Program to select + example: "Dishcare.Dishwasher.Program.Auto2" + required: true + selector: + text: + key: + name: Option key + description: Key of the option. + example: "BSH.Common.Option.StartInRelative" + selector: + text: + value: + name: Option value + description: Value of the option. + example: 1800 + selector: + object: + unit: + name: Option unit + description: Unit for the option. + example: "seconds" + selector: + text: +select_program: + name: Select program + description: Selects a program without starting it. + fields: + device_id: + name: Device ID + description: Id of the device. + required: true + selector: + device: + integration: home_connect + program: + name: Program + description: Program to select + example: "Dishcare.Dishwasher.Program.Auto2" + required: true + selector: + text: + key: + name: Option key + description: Key of the option. + example: "BSH.Common.Option.StartInRelative" + selector: + text: + value: + name: Option value + description: Value of the option. + example: 1800 + selector: + object: + unit: + name: Option unit + description: Unit for the option. + example: "seconds" + selector: + text: +pause_program: + name: Pause program + description: Pauses the current running program. + fields: + device_id: + name: Device ID + description: Id of the device. + required: true + selector: + device: + integration: home_connect +resume_program: + name: Resume program + description: Resumes a paused program. + fields: + device_id: + name: Device ID + description: Id of the device. + required: true + selector: + device: + integration: home_connect +set_option_active: + name: Set active program option + description: Sets an option for the active program. + fields: + device_id: + name: Device ID + description: Id of the device. + required: true + selector: + device: + integration: home_connect + key: + name: Key + description: Key of the option. + example: "LaundryCare.Dryer.Option.DryingTarget" + required: true + selector: + text: + value: + name: Value + description: Value of the option. + example: "LaundryCare.Dryer.EnumType.DryingTarget.IronDry" + required: true + selector: + object: +set_option_selected: + name: Set selected program option + description: Sets an option for the selected program. + fields: + device_id: + name: Device ID + description: Id of the device. + required: true + selector: + device: + integration: home_connect + key: + name: Key + description: Key of the option. + example: "LaundryCare.Dryer.Option.DryingTarget" + required: true + selector: + text: + value: + name: Value + description: Value of the option. + example: "LaundryCare.Dryer.EnumType.DryingTarget.IronDry" + required: true + selector: + object: +change_setting: + name: Change setting + description: Changes a setting. + fields: + device_id: + name: Device ID + description: Id of the device. + required: true + selector: + device: + integration: home_connect + key: + name: Key + description: Key of the setting. + example: "BSH.Common.Setting.ChildLock" + required: true + selector: + text: + value: + name: Value + description: Value of the setting. + example: "true" + required: true + selector: + object: