diff --git a/homeassistant/components/schlage/__init__.py b/homeassistant/components/schlage/__init__.py index 7991645e20d..95cfd16958c 100644 --- a/homeassistant/components/schlage/__init__.py +++ b/homeassistant/components/schlage/__init__.py @@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN, LOGGER from .coordinator import SchlageDataUpdateCoordinator -PLATFORMS: list[Platform] = [Platform.LOCK] +PLATFORMS: list[Platform] = [Platform.LOCK, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/schlage/entity.py b/homeassistant/components/schlage/entity.py new file mode 100644 index 00000000000..3a1a11bc098 --- /dev/null +++ b/homeassistant/components/schlage/entity.py @@ -0,0 +1,41 @@ +"""Base entity class for Schlage.""" + +from pyschlage.lock import Lock + +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, MANUFACTURER +from .coordinator import SchlageDataUpdateCoordinator + + +class SchlageEntity(CoordinatorEntity[SchlageDataUpdateCoordinator]): + """Base Schlage entity.""" + + _attr_has_entity_name = True + + def __init__( + self, coordinator: SchlageDataUpdateCoordinator, device_id: str + ) -> None: + """Initialize a Schlage entity.""" + super().__init__(coordinator=coordinator) + self.device_id = device_id + self._attr_unique_id = device_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device_id)}, + name=self._lock.name, + manufacturer=MANUFACTURER, + model=self._lock.model_name, + sw_version=self._lock.firmware_version, + ) + + @property + def _lock(self) -> Lock: + """Fetch the Schlage lock from our coordinator.""" + return self.coordinator.data.locks[self.device_id] + + @property + def available(self) -> bool: + """Return if entity is available.""" + # When is_locked is None the lock is unavailable. + return super().available and self._lock.is_locked is not None diff --git a/homeassistant/components/schlage/lock.py b/homeassistant/components/schlage/lock.py index ad7ff863d40..65758c3442f 100644 --- a/homeassistant/components/schlage/lock.py +++ b/homeassistant/components/schlage/lock.py @@ -3,17 +3,14 @@ from __future__ import annotations from typing import Any -from pyschlage.lock import Lock - from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, MANUFACTURER +from .const import DOMAIN from .coordinator import SchlageDataUpdateCoordinator +from .entity import SchlageEntity async def async_setup_entry( @@ -29,39 +26,18 @@ async def async_setup_entry( ) -class SchlageLockEntity(CoordinatorEntity[SchlageDataUpdateCoordinator], LockEntity): +class SchlageLockEntity(SchlageEntity, LockEntity): """Schlage lock entity.""" - _attr_has_entity_name = True _attr_name = None def __init__( self, coordinator: SchlageDataUpdateCoordinator, device_id: str ) -> None: """Initialize a Schlage Lock.""" - super().__init__(coordinator=coordinator) - self.device_id = device_id - self._attr_unique_id = device_id - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, device_id)}, - name=self._lock.name, - manufacturer=MANUFACTURER, - model=self._lock.model_name, - sw_version=self._lock.firmware_version, - ) + super().__init__(coordinator=coordinator, device_id=device_id) self._update_attrs() - @property - def _lock(self) -> Lock: - """Fetch the Schlage lock from our coordinator.""" - return self.coordinator.data.locks[self.device_id] - - @property - def available(self) -> bool: - """Return if entity is available.""" - # When is_locked is None the lock is unavailable. - return super().available and self._lock.is_locked is not None - @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" diff --git a/homeassistant/components/schlage/sensor.py b/homeassistant/components/schlage/sensor.py new file mode 100644 index 00000000000..aa2ff87e5bf --- /dev/null +++ b/homeassistant/components/schlage/sensor.py @@ -0,0 +1,66 @@ +"""Platform for Schlage sensor integration.""" + +from __future__ import annotations + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, EntityCategory +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import SchlageDataUpdateCoordinator +from .entity import SchlageEntity + +_SENSOR_DESCRIPTIONS: list[SensorEntityDescription] = [ + SensorEntityDescription( + key="battery_level", + device_class=SensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up sensors based on a config entry.""" + coordinator: SchlageDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + SchlageBatterySensor( + coordinator=coordinator, + description=description, + device_id=device_id, + ) + for description in _SENSOR_DESCRIPTIONS + for device_id in coordinator.data.locks + ) + + +class SchlageBatterySensor(SchlageEntity, SensorEntity): + """Schlage battery sensor entity.""" + + def __init__( + self, + coordinator: SchlageDataUpdateCoordinator, + description: SensorEntityDescription, + device_id: str, + ) -> None: + """Initialize a Schlage battery sensor.""" + super().__init__(coordinator=coordinator, device_id=device_id) + self.entity_description = description + self._attr_unique_id = f"{device_id}_{description.key}" + self._attr_native_value = getattr(self._lock, self.entity_description.key) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._attr_native_value = getattr(self._lock, self.entity_description.key) + return super()._handle_coordinator_update() diff --git a/tests/components/schlage/conftest.py b/tests/components/schlage/conftest.py index 681024358c6..3445d653a81 100644 --- a/tests/components/schlage/conftest.py +++ b/tests/components/schlage/conftest.py @@ -1,11 +1,13 @@ """Common fixtures for the Schlage tests.""" from collections.abc import Generator -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, Mock, create_autospec, patch +from pyschlage.lock import Lock import pytest from homeassistant.components.schlage.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -24,6 +26,23 @@ def mock_config_entry() -> MockConfigEntry: ) +@pytest.fixture +async def mock_added_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_pyschlage_auth: Mock, + mock_schlage: Mock, + mock_lock: Mock, +) -> MockConfigEntry: + """Mock ConfigEntry that's been added to HA.""" + mock_schlage.locks.return_value = [mock_lock] + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert DOMAIN in hass.config_entries.async_domains() + return mock_config_entry + + @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock, None, None]: """Override async_setup_entry.""" @@ -46,3 +65,19 @@ def mock_pyschlage_auth(): with patch("pyschlage.Auth", autospec=True) as mock_auth: mock_auth.return_value.user_id = "abc123" yield mock_auth.return_value + + +@pytest.fixture +def mock_lock(): + """Mock Lock fixture.""" + mock_lock = create_autospec(Lock) + mock_lock.configure_mock( + device_id="test", + name="Vault Door", + model_name="", + is_locked=False, + is_jammed=False, + battery_level=20, + firmware_version="1.0", + ) + return mock_lock diff --git a/tests/components/schlage/test_lock.py b/tests/components/schlage/test_lock.py index 8819d8558fd..b164b4f6b79 100644 --- a/tests/components/schlage/test_lock.py +++ b/tests/components/schlage/test_lock.py @@ -1,56 +1,15 @@ """Test schlage lock.""" -from unittest.mock import Mock, create_autospec - -from pyschlage.lock import Lock -import pytest +from unittest.mock import Mock from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN -from homeassistant.components.schlage.const import DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, SERVICE_LOCK, SERVICE_UNLOCK from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from tests.common import MockConfigEntry - - -@pytest.fixture -def mock_lock(): - """Mock Lock fixture.""" - mock_lock = create_autospec(Lock) - mock_lock.configure_mock( - device_id="test", - name="Vault Door", - model_name="", - is_locked=False, - is_jammed=False, - battery_level=0, - firmware_version="1.0", - ) - return mock_lock - - -@pytest.fixture -async def mock_entry( - hass: HomeAssistant, mock_pyschlage_auth: Mock, mock_schlage: Mock, mock_lock: Mock -) -> ConfigEntry: - """Create and add a mock ConfigEntry.""" - mock_schlage.locks.return_value = [mock_lock] - entry = MockConfigEntry( - domain=DOMAIN, - data={"username": "test-username", "password": "test-password"}, - entry_id="test-username", - ) - entry.add_to_hass(hass) - - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - assert DOMAIN in hass.config_entries.async_domains() - return entry - async def test_lock_device_registry( - hass: HomeAssistant, mock_entry: ConfigEntry + hass: HomeAssistant, mock_added_config_entry: ConfigEntry ) -> None: """Test lock is added to device registry.""" device_registry = dr.async_get(hass) @@ -62,7 +21,7 @@ async def test_lock_device_registry( async def test_lock_services( - hass: HomeAssistant, mock_lock: Mock, mock_entry: ConfigEntry + hass: HomeAssistant, mock_lock: Mock, mock_added_config_entry: ConfigEntry ) -> None: """Test lock services.""" await hass.services.async_call( @@ -83,4 +42,4 @@ async def test_lock_services( await hass.async_block_till_done() mock_lock.unlock.assert_called_once_with() - await hass.config_entries.async_unload(mock_entry.entry_id) + await hass.config_entries.async_unload(mock_added_config_entry.entry_id) diff --git a/tests/components/schlage/test_sensor.py b/tests/components/schlage/test_sensor.py new file mode 100644 index 00000000000..775438795ff --- /dev/null +++ b/tests/components/schlage/test_sensor.py @@ -0,0 +1,30 @@ +"""Test schlage sensor.""" + +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + + +async def test_sensor_device_registry( + hass: HomeAssistant, mock_added_config_entry: ConfigEntry +) -> None: + """Test sensor is added to device registry.""" + device_registry = dr.async_get(hass) + device = device_registry.async_get_device(identifiers={("schlage", "test")}) + assert device.model == "" + assert device.sw_version == "1.0" + assert device.name == "Vault Door" + assert device.manufacturer == "Schlage" + + +async def test_battery_sensor( + hass: HomeAssistant, mock_added_config_entry: ConfigEntry +) -> None: + """Test the battery sensor.""" + battery_sensor = hass.states.get("sensor.vault_door_battery") + assert battery_sensor is not None + assert battery_sensor.state == "20" + assert battery_sensor.attributes["unit_of_measurement"] == PERCENTAGE + assert battery_sensor.attributes["device_class"] == SensorDeviceClass.BATTERY