bachya/simplisafe-python

View on GitHub
simplipy/device/lock.py

Summary

Maintainability
A
0 mins
Test Coverage
"""Define a SimpliSafe lock."""

from __future__ import annotations

from collections.abc import Awaitable, Callable
from enum import Enum
from typing import TYPE_CHECKING, Any, cast

from simplipy.const import LOGGER
from simplipy.device import DeviceTypes, DeviceV3

if TYPE_CHECKING:
    from simplipy.system import System


class LockStates(Enum):
    """States that a lock can be in."""

    UNLOCKED = 0
    LOCKED = 1
    JAMMED = 2
    UNKNOWN = 99


class Lock(DeviceV3):
    """A lock that works with V3 systems.

    Note that this class shouldn't be instantiated directly; it will be
    instantiated as appropriate via :meth:`simplipy.API.async_get_systems`.

    Args:
        request: The request method from the :meth:`simplipy.API` object.
        system: A :meth:`simplipy.system.System` object (or one of its subclasses).
        device_type: The type of device represented.
        serial: The serial number of the device.
    """

    class _InternalStates(Enum):
        """Define an enum to map internal lock states to values we understand."""

        LOCKED = 1
        UNLOCKED = 2

    def __init__(
        self,
        request: Callable[..., Awaitable[dict[str, Any]]],
        system: System,
        device_type: DeviceTypes,
        serial: str,
    ) -> None:
        """Initialize.

        Args:
            request: The request method from the :meth:`simplipy.API` object.
            system: A :meth:`simplipy.system.System` object (or one of its subclasses).
            device_type: The type of device represented.
            serial: The serial number of the device.
        """
        super().__init__(system, device_type, serial)

        self._request = request

    @property
    def disabled(self) -> bool:
        """Return whether the lock is disabled.

        Returns:
            The lock's disable status.
        """
        return cast(
            bool, self._system.sensor_data[self._serial]["status"]["lockDisabled"]
        )

    @property
    def lock_low_battery(self) -> bool:
        """Return whether the lock's battery is low.

        Returns:
            The lock's low battery status.
        """
        return cast(
            bool, self._system.sensor_data[self._serial]["status"]["lockLowBattery"]
        )

    @property
    def pin_pad_low_battery(self) -> bool:
        """Return whether the pin pad's battery is low.

        Returns:
            The pinpad's low battery status.
        """
        return cast(
            bool, self._system.sensor_data[self._serial]["status"]["pinPadLowBattery"]
        )

    @property
    def pin_pad_offline(self) -> bool:
        """Return whether the pin pad is offline.

        Returns:
            The pinpad's offline status.
        """
        return cast(
            bool, self._system.sensor_data[self._serial]["status"]["pinPadOffline"]
        )

    @property
    def state(self) -> LockStates:
        """Return the current state of the lock.

        Returns:
            The lock's state.
        """
        if bool(self._system.sensor_data[self._serial]["status"]["lockJamState"]):
            return LockStates.JAMMED

        raw_state = self._system.sensor_data[self._serial]["status"]["lockState"]

        try:
            internal_state = self._InternalStates(raw_state)
        except ValueError:
            LOGGER.error("Unknown raw lock state: %s", raw_state)
            return LockStates.UNKNOWN

        if internal_state == self._InternalStates.LOCKED:
            return LockStates.LOCKED
        return LockStates.UNLOCKED

    def as_dict(self) -> dict[str, Any]:
        """Return dictionary version of this device.

        Returns:
            A dict representation of this device.
        """
        return {
            **super().as_dict(),
            "disabled": self.disabled,
            "lock_low_battery": self.lock_low_battery,
            "pin_pad_low_battery": self.pin_pad_low_battery,
            "pin_pad_offline": self.pin_pad_offline,
            "state": self.state.value,
        }

    async def async_lock(self) -> None:
        """Lock the lock."""
        await self._request(
            "post",
            f"doorlock/{self._system.system_id}/{self.serial}/state",
            json={"state": "lock"},
        )

        # Update the internal state representation:
        self._system.sensor_data[self._serial]["status"]["lockState"] = (
            self._InternalStates.LOCKED.value
        )

    async def async_unlock(self) -> None:
        """Unlock the lock."""
        await self._request(
            "post",
            f"doorlock/{self._system.system_id}/{self.serial}/state",
            json={"state": "unlock"},
        )

        # Update the internal state representation:
        self._system.sensor_data[self._serial]["status"]["lockState"] = (
            self._InternalStates.UNLOCKED.value
        )