Skip to content

Commit

Permalink
Add device HmIP-DLD (home-assistant#83380)
Browse files Browse the repository at this point in the history
* 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
hahn-th and bachya authored Feb 26, 2023
1 parent e00ff54 commit c9dfa15
Show file tree
Hide file tree
Showing 7 changed files with 457 additions and 2 deletions.
1 change: 1 addition & 0 deletions homeassistant/components/homematicip_cloud/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
Platform.CLIMATE,
Platform.COVER,
Platform.LIGHT,
Platform.LOCK,
Platform.SENSOR,
Platform.SWITCH,
Platform.WEATHER,
Expand Down
39 changes: 39 additions & 0 deletions homeassistant/components/homematicip_cloud/helpers.py
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
95 changes: 95 additions & 0 deletions homeassistant/components/homematicip_cloud/lock.py
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
}
2 changes: 1 addition & 1 deletion tests/components/homematicip_cloud/test_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
18 changes: 18 additions & 0 deletions tests/components/homematicip_cloud/test_helpers.py
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": ""}'))
127 changes: 127 additions & 0 deletions tests/components/homematicip_cloud/test_lock.py
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,
)
Loading

0 comments on commit c9dfa15

Please sign in to comment.