Add device HmIP-DLD (#83380)

* Add HmIP-DLD

* Remove commented code

* Fix errors

* Format using black

* Fix device count

* Add missing tests

* Apply changes by reviewer

* Change setup entry code

* Remove jammed state

* Add error messages

* Update homeassistant/components/homematicip_cloud/helpers.py

Co-authored-by: Aaron Bach <bachya1208@gmail.com>

* Add decorator

* Add error log output

* Update test_device.py

---------

Co-authored-by: Aaron Bach <bachya1208@gmail.com>
This commit is contained in:
hahn-th 2023-02-26 18:49:25 +01:00 committed by GitHub
parent e00ff54869
commit c9dfa15ed6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 457 additions and 2 deletions

View File

@ -14,6 +14,7 @@ PLATFORMS = [
Platform.CLIMATE,
Platform.COVER,
Platform.LIGHT,
Platform.LOCK,
Platform.SENSOR,
Platform.SWITCH,
Platform.WEATHER,

View File

@ -0,0 +1,39 @@
"""Helper functions for Homematicip Cloud Integration."""
from functools import wraps
import json
import logging
from homeassistant.exceptions import HomeAssistantError
from . import HomematicipGenericEntity
_LOGGER = logging.getLogger(__name__)
def is_error_response(response) -> bool:
"""Response from async call contains errors or not."""
if isinstance(response, dict):
return response.get("errorCode") not in ("", None)
return False
def handle_errors(func):
"""Handle async errors."""
@wraps(func)
async def inner(self: HomematicipGenericEntity) -> None:
"""Handle errors from async call."""
result = await func(self)
if is_error_response(result):
_LOGGER.error(
"Error while execute function %s: %s",
__name__,
json.dumps(result),
)
raise HomeAssistantError(
f"Error while execute function {func.__name__}: {result.get('errorCode')}. See log for more information."
)
return inner

View File

@ -0,0 +1,95 @@
"""Support for HomematicIP Cloud lock devices."""
from __future__ import annotations
import logging
from typing import Any
from homematicip.aio.device import AsyncDoorLockDrive
from homematicip.base.enums import LockState, MotorState
from homeassistant.components.lock import LockEntity, LockEntityFeature
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity
from .helpers import handle_errors
_LOGGER = logging.getLogger(__name__)
ATTR_AUTO_RELOCK_DELAY = "auto_relock_delay"
ATTR_DOOR_HANDLE_TYPE = "door_handle_type"
ATTR_DOOR_LOCK_DIRECTION = "door_lock_direction"
ATTR_DOOR_LOCK_NEUTRAL_POSITION = "door_lock_neutral_position"
ATTR_DOOR_LOCK_TURNS = "door_lock_turns"
DEVICE_DLD_ATTRIBUTES = {
"autoRelockDelay": ATTR_AUTO_RELOCK_DELAY,
"doorHandleType": ATTR_DOOR_HANDLE_TYPE,
"doorLockDirection": ATTR_DOOR_LOCK_DIRECTION,
"doorLockNeutralPosition": ATTR_DOOR_LOCK_NEUTRAL_POSITION,
"doorLockTurns": ATTR_DOOR_LOCK_TURNS,
}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the HomematicIP locks from a config entry."""
hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id]
async_add_entities(
HomematicipDoorLockDrive(hap, device)
for device in hap.home.devices
if isinstance(device, AsyncDoorLockDrive)
)
class HomematicipDoorLockDrive(HomematicipGenericEntity, LockEntity):
"""Representation of the HomematicIP DoorLockDrive."""
_attr_supported_features = LockEntityFeature.OPEN
@property
def is_locked(self) -> bool | None:
"""Return true if device is locked."""
return (
self._device.lockState == LockState.LOCKED
and self._device.motorState == MotorState.STOPPED
)
@property
def is_locking(self) -> bool:
"""Return true if device is locking."""
return self._device.motorState == MotorState.CLOSING
@property
def is_unlocking(self) -> bool:
"""Return true if device is unlocking."""
return self._device.motorState == MotorState.OPENING
@handle_errors
async def async_lock(self, **kwargs: Any) -> None:
"""Lock the device."""
return await self._device.set_lock_state(LockState.LOCKED)
@handle_errors
async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock the device."""
return await self._device.set_lock_state(LockState.UNLOCKED)
@handle_errors
async def async_open(self, **kwargs: Any) -> None:
"""Open the door latch."""
return await self._device.set_lock_state(LockState.OPEN)
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the device."""
return super().extra_state_attributes | {
attr_key: attr_value
for attr, attr_key in DEVICE_DLD_ATTRIBUTES.items()
if (attr_value := getattr(self._device, attr, None)) is not None
}

View File

@ -25,7 +25,7 @@ async def test_hmip_load_all_supported_devices(
test_devices=None, test_groups=None
)
assert len(mock_hap.hmip_device_by_entity_id) == 270
assert len(mock_hap.hmip_device_by_entity_id) == 272
async def test_hmip_remove_device(

View File

@ -0,0 +1,18 @@
"""Test HomematicIP Cloud helper functions."""
import json
from homeassistant.components.homematicip_cloud.helpers import is_error_response
async def test_is_error_response():
"""Test, if an response is a normal result or an error."""
assert not is_error_response("True")
assert not is_error_response(True)
assert not is_error_response("")
assert is_error_response(
json.loads(
'{"errorCode": "INVALID_NUMBER_PARAMETER_VALUE", "minValue": 0.0, "maxValue": 1.01}'
)
)
assert not is_error_response(json.loads('{"errorCode": ""}'))

View File

@ -0,0 +1,127 @@
"""Tests for HomematicIP Cloud locks."""
from unittest.mock import patch
from homematicip.base.enums import LockState, MotorState
import pytest
from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN
from homeassistant.components.lock import (
DOMAIN,
STATE_LOCKING,
STATE_UNLOCKING,
LockEntityFeature,
)
from homeassistant.const import ATTR_SUPPORTED_FEATURES
from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component
from .helper import async_manipulate_test_data, get_and_check_entity_basics
async def test_manually_configured_platform(hass):
"""Test that we do not set up an access point."""
assert await async_setup_component(
hass, DOMAIN, {DOMAIN: {"platform": HMIPC_DOMAIN}}
)
assert not hass.data.get(HMIPC_DOMAIN)
async def test_hmip_doorlockdrive(hass, default_mock_hap_factory):
"""Test HomematicipDoorLockDrive."""
entity_id = "lock.haustuer"
entity_name = "Haustuer"
device_model = "HmIP-DLD"
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.attributes[ATTR_SUPPORTED_FEATURES] == LockEntityFeature.OPEN
await hass.services.async_call(
"lock",
"open",
{"entity_id": entity_id},
blocking=True,
)
assert hmip_device.mock_calls[-1][0] == "set_lock_state"
assert hmip_device.mock_calls[-1][1] == (LockState.OPEN,)
await hass.services.async_call(
"lock",
"lock",
{"entity_id": entity_id},
blocking=True,
)
assert hmip_device.mock_calls[-1][0] == "set_lock_state"
assert hmip_device.mock_calls[-1][1] == (LockState.LOCKED,)
await hass.services.async_call(
"lock",
"unlock",
{"entity_id": entity_id},
blocking=True,
)
assert hmip_device.mock_calls[-1][0] == "set_lock_state"
assert hmip_device.mock_calls[-1][1] == (LockState.UNLOCKED,)
await async_manipulate_test_data(
hass, hmip_device, "motorState", MotorState.CLOSING
)
ha_state = hass.states.get(entity_id)
assert ha_state.state == STATE_LOCKING
await async_manipulate_test_data(
hass, hmip_device, "motorState", MotorState.OPENING
)
ha_state = hass.states.get(entity_id)
assert ha_state.state == STATE_UNLOCKING
async def test_hmip_doorlockdrive_handle_errors(hass, default_mock_hap_factory):
"""Test HomematicipDoorLockDrive."""
entity_id = "lock.haustuer"
entity_name = "Haustuer"
device_model = "HmIP-DLD"
mock_hap = await default_mock_hap_factory.async_get_mock_hap(
test_devices=[entity_name]
)
with patch(
"homematicip.aio.device.AsyncDoorLockDrive.set_lock_state",
return_value={
"errorCode": "INVALID_NUMBER_PARAMETER_VALUE",
"minValue": 0.0,
"maxValue": 1.01,
},
):
get_and_check_entity_basics(
hass, mock_hap, entity_id, entity_name, device_model
)
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
"lock",
"open",
{"entity_id": entity_id},
blocking=True,
)
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
"lock",
"lock",
{"entity_id": entity_id},
blocking=True,
)
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
"lock",
"unlock",
{"entity_id": entity_id},
blocking=True,
)

View File

@ -7119,6 +7119,181 @@
"serializedGlobalTradeItemNumber": "3014F711A000DIN_RAIL_DIMMER3",
"type": "DIN_RAIL_DIMMER_3",
"updateState": "UP_TO_DATE"
},
"3014F7110000000000000DLD": {
"availableFirmwareVersion": "1.2.0",
"connectionType": "HMIP_RF",
"firmwareVersion": "1.2.0",
"firmwareVersionInteger": 66048,
"functionalChannels": {
"0": {
"busConfigMismatch": null,
"coProFaulty": false,
"coProRestartNeeded": false,
"coProUpdateFailure": false,
"configPending": false,
"deviceId": "3014F7110000000000000DLD",
"deviceOverheated": false,
"deviceOverloaded": false,
"devicePowerFailureDetected": false,
"deviceUndervoltage": false,
"dutyCycle": false,
"functionalChannelType": "DEVICE_OPERATIONLOCK",
"groupIndex": 0,
"groups": ["00000000-0000-0000-0000-000000000025"],
"index": 0,
"label": "",
"lowBat": false,
"mountingOrientation": null,
"multicastRoutingEnabled": false,
"operationLockActive": false,
"particulateMatterSensorCommunicationError": null,
"particulateMatterSensorError": null,
"powerShortCircuit": null,
"profilePeriodLimitReached": false,
"routerModuleEnabled": false,
"routerModuleSupported": false,
"rssiDeviceValue": -63,
"rssiPeerValue": -64,
"shortCircuitDataLine": null,
"supportedOptionalFeatures": {
"IFeatureBusConfigMismatch": false,
"IFeatureDeviceCoProError": false,
"IFeatureDeviceCoProRestart": false,
"IFeatureDeviceCoProUpdate": false,
"IFeatureDeviceIdentify": false,
"IFeatureDeviceOverheated": false,
"IFeatureDeviceOverloaded": false,
"IFeatureDeviceParticulateMatterSensorCommunicationError": false,
"IFeatureDeviceParticulateMatterSensorError": false,
"IFeatureDevicePowerFailure": false,
"IFeatureDeviceTemperatureHumiditySensorCommunicationError": false,
"IFeatureDeviceTemperatureHumiditySensorError": false,
"IFeatureDeviceTemperatureOutOfRange": false,
"IFeatureDeviceUndervoltage": false,
"IFeatureMulticastRouter": false,
"IFeaturePowerShortCircuit": false,
"IFeatureProfilePeriodLimit": true,
"IFeatureRssiValue": true,
"IFeatureShortCircuitDataLine": false,
"IOptionalFeatureDutyCycle": true,
"IOptionalFeatureLowBat": true,
"IOptionalFeatureMountingOrientation": false
},
"temperatureHumiditySensorCommunicationError": null,
"temperatureHumiditySensorError": null,
"temperatureOutOfRange": false,
"unreach": false
},
"1": {
"autoRelockDelay": 300.0,
"autoRelockEnabled": false,
"deviceId": "3014F7110000000000000DLD",
"doorHandleType": "LEVER_HANDLE",
"doorLockDirection": "RIGHT",
"doorLockNeutralPosition": "VERTICAL",
"doorLockTurns": 2,
"functionalChannelType": "DOOR_LOCK_CHANNEL",
"groupIndex": 1,
"groups": [
"00000000-0000-0000-0000-000000000026",
"00000000-0000-0000-0000-000000000027",
"00000000-0000-0000-0000-000000000028"
],
"index": 1,
"label": "",
"lockState": "LOCKED",
"motorState": "STOPPED"
},
"2": {
"authorized": true,
"deviceId": "3014F7110000000000000DLD",
"functionalChannelType": "ACCESS_AUTHORIZATION_CHANNEL",
"groupIndex": 1,
"groups": ["00000000-0000-0000-0000-000000000033"],
"index": 2,
"label": ""
},
"3": {
"authorized": true,
"deviceId": "3014F7110000000000000DLD",
"functionalChannelType": "ACCESS_AUTHORIZATION_CHANNEL",
"groupIndex": 1,
"groups": ["00000000-0000-0000-0000-000000000033"],
"index": 3,
"label": ""
},
"4": {
"authorized": true,
"deviceId": "3014F7110000000000000DLD",
"functionalChannelType": "ACCESS_AUTHORIZATION_CHANNEL",
"groupIndex": 1,
"groups": ["00000000-0000-0000-0000-000000000033"],
"index": 4,
"label": ""
},
"5": {
"authorized": true,
"deviceId": "3014F7110000000000000DLD",
"functionalChannelType": "ACCESS_AUTHORIZATION_CHANNEL",
"groupIndex": 1,
"groups": ["00000000-0000-0000-0000-000000000033"],
"index": 5,
"label": ""
},
"6": {
"authorized": true,
"deviceId": "3014F7110000000000000DLD",
"functionalChannelType": "ACCESS_AUTHORIZATION_CHANNEL",
"groupIndex": 1,
"groups": ["00000000-0000-0000-0000-000000000033"],
"index": 6,
"label": ""
},
"7": {
"authorized": true,
"deviceId": "3014F7110000000000000DLD",
"functionalChannelType": "ACCESS_AUTHORIZATION_CHANNEL",
"groupIndex": 1,
"groups": ["00000000-0000-0000-0000-000000000033"],
"index": 7,
"label": ""
},
"8": {
"authorized": true,
"deviceId": "3014F7110000000000000DLD",
"functionalChannelType": "ACCESS_AUTHORIZATION_CHANNEL",
"groupIndex": 1,
"groups": ["00000000-0000-0000-0000-000000000033"],
"index": 8,
"label": ""
},
"9": {
"authorized": true,
"deviceId": "3014F7110000000000000DLD",
"functionalChannelType": "ACCESS_AUTHORIZATION_CHANNEL",
"groupIndex": 1,
"groups": [
"00000000-0000-0000-0000-000000000033",
"00000000-0000-0000-0000-000000000032"
],
"index": 9,
"label": ""
}
},
"homeId": "00000000-0000-0000-0000-000000000001",
"id": "3014F7110000000000000DLD",
"label": "Haustuer",
"lastStatusUpdate": 1618727020725,
"liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED",
"manufacturerCode": 1,
"modelId": 423,
"modelType": "HmIP-DLD",
"oem": "eQ-3",
"permanentlyReachable": true,
"serializedGlobalTradeItemNumber": "3014F7110000000000000DLD",
"type": "DOOR_LOCK_DRIVE",
"updateState": "UP_TO_DATE"
}
},
"groups": {