Add climate profiles to Homematic IP Cloud (#27772)

* Add climate service to Homematic IP Cloud to select the active profile

* Add  profiles ass presets

* fix spelling

* Re-Add PRESET_NONE for selection

* Boost is a manual mode

* Fixes based on review

* Fixes after review
pull/27930/head
SukramJ 2019-10-19 17:44:40 +02:00 committed by Martin Hjelmare
parent f2617fd74a
commit eb48898687
5 changed files with 170 additions and 36 deletions

View File

@ -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)

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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,