Add cool mode to HomematicIP climate (#28525)

* Add cool mode to HomematicIP climate

* Update test

* remove preset_party

* Fix profile_names check
This commit is contained in:
SukramJ 2019-11-07 16:41:33 +01:00 committed by Martin Hjelmare
parent 899306c8ec
commit 9d3d35ad79
3 changed files with 293 additions and 59 deletions

View File

@ -4,13 +4,15 @@ 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, GroupType from homematicip.base.enums import AbsenceType
from homematicip.functionalHomes import IndoorClimateHome from homematicip.functionalHomes import IndoorClimateHome
from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate import ClimateDevice
from homeassistant.components.climate.const import ( from homeassistant.components.climate.const import (
HVAC_MODE_AUTO, HVAC_MODE_AUTO,
HVAC_MODE_COOL,
HVAC_MODE_HEAT, HVAC_MODE_HEAT,
HVAC_MODE_OFF,
PRESET_AWAY, PRESET_AWAY,
PRESET_BOOST, PRESET_BOOST,
PRESET_ECO, PRESET_ECO,
@ -30,6 +32,9 @@ COOLING_PROFILES = {"PROFILE_4": 3, "PROFILE_5": 4, "PROFILE_6": 5}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_PRESET_END_TIME = "preset_end_time"
PERMANENT_END_TIME = "permanent"
HMIP_AUTOMATIC_CM = "AUTOMATIC" HMIP_AUTOMATIC_CM = "AUTOMATIC"
HMIP_MANUAL_CM = "MANUAL" HMIP_MANUAL_CM = "MANUAL"
HMIP_ECO_CM = "ECO" HMIP_ECO_CM = "ECO"
@ -55,15 +60,20 @@ 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.
Heat mode is supported for all heating devices incl. their defined profiles.
Boost is available for radiator thermostats only.
Cool mode is only available for floor heating systems, if basically enabled in the hmip app.
"""
def __init__(self, hap: HomematicipHAP, device: AsyncHeatingGroup) -> 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"
super().__init__(hap, device)
self._simple_heating = None self._simple_heating = None
if device.actualTemperature is None: if device.actualTemperature is None:
self._simple_heating = _get_first_heating_thermostat(device) self._simple_heating = self._get_first_radiator_thermostat()
super().__init__(hap, device)
@property @property
def device_info(self): def device_info(self):
@ -105,54 +115,66 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice):
@property @property
def hvac_mode(self) -> str: def hvac_mode(self) -> str:
"""Return hvac operation ie. heat, cool mode. """Return hvac operation ie."""
if self._disabled_by_cooling_mode:
Need to be one of HVAC_MODE_*. return HVAC_MODE_OFF
"""
if self._device.boostMode: if self._device.boostMode:
return HVAC_MODE_HEAT 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 if self._heat_mode_enabled else HVAC_MODE_COOL
return HVAC_MODE_AUTO return HVAC_MODE_AUTO
@property @property
def hvac_modes(self): def hvac_modes(self):
"""Return the list of available hvac operation modes. """Return the list of available hvac operation modes."""
if self._disabled_by_cooling_mode:
return [HVAC_MODE_OFF]
Need to be a subset of HVAC_MODES. return (
""" [HVAC_MODE_AUTO, HVAC_MODE_HEAT]
return [HVAC_MODE_AUTO, HVAC_MODE_HEAT] if self._heat_mode_enabled
else [HVAC_MODE_AUTO, HVAC_MODE_COOL]
)
@property @property
def preset_mode(self): def preset_mode(self):
"""Return the current preset mode, e.g., home, away, temp. """Return the current preset mode."""
Requires SUPPORT_PRESET_MODE.
"""
if self._device.boostMode: if self._device.boostMode:
return PRESET_BOOST return PRESET_BOOST
if self.hvac_mode == HVAC_MODE_HEAT: if self.hvac_mode in (HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF):
return PRESET_NONE 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 if self._indoor_climate.absenceType == AbsenceType.VACATION:
if absence_type == AbsenceType.VACATION:
return PRESET_AWAY return PRESET_AWAY
if absence_type in [ if self._indoor_climate.absenceType in [
AbsenceType.PARTY,
AbsenceType.PERIOD, AbsenceType.PERIOD,
AbsenceType.PERMANENT, AbsenceType.PERMANENT,
AbsenceType.PARTY,
]: ]:
return PRESET_ECO return PRESET_ECO
if self._device.activeProfile: return (
return self._device.activeProfile.name self._device.activeProfile.name
if self._device.activeProfile.name in self._device_profile_names
else None
)
@property @property
def preset_modes(self): def preset_modes(self):
"""Return a list of available preset modes incl profiles.""" """Return a list of available preset modes incl. hmip profiles."""
presets = [PRESET_NONE, PRESET_BOOST] # Boost is only available if a radiator thermostat is in the room,
presets.extend(self._device_profile_names) # and heat mode is enabled.
profile_names = self._device_profile_names
presets = []
if self._heat_mode_enabled and self._has_radiator_thermostat:
if not profile_names:
presets.append(PRESET_NONE)
presets.append(PRESET_BOOST)
presets.extend(profile_names)
return presets return presets
@property @property
@ -170,10 +192,15 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice):
temperature = kwargs.get(ATTR_TEMPERATURE) temperature = kwargs.get(ATTR_TEMPERATURE)
if temperature is None: if temperature is None:
return return
if self.min_temp <= temperature <= self.max_temp:
await self._device.set_point_temperature(temperature) await self._device.set_point_temperature(temperature)
async def async_set_hvac_mode(self, hvac_mode: str) -> Awaitable[None]: async def async_set_hvac_mode(self, hvac_mode: str) -> Awaitable[None]:
"""Set new target hvac mode.""" """Set new target hvac mode."""
if hvac_mode not in self.hvac_modes:
return
if hvac_mode == HVAC_MODE_AUTO: if hvac_mode == HVAC_MODE_AUTO:
await self._device.set_control_mode(HMIP_AUTOMATIC_CM) await self._device.set_control_mode(HMIP_AUTOMATIC_CM)
else: else:
@ -181,18 +208,44 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice):
async def async_set_preset_mode(self, preset_mode: str) -> Awaitable[None]: async def async_set_preset_mode(self, preset_mode: str) -> Awaitable[None]:
"""Set new preset mode.""" """Set new preset mode."""
if preset_mode not in self.preset_modes:
return
if self._device.boostMode and preset_mode != PRESET_BOOST: if self._device.boostMode and preset_mode != PRESET_BOOST:
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: if preset_mode in self._device_profile_names:
profile_idx = self._get_profile_idx_by_name(preset_mode) profile_idx = self._get_profile_idx_by_name(preset_mode)
if self._device.controlMode != HMIP_AUTOMATIC_CM:
await self.async_set_hvac_mode(HVAC_MODE_AUTO) await self.async_set_hvac_mode(HVAC_MODE_AUTO)
await self._device.set_active_profile(profile_idx) await self._device.set_active_profile(profile_idx)
@property
def device_state_attributes(self):
"""Return the state attributes of the access point."""
state_attr = super().device_state_attributes
if self._device.controlMode == HMIP_ECO_CM:
if self._indoor_climate.absenceType in [
AbsenceType.PARTY,
AbsenceType.PERIOD,
AbsenceType.VACATION,
]:
state_attr[ATTR_PRESET_END_TIME] = self._indoor_climate.absenceEndTime
elif self._indoor_climate.absenceType == AbsenceType.PERMANENT:
state_attr[ATTR_PRESET_END_TIME] = PERMANENT_END_TIME
return state_attr
@property
def _indoor_climate(self):
"""Return the hmip indoor climate functional home of this group."""
return self._home.get_functionalHome(IndoorClimateHome)
@property @property
def _device_profiles(self): def _device_profiles(self):
"""Return the relevant profiles of the device.""" """Return the relevant profiles."""
return [ return [
profile profile
for profile in self._device.profiles for profile in self._device.profiles
@ -218,17 +271,36 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice):
return relevant_index[index_name[0]] return relevant_index[index_name[0]]
@property @property
def _relevant_profile_group(self): def _heat_mode_enabled(self):
"""Return the relevant profile groups.""" """Return, if heating mode is enabled."""
return ( return not self._device.cooling
HEATING_PROFILES
if self._device.groupType == GroupType.HEATING @property
else COOLING_PROFILES def _disabled_by_cooling_mode(self):
"""Return, if group is disabled by the cooling mode."""
return self._device.cooling and (
self._device.coolingIgnored or not self._device.coolingAllowed
) )
@property
def _relevant_profile_group(self):
"""Return the relevant profile groups."""
if self._disabled_by_cooling_mode:
return []
def _get_first_heating_thermostat(heating_group: AsyncHeatingGroup): return HEATING_PROFILES if self._heat_mode_enabled else COOLING_PROFILES
"""Return the first HeatingThermostat from a HeatingGroup."""
for device in heating_group.devices: @property
if isinstance(device, (AsyncHeatingThermostat, AsyncHeatingThermostatCompact)): def _has_radiator_thermostat(self) -> bool:
"""Return, if a radiator thermostat is in the hmip heating group."""
return bool(self._get_first_radiator_thermostat())
def _get_first_radiator_thermostat(self):
"""Return the first radiator thermostat from the hmip heating group."""
for device in self._device.devices:
if isinstance(
device, (AsyncHeatingThermostat, AsyncHeatingThermostatCompact)
):
return device return device
return None

View File

@ -10,13 +10,18 @@ from homeassistant.components.climate.const import (
ATTR_PRESET_MODE, ATTR_PRESET_MODE,
ATTR_PRESET_MODES, ATTR_PRESET_MODES,
HVAC_MODE_AUTO, HVAC_MODE_AUTO,
HVAC_MODE_COOL,
HVAC_MODE_HEAT, HVAC_MODE_HEAT,
HVAC_MODE_OFF,
PRESET_AWAY, PRESET_AWAY,
PRESET_BOOST, PRESET_BOOST,
PRESET_ECO, PRESET_ECO,
PRESET_NONE,
) )
from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN
from homeassistant.components.homematicip_cloud.climate import (
ATTR_PRESET_END_TIME,
PERMANENT_END_TIME,
)
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from .helper import HAPID, async_manipulate_test_data, get_and_check_entity_basics from .helper import HAPID, async_manipulate_test_data, get_and_check_entity_basics
@ -33,7 +38,7 @@ async def test_manually_configured_platform(hass):
assert not hass.data.get(HMIPC_DOMAIN) assert not hass.data.get(HMIPC_DOMAIN)
async def test_hmip_heating_group(hass, default_mock_hap): async def test_hmip_heating_group_heat(hass, default_mock_hap):
"""Test HomematicipHeatingGroup.""" """Test HomematicipHeatingGroup."""
entity_id = "climate.badezimmer" entity_id = "climate.badezimmer"
entity_name = "Badezimmer" entity_name = "Badezimmer"
@ -50,12 +55,7 @@ async def test_hmip_heating_group(hass, default_mock_hap):
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] == "STD" assert ha_state.attributes[ATTR_PRESET_MODE] == "STD"
assert ha_state.attributes[ATTR_PRESET_MODES] == [ assert ha_state.attributes[ATTR_PRESET_MODES] == [PRESET_BOOST, "STD", "Winter"]
PRESET_NONE,
PRESET_BOOST,
"STD",
"Winter",
]
service_call_counter = len(hmip_device.mock_calls) service_call_counter = len(hmip_device.mock_calls)
@ -114,12 +114,12 @@ async def test_hmip_heating_group(hass, default_mock_hap):
await hass.services.async_call( await hass.services.async_call(
"climate", "climate",
"set_preset_mode", "set_preset_mode",
{"entity_id": entity_id, "preset_mode": PRESET_NONE}, {"entity_id": entity_id, "preset_mode": "STD"},
blocking=True, blocking=True,
) )
assert len(hmip_device.mock_calls) == service_call_counter + 9 assert len(hmip_device.mock_calls) == service_call_counter + 11
assert hmip_device.mock_calls[-1][0] == "set_boost" assert hmip_device.mock_calls[-1][0] == "set_active_profile"
assert hmip_device.mock_calls[-1][1] == (False,) assert hmip_device.mock_calls[-1][1] == (0,)
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] == "STD" assert ha_state.attributes[ATTR_PRESET_MODE] == "STD"
@ -132,7 +132,7 @@ async def test_hmip_heating_group(hass, default_mock_hap):
blocking=True, blocking=True,
) )
# No new service call should be in mock_calls. # No new service call should be in mock_calls.
assert len(hmip_device.mock_calls) == service_call_counter + 10 assert len(hmip_device.mock_calls) == service_call_counter + 12
# Only fire event from last async_manipulate_test_data available. # Only fire event from last async_manipulate_test_data available.
assert hmip_device.mock_calls[-1][0] == "fire_update_event" assert hmip_device.mock_calls[-1][0] == "fire_update_event"
@ -158,7 +158,6 @@ 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( await hass.services.async_call(
"climate", "climate",
"set_preset_mode", "set_preset_mode",
@ -166,10 +165,173 @@ async def test_hmip_heating_group(hass, default_mock_hap):
blocking=True, blocking=True,
) )
assert len(hmip_device.mock_calls) == service_call_counter + 16 assert len(hmip_device.mock_calls) == service_call_counter + 18
assert hmip_device.mock_calls[-1][0] == "set_active_profile" assert hmip_device.mock_calls[-1][0] == "set_active_profile"
assert hmip_device.mock_calls[-1][1] == (1,) assert hmip_device.mock_calls[-1][1] == (1,)
default_mock_hap.home.get_functionalHome(
IndoorClimateHome
).absenceType = AbsenceType.PERMANENT
await async_manipulate_test_data(hass, hmip_device, "controlMode", "ECO")
ha_state = hass.states.get(entity_id)
assert ha_state.attributes[ATTR_PRESET_END_TIME] == PERMANENT_END_TIME
await hass.services.async_call(
"climate",
"set_hvac_mode",
{"entity_id": entity_id, "hvac_mode": HVAC_MODE_HEAT},
blocking=True,
)
assert len(hmip_device.mock_calls) == service_call_counter + 20
assert hmip_device.mock_calls[-1][0] == "set_control_mode"
assert hmip_device.mock_calls[-1][1] == ("MANUAL",)
await async_manipulate_test_data(hass, hmip_device, "controlMode", "MANUAL")
ha_state = hass.states.get(entity_id)
assert ha_state.state == HVAC_MODE_HEAT
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 + 23
assert hmip_device.mock_calls[-1][0] == "set_active_profile"
assert hmip_device.mock_calls[-1][1] == (1,)
hmip_device.activeProfile = hmip_device.profiles[0]
await async_manipulate_test_data(hass, hmip_device, "controlMode", "AUTOMATIC")
ha_state = hass.states.get(entity_id)
assert ha_state.state == HVAC_MODE_AUTO
await hass.services.async_call(
"climate",
"set_hvac_mode",
{"entity_id": entity_id, "hvac_mode": "dry"},
blocking=True,
)
assert len(hmip_device.mock_calls) == service_call_counter + 24
# Only fire event from last async_manipulate_test_data available.
assert hmip_device.mock_calls[-1][0] == "fire_update_event"
async def test_hmip_heating_group_cool(hass, default_mock_hap):
"""Test HomematicipHeatingGroup."""
entity_id = "climate.badezimmer"
entity_name = "Badezimmer"
device_model = None
ha_state, hmip_device = get_and_check_entity_basics(
hass, default_mock_hap, entity_id, entity_name, device_model
)
hmip_device.activeProfile = hmip_device.profiles[3]
await async_manipulate_test_data(hass, hmip_device, "cooling", True)
await async_manipulate_test_data(hass, hmip_device, "coolingAllowed", True)
await async_manipulate_test_data(hass, hmip_device, "coolingIgnored", False)
ha_state = hass.states.get(entity_id)
assert ha_state.state == HVAC_MODE_AUTO
assert ha_state.attributes["current_temperature"] == 23.8
assert ha_state.attributes["min_temp"] == 5.0
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] == "Cool1"
assert ha_state.attributes[ATTR_PRESET_MODES] == ["Cool1", "Cool2"]
service_call_counter = len(hmip_device.mock_calls)
await hass.services.async_call(
"climate",
"set_hvac_mode",
{"entity_id": entity_id, "hvac_mode": HVAC_MODE_COOL},
blocking=True,
)
assert len(hmip_device.mock_calls) == service_call_counter + 1
assert hmip_device.mock_calls[-1][0] == "set_control_mode"
assert hmip_device.mock_calls[-1][1] == ("MANUAL",)
await async_manipulate_test_data(hass, hmip_device, "controlMode", "MANUAL")
ha_state = hass.states.get(entity_id)
assert ha_state.state == HVAC_MODE_COOL
await hass.services.async_call(
"climate",
"set_hvac_mode",
{"entity_id": entity_id, "hvac_mode": HVAC_MODE_AUTO},
blocking=True,
)
assert len(hmip_device.mock_calls) == service_call_counter + 3
assert hmip_device.mock_calls[-1][0] == "set_control_mode"
assert hmip_device.mock_calls[-1][1] == ("AUTOMATIC",)
await async_manipulate_test_data(hass, hmip_device, "controlMode", "AUTO")
ha_state = hass.states.get(entity_id)
assert ha_state.state == HVAC_MODE_AUTO
await hass.services.async_call(
"climate",
"set_preset_mode",
{"entity_id": entity_id, "preset_mode": "Cool2"},
blocking=True,
)
assert len(hmip_device.mock_calls) == service_call_counter + 6
assert hmip_device.mock_calls[-1][0] == "set_active_profile"
assert hmip_device.mock_calls[-1][1] == (4,)
hmip_device.activeProfile = hmip_device.profiles[4]
await async_manipulate_test_data(hass, hmip_device, "cooling", True)
await async_manipulate_test_data(hass, hmip_device, "coolingAllowed", False)
await async_manipulate_test_data(hass, hmip_device, "coolingIgnored", False)
ha_state = hass.states.get(entity_id)
assert ha_state.state == HVAC_MODE_OFF
assert ha_state.attributes[ATTR_PRESET_MODE] == "none"
assert ha_state.attributes[ATTR_PRESET_MODES] == []
hmip_device.activeProfile = hmip_device.profiles[4]
await async_manipulate_test_data(hass, hmip_device, "cooling", True)
await async_manipulate_test_data(hass, hmip_device, "coolingAllowed", True)
await async_manipulate_test_data(hass, hmip_device, "coolingIgnored", True)
ha_state = hass.states.get(entity_id)
assert ha_state.state == HVAC_MODE_OFF
assert ha_state.attributes[ATTR_PRESET_MODE] == "none"
assert ha_state.attributes[ATTR_PRESET_MODES] == []
await hass.services.async_call(
"climate",
"set_preset_mode",
{"entity_id": entity_id, "preset_mode": "Cool2"},
blocking=True,
)
assert len(hmip_device.mock_calls) == service_call_counter + 12
# fire_update_event shows that set_active_profile has not been called.
assert hmip_device.mock_calls[-1][0] == "fire_update_event"
hmip_device.activeProfile = hmip_device.profiles[4]
await async_manipulate_test_data(hass, hmip_device, "cooling", True)
await async_manipulate_test_data(hass, hmip_device, "coolingAllowed", True)
await async_manipulate_test_data(hass, hmip_device, "coolingIgnored", False)
ha_state = hass.states.get(entity_id)
assert ha_state.state == HVAC_MODE_AUTO
assert ha_state.attributes[ATTR_PRESET_MODE] == "Cool2"
assert ha_state.attributes[ATTR_PRESET_MODES] == ["Cool1", "Cool2"]
await hass.services.async_call(
"climate",
"set_preset_mode",
{"entity_id": entity_id, "preset_mode": "Cool2"},
blocking=True,
)
assert len(hmip_device.mock_calls) == service_call_counter + 17
assert hmip_device.mock_calls[-1][0] == "set_active_profile"
assert hmip_device.mock_calls[-1][1] == (4,)
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."""

View File

@ -4662,7 +4662,7 @@
"enabled": true, "enabled": true,
"groupId": "00000000-0000-0000-0000-000000000021", "groupId": "00000000-0000-0000-0000-000000000021",
"index": "PROFILE_4", "index": "PROFILE_4",
"name": "", "name": "Cool1",
"profileId": "00000000-0000-0000-0000-000000000041", "profileId": "00000000-0000-0000-0000-000000000041",
"visible": true "visible": true
}, },
@ -4670,9 +4670,9 @@
"enabled": true, "enabled": true,
"groupId": "00000000-0000-0000-0000-000000000021", "groupId": "00000000-0000-0000-0000-000000000021",
"index": "PROFILE_5", "index": "PROFILE_5",
"name": "", "name": "Cool2",
"profileId": "00000000-0000-0000-0000-000000000042", "profileId": "00000000-0000-0000-0000-000000000042",
"visible": false "visible": true
}, },
"PROFILE_6": { "PROFILE_6": {
"enabled": true, "enabled": true,