diff --git a/homeassistant/components/homematicip_cloud/const.py b/homeassistant/components/homematicip_cloud/const.py index 5c48de975f9..4fb21febb40 100644 --- a/homeassistant/components/homematicip_cloud/const.py +++ b/homeassistant/components/homematicip_cloud/const.py @@ -1,19 +1,30 @@ """Constants for the HomematicIP Cloud component.""" import logging +from homeassistant.components.alarm_control_panel import ( + DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, +) +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN +from homeassistant.components.cover import DOMAIN as COVER_DOMAIN +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN + _LOGGER = logging.getLogger(".") DOMAIN = "homematicip_cloud" COMPONENTS = [ - "alarm_control_panel", - "binary_sensor", - "climate", - "cover", - "light", - "sensor", - "switch", - "weather", + ALARM_CONTROL_PANEL_DOMAIN, + BINARY_SENSOR_DOMAIN, + CLIMATE_DOMAIN, + COVER_DOMAIN, + LIGHT_DOMAIN, + SENSOR_DOMAIN, + SWITCH_DOMAIN, + WEATHER_DOMAIN, ] CONF_ACCESSPOINT = "accesspoint" diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index 60d3867d05a..3d5af9b7c4d 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -2,6 +2,7 @@ from typing import Optional from homematicip.aio.device import ( + AsyncBlindModule, AsyncFullFlushBlind, AsyncFullFlushShutter, AsyncGarageDoorModuleTormatic, @@ -34,7 +35,9 @@ async def async_setup_entry( hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] entities = [] for device in hap.home.devices: - if isinstance(device, AsyncFullFlushBlind): + if isinstance(device, AsyncBlindModule): + entities.append(HomematicipBlindModule(hap, device)) + elif isinstance(device, AsyncFullFlushBlind): entities.append(HomematicipCoverSlats(hap, device)) elif isinstance(device, AsyncFullFlushShutter): entities.append(HomematicipCoverShutter(hap, device)) @@ -51,6 +54,82 @@ async def async_setup_entry( async_add_entities(entities) +class HomematicipBlindModule(HomematicipGenericEntity, CoverEntity): + """Representation of the HomematicIP blind module.""" + + @property + def current_cover_position(self) -> int: + """Return current position of cover.""" + if self._device.primaryShadingLevel is not None: + return int((1 - self._device.primaryShadingLevel) * 100) + return None + + @property + def current_cover_tilt_position(self) -> int: + """Return current tilt position of cover.""" + if self._device.secondaryShadingLevel is not None: + return int((1 - self._device.secondaryShadingLevel) * 100) + return None + + async def async_set_cover_position(self, **kwargs) -> None: + """Move the cover to a specific position.""" + position = kwargs[ATTR_POSITION] + # HmIP cover is closed:1 -> open:0 + level = 1 - position / 100.0 + await self._device.set_primary_shading_level(primaryShadingLevel=level) + + async def async_set_cover_tilt_position(self, **kwargs) -> None: + """Move the cover to a specific tilt position.""" + position = kwargs[ATTR_TILT_POSITION] + # HmIP slats is closed:1 -> open:0 + level = 1 - position / 100.0 + await self._device.set_secondary_shading_level( + primaryShadingLevel=self._device.primaryShadingLevel, + secondaryShadingLevel=level, + ) + + @property + def is_closed(self) -> Optional[bool]: + """Return if the cover is closed.""" + if self._device.primaryShadingLevel is not None: + return self._device.primaryShadingLevel == HMIP_COVER_CLOSED + return None + + async def async_open_cover(self, **kwargs) -> None: + """Open the cover.""" + await self._device.set_primary_shading_level( + primaryShadingLevel=HMIP_COVER_OPEN + ) + + async def async_close_cover(self, **kwargs) -> None: + """Close the cover.""" + await self._device.set_primary_shading_level( + primaryShadingLevel=HMIP_COVER_CLOSED + ) + + async def async_stop_cover(self, **kwargs) -> None: + """Stop the device if in motion.""" + await self._device.stop() + + async def async_open_cover_tilt(self, **kwargs) -> None: + """Open the slats.""" + await self._device.set_secondary_shading_level( + primaryShadingLevel=self._device.primaryShadingLevel, + secondaryShadingLevel=HMIP_SLATS_OPEN, + ) + + async def async_close_cover_tilt(self, **kwargs) -> None: + """Close the slats.""" + await self._device.set_secondary_shading_level( + primaryShadingLevel=self._device.primaryShadingLevel, + secondaryShadingLevel=HMIP_SLATS_CLOSED, + ) + + async def async_stop_cover_tilt(self, **kwargs) -> None: + """Stop the device if in motion.""" + await self._device.stop() + + class HomematicipCoverShutter(HomematicipGenericEntity, CoverEntity): """Representation of the HomematicIP cover shutter.""" diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index f387e7bfda3..f0c191ac1a9 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -8,6 +8,7 @@ from homematicip.aio.device import ( AsyncDimmer, AsyncFullFlushDimmer, AsyncPluggableDimmer, + AsyncWiredDimmer3, ) from homematicip.base.enums import RGBColorState from homematicip.base.functionalChannels import NotificationLightChannel @@ -51,6 +52,9 @@ async def async_setup_entry( hap, device, device.bottomLightChannelIndex ) ) + elif isinstance(device, AsyncWiredDimmer3): + for channel in range(1, 4): + entities.append(HomematicipMultiDimmer(hap, device, channel=channel)) elif isinstance( device, (AsyncDimmer, AsyncPluggableDimmer, AsyncBrandDimmer, AsyncFullFlushDimmer), @@ -99,6 +103,45 @@ class HomematicipLightMeasuring(HomematicipLight): return state_attr +class HomematicipMultiDimmer(HomematicipGenericEntity, LightEntity): + """Representation of HomematicIP Cloud dimmer.""" + + def __init__(self, hap: HomematicipHAP, device, channel: int) -> None: + """Initialize the dimmer light entity.""" + super().__init__(hap, device, channel=channel) + + @property + def is_on(self) -> bool: + """Return true if dimmer is on.""" + func_channel = self._device.functionalChannels[self._channel] + return func_channel.dimLevel is not None and func_channel.dimLevel > 0.0 + + @property + def brightness(self) -> int: + """Return the brightness of this light between 0..255.""" + return int( + (self._device.functionalChannels[self._channel].dimLevel or 0.0) * 255 + ) + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return SUPPORT_BRIGHTNESS + + async def async_turn_on(self, **kwargs) -> None: + """Turn the dimmer on.""" + if ATTR_BRIGHTNESS in kwargs: + await self._device.set_dim_level( + kwargs[ATTR_BRIGHTNESS] / 255.0, self._channel + ) + else: + await self._device.set_dim_level(1, self._channel) + + async def async_turn_off(self, **kwargs) -> None: + """Turn the dimmer off.""" + await self._device.set_dim_level(0, self._channel) + + class HomematicipDimmer(HomematicipGenericEntity, LightEntity): """Representation of HomematicIP Cloud dimmer.""" diff --git a/tests/components/homematicip_cloud/test_cover.py b/tests/components/homematicip_cloud/test_cover.py index 7ef0e3d6703..82d2f41de59 100644 --- a/tests/components/homematicip_cloud/test_cover.py +++ b/tests/components/homematicip_cloud/test_cover.py @@ -160,6 +160,109 @@ async def test_hmip_cover_slats(hass, default_mock_hap_factory): assert ha_state.state == STATE_UNKNOWN +async def test_hmip_blind_module(hass, default_mock_hap_factory): + """Test HomematicipBlindModule.""" + entity_id = "cover.sonnenschutz_balkontur" + entity_name = "Sonnenschutz Balkontür" + device_model = "HmIP-HDM1" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[entity_name] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 5 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 100 + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "cover", "open_cover_tilt", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "set_secondary_shading_level" + assert hmip_device.mock_calls[-1][2] == { + "primaryShadingLevel": 0.94956, + "secondaryShadingLevel": 0, + } + + await async_manipulate_test_data(hass, hmip_device, "primaryShadingLevel", 0) + await async_manipulate_test_data(hass, hmip_device, "secondaryShadingLevel", 0) + await hass.services.async_call( + "cover", "open_cover", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 4 + + assert hmip_device.mock_calls[-1][0] == "set_primary_shading_level" + assert hmip_device.mock_calls[-1][2] == {"primaryShadingLevel": 0} + + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 100 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 100 + + await async_manipulate_test_data(hass, hmip_device, "primaryShadingLevel", 0.5) + await async_manipulate_test_data(hass, hmip_device, "secondaryShadingLevel", 0.5) + await hass.services.async_call( + "cover", + "set_cover_tilt_position", + {"entity_id": entity_id, "tilt_position": "50"}, + blocking=True, + ) + await hass.services.async_call( + "cover", + "set_cover_position", + {"entity_id": entity_id, "position": "50"}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 8 + + assert hmip_device.mock_calls[-1][0] == "set_primary_shading_level" + assert hmip_device.mock_calls[-1][2] == {"primaryShadingLevel": 0.5} + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 50 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 50 + + await async_manipulate_test_data(hass, hmip_device, "primaryShadingLevel", 1) + await async_manipulate_test_data(hass, hmip_device, "secondaryShadingLevel", 1) + await hass.services.async_call( + "cover", "close_cover", {"entity_id": entity_id}, blocking=True + ) + await hass.services.async_call( + "cover", "close_cover_tilt", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 12 + + assert hmip_device.mock_calls[-1][0] == "set_secondary_shading_level" + assert hmip_device.mock_calls[-1][2] == { + "primaryShadingLevel": 1, + "secondaryShadingLevel": 1, + } + + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_CLOSED + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 0 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 0 + + await hass.services.async_call( + "cover", "stop_cover", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 13 + assert hmip_device.mock_calls[-1][0] == "stop" + assert hmip_device.mock_calls[-1][1] == () + + await async_manipulate_test_data(hass, hmip_device, "secondaryShadingLevel", None) + ha_state = hass.states.get(entity_id) + assert not ha_state.attributes.get(ATTR_CURRENT_TILT_POSITION) + + await async_manipulate_test_data(hass, hmip_device, "primaryShadingLevel", None) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_UNKNOWN + + async def test_hmip_garage_door_tormatic(hass, default_mock_hap_factory): """Test HomematicipCoverShutte.""" entity_id = "cover.garage_door_module" diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 31e62a1a719..4047a8ef28c 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -22,7 +22,7 @@ async def test_hmip_load_all_supported_devices(hass, default_mock_hap_factory): test_devices=None, test_groups=None ) - assert len(mock_hap.hmip_device_by_entity_id) == 233 + assert len(mock_hap.hmip_device_by_entity_id) == 236 async def test_hmip_remove_device(hass, default_mock_hap_factory): diff --git a/tests/components/homematicip_cloud/test_light.py b/tests/components/homematicip_cloud/test_light.py index 8ab62019c3d..b62a98fd03f 100644 --- a/tests/components/homematicip_cloud/test_light.py +++ b/tests/components/homematicip_cloud/test_light.py @@ -245,3 +245,55 @@ async def test_hmip_light_measuring(hass, default_mock_hap_factory): await async_manipulate_test_data(hass, hmip_device, "on", False) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OFF + + +async def test_hmip_wired_multi_dimmer(hass, default_mock_hap_factory): + """Test HomematicipMultiDimmer.""" + entity_id = "light.raumlich_kuche" + entity_name = "Raumlich (Küche)" + device_model = "HmIPW-DRD3" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["Wired Dimmaktor – 3-fach (Küche)"] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "light", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert hmip_device.mock_calls[-1][0] == "set_dim_level" + assert hmip_device.mock_calls[-1][1] == (1, 1) + + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": entity_id, "brightness": "100"}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 2 + assert hmip_device.mock_calls[-1][0] == "set_dim_level" + assert hmip_device.mock_calls[-1][1] == (0.39215686274509803, 1) + await async_manipulate_test_data(hass, hmip_device, "dimLevel", 1, channel=1) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_BRIGHTNESS] == 255 + + await hass.services.async_call( + "light", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 4 + assert hmip_device.mock_calls[-1][0] == "set_dim_level" + assert hmip_device.mock_calls[-1][1] == (0, 1) + await async_manipulate_test_data(hass, hmip_device, "dimLevel", 0, channel=1) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + await async_manipulate_test_data(hass, hmip_device, "dimLevel", None, channel=1) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + assert not ha_state.attributes.get(ATTR_BRIGHTNESS) diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json index 9c2a1b1e371..7adf4b85b1a 100644 --- a/tests/fixtures/homematicip_cloud.json +++ b/tests/fixtures/homematicip_cloud.json @@ -233,7 +233,7 @@ "profileMode": "AUTOMATIC", "secondaryCloseAdjustable": false, "secondaryOpenAdjustable": false, - "secondaryShadingLevel": null, + "secondaryShadingLevel": 0, "secondaryShadingStateType": "NOT_EXISTENT", "shadingDriveVersion": null, "shadingPackagePosition": "TOP",