diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index da5bf844e08..d2fa79d667e 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -20,6 +20,7 @@ from homematicip.aio.device import ( AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro, + AsyncWiredInput32, ) from homematicip.aio.group import AsyncSecurityGroup, AsyncSecurityZoneGroup from homematicip.base.enums import SmokeDetectorAlarmType, WindowState @@ -42,6 +43,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity +from .generic_entity import async_add_base_multi_area_device from .hap import HomematicipHAP ATTR_ACCELERATION_SENSOR_MODE = "acceleration_sensor_mode" @@ -85,7 +87,17 @@ async def async_setup_entry( entities.append(HomematicipAccelerationSensor(hap, device)) if isinstance(device, AsyncTiltVibrationSensor): entities.append(HomematicipTiltVibrationSensor(hap, device)) - if isinstance(device, (AsyncContactInterface, AsyncFullFlushContactInterface)): + if isinstance(device, AsyncWiredInput32): + await async_add_base_multi_area_device(hass, config_entry, device) + for channel in range(1, 33): + entities.append( + HomematicipMultiContactInterface( + hap, device, channel=channel, is_multi_area=True + ) + ) + elif isinstance( + device, (AsyncContactInterface, AsyncFullFlushContactInterface) + ): entities.append(HomematicipContactInterface(hap, device)) if isinstance( device, @@ -205,6 +217,31 @@ class HomematicipTiltVibrationSensor(HomematicipBaseActionSensor): """Representation of the HomematicIP tilt vibration sensor.""" +class HomematicipMultiContactInterface(HomematicipGenericEntity, BinarySensorEntity): + """Representation of the HomematicIP multi room/area contact interface.""" + + def __init__( + self, hap: HomematicipHAP, device, channel: int, is_multi_area: bool = False + ) -> None: + """Initialize the multi contact entity.""" + super().__init__(hap, device, channel=channel, is_multi_area=is_multi_area) + + @property + def device_class(self) -> str: + """Return the class of this sensor.""" + return DEVICE_CLASS_OPENING + + @property + def is_on(self) -> bool: + """Return true if the contact interface is on/open.""" + if self._device.functionalChannels[self._channel].windowState is None: + return None + return ( + self._device.functionalChannels[self._channel].windowState + != WindowState.CLOSED + ) + + class HomematicipContactInterface(HomematicipGenericEntity, BinarySensorEntity): """Representation of the HomematicIP contact interface.""" diff --git a/homeassistant/components/homematicip_cloud/generic_entity.py b/homeassistant/components/homematicip_cloud/generic_entity.py index ce8b44f5702..4e5265f5c0f 100644 --- a/homeassistant/components/homematicip_cloud/generic_entity.py +++ b/homeassistant/components/homematicip_cloud/generic_entity.py @@ -5,9 +5,11 @@ from typing import Any, Dict, Optional from homematicip.aio.device import AsyncDevice from homematicip.aio.group import AsyncGroup +from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType from .const import DOMAIN as HMIPC_DOMAIN from .hap import HomematicipHAP @@ -76,6 +78,7 @@ class HomematicipGenericEntity(Entity): device, post: Optional[str] = None, channel: Optional[int] = None, + is_multi_area: Optional[bool] = False, ) -> None: """Initialize the generic entity.""" self._hap = hap @@ -83,6 +86,7 @@ class HomematicipGenericEntity(Entity): self._device = device self._post = post self._channel = channel + self._is_multi_area = is_multi_area # Marker showing that the HmIP device hase been removed. self.hmip_device_removed = False _LOGGER.info("Setting up %s (%s)", self.name, self._device.modelType) @@ -92,6 +96,20 @@ class HomematicipGenericEntity(Entity): """Return device specific attributes.""" # Only physical devices should be HA devices. if isinstance(self._device, AsyncDevice): + if self._is_multi_area: + return { + "identifiers": { + # Unique ID of Homematic IP device + (HMIPC_DOMAIN, self.unique_id) + }, + "name": self.name, + "manufacturer": self._device.oem, + "model": self._device.modelType, + "sw_version": self._device.firmwareVersion, + # Link to the base device. + "via_device": (HMIPC_DOMAIN, self._device.id), + } + return { "identifiers": { # Serial numbers of Homematic IP device @@ -251,3 +269,19 @@ class HomematicipGenericEntity(Entity): state_attr[ATTR_IS_GROUP] = True return state_attr + + +async def async_add_base_multi_area_device( + hass: HomeAssistantType, entry: ConfigEntry, device: AsyncDevice +): + """Register base multi area device in registry.""" + device_registry = await dr.async_get_registry(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(HMIPC_DOMAIN, device.id)}, + manufacturer=device.oem, + name=device.label, + model=device.modelType, + sw_version=device.firmwareVersion, + via_device=(HMIPC_DOMAIN, device.homeId), + ) diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 22fd1c25078..e63fe39f81e 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -12,6 +12,7 @@ from homematicip.aio.device import ( AsyncPlugableSwitchMeasuring, AsyncPrintedCircuitBoardSwitch2, AsyncPrintedCircuitBoardSwitchBattery, + AsyncWiredSwitch8, ) from homematicip.aio.group import AsyncExtendedLinkedSwitchingGroup, AsyncSwitchingGroup @@ -20,7 +21,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity -from .generic_entity import ATTR_GROUP_MEMBER_UNREACHABLE +from .generic_entity import ( + ATTR_GROUP_MEMBER_UNREACHABLE, + async_add_base_multi_area_device, +) from .hap import HomematicipHAP @@ -40,6 +44,14 @@ async def async_setup_entry( device, (AsyncPlugableSwitchMeasuring, AsyncFullFlushSwitchMeasuring) ): entities.append(HomematicipSwitchMeasuring(hap, device)) + elif isinstance(device, AsyncWiredSwitch8): + await async_add_base_multi_area_device(hass, config_entry, device) + for channel in range(1, 9): + entities.append( + HomematicipMultiSwitch( + hap, device, channel=channel, is_multi_area=True + ) + ) elif isinstance( device, ( @@ -70,6 +82,29 @@ async def async_setup_entry( async_add_entities(entities) +class HomematicipMultiSwitch(HomematicipGenericEntity, SwitchEntity): + """Representation of the HomematicIP multi switch.""" + + def __init__( + self, hap: HomematicipHAP, device, channel: int, is_multi_area: bool = False + ) -> None: + """Initialize the multi switch device.""" + super().__init__(hap, device, channel=channel, is_multi_area=is_multi_area) + + @property + def is_on(self) -> bool: + """Return true if switch is on.""" + return self._device.functionalChannels[self._channel].on + + async def async_turn_on(self, **kwargs) -> None: + """Turn the switch on.""" + await self._device.turn_on(self._channel) + + async def async_turn_off(self, **kwargs) -> None: + """Turn the switch off.""" + await self._device.turn_off(self._channel) + + class HomematicipSwitch(HomematicipGenericEntity, SwitchEntity): """Representation of the HomematicIP switch.""" @@ -146,24 +181,3 @@ class HomematicipSwitchMeasuring(HomematicipSwitch): if self._device.energyCounter is None: return 0 return round(self._device.energyCounter) - - -class HomematicipMultiSwitch(HomematicipGenericEntity, SwitchEntity): - """Representation of the HomematicIP multi switch.""" - - def __init__(self, hap: HomematicipHAP, device, channel: int) -> None: - """Initialize the multi switch device.""" - super().__init__(hap, device, channel=channel) - - @property - def is_on(self) -> bool: - """Return true if switch is on.""" - return self._device.functionalChannels[self._channel].on - - async def async_turn_on(self, **kwargs) -> None: - """Turn the switch on.""" - await self._device.turn_on(self._channel) - - async def async_turn_off(self, **kwargs) -> None: - """Turn the switch off.""" - await self._device.turn_off(self._channel) diff --git a/tests/components/homematicip_cloud/test_binary_sensor.py b/tests/components/homematicip_cloud/test_binary_sensor.py index 6d6ba84e243..f7bfcbf2f96 100644 --- a/tests/components/homematicip_cloud/test_binary_sensor.py +++ b/tests/components/homematicip_cloud/test_binary_sensor.py @@ -540,3 +540,28 @@ async def test_hmip_security_sensor_group(hass, default_mock_hap_factory): ) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_ON + + +async def test_hmip_wired_multi_contact_interface(hass, default_mock_hap_factory): + """Test HomematicipMultiContactInterface.""" + entity_id = "binary_sensor.wired_eingangsmodul_32_fach_channel5" + entity_name = "Wired Eingangsmodul – 32-fach Channel5" + device_model = "HmIPW-DRI32" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["Wired Eingangsmodul – 32-fach"] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + await async_manipulate_test_data( + hass, hmip_device, "windowState", WindowState.OPEN, channel=5 + ) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + await async_manipulate_test_data(hass, hmip_device, "windowState", None, channel=5) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index c47f0bf25ea..385f106a540 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) == 192 + assert len(mock_hap.hmip_device_by_entity_id) == 231 async def test_hmip_remove_device(hass, default_mock_hap_factory): @@ -240,3 +240,36 @@ async def test_hmip_reset_energy_counter_services(hass, default_mock_hap_factory ) assert hmip_device.mock_calls[-1][0] == "reset_energy_counter" assert len(hmip_device._connection.mock_calls) == 4 # pylint: disable=W0212 + + +async def test_hmip_multi_area_device(hass, default_mock_hap_factory): + """Test multi area device. Check if devices are created and referenced.""" + entity_id = "binary_sensor.wired_eingangsmodul_32_fach_channel5" + entity_name = "Wired Eingangsmodul – 32-fach Channel5" + device_model = "HmIPW-DRI32" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["Wired Eingangsmodul – 32-fach"] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + assert ha_state + + # get the entity + entity_registry = await er.async_get_registry(hass) + entity = entity_registry.async_get(ha_state.entity_id) + assert entity + + # get the device + device_registry = await dr.async_get_registry(hass) + device = device_registry.async_get(entity.device_id) + assert device.name == entity_name + + # get the base device + via_device = device_registry.async_get(device.via_device_id) + assert via_device.name == "Wired Eingangsmodul – 32-fach" + + # get the hap + hap_device = device_registry.async_get(via_device.via_device_id) + assert hap_device.name == "Access Point" diff --git a/tests/components/homematicip_cloud/test_switch.py b/tests/components/homematicip_cloud/test_switch.py index 85bfaaa4ec5..034ca33aece 100644 --- a/tests/components/homematicip_cloud/test_switch.py +++ b/tests/components/homematicip_cloud/test_switch.py @@ -220,3 +220,42 @@ async def test_hmip_multi_switch(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_switch(hass, default_mock_hap_factory): + """Test HomematicipMultiSwitch.""" + entity_id = "switch.fernseher_wohnzimmer" + entity_name = "Fernseher (Wohnzimmer)" + device_model = "HmIPW-DRS8" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[ + "Wired Schaltaktor – 8-fach", + ] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "switch", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "turn_off" + assert hmip_device.mock_calls[-1][1] == (1,) + await async_manipulate_test_data(hass, hmip_device, "on", False) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + await hass.services.async_call( + "switch", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 3 + assert hmip_device.mock_calls[-1][0] == "turn_on" + assert hmip_device.mock_calls[-1][1] == (1,) + await async_manipulate_test_data(hass, hmip_device, "on", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON