Add switch platform to Schlage (#98004)

* Add switch platform to Schlage

* Add a generic SchlageSwitch

* Use an is_on property instead of _attr_is_on

* Make value_fn always return a bool
pull/98116/head
David Knowles 2023-08-09 09:32:50 -04:00 committed by GitHub
parent e1f0b44ba4
commit 023f2f8bb7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 208 additions and 1 deletions

View File

@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant
from .const import DOMAIN, LOGGER
from .coordinator import SchlageDataUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.LOCK, Platform.SENSOR]
PLATFORMS: list[Platform] = [Platform.LOCK, Platform.SENSOR, Platform.SWITCH]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

View File

@ -15,5 +15,15 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"entity": {
"switch": {
"beeper": {
"name": "Keypress Beep"
},
"lock_and_leave": {
"name": "1-Touch Locking"
}
}
}
}

View File

@ -0,0 +1,123 @@
"""Platform for Schlage switch integration."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from functools import partial
from typing import Any
from pyschlage.lock import Lock
from homeassistant.components.switch import (
SwitchDeviceClass,
SwitchEntity,
SwitchEntityDescription,
)
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 SchlageDataUpdateCoordinator
from .entity import SchlageEntity
@dataclass
class SchlageSwitchEntityDescriptionMixin:
"""Mixin for required keys."""
# NOTE: This has to be a mixin because these are required keys.
# SwitchEntityDescription 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.
on_fn: Callable[[Lock], None]
off_fn: Callable[[Lock], None]
value_fn: Callable[[Lock], bool]
@dataclass
class SchlageSwitchEntityDescription(
SwitchEntityDescription, SchlageSwitchEntityDescriptionMixin
):
"""Entity description for a Schlage switch."""
SWITCHES: tuple[SchlageSwitchEntityDescription, ...] = (
SchlageSwitchEntityDescription(
key="beeper",
translation_key="beeper",
device_class=SwitchDeviceClass.SWITCH,
entity_category=EntityCategory.CONFIG,
on_fn=lambda lock: lock.set_beeper(True),
off_fn=lambda lock: lock.set_beeper(False),
value_fn=lambda lock: lock.beeper_enabled,
),
SchlageSwitchEntityDescription(
key="lock_and_leve",
translation_key="lock_and_leave",
device_class=SwitchDeviceClass.SWITCH,
entity_category=EntityCategory.CONFIG,
on_fn=lambda lock: lock.set_lock_and_leave(True),
off_fn=lambda lock: lock.set_lock_and_leave(False),
value_fn=lambda lock: lock.lock_and_leave_enabled,
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up switches 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 SWITCHES:
entities.append(
SchlageSwitch(
coordinator=coordinator,
description=description,
device_id=device_id,
)
)
async_add_entities(entities)
class SchlageSwitch(SchlageEntity, SwitchEntity):
"""Schlage switch entity."""
entity_description: SchlageSwitchEntityDescription
def __init__(
self,
coordinator: SchlageDataUpdateCoordinator,
description: SchlageSwitchEntityDescription,
device_id: str,
) -> None:
"""Initialize a SchlageSwitch."""
super().__init__(coordinator=coordinator, device_id=device_id)
self.entity_description = description
self._attr_unique_id = f"{device_id}_{self.entity_description.key}"
@property
def is_on(self) -> bool:
"""Return True if the switch is on."""
return self.entity_description.value_fn(self._lock)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
await self.hass.async_add_executor_job(
partial(self.entity_description.on_fn, self._lock)
)
await self.coordinator.async_request_refresh()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
await self.hass.async_add_executor_job(
partial(self.entity_description.off_fn, self._lock)
)
await self.coordinator.async_request_refresh()

View File

@ -80,6 +80,8 @@ def mock_lock():
is_jammed=False,
battery_level=20,
firmware_version="1.0",
lock_and_leave_enabled=True,
beeper_enabled=True,
)
mock_lock.logs.return_value = []
mock_lock.last_changed_by.return_value = "thumbturn"

View File

@ -0,0 +1,72 @@
"""Test schlage switch."""
from unittest.mock import Mock
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
async def test_switch_device_registry(
hass: HomeAssistant, mock_added_config_entry: ConfigEntry
) -> None:
"""Test switch is added to device registry."""
device_registry = dr.async_get(hass)
device = device_registry.async_get_device(identifiers={("schlage", "test")})
assert device.model == "<model-name>"
assert device.sw_version == "1.0"
assert device.name == "Vault Door"
assert device.manufacturer == "Schlage"
async def test_beeper_services(
hass: HomeAssistant, mock_lock: Mock, mock_added_config_entry: ConfigEntry
) -> None:
"""Test BeeperSwitch services."""
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
service_data={ATTR_ENTITY_ID: "switch.vault_door_keypress_beep"},
blocking=True,
)
await hass.async_block_till_done()
mock_lock.set_beeper.assert_called_once_with(False)
mock_lock.set_beeper.reset_mock()
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
service_data={ATTR_ENTITY_ID: "switch.vault_door_keypress_beep"},
blocking=True,
)
await hass.async_block_till_done()
mock_lock.set_beeper.assert_called_once_with(True)
await hass.config_entries.async_unload(mock_added_config_entry.entry_id)
async def test_lock_and_leave_services(
hass: HomeAssistant, mock_lock: Mock, mock_added_config_entry: ConfigEntry
) -> None:
"""Test LockAndLeaveSwitch services."""
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
service_data={ATTR_ENTITY_ID: "switch.vault_door_1_touch_locking"},
blocking=True,
)
await hass.async_block_till_done()
mock_lock.set_lock_and_leave.assert_called_once_with(False)
mock_lock.set_lock_and_leave.reset_mock()
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
service_data={ATTR_ENTITY_ID: "switch.vault_door_1_touch_locking"},
blocking=True,
)
await hass.async_block_till_done()
mock_lock.set_lock_and_leave.assert_called_once_with(True)
await hass.config_entries.async_unload(mock_added_config_entry.entry_id)