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
This commit is contained in:
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.""" """Support for HomematicIP Cloud devices."""
import logging import logging
from homematicip.aio.group import AsyncHeatingGroup
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry 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.core import HomeAssistant
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import comp_entity_ids
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .config_flow import configured_haps from .config_flow import configured_haps
@ -25,6 +27,7 @@ from .hap import HomematicipAuth, HomematicipHAP # noqa: F401
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_CLIMATE_PROFILE_INDEX = "climate_profile_index"
ATTR_DURATION = "duration" ATTR_DURATION = "duration"
ATTR_ENDTIME = "endtime" ATTR_ENDTIME = "endtime"
ATTR_TEMPERATURE = "temperature" ATTR_TEMPERATURE = "temperature"
@ -35,6 +38,7 @@ SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD = "activate_eco_mode_with_period"
SERVICE_ACTIVATE_VACATION = "activate_vacation" SERVICE_ACTIVATE_VACATION = "activate_vacation"
SERVICE_DEACTIVATE_ECO_MODE = "deactivate_eco_mode" SERVICE_DEACTIVATE_ECO_MODE = "deactivate_eco_mode"
SERVICE_DEACTIVATE_VACATION = "deactivate_vacation" SERVICE_DEACTIVATE_VACATION = "deactivate_vacation"
SERVICE_SET_ACTIVE_CLIMATE_PROFILE = "set_active_climate_profile"
CONFIG_SCHEMA = vol.Schema( 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))} {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: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the HomematicIP Cloud component.""" """Set up the HomematicIP Cloud component."""
@ -117,9 +128,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if home: if home:
await home.activate_absence_with_duration(duration) await home.activate_absence_with_duration(duration)
else: else:
for hapid in hass.data[DOMAIN]: for hap in hass.data[DOMAIN].values():
home = hass.data[DOMAIN][hapid].home await hap.home.activate_absence_with_duration(duration)
await home.activate_absence_with_duration(duration)
hass.services.async_register( hass.services.async_register(
DOMAIN, DOMAIN,
@ -138,9 +148,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if home: if home:
await home.activate_absence_with_period(endtime) await home.activate_absence_with_period(endtime)
else: else:
for hapid in hass.data[DOMAIN]: for hap in hass.data[DOMAIN].values():
home = hass.data[DOMAIN][hapid].home await hap.home.activate_absence_with_period(endtime)
await home.activate_absence_with_period(endtime)
hass.services.async_register( hass.services.async_register(
DOMAIN, DOMAIN,
@ -160,9 +169,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if home: if home:
await home.activate_vacation(endtime, temperature) await home.activate_vacation(endtime, temperature)
else: else:
for hapid in hass.data[DOMAIN]: for hap in hass.data[DOMAIN].values():
home = hass.data[DOMAIN][hapid].home await hap.home.activate_vacation(endtime, temperature)
await home.activate_vacation(endtime, temperature)
hass.services.async_register( hass.services.async_register(
DOMAIN, DOMAIN,
@ -180,9 +188,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if home: if home:
await home.deactivate_absence() await home.deactivate_absence()
else: else:
for hapid in hass.data[DOMAIN]: for hap in hass.data[DOMAIN].values():
home = hass.data[DOMAIN][hapid].home await hap.home.deactivate_absence()
await home.deactivate_absence()
hass.services.async_register( hass.services.async_register(
DOMAIN, DOMAIN,
@ -200,9 +207,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if home: if home:
await home.deactivate_vacation() await home.deactivate_vacation()
else: else:
for hapid in hass.data[DOMAIN]: for hap in hass.data[DOMAIN].values():
home = hass.data[DOMAIN][hapid].home await hap.home.deactivate_vacation()
await home.deactivate_vacation()
hass.services.async_register( hass.services.async_register(
DOMAIN, DOMAIN,
@ -211,6 +217,29 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
schema=SCHEMA_DEACTIVATE_VACATION, 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): def _get_home(hapid: str):
"""Return a HmIP home.""" """Return a HmIP home."""
hap = hass.data[DOMAIN].get(hapid) 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.device import AsyncHeatingThermostat, AsyncHeatingThermostatCompact
from homematicip.aio.group import AsyncHeatingGroup 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 homematicip.functionalHomes import IndoorClimateHome
from homeassistant.components.climate import ClimateDevice 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 . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice
from .hap import HomematicipHAP 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__) _LOGGER = logging.getLogger(__name__)
HMIP_AUTOMATIC_CM = "AUTOMATIC" HMIP_AUTOMATIC_CM = "AUTOMATIC"
@ -54,7 +57,7 @@ async def async_setup_entry(
class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice):
"""Representation of a HomematicIP heating group.""" """Representation of a HomematicIP heating group."""
def __init__(self, hap: HomematicipHAP, device) -> None: def __init__(self, hap: HomematicipHAP, device: AsyncHeatingGroup) -> None:
"""Initialize heating group.""" """Initialize heating group."""
device.modelType = "HmIP-Heating-Group" device.modelType = "HmIP-Heating-Group"
self._simple_heating = None self._simple_heating = None
@ -107,7 +110,7 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice):
Need to be one of HVAC_MODE_*. Need to be one of HVAC_MODE_*.
""" """
if self._device.boostMode: if self._device.boostMode:
return HVAC_MODE_AUTO return HVAC_MODE_HEAT
if self._device.controlMode == HMIP_MANUAL_CM: if self._device.controlMode == HMIP_MANUAL_CM:
return HVAC_MODE_HEAT return HVAC_MODE_HEAT
@ -129,6 +132,8 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice):
""" """
if self._device.boostMode: if self._device.boostMode:
return PRESET_BOOST return PRESET_BOOST
if self.hvac_mode == HVAC_MODE_HEAT:
return PRESET_NONE
if self._device.controlMode == HMIP_ECO_CM: if self._device.controlMode == HMIP_ECO_CM:
absence_type = self._home.get_functionalHome(IndoorClimateHome).absenceType absence_type = self._home.get_functionalHome(IndoorClimateHome).absenceType
if absence_type == AbsenceType.VACATION: if absence_type == AbsenceType.VACATION:
@ -140,15 +145,15 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice):
]: ]:
return PRESET_ECO return PRESET_ECO
return PRESET_NONE if self._device.activeProfile:
return self._device.activeProfile.name
@property @property
def preset_modes(self): def preset_modes(self):
"""Return a list of available preset modes. """Return a list of available preset modes incl profiles."""
presets = [PRESET_NONE, PRESET_BOOST]
Requires SUPPORT_PRESET_MODE. presets.extend(self._device_profile_names)
""" return presets
return [PRESET_NONE, PRESET_BOOST]
@property @property
def min_temp(self) -> float: def min_temp(self) -> float:
@ -180,6 +185,46 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice):
await self._device.set_boost(False) await self._device.set_boost(False)
if preset_mode == PRESET_BOOST: if preset_mode == PRESET_BOOST:
await self._device.set_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): 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. description: The duration of eco mode in minutes.
example: 60 example: 60
accesspoint_id: accesspoint_id:
description: The ID of the Homematic IP Access Point description: The ID of the Homematic IP Access Point (optional)
example: 3014xxxxxxxxxxxxxxxxxxxx example: 3014xxxxxxxxxxxxxxxxxxxx
activate_eco_mode_with_period: 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. description: The time when the eco mode should automatically be disabled.
example: 2019-02-17 14:00 example: 2019-02-17 14:00
accesspoint_id: accesspoint_id:
description: The ID of the Homematic IP Access Point description: The ID of the Homematic IP Access Point (optional)
example: 3014xxxxxxxxxxxxxxxxxxxx example: 3014xxxxxxxxxxxxxxxxxxxx
activate_vacation: activate_vacation:
@ -30,20 +30,31 @@ activate_vacation:
description: the set temperature during the vacation mode. description: the set temperature during the vacation mode.
example: 18.5 example: 18.5
accesspoint_id: accesspoint_id:
description: The ID of the Homematic IP Access Point description: The ID of the Homematic IP Access Point (optional)
example: 3014xxxxxxxxxxxxxxxxxxxx example: 3014xxxxxxxxxxxxxxxxxxxx
deactivate_eco_mode: deactivate_eco_mode:
description: Deactivates the eco mode immediately. description: Deactivates the eco mode immediately.
fields: fields:
accesspoint_id: accesspoint_id:
description: The ID of the Homematic IP Access Point description: The ID of the Homematic IP Access Point (optional)
example: 3014xxxxxxxxxxxxxxxxxxxx example: 3014xxxxxxxxxxxxxxxxxxxx
deactivate_vacation: deactivate_vacation:
description: Deactivates the vacation mode immediately. description: Deactivates the vacation mode immediately.
fields: fields:
accesspoint_id: accesspoint_id:
description: The ID of the Homematic IP Access Point description: The ID of the Homematic IP Access Point (optional)
example: 3014xxxxxxxxxxxxxxxxxxxx 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["max_temp"] == 30.0
assert ha_state.attributes["temperature"] == 5.0 assert ha_state.attributes["temperature"] == 5.0
assert ha_state.attributes["current_humidity"] == 47 assert ha_state.attributes["current_humidity"] == 47
assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_NONE assert ha_state.attributes[ATTR_PRESET_MODE] == "STD"
assert ha_state.attributes[ATTR_PRESET_MODES] == [PRESET_NONE, PRESET_BOOST] assert ha_state.attributes[ATTR_PRESET_MODES] == [
PRESET_NONE,
PRESET_BOOST,
"STD",
"Winter",
]
service_call_counter = len(hmip_device.mock_calls) 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,) assert hmip_device.mock_calls[-1][1] == (False,)
await async_manipulate_test_data(hass, hmip_device, "boostMode", False) await async_manipulate_test_data(hass, hmip_device, "boostMode", False)
ha_state = hass.states.get(entity_id) 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. # Not required for hmip, but a posiblity to send no temperature.
await hass.services.async_call( 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) ha_state = hass.states.get(entity_id)
assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_ECO 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): async def test_hmip_climate_services(hass, mock_hap_with_service):
"""Test HomematicipHeatingGroup.""" """Test HomematicipHeatingGroup."""
@ -264,3 +281,35 @@ async def test_hmip_climate_services(hass, mock_hap_with_service):
assert home.mock_calls[-1][1] == () assert home.mock_calls[-1][1] == ()
# There is no further call on connection. # There is no further call on connection.
assert len(home._connection.mock_calls) == 10 # pylint: disable=W0212 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, "enabled": true,
"groupId": "00000000-0000-0000-0000-000000000021", "groupId": "00000000-0000-0000-0000-000000000021",
"index": "PROFILE_1", "index": "PROFILE_1",
"name": "", "name": "STD",
"profileId": "00000000-0000-0000-0000-000000000038", "profileId": "00000000-0000-0000-0000-000000000038",
"visible": true "visible": true
}, },
@ -4646,9 +4646,9 @@
"enabled": true, "enabled": true,
"groupId": "00000000-0000-0000-0000-000000000021", "groupId": "00000000-0000-0000-0000-000000000021",
"index": "PROFILE_2", "index": "PROFILE_2",
"name": "", "name": "Winter",
"profileId": "00000000-0000-0000-0000-000000000039", "profileId": "00000000-0000-0000-0000-000000000039",
"visible": false "visible": true
}, },
"PROFILE_3": { "PROFILE_3": {
"enabled": true, "enabled": true,