Skip to content
Snippets Groups Projects
Unverified Commit ff622af8 authored by Keith's avatar Keith Committed by GitHub
Browse files

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: default avatarJosef Zweck <josef@zweck.dev>
parent 8b4d9f96
No related branches found
No related tags found
No related merge requests found
...@@ -19,7 +19,7 @@ from homeassistant.core import HomeAssistant ...@@ -19,7 +19,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
PLATFORMS: list[Platform] = [Platform.SENSOR] PLATFORMS: list[Platform] = [Platform.LOCK, Platform.SENSOR]
@dataclass @dataclass
...@@ -35,7 +35,6 @@ type IgloohomeConfigEntry = ConfigEntry[IgloohomeRuntimeData] ...@@ -35,7 +35,6 @@ type IgloohomeConfigEntry = ConfigEntry[IgloohomeRuntimeData]
async def async_setup_entry(hass: HomeAssistant, entry: IgloohomeConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: IgloohomeConfigEntry) -> bool:
"""Set up igloohome from a config entry.""" """Set up igloohome from a config entry."""
authentication = IgloohomeAuth( authentication = IgloohomeAuth(
session=async_get_clientsession(hass), session=async_get_clientsession(hass),
client_id=entry.data[CONF_CLIENT_ID], client_id=entry.data[CONF_CLIENT_ID],
......
"""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
"""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 @@ ...@@ -3,7 +3,7 @@
from collections.abc import Generator from collections.abc import Generator
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
from igloohome_api import GetDeviceInfoResponse, GetDevicesResponse from igloohome_api import GetDeviceInfoResponse, GetDevicesResponse, LinkedDevice
import pytest import pytest
from homeassistant.components.igloohome.const import DOMAIN from homeassistant.components.igloohome.const import DOMAIN
...@@ -23,6 +23,28 @@ GET_DEVICE_INFO_RESPONSE_LOCK = GetDeviceInfoResponse( ...@@ -23,6 +23,28 @@ GET_DEVICE_INFO_RESPONSE_LOCK = GetDeviceInfoResponse(
batteryLevel=100, 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 @pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]: def mock_setup_entry() -> Generator[AsyncMock]:
...@@ -66,7 +88,10 @@ def mock_api() -> Generator[AsyncMock]: ...@@ -66,7 +88,10 @@ def mock_api() -> Generator[AsyncMock]:
api = api_mock.return_value api = api_mock.return_value
api.get_devices.return_value = GetDevicesResponse( api.get_devices.return_value = GetDevicesResponse(
nextCursor="", 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 api.get_device_info.return_value = GET_DEVICE_INFO_RESPONSE_LOCK
yield api yield api
# 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',
})
# ---
"""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)
"""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
)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment