mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 18:57:06 +00:00
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:
parent
e00ff54869
commit
c9dfa15ed6
@ -14,6 +14,7 @@ PLATFORMS = [
|
||||
Platform.CLIMATE,
|
||||
Platform.COVER,
|
||||
Platform.LIGHT,
|
||||
Platform.LOCK,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
Platform.WEATHER,
|
||||
|
39
homeassistant/components/homematicip_cloud/helpers.py
Normal file
39
homeassistant/components/homematicip_cloud/helpers.py
Normal 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
|
95
homeassistant/components/homematicip_cloud/lock.py
Normal file
95
homeassistant/components/homematicip_cloud/lock.py
Normal 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
|
||||
}
|
@ -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(
|
||||
|
18
tests/components/homematicip_cloud/test_helpers.py
Normal file
18
tests/components/homematicip_cloud/test_helpers.py
Normal 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": ""}'))
|
127
tests/components/homematicip_cloud/test_lock.py
Normal file
127
tests/components/homematicip_cloud/test_lock.py
Normal 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,
|
||||
)
|
177
tests/fixtures/homematicip_cloud.json
vendored
177
tests/fixtures/homematicip_cloud.json
vendored
@ -6978,7 +6978,7 @@
|
||||
"supported": true,
|
||||
"type": "EXTERNAL"
|
||||
},
|
||||
"3014F711A000DIN_RAIL_DIMMER3": {
|
||||
"3014F711A000DIN_RAIL_DIMMER3": {
|
||||
"availableFirmwareVersion": "1.2.0",
|
||||
"connectionType": "HMIP_RF",
|
||||
"firmwareVersion": "1.2.0",
|
||||
@ -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": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user