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 reviewpull/27930/head
parent
f2617fd74a
commit
eb48898687
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue