Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
536 changes: 535 additions & 1 deletion tests/manager/test_data.py

Large diffs are not rendered by default.

51 changes: 51 additions & 0 deletions tests/test_api_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from yalexs import api_async
from yalexs.api_async import ApiAsync, _raise_response_exceptions
from yalexs.api_common import (
API_GET_CAPABILITIES_URL,
API_GET_DOORBELL_URL,
API_GET_DOORBELLS_URL,
API_GET_HOUSE_ACTIVITIES_URL,
Expand Down Expand Up @@ -462,6 +463,56 @@ async def test_async_get_lock_with_unlatch(self, mock):

assert lock.unlatch_supported is True

@aioresponses()
async def test_async_get_lock_capabilities(self, mock):
capabilities_response = {
"lock": {
"concurrentBLE": 2,
"batteryType": "AA",
"doorSense": True,
"hasMagnetometer": False,
"hasIntegratedWiFi": False,
"scheduledSmartAlerts": True,
"standalone": False,
"bluetooth": True,
"slotRange": None,
"integratedKeypad": True,
"entryCodeSlots": True,
"pinSlotMax": 100,
"pinSlotMin": 1,
"supportsRFID": True,
"supportsRFIDLegacy": False,
"supportsRFIDCredential": True,
"supportsRFIDOnlyAccess": True,
"supportsRFIDWithCode": False,
"supportsSecureMode": False,
"supportsSecureModeCodeDisable": False,
"supportsSecureModeMobileControl": False,
"supportsFingerprintCredential": True,
"supportsDeliveryMode": False,
"supportsSchedulePerUser": True,
"supportsFingerprintOnlyAccess": True,
"batteryLifeMS": 21513600000,
"supportedPartners": [],
"unlatch": True,
}
}

serial_number = "TEST123"
mock.get(
ApiCommon(DEFAULT_BRAND).get_brand_url(API_GET_CAPABILITIES_URL)
+ f"?serialNumber={serial_number}&topLevelHost=true",
payload=capabilities_response,
)

api = ApiAsync(ClientSession())
capabilities = await api.async_get_lock_capabilities(
ACCESS_TOKEN, serial_number
)

self.assertEqual(capabilities, capabilities_response)
self.assertTrue(capabilities["lock"]["unlatch"])

@aioresponses()
async def test_async_get_v2_lock_detail_bridge_online(self, mock):
mock.get(
Expand Down
191 changes: 191 additions & 0 deletions tests/test_capabilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
"""Tests for device capabilities functionality."""

from typing import Any

import pytest
from aiohttp import ClientSession
from aioresponses import aioresponses

from yalexs.api_async import ApiAsync
from yalexs.api_common import API_GET_CAPABILITIES_URL, ApiCommon
from yalexs.capabilities import CapabilitiesResponse
from yalexs.const import DEFAULT_BRAND
from yalexs.lock import LockDetail

ACCESS_TOKEN = "test-token"


@pytest.mark.asyncio
async def test_async_get_device_capabilities() -> None:
"""Test fetching device capabilities from the API."""
capabilities_response: CapabilitiesResponse = {
"lock": {
"concurrentBLE": 2,
"batteryType": "AA",
"doorSense": True,
"hasMagnetometer": False,
"hasIntegratedWiFi": False,
"scheduledSmartAlerts": True,
"standalone": False,
"bluetooth": True,
"slotRange": None,
"integratedKeypad": True,
"entryCodeSlots": True,
"pinSlotMax": 100,
"pinSlotMin": 1,
"supportsRFID": True,
"supportsRFIDLegacy": False,
"supportsRFIDCredential": True,
"supportsRFIDOnlyAccess": True,
"supportsRFIDWithCode": False,
"supportsSecureMode": False,
"supportsSecureModeCodeDisable": False,
"supportsSecureModeMobileControl": False,
"supportsFingerprintCredential": True,
"supportsDeliveryMode": False,
"supportsSchedulePerUser": True,
"supportsFingerprintOnlyAccess": True,
"batteryLifeMS": 21513600000,
"supportedPartners": [],
"unlatch": True,
}
}

serial_number = "TEST123"

with aioresponses() as mock:
mock.get(
ApiCommon(DEFAULT_BRAND).get_brand_url(API_GET_CAPABILITIES_URL)
+ f"?serialNumber={serial_number}&topLevelHost=true",
payload=capabilities_response,
)

async with ClientSession() as session:
api = ApiAsync(session)
capabilities = await api.async_get_lock_capabilities(
ACCESS_TOKEN, serial_number
)

assert capabilities == capabilities_response
assert capabilities["lock"]["unlatch"] is True


def test_lock_detail_unlatch_supported_with_capabilities() -> None:
"""Test that LockDetail uses capabilities for unlatch_supported when available."""
lock_data: dict[str, Any] = {
"LockID": "test-lock-id",
"LockName": "Test Lock",
"HouseID": "test-house",
"SerialNumber": "ABC123",
"currentFirmwareVersion": "1.0.0",
"Type": 5, # Type that doesn't normally support unlatch
"battery": 0.85,
"LockStatus": {"status": "locked", "doorState": "closed"},
}

# Create lock detail without capabilities
lock_detail = LockDetail(lock_data)

# Should be False based on Type
assert lock_detail.unlatch_supported is False

# Set capabilities that indicate unlatch is supported
capabilities: CapabilitiesResponse = {
"lock": {
"unlatch": True,
"doorSense": True,
"batteryType": "AA",
}
}
lock_detail.set_capabilities(capabilities)

# Now should be True based on capabilities
assert lock_detail.unlatch_supported is True


def test_lock_detail_unlatch_supported_fallback_to_type() -> None:
"""Test that LockDetail falls back to Type-based check when no capabilities."""
lock_data: dict[str, Any] = {
"LockID": "test-lock-id",
"LockName": "Test Lock",
"HouseID": "test-house",
"SerialNumber": "ABC123",
"currentFirmwareVersion": "1.0.0",
"Type": 17, # Type 17 supports unlatch
"battery": 0.85,
"LockStatus": {"status": "locked", "doorState": "closed"},
}

# Create lock detail without capabilities
lock_detail = LockDetail(lock_data)

# Should be True based on Type 17
assert lock_detail.unlatch_supported is True


def test_lock_detail_unlatch_supported_capabilities_override() -> None:
"""Test that capabilities override Type-based unlatch support."""
lock_data: dict[str, Any] = {
"LockID": "test-lock-id",
"LockName": "Test Lock",
"HouseID": "test-house",
"SerialNumber": "ABC123",
"currentFirmwareVersion": "1.0.0",
"Type": 17, # Type 17 normally supports unlatch
"battery": 0.85,
"LockStatus": {"status": "locked", "doorState": "closed"},
}

# Create lock detail
lock_detail = LockDetail(lock_data)

# Should be True based on Type 17
assert lock_detail.unlatch_supported is True

# Set capabilities that indicate unlatch is NOT supported
capabilities: CapabilitiesResponse = {
"lock": {
"unlatch": False, # Override: not supported
"doorSense": True,
"batteryType": "AA",
}
}
lock_detail.set_capabilities(capabilities)

# Now should be False based on capabilities override
assert lock_detail.unlatch_supported is False


def test_lock_detail_set_capabilities() -> None:
"""Test setting capabilities on a lock detail."""
lock_data: dict[str, Any] = {
"LockID": "test-lock-id",
"LockName": "Test Lock",
"HouseID": "test-house",
"SerialNumber": "ABC123",
"currentFirmwareVersion": "1.0.0",
"Type": 5,
"battery": 0.85,
"LockStatus": {"status": "locked", "doorState": "closed"},
}

lock_detail = LockDetail(lock_data)

# Initially no capabilities
assert lock_detail._capabilities is None

# Set capabilities
capabilities: CapabilitiesResponse = {
"lock": {
"unlatch": True,
"doorSense": True,
"batteryType": "AA",
"pinSlotMax": 100,
"pinSlotMin": 1,
}
}
lock_detail.set_capabilities(capabilities)

# Verify capabilities are stored
assert lock_detail._capabilities == capabilities
assert lock_detail._capabilities["lock"]["unlatch"] is True
13 changes: 12 additions & 1 deletion yalexs/api_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
import asyncio
import logging
from http import HTTPStatus
from typing import Any
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
from .capabilities import CapabilitiesResponse

from aiohttp import (
ClientConnectionError,
Expand Down Expand Up @@ -237,6 +240,14 @@ async def async_get_pins(self, access_token: str, lock_id: str) -> list[Pin]:

return [Pin(pin_json) for pin_json in json_dict.get("loaded", [])]

async def async_get_lock_capabilities(
self, access_token: str, serial_number: str
) -> CapabilitiesResponse:
response = await self._async_dict_to_api(
self._build_get_capabilities_request(access_token, serial_number)
)
return await response.json()

async def _async_call_lock_operation(
self, url_str: str, access_token: str, lock_id: str
) -> dict[str, Any]:
Expand Down
10 changes: 10 additions & 0 deletions yalexs/api_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
API_GET_ALARMS_URL = "/users/alarms/mine"
API_GET_ALARM_DEVICES_URL = "/alarms/{alarm_id}/devices"
API_PUT_ALARM_URL = "/alarms/{alarm_id}/state/{arm_state}"
API_GET_CAPABILITIES_URL = "/devices/capabilities"


_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -337,6 +338,15 @@ def _build_get_pins_request(
"url": self.get_brand_url(API_GET_PINS_URL.format(lock_id=lock_id)),
}

def _build_get_capabilities_request(
self, access_token: str, serial_number: str
) -> dict[str, Any]:
return {
**self._build_base_request(access_token),
"url": self.get_brand_url(API_GET_CAPABILITIES_URL),
"params": {"serialNumber": serial_number, "topLevelHost": "true"},
}

def _build_refresh_access_token_request(self, access_token: str) -> dict[str, Any]:
return {
**self._build_base_request(access_token),
Expand Down
42 changes: 42 additions & 0 deletions yalexs/capabilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Device capabilities type definitions."""

from typing import Any, TypedDict


class LockCapabilities(TypedDict, total=False):
"""TypedDict for lock capabilities."""

concurrentBLE: int
batteryType: str
doorSense: bool
hasMagnetometer: bool
hasIntegratedWiFi: bool
scheduledSmartAlerts: bool
standalone: bool
bluetooth: bool
slotRange: Any # Can be None or other values
integratedKeypad: bool
entryCodeSlots: bool
pinSlotMax: int
pinSlotMin: int
supportsRFID: bool
supportsRFIDLegacy: bool
supportsRFIDCredential: bool
supportsRFIDOnlyAccess: bool
supportsRFIDWithCode: bool
supportsSecureMode: bool
supportsSecureModeCodeDisable: bool
supportsSecureModeMobileControl: bool
supportsFingerprintCredential: bool
supportsDeliveryMode: bool
supportsSchedulePerUser: bool
supportsFingerprintOnlyAccess: bool
batteryLifeMS: int
supportedPartners: list[str]
unlatch: bool


class CapabilitiesResponse(TypedDict, total=False):
"""TypedDict for the full capabilities API response."""

lock: LockCapabilities
Loading