forked from home-assistant/core
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add device HmIP-DLD (home-assistant#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>
- Loading branch information
Showing
7 changed files
with
457 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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": ""}')) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
) |
Oops, something went wrong.