Add wired devices to Homematic IP Cloud (#41232)
* Add wired devices to HomematicIP Cloud * Add test to check device registry * Add comments * Update commentpull/42286/head
parent
ea0e359d90
commit
6efb782871
|
@ -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."""
|
||||
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue