From bb765449eb32802c4db4396d8c79fec26bc2e418 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Wed, 6 Sep 2023 09:03:54 -0400 Subject: [PATCH] Add binary_sensor to Schlage (#99637) * Add binary_sensor to Schlage * Apply suggestions from code review Co-authored-by: Joost Lekkerkerker --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/schlage/__init__.py | 7 +- .../components/schlage/binary_sensor.py | 92 +++++++++++++++++++ homeassistant/components/schlage/strings.json | 5 + tests/components/schlage/conftest.py | 1 + .../components/schlage/test_binary_sensor.py | 53 +++++++++++ 5 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/schlage/binary_sensor.py create mode 100644 tests/components/schlage/test_binary_sensor.py diff --git a/homeassistant/components/schlage/__init__.py b/homeassistant/components/schlage/__init__.py index cf95e190e88..feaa95864d5 100644 --- a/homeassistant/components/schlage/__init__.py +++ b/homeassistant/components/schlage/__init__.py @@ -11,7 +11,12 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN, LOGGER from .coordinator import SchlageDataUpdateCoordinator -PLATFORMS: list[Platform] = [Platform.LOCK, Platform.SENSOR, Platform.SWITCH] +PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, + Platform.LOCK, + Platform.SENSOR, + Platform.SWITCH, +] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/schlage/binary_sensor.py b/homeassistant/components/schlage/binary_sensor.py new file mode 100644 index 00000000000..749a961a53b --- /dev/null +++ b/homeassistant/components/schlage/binary_sensor.py @@ -0,0 +1,92 @@ +"""Platform for Schlage binary_sensor integration.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import LockData, SchlageDataUpdateCoordinator +from .entity import SchlageEntity + + +@dataclass +class SchlageBinarySensorEntityDescriptionMixin: + """Mixin for required keys.""" + + # NOTE: This has to be a mixin because these are required keys. + # BinarySensorEntityDescription has attributes with default values, + # which means we can't inherit from it because you haven't have + # non-default arguments follow default arguments in an initializer. + + value_fn: Callable[[LockData], bool] + + +@dataclass +class SchlageBinarySensorEntityDescription( + BinarySensorEntityDescription, SchlageBinarySensorEntityDescriptionMixin +): + """Entity description for a Schlage binary_sensor.""" + + +_DESCRIPTIONS: tuple[SchlageBinarySensorEntityDescription] = ( + SchlageBinarySensorEntityDescription( + key="keypad_disabled", + translation_key="keypad_disabled", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.lock.keypad_disabled(data.logs), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up binary_sensors based on a config entry.""" + coordinator: SchlageDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + entities = [] + for device_id in coordinator.data.locks: + for description in _DESCRIPTIONS: + entities.append( + SchlageBinarySensor( + coordinator=coordinator, + description=description, + device_id=device_id, + ) + ) + async_add_entities(entities) + + +class SchlageBinarySensor(SchlageEntity, BinarySensorEntity): + """Schlage binary_sensor entity.""" + + entity_description: SchlageBinarySensorEntityDescription + + def __init__( + self, + coordinator: SchlageDataUpdateCoordinator, + description: SchlageBinarySensorEntityDescription, + device_id: str, + ) -> None: + """Initialize a SchlageBinarySensor.""" + super().__init__(coordinator, device_id) + self.entity_description = description + self._attr_unique_id = f"{device_id}_{self.entity_description.key}" + + @property + def is_on(self) -> bool | None: + """Return true if the binary_sensor is on.""" + return self.entity_description.value_fn(self._lock_data) diff --git a/homeassistant/components/schlage/strings.json b/homeassistant/components/schlage/strings.json index f3612bb96b8..076ed97e298 100644 --- a/homeassistant/components/schlage/strings.json +++ b/homeassistant/components/schlage/strings.json @@ -17,6 +17,11 @@ } }, "entity": { + "binary_sensor": { + "keypad_disabled": { + "name": "Keypad disabled" + } + }, "switch": { "beeper": { "name": "Keypress Beep" diff --git a/tests/components/schlage/conftest.py b/tests/components/schlage/conftest.py index 0078e6a5553..7b610a6b4da 100644 --- a/tests/components/schlage/conftest.py +++ b/tests/components/schlage/conftest.py @@ -85,4 +85,5 @@ def mock_lock(): ) mock_lock.logs.return_value = [] mock_lock.last_changed_by.return_value = "thumbturn" + mock_lock.keypad_disabled.return_value = False return mock_lock diff --git a/tests/components/schlage/test_binary_sensor.py b/tests/components/schlage/test_binary_sensor.py new file mode 100644 index 00000000000..4673f263c8c --- /dev/null +++ b/tests/components/schlage/test_binary_sensor.py @@ -0,0 +1,53 @@ +"""Test Schlage binary_sensor.""" + +from datetime import timedelta +from unittest.mock import Mock + +from pyschlage.exceptions import UnknownError + +from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.util.dt import utcnow + +from tests.common import async_fire_time_changed + + +async def test_keypad_disabled_binary_sensor( + hass: HomeAssistant, mock_lock: Mock, mock_added_config_entry: ConfigEntry +) -> None: + """Test the keypad_disabled binary_sensor.""" + mock_lock.keypad_disabled.reset_mock() + mock_lock.keypad_disabled.return_value = True + + # Make the coordinator refresh data. + async_fire_time_changed(hass, utcnow() + timedelta(seconds=31)) + await hass.async_block_till_done() + + keypad = hass.states.get("binary_sensor.vault_door_keypad_disabled") + assert keypad is not None + assert keypad.state == "on" + assert keypad.attributes["device_class"] == BinarySensorDeviceClass.PROBLEM + + mock_lock.keypad_disabled.assert_called_once_with([]) + + +async def test_keypad_disabled_binary_sensor_use_previous_logs_on_failure( + hass: HomeAssistant, mock_lock: Mock, mock_added_config_entry: ConfigEntry +) -> None: + """Test the keypad_disabled binary_sensor.""" + mock_lock.keypad_disabled.reset_mock() + mock_lock.keypad_disabled.return_value = True + mock_lock.logs.reset_mock() + mock_lock.logs.side_effect = UnknownError("Cannot load logs") + + # Make the coordinator refresh data. + async_fire_time_changed(hass, utcnow() + timedelta(seconds=31)) + await hass.async_block_till_done() + + keypad = hass.states.get("binary_sensor.vault_door_keypad_disabled") + assert keypad is not None + assert keypad.state == "on" + assert keypad.attributes["device_class"] == BinarySensorDeviceClass.PROBLEM + + mock_lock.keypad_disabled.assert_called_once_with([])