1
0
mirror of https://github.com/home-assistant/core.git synced 2026-04-02 08:26:41 +01:00

Add support for switchbot keypad vision (#160484)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
Retha Runolfsson
2026-03-05 21:54:07 +08:00
committed by GitHub
parent 69a98dd53e
commit 05d57167d2
5 changed files with 235 additions and 3 deletions

View File

@@ -127,8 +127,16 @@ PLATFORMS_BY_TYPE = {
Platform.BINARY_SENSOR,
Platform.BUTTON,
],
SupportedModels.KEYPAD_VISION.value: [Platform.SENSOR, Platform.BINARY_SENSOR],
SupportedModels.KEYPAD_VISION_PRO.value: [Platform.SENSOR, Platform.BINARY_SENSOR],
SupportedModels.KEYPAD_VISION.value: [
Platform.SENSOR,
Platform.BINARY_SENSOR,
Platform.EVENT,
],
SupportedModels.KEYPAD_VISION_PRO.value: [
Platform.SENSOR,
Platform.BINARY_SENSOR,
Platform.EVENT,
],
}
CLASS_BY_DEVICE = {
SupportedModels.CEILING_LIGHT.value: switchbot.SwitchbotCeilingLight,

View File

@@ -85,10 +85,13 @@ BINARY_SENSOR_TYPES: dict[str, SwitchbotBinarySensorEntityDescription] = {
),
"battery_charging": SwitchbotBinarySensorEntityDescription(
key="battery_charging",
translation_key="battery_charging",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
),
"tamper_alarm": SwitchbotBinarySensorEntityDescription(
key="tamper_alarm",
device_class=BinarySensorDeviceClass.TAMPER,
),
}

View File

