mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
Add locking and unlocking feature to igloohome integration (#136002)
* - Added lock platform - Added creation of IgloohomeLockEntity when bridge devices are included. * - Migrated retrieval of linked_bridge utility to utils module. - Added ability for lock to update it's own linked bridge automatically * - Added mock bridge device to test fixture * - Added snapshot test for lock module * - Added bridge with no linked devices - Added test for util.get_linked_bridge * - Added handling of errors from API call * - Bump igloohome-api to v0.1.0 * - Minor change * - Removed async update for locks. Focus on MVP * - Removed need for update on entity creation * - Updated snapshot test * - Updated snapshot * - Updated to use walrus during lock entity creation - Updated callback class for async_setup_entry based on lint suggestion * - Set _attr_name as None - Updated snapshot test * Update homeassistant/components/igloohome/lock.py * Update homeassistant/components/igloohome/lock.py --------- Co-authored-by: Josef Zweck <josef@zweck.dev>
This commit is contained in:
parent
8b4d9f96d4
commit
ff622af888
@ -19,7 +19,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
PLATFORMS: list[Platform] = [Platform.LOCK, Platform.SENSOR]
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -35,7 +35,6 @@ type IgloohomeConfigEntry = ConfigEntry[IgloohomeRuntimeData]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: IgloohomeConfigEntry) -> bool:
|
||||
"""Set up igloohome from a config entry."""
|
||||
|
||||
authentication = IgloohomeAuth(
|
||||
session=async_get_clientsession(hass),
|
||||
client_id=entry.data[CONF_CLIENT_ID],
|
||||
|
91
homeassistant/components/igloohome/lock.py
Normal file
91
homeassistant/components/igloohome/lock.py
Normal file
@ -0,0 +1,91 @@
|
||||
"""Implementation of the lock platform."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from aiohttp import ClientError
|
||||
from igloohome_api import (
|
||||
BRIDGE_JOB_LOCK,
|
||||
BRIDGE_JOB_UNLOCK,
|
||||
DEVICE_TYPE_LOCK,
|
||||
Api as IgloohomeApi,
|
||||
ApiException,
|
||||
GetDeviceInfoResponse,
|
||||
)
|
||||
|
||||
from homeassistant.components.lock import LockEntity, LockEntityFeature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import IgloohomeConfigEntry
|
||||
from .entity import IgloohomeBaseEntity
|
||||
from .utils import get_linked_bridge
|
||||
|
||||
# Scan interval set to allow Lock entity update the bridge linked to it.
|
||||
SCAN_INTERVAL = timedelta(hours=1)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: IgloohomeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up lock entities."""
|
||||
async_add_entities(
|
||||
IgloohomeLockEntity(
|
||||
api_device_info=device,
|
||||
api=entry.runtime_data.api,
|
||||
bridge_id=str(bridge),
|
||||
)
|
||||
for device in entry.runtime_data.devices
|
||||
if device.type == DEVICE_TYPE_LOCK
|
||||
and (bridge := get_linked_bridge(device.deviceId, entry.runtime_data.devices))
|
||||
is not None
|
||||
)
|
||||
|
||||
|
||||
class IgloohomeLockEntity(IgloohomeBaseEntity, LockEntity):
|
||||
"""Implementation of a device that has locking capabilities."""
|
||||
|
||||
# Operating on assumed state because there is no API to query the state.
|
||||
_attr_assumed_state = True
|
||||
_attr_supported_features = LockEntityFeature.OPEN
|
||||
_attr_name = None
|
||||
|
||||
def __init__(
|
||||
self, api_device_info: GetDeviceInfoResponse, api: IgloohomeApi, bridge_id: str
|
||||
) -> None:
|
||||
"""Initialize the class."""
|
||||
super().__init__(
|
||||
api_device_info=api_device_info,
|
||||
api=api,
|
||||
unique_key="lock",
|
||||
)
|
||||
self.bridge_id = bridge_id
|
||||
|
||||
async def async_lock(self, **kwargs):
|
||||
"""Lock this lock."""
|
||||
try:
|
||||
await self.api.create_bridge_proxied_job(
|
||||
self.api_device_info.deviceId, self.bridge_id, BRIDGE_JOB_LOCK
|
||||
)
|
||||
except (ApiException, ClientError) as err:
|
||||
raise HomeAssistantError from err
|
||||
|
||||
async def async_unlock(self, **kwargs):
|
||||
"""Unlock this lock."""
|
||||
try:
|
||||
await self.api.create_bridge_proxied_job(
|
||||
self.api_device_info.deviceId, self.bridge_id, BRIDGE_JOB_UNLOCK
|
||||
)
|
||||
except (ApiException, ClientError) as err:
|
||||
raise HomeAssistantError from err
|
||||
|
||||
async def async_open(self, **kwargs):
|
||||
"""Open (unlatch) this lock."""
|
||||
try:
|
||||
await self.api.create_bridge_proxied_job(
|
||||
self.api_device_info.deviceId, self.bridge_id, BRIDGE_JOB_UNLOCK
|
||||
)
|
||||
except (ApiException, ClientError) as err:
|
||||
raise HomeAssistantError from err
|
16
homeassistant/components/igloohome/utils.py
Normal file
16
homeassistant/components/igloohome/utils.py
Normal file
@ -0,0 +1,16 @@
|
||||
"""House utility functions."""
|
||||
|
||||
from igloohome_api import DEVICE_TYPE_BRIDGE, GetDeviceInfoResponse
|
||||
|
||||
|
||||
def get_linked_bridge(
|
||||
device_id: str, devices: list[GetDeviceInfoResponse]
|
||||
) -> str | None:
|
||||
"""Return the ID of the bridge that is linked to the device. None if no bridge is linked."""
|
||||
bridges = (bridge for bridge in devices if bridge.type == DEVICE_TYPE_BRIDGE)
|
||||
for bridge in bridges:
|
||||
if device_id in (
|
||||
linked_device.deviceId for linked_device in bridge.linkedDevices
|
||||
):
|
||||
return bridge.deviceId
|
||||
return None
|
@ -3,7 +3,7 @@
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from igloohome_api import GetDeviceInfoResponse, GetDevicesResponse
|
||||
from igloohome_api import GetDeviceInfoResponse, GetDevicesResponse, LinkedDevice
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.igloohome.const import DOMAIN
|
||||
@ -23,6 +23,28 @@ GET_DEVICE_INFO_RESPONSE_LOCK = GetDeviceInfoResponse(
|
||||
batteryLevel=100,
|
||||
)
|
||||
|
||||
GET_DEVICE_INFO_RESPONSE_BRIDGE_LINKED_LOCK = GetDeviceInfoResponse(
|
||||
id="001",
|
||||
type="Bridge",
|
||||
deviceId="EB1X04eeeeee",
|
||||
deviceName="Home Bridge",
|
||||
pairedAt="2024-11-09T12:19:25+00:00",
|
||||
homeId=[],
|
||||
linkedDevices=[LinkedDevice(type="Lock", deviceId="OE1X123cbb11")],
|
||||
batteryLevel=None,
|
||||
)
|
||||
|
||||
GET_DEVICE_INFO_RESPONSE_BRIDGE_NO_LINKED_DEVICE = GetDeviceInfoResponse(
|
||||
id="001",
|
||||
type="Bridge",
|
||||
deviceId="EB1X04eeeeee",
|
||||
deviceName="Home Bridge",
|
||||
pairedAt="2024-11-09T12:19:25+00:00",
|
||||
homeId=[],
|
||||
linkedDevices=[],
|
||||
batteryLevel=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
@ -66,7 +88,10 @@ def mock_api() -> Generator[AsyncMock]:
|
||||
api = api_mock.return_value
|
||||
api.get_devices.return_value = GetDevicesResponse(
|
||||
nextCursor="",
|
||||
payload=[GET_DEVICE_INFO_RESPONSE_LOCK],
|
||||
payload=[
|
||||
GET_DEVICE_INFO_RESPONSE_LOCK,
|
||||
GET_DEVICE_INFO_RESPONSE_BRIDGE_LINKED_LOCK,
|
||||
],
|
||||
)
|
||||
api.get_device_info.return_value = GET_DEVICE_INFO_RESPONSE_LOCK
|
||||
yield api
|
||||
|
50
tests/components/igloohome/snapshots/test_lock.ambr
Normal file
50
tests/components/igloohome/snapshots/test_lock.ambr
Normal file
@ -0,0 +1,50 @@
|
||||
# serializer version: 1
|
||||
# name: test_lock[lock.front_door-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'lock',
|
||||
'entity_category': None,
|
||||
'entity_id': 'lock.front_door',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'igloohome',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': <LockEntityFeature: 1>,
|
||||
'translation_key': None,
|
||||
'unique_id': 'lock_OE1X123cbb11',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_lock[lock.front_door-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'assumed_state': True,
|
||||
'friendly_name': 'Front Door',
|
||||
'supported_features': <LockEntityFeature: 1>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'lock.front_door',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
26
tests/components/igloohome/test_lock.py
Normal file
26
tests/components/igloohome/test_lock.py
Normal file
@ -0,0 +1,26 @@
|
||||
"""Test lock module for igloohome integration."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
async def test_lock(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test lock entity created."""
|
||||
with patch("homeassistant.components.igloohome.PLATFORMS", [Platform.LOCK]):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
31
tests/components/igloohome/test_utils.py
Normal file
31
tests/components/igloohome/test_utils.py
Normal file
@ -0,0 +1,31 @@
|
||||
"""Test functions in utils module."""
|
||||
|
||||
from homeassistant.components.igloohome.utils import get_linked_bridge
|
||||
|
||||
from .conftest import (
|
||||
GET_DEVICE_INFO_RESPONSE_BRIDGE_LINKED_LOCK,
|
||||
GET_DEVICE_INFO_RESPONSE_BRIDGE_NO_LINKED_DEVICE,
|
||||
GET_DEVICE_INFO_RESPONSE_LOCK,
|
||||
)
|
||||
|
||||
|
||||
def test_get_linked_bridge_expect_bridge_id_returned() -> None:
|
||||
"""Test that get_linked_bridge returns the bridge ID."""
|
||||
assert (
|
||||
get_linked_bridge(
|
||||
GET_DEVICE_INFO_RESPONSE_LOCK.deviceId,
|
||||
[GET_DEVICE_INFO_RESPONSE_BRIDGE_LINKED_LOCK],
|
||||
)
|
||||
== GET_DEVICE_INFO_RESPONSE_BRIDGE_LINKED_LOCK.deviceId
|
||||
)
|
||||
|
||||
|
||||
def test_get_linked_bridge_expect_none_returned() -> None:
|
||||
"""Test that get_linked_bridge returns None."""
|
||||
assert (
|
||||
get_linked_bridge(
|
||||
GET_DEVICE_INFO_RESPONSE_LOCK.deviceId,
|
||||
[GET_DEVICE_INFO_RESPONSE_BRIDGE_NO_LINKED_DEVICE],
|
||||
)
|
||||
is None
|
||||
)
|
Loading…
x
Reference in New Issue
Block a user