Add service for switchbot keypad vision (#160659)

Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
Retha Runolfsson
2026-01-29 12:23:38 +01:00
committed by GitHub
co-authored by Joostlek
parent 15ff5d0f74
commit dfe1990484
8 changed files with 493 additions and 1 deletions
+50
View File
@@ -1297,3 +1297,53 @@ ART_FRAME_INFO = BluetoothServiceInfoBleak(
connectable=True,
tx_power=-127,
)
KEYPAD_VISION_INFO = BluetoothServiceInfoBleak(
name="Keypad Vision",
manufacturer_data={
2409: b"\xb0\xe9\xfe\xe5\x04\x1e\xac\xdf\x00\x00\x00\x00\x00\x02"
},
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00\x00_\x01\x11\x03x"},
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
address="AA:BB:CC:DD:EE:FF",
rssi=-60,
source="local",
advertisement=generate_advertisement_data(
local_name="Keypad Vision",
manufacturer_data={
2409: b"\xb0\xe9\xfe\xe5\x04\x1e\xac\xdf\x00\x00\x00\x00\x00\x02"
},
service_data={
"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00\x00_\x01\x11\x03x"
},
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
),
device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Keypad Vision"),
time=0,
connectable=True,
tx_power=-127,
)
KEYPAD_VISION_PRO_INFO = BluetoothServiceInfoBleak(
name="Keypad Vision Pro",
manufacturer_data={2409: b"\xb0\xe9\xfe\xde\xb6\x8c+`\x00\x00\x00\x00\x00\x002"},
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00\x00`\x01\x11Q\x98"},
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
address="AA:BB:CC:DD:EE:FF",
rssi=-60,
source="local",
advertisement=generate_advertisement_data(
local_name="Keypad Vision Pro",
manufacturer_data={
2409: b"\xb0\xe9\xfe\xde\xb6\x8c+`\x00\x00\x00\x00\x00\x002"
},
service_data={
"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00\x00`\x01\x11Q\x98"
},
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
),
device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Keypad Vision Pro"),
time=0,
connectable=True,
tx_power=-127,
)
+250
View File
@@ -0,0 +1,250 @@
"""Test the switchbot services."""
from collections.abc import Callable
from unittest.mock import AsyncMock, patch
import pytest
from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
from homeassistant.components.switchbot.const import DOMAIN
from homeassistant.components.switchbot.services import (
SERVICE_ADD_PASSWORD,
async_setup_services,
)
from homeassistant.const import ATTR_DEVICE_ID
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import device_registry as dr
from . import (
KEYPAD_VISION_INFO,
KEYPAD_VISION_PRO_INFO,
SMART_THERMOSTAT_RADIATOR_SERVICE_INFO,
)
from tests.common import MockConfigEntry
from tests.components.bluetooth import inject_bluetooth_service_info
@pytest.mark.parametrize(
("ble_service_info", "sensor_type"),
[
(KEYPAD_VISION_INFO, "keypad_vision"),
(KEYPAD_VISION_PRO_INFO, "keypad_vision_pro"),
],
)
async def test_add_password_service(
hass: HomeAssistant,
mock_entry_encrypted_factory: Callable[[str], MockConfigEntry],
ble_service_info: BluetoothServiceInfoBleak,
sensor_type: str,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test the add_password service."""
inject_bluetooth_service_info(hass, ble_service_info)
entry = mock_entry_encrypted_factory(sensor_type=sensor_type)
entry.add_to_hass(hass)
mocked_instance = AsyncMock(return_value=True)
with patch.multiple(
"homeassistant.components.switchbot.switchbot.SwitchbotKeypadVision",
update=AsyncMock(return_value=None),
add_password=mocked_instance,
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
device_entry = dr.async_entries_for_config_entry(
device_registry, entry.entry_id
)[0]
await hass.services.async_call(
DOMAIN,
SERVICE_ADD_PASSWORD,
{
ATTR_DEVICE_ID: device_entry.id,
"password": "123456",
},
blocking=True,
)
mocked_instance.assert_called_once_with("123456")
async def test_device_not_found(
hass: HomeAssistant,
mock_entry_encrypted_factory: Callable[[str], MockConfigEntry],
) -> None:
"""Test the add_password service with non-existent device."""
inject_bluetooth_service_info(hass, KEYPAD_VISION_INFO)
entry = mock_entry_encrypted_factory(sensor_type="keypad_vision")
entry.add_to_hass(hass)
with patch.multiple(
"homeassistant.components.switchbot.switchbot.SwitchbotKeypadVision",
update=AsyncMock(return_value=None),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
with pytest.raises(ServiceValidationError) as err:
await hass.services.async_call(
DOMAIN,
SERVICE_ADD_PASSWORD,
{
ATTR_DEVICE_ID: "nonexistent_device",
"password": "123456",
},
blocking=True,
)
assert err.value.translation_domain == DOMAIN
assert err.value.translation_key == "invalid_device_id"
assert err.value.translation_placeholders == {"device_id": "nonexistent_device"}
async def test_device_not_belonging(
hass: HomeAssistant,
mock_entry_encrypted_factory: Callable[[str], MockConfigEntry],
device_registry: dr.DeviceRegistry,
) -> None:
"""Test service errors when device belongs to a different integration."""
inject_bluetooth_service_info(hass, KEYPAD_VISION_INFO)
entry = mock_entry_encrypted_factory(sensor_type="keypad_vision")
entry.add_to_hass(hass)
with patch.multiple(
"homeassistant.components.switchbot.switchbot.SwitchbotKeypadVision",
update=AsyncMock(return_value=None),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
other_entry = MockConfigEntry(domain="not_switchbot", data={}, title="Other")
other_entry.add_to_hass(hass)
device_entry = device_registry.async_get_or_create(
config_entry_id=other_entry.entry_id,
identifiers={("not_switchbot", "other_unique_id")},
name="Other device",
)
with pytest.raises(ServiceValidationError) as err:
await hass.services.async_call(
DOMAIN,
SERVICE_ADD_PASSWORD,
{
ATTR_DEVICE_ID: device_entry.id,
"password": "123456",
},
blocking=True,
)
assert err.value.translation_domain == DOMAIN
assert err.value.translation_key == "device_not_belonging"
assert err.value.translation_placeholders == {"device_id": device_entry.id}
async def test_device_entry_not_loaded(
hass: HomeAssistant,
mock_entry_encrypted_factory: Callable[[str], MockConfigEntry],
device_registry: dr.DeviceRegistry,
) -> None:
"""Test service errors when the config entry is not loaded."""
inject_bluetooth_service_info(hass, KEYPAD_VISION_INFO)
entry = mock_entry_encrypted_factory(sensor_type="keypad_vision")
entry.add_to_hass(hass)
with patch.multiple(
"homeassistant.components.switchbot.switchbot.SwitchbotKeypadVision",
update=AsyncMock(return_value=None),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
second_entry = mock_entry_encrypted_factory(sensor_type="keypad_vision")
second_entry.add_to_hass(hass)
device_entry = device_registry.async_get_or_create(
config_entry_id=second_entry.entry_id,
identifiers={(DOMAIN, "not_loaded_unique_id")},
name="Not loaded device",
)
with pytest.raises(ServiceValidationError) as err:
await hass.services.async_call(
DOMAIN,
SERVICE_ADD_PASSWORD,
{
ATTR_DEVICE_ID: device_entry.id,
"password": "123456",
},
blocking=True,
)
assert err.value.translation_domain == DOMAIN
assert err.value.translation_key == "device_entry_not_loaded"
assert err.value.translation_placeholders == {"device_id": device_entry.id}
async def test_service_unsupported_device(
hass: HomeAssistant,
mock_entry_encrypted_factory: Callable[[str], MockConfigEntry],
device_registry: dr.DeviceRegistry,
) -> None:
"""Test service errors when the device does not support the service."""
inject_bluetooth_service_info(hass, SMART_THERMOSTAT_RADIATOR_SERVICE_INFO)
entry = mock_entry_encrypted_factory(sensor_type="smart_thermostat_radiator")
entry.add_to_hass(hass)
with patch.multiple(
"homeassistant.components.switchbot.switchbot.SwitchbotSmartThermostatRadiator",
update=AsyncMock(return_value=None),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
device_entry = dr.async_entries_for_config_entry(device_registry, entry.entry_id)[0]
with pytest.raises(ServiceValidationError) as err:
await hass.services.async_call(
DOMAIN,
SERVICE_ADD_PASSWORD,
{
ATTR_DEVICE_ID: device_entry.id,
"password": "123456",
},
blocking=True,
)
assert err.value.translation_domain == DOMAIN
assert err.value.translation_key == "not_keypad_vision_device"
async def test_device_without_config_entry_id(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test service errors when device has no config entry id."""
async_setup_services(hass)
entry = MockConfigEntry(domain=DOMAIN, data={}, title="No entry device")
entry.add_to_hass(hass)
with pytest.raises(ServiceValidationError) as err:
await hass.services.async_call(
DOMAIN,
SERVICE_ADD_PASSWORD,
{
ATTR_DEVICE_ID: "abc",
"password": "123456",
},
blocking=True,
)
assert err.value.translation_domain == DOMAIN
assert err.value.translation_key == "invalid_device_id"
assert err.value.translation_placeholders == {"device_id": "abc"}