@@ -0,0 +1,63 @@
"""Support for SwitchBot event entities."""
from __future__ import annotations
from homeassistant.components.event import (
EventDeviceClass,
EventEntity,
EventEntityDescription,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import SwitchbotConfigEntry, SwitchbotDataUpdateCoordinator
from .entity import SwitchbotEntity
PARALLEL_UPDATES = 0
EVENT_TYPES = {
"doorbell": EventEntityDescription(
key="doorbell",
device_class=EventDeviceClass.DOORBELL,
event_types=["ring"],
),
}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: SwitchbotConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the SwitchBot event platform."""
coordinator = config_entry.runtime_data
async_add_entities(
SwitchbotEventEntity(coordinator, event, description)
for event, description in EVENT_TYPES.items()
if event in coordinator.device.parsed_data
)
class SwitchbotEventEntity(SwitchbotEntity, EventEntity):
"""Representation of a SwitchBot event."""
def __init__(
self,
coordinator: SwitchbotDataUpdateCoordinator,
event: str,
description: EventEntityDescription,
) -> None:
"""Initialize the SwitchBot event."""
super().__init__(coordinator)
self._event = event
self.entity_description = description
self._attr_unique_id = f"{coordinator.base_unique_id}-{event}"
self._previous_value = False
@callback
def _async_update_attrs(self) -> None:
"""Update the entity attributes."""
value = bool(self.parsed_data.get(self._event, False))
if value and not self._previous_value:
self._trigger_event("ring")
self._previous_value = value

View File

@@ -0,0 +1,85 @@
"""Test the switchbot event entities."""
from collections.abc import Callable
from unittest.mock import patch
import pytest
from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
from homeassistant.components.switchbot.const import DOMAIN
from homeassistant.const import STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from . import KEYPAD_VISION_PRO_INFO
from tests.common import MockConfigEntry
from tests.components.bluetooth import (
generate_advertisement_data,
generate_ble_device,
inject_bluetooth_service_info,
)
def _with_doorbell_event(
info: BluetoothServiceInfoBleak,
) -> BluetoothServiceInfoBleak:
"""Return a BLE service info with the doorbell bit set."""
mfr_data = bytearray(info.manufacturer_data[2409])
mfr_data[12] |= 0b00001000
updated_mfr_data = {2409: bytes(mfr_data)}
return BluetoothServiceInfoBleak(
name=info.name,
manufacturer_data=updated_mfr_data,
service_data=info.service_data,
service_uuids=info.service_uuids,
address=info.address,
rssi=info.rssi,
source=info.source,
advertisement=generate_advertisement_data(
local_name=info.name,
manufacturer_data=updated_mfr_data,
service_data=info.service_data,
service_uuids=info.service_uuids,
),
device=generate_ble_device(info.address, info.name),
time=info.time,
connectable=info.connectable,
tx_power=info.tx_power,
)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_keypad_vision_pro_doorbell_event(
hass: HomeAssistant,
mock_entry_encrypted_factory: Callable[[str], MockConfigEntry],
) -> None:
"""Test keypad vision pro doorbell event entity (encrypted device)."""
await async_setup_component(hass, DOMAIN, {})
inject_bluetooth_service_info(hass, KEYPAD_VISION_PRO_INFO)
entry = mock_entry_encrypted_factory(sensor_type="keypad_vision_pro")
entry.add_to_hass(hass)
with patch(
"homeassistant.components.switchbot.sensor.switchbot.SwitchbotKeypadVision.update",
return_value=True,
):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
entity_id = "event.test_name_doorbell"
state = hass.states.get(entity_id)
assert state
assert state.state == STATE_UNKNOWN
inject_bluetooth_service_info(
hass, _with_doorbell_event(KEYPAD_VISION_PRO_INFO)
)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state
assert state.state != STATE_UNKNOWN
assert state.attributes["event_type"] == "ring"
assert state.attributes["event_types"] == ["ring"]

View File

@@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, patch
import pytest
from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
from homeassistant.components.sensor import ATTR_STATE_CLASS
from homeassistant.components.switchbot.const import (
CONF_ENCRYPTION_KEY,
@@ -18,6 +19,7 @@ from homeassistant.const import (
CONF_NAME,
CONF_PASSWORD,
CONF_SENSOR_TYPE,
STATE_OFF,
STATE_ON,
)
from homeassistant.core import HomeAssistant
@@ -29,6 +31,8 @@ from . import (
EVAPORATIVE_HUMIDIFIER_SERVICE_INFO,
HUB3_SERVICE_INFO,
HUBMINI_MATTER_SERVICE_INFO,
KEYPAD_VISION_INFO,
KEYPAD_VISION_PRO_INFO,
LEAK_SERVICE_INFO,
PLUG_MINI_EU_SERVICE_INFO,
PRESENCE_SENSOR_SERVICE_INFO,
@@ -843,3 +847,72 @@ async def test_presence_sensor(hass: HomeAssistant) -> None:
assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize(
("adv_info", "sensor_type", "charging_state"),
[
(KEYPAD_VISION_INFO, "keypad_vision", STATE_ON),
(KEYPAD_VISION_PRO_INFO, "keypad_vision_pro", STATE_OFF),
],
)
async def test_keypad_vision_sensor(
hass: HomeAssistant,
adv_info: BluetoothServiceInfoBleak,
sensor_type: str,
charging_state: str,
) -> None:
"""Test setting up creates the sensors for Keypad Vision (Pro)."""
await async_setup_component(hass, DOMAIN, {})
inject_bluetooth_service_info(hass, adv_info)
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_ADDRESS: "AA:BB:CC:DD:EE:FF",
CONF_NAME: "test-name",
CONF_SENSOR_TYPE: sensor_type,
CONF_KEY_ID: "ff",
CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff",
},
unique_id="aabbccddeeff",
)
entry.add_to_hass(hass)
with patch(
"homeassistant.components.switchbot.sensor.switchbot.SwitchbotKeypadVision.update",
return_value=True,
):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert len(hass.states.async_all("sensor")) == 2
assert len(hass.states.async_all("binary_sensor")) == 2
battery_sensor = hass.states.get("sensor.test_name_battery")
battery_sensor_attrs = battery_sensor.attributes
assert battery_sensor
assert battery_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Battery"
assert battery_sensor_attrs[ATTR_STATE_CLASS] == "measurement"
rssi_sensor = hass.states.get("sensor.test_name_bluetooth_signal")
rssi_sensor_attrs = rssi_sensor.attributes
assert rssi_sensor
assert rssi_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Bluetooth signal"
assert rssi_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "dBm"
tamper_sensor = hass.states.get("binary_sensor.test_name_tamper")
tamper_sensor_attrs = tamper_sensor.attributes
assert tamper_sensor
assert tamper_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Tamper"
assert tamper_sensor.state == STATE_OFF
charging_sensor = hass.states.get("binary_sensor.test_name_charging")
charging_sensor_attrs = charging_sensor.attributes
assert charging_sensor
assert charging_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Charging"
assert charging_sensor.state == charging_state
assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()