diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index 139565bf249..9a3191ac168 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -1,14 +1,16 @@ """Support for HomematicIP Cloud devices.""" import logging +from homematicip.aio.group import AsyncHeatingGroup import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME +from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import comp_entity_ids from homeassistant.helpers.typing import ConfigType from .config_flow import configured_haps @@ -25,6 +27,7 @@ from .hap import HomematicipAuth, HomematicipHAP # noqa: F401 _LOGGER = logging.getLogger(__name__) +ATTR_CLIMATE_PROFILE_INDEX = "climate_profile_index" ATTR_DURATION = "duration" ATTR_ENDTIME = "endtime" ATTR_TEMPERATURE = "temperature" @@ -35,6 +38,7 @@ SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD = "activate_eco_mode_with_period" SERVICE_ACTIVATE_VACATION = "activate_vacation" SERVICE_DEACTIVATE_ECO_MODE = "deactivate_eco_mode" SERVICE_DEACTIVATE_VACATION = "deactivate_vacation" +SERVICE_SET_ACTIVE_CLIMATE_PROFILE = "set_active_climate_profile" CONFIG_SCHEMA = vol.Schema( { @@ -86,6 +90,13 @@ SCHEMA_DEACTIVATE_VACATION = vol.Schema( {vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24))} ) +SCHEMA_SET_ACTIVE_CLIMATE_PROFILE = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): comp_entity_ids, + vol.Required(ATTR_CLIMATE_PROFILE_INDEX): cv.positive_int, + } +) + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the HomematicIP Cloud component.""" @@ -117,9 +128,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if home: await home.activate_absence_with_duration(duration) else: - for hapid in hass.data[DOMAIN]: - home = hass.data[DOMAIN][hapid].home - await home.activate_absence_with_duration(duration) + for hap in hass.data[DOMAIN].values(): + await hap.home.activate_absence_with_duration(duration) hass.services.async_register( DOMAIN, @@ -138,9 +148,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if home: await home.activate_absence_with_period(endtime) else: - for hapid in hass.data[DOMAIN]: - home = hass.data[DOMAIN][hapid].home - await home.activate_absence_with_period(endtime) + for hap in hass.data[DOMAIN].values(): + await hap.home.activate_absence_with_period(endtime) hass.services.async_register( DOMAIN, @@ -160,9 +169,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if home: await home.activate_vacation(endtime, temperature) else: - for hapid in hass.data[DOMAIN]: - home = hass.data[DOMAIN][hapid].home - await home.activate_vacation(endtime, temperature) + for hap in hass.data[DOMAIN].values(): + await hap.home.activate_vacation(endtime, temperature) hass.services.async_register( DOMAIN, @@ -180,9 +188,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if home: await home.deactivate_absence() else: - for hapid in hass.data[DOMAIN]: - home = hass.data[DOMAIN][hapid].home - await home.deactivate_absence() + for hap in hass.data[DOMAIN].values(): + await hap.home.deactivate_absence() hass.services.async_register( DOMAIN, @@ -200,9 +207,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if home: await home.deactivate_vacation() else: - for hapid in hass.data[DOMAIN]: - home = hass.data[DOMAIN][hapid].home - await home.deactivate_vacation() + for hap in hass.data[DOMAIN].values(): + await hap.home.deactivate_vacation() hass.services.async_register( DOMAIN, @@ -211,6 +217,29 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: schema=SCHEMA_DEACTIVATE_VACATION, ) + async def _set_active_climate_profile(service): + """Service to set the active climate profile.""" + entity_id_list = service.data[ATTR_ENTITY_ID] + climate_profile_index = service.data[ATTR_CLIMATE_PROFILE_INDEX] - 1 + + for hap in hass.data[DOMAIN].values(): + if entity_id_list != "all": + for entity_id in entity_id_list: + group = hap.hmip_device_by_entity_id.get(entity_id) + if group: + await group.set_active_profile(climate_profile_index) + else: + for group in hap.home.groups: + if isinstance(group, AsyncHeatingGroup): + await group.set_active_profile(climate_profile_index) + + hass.services.async_register( + DOMAIN, + SERVICE_SET_ACTIVE_CLIMATE_PROFILE, + _set_active_climate_profile, + schema=SCHEMA_SET_ACTIVE_CLIMATE_PROFILE, + ) + def _get_home(hapid: str): """Return a HmIP home.""" hap = hass.data[DOMAIN].get(hapid) diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index b8c055dda1f..f1f414169f6 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -4,7 +4,7 @@ from typing import Awaitable from homematicip.aio.device import AsyncHeatingThermostat, AsyncHeatingThermostatCompact from homematicip.aio.group import AsyncHeatingGroup -from homematicip.base.enums import AbsenceType +from homematicip.base.enums import AbsenceType, GroupType from homematicip.functionalHomes import IndoorClimateHome from homeassistant.components.climate import ClimateDevice @@ -25,6 +25,9 @@ from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice from .hap import HomematicipHAP +HEATING_PROFILES = {"PROFILE_1": 0, "PROFILE_2": 1, "PROFILE_3": 2} +COOLING_PROFILES = {"PROFILE_4": 3, "PROFILE_5": 4, "PROFILE_6": 5} + _LOGGER = logging.getLogger(__name__) HMIP_AUTOMATIC_CM = "AUTOMATIC" @@ -54,7 +57,7 @@ async def async_setup_entry( class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): """Representation of a HomematicIP heating group.""" - def __init__(self, hap: HomematicipHAP, device) -> None: + def __init__(self, hap: HomematicipHAP, device: AsyncHeatingGroup) -> None: """Initialize heating group.""" device.modelType = "HmIP-Heating-Group" self._simple_heating = None @@ -107,7 +110,7 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): Need to be one of HVAC_MODE_*. """ if self._device.boostMode: - return HVAC_MODE_AUTO + return HVAC_MODE_HEAT if self._device.controlMode == HMIP_MANUAL_CM: return HVAC_MODE_HEAT @@ -129,6 +132,8 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): """ if self._device.boostMode: return PRESET_BOOST + if self.hvac_mode == HVAC_MODE_HEAT: + return PRESET_NONE if self._device.controlMode == HMIP_ECO_CM: absence_type = self._home.get_functionalHome(IndoorClimateHome).absenceType if absence_type == AbsenceType.VACATION: @@ -140,15 +145,15 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): ]: return PRESET_ECO - return PRESET_NONE + if self._device.activeProfile: + return self._device.activeProfile.name @property def preset_modes(self): - """Return a list of available preset modes. - - Requires SUPPORT_PRESET_MODE. - """ - return [PRESET_NONE, PRESET_BOOST] + """Return a list of available preset modes incl profiles.""" + presets = [PRESET_NONE, PRESET_BOOST] + presets.extend(self._device_profile_names) + return presets @property def min_temp(self) -> float: @@ -180,6 +185,46 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): await self._device.set_boost(False) if preset_mode == PRESET_BOOST: await self._device.set_boost() + if preset_mode in self._device_profile_names: + profile_idx = self._get_profile_idx_by_name(preset_mode) + await self.async_set_hvac_mode(HVAC_MODE_AUTO) + await self._device.set_active_profile(profile_idx) + + @property + def _device_profiles(self): + """Return the relevant profiles of the device.""" + return [ + profile + for profile in self._device.profiles + if profile.visible + and profile.name != "" + and profile.index in self._relevant_profile_group + ] + + @property + def _device_profile_names(self): + """Return a collection of profile names.""" + return [profile.name for profile in self._device_profiles] + + def _get_profile_idx_by_name(self, profile_name): + """Return a profile index by name.""" + relevant_index = self._relevant_profile_group + index_name = [ + profile.index + for profile in self._device_profiles + if profile.name == profile_name + ] + + return relevant_index[index_name[0]] + + @property + def _relevant_profile_group(self): + """Return the relevant profile groups.""" + return ( + HEATING_PROFILES + if self._device.groupType == GroupType.HEATING + else COOLING_PROFILES + ) def _get_first_heating_thermostat(heating_group: AsyncHeatingGroup): diff --git a/homeassistant/components/homematicip_cloud/services.yaml b/homeassistant/components/homematicip_cloud/services.yaml index cf93b3065ee..f426c9b5d22 100644 --- a/homeassistant/components/homematicip_cloud/services.yaml +++ b/homeassistant/components/homematicip_cloud/services.yaml @@ -7,7 +7,7 @@ activate_eco_mode_with_duration: description: The duration of eco mode in minutes. example: 60 accesspoint_id: - description: The ID of the Homematic IP Access Point + description: The ID of the Homematic IP Access Point (optional) example: 3014xxxxxxxxxxxxxxxxxxxx activate_eco_mode_with_period: @@ -17,7 +17,7 @@ activate_eco_mode_with_period: description: The time when the eco mode should automatically be disabled. example: 2019-02-17 14:00 accesspoint_id: - description: The ID of the Homematic IP Access Point + description: The ID of the Homematic IP Access Point (optional) example: 3014xxxxxxxxxxxxxxxxxxxx activate_vacation: @@ -30,20 +30,31 @@ activate_vacation: description: the set temperature during the vacation mode. example: 18.5 accesspoint_id: - description: The ID of the Homematic IP Access Point + description: The ID of the Homematic IP Access Point (optional) example: 3014xxxxxxxxxxxxxxxxxxxx deactivate_eco_mode: description: Deactivates the eco mode immediately. fields: accesspoint_id: - description: The ID of the Homematic IP Access Point + description: The ID of the Homematic IP Access Point (optional) example: 3014xxxxxxxxxxxxxxxxxxxx deactivate_vacation: description: Deactivates the vacation mode immediately. fields: accesspoint_id: - description: The ID of the Homematic IP Access Point + description: The ID of the Homematic IP Access Point (optional) example: 3014xxxxxxxxxxxxxxxxxxxx +set_active_climate_profile: + description: Set the active climate profile index. + fields: + entity_id: + description: The ID of the climte entity. Use 'all' keyword to switch the profile for all entities. + example: climate.livingroom + climate_profile_index: + description: The index of the climate profile (1 based) + example: 1 + + diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index bdfd26319e6..80e4e74e451 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -49,8 +49,13 @@ async def test_hmip_heating_group(hass, default_mock_hap): assert ha_state.attributes["max_temp"] == 30.0 assert ha_state.attributes["temperature"] == 5.0 assert ha_state.attributes["current_humidity"] == 47 - assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_NONE - assert ha_state.attributes[ATTR_PRESET_MODES] == [PRESET_NONE, PRESET_BOOST] + assert ha_state.attributes[ATTR_PRESET_MODE] == "STD" + assert ha_state.attributes[ATTR_PRESET_MODES] == [ + PRESET_NONE, + PRESET_BOOST, + "STD", + "Winter", + ] service_call_counter = len(hmip_device.mock_calls) @@ -117,7 +122,7 @@ async def test_hmip_heating_group(hass, default_mock_hap): assert hmip_device.mock_calls[-1][1] == (False,) await async_manipulate_test_data(hass, hmip_device, "boostMode", False) ha_state = hass.states.get(entity_id) - assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_NONE + assert ha_state.attributes[ATTR_PRESET_MODE] == "STD" # Not required for hmip, but a posiblity to send no temperature. await hass.services.async_call( @@ -153,6 +158,18 @@ async def test_hmip_heating_group(hass, default_mock_hap): ha_state = hass.states.get(entity_id) assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_ECO + # Not required for hmip, but a posiblity to send no temperature. + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": entity_id, "preset_mode": "Winter"}, + blocking=True, + ) + + assert len(hmip_device.mock_calls) == service_call_counter + 16 + assert hmip_device.mock_calls[-1][0] == "set_active_profile" + assert hmip_device.mock_calls[-1][1] == (1,) + async def test_hmip_climate_services(hass, mock_hap_with_service): """Test HomematicipHeatingGroup.""" @@ -264,3 +281,35 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): assert home.mock_calls[-1][1] == () # There is no further call on connection. assert len(home._connection.mock_calls) == 10 # pylint: disable=W0212 + + +async def test_hmip_heating_group_services(hass, mock_hap_with_service): + """Test HomematicipHeatingGroup services.""" + entity_id = "climate.badezimmer" + entity_name = "Badezimmer" + device_model = None + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap_with_service, entity_id, entity_name, device_model + ) + assert ha_state + + await hass.services.async_call( + "homematicip_cloud", + "set_active_climate_profile", + {"climate_profile_index": 2, "entity_id": "climate.badezimmer"}, + blocking=True, + ) + assert hmip_device.mock_calls[-1][0] == "set_active_profile" + assert hmip_device.mock_calls[-1][1] == (1,) + assert len(hmip_device._connection.mock_calls) == 2 # pylint: disable=W0212 + + await hass.services.async_call( + "homematicip_cloud", + "set_active_climate_profile", + {"climate_profile_index": 2, "entity_id": "all"}, + blocking=True, + ) + assert hmip_device.mock_calls[-1][0] == "set_active_profile" + assert hmip_device.mock_calls[-1][1] == (1,) + assert len(hmip_device._connection.mock_calls) == 12 # pylint: disable=W0212 diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json index 1d3d5bfd8f4..e17df9c2039 100644 --- a/tests/fixtures/homematicip_cloud.json +++ b/tests/fixtures/homematicip_cloud.json @@ -4638,7 +4638,7 @@ "enabled": true, "groupId": "00000000-0000-0000-0000-000000000021", "index": "PROFILE_1", - "name": "", + "name": "STD", "profileId": "00000000-0000-0000-0000-000000000038", "visible": true }, @@ -4646,9 +4646,9 @@ "enabled": true, "groupId": "00000000-0000-0000-0000-000000000021", "index": "PROFILE_2", - "name": "", + "name": "Winter", "profileId": "00000000-0000-0000-0000-000000000039", - "visible": false + "visible": true }, "PROFILE_3": { "enabled": true,