diff --git a/switchbot/__init__.py b/switchbot/__init__.py index bc87409e..147aadf4 100644 --- a/switchbot/__init__.py +++ b/switchbot/__init__.py @@ -50,6 +50,7 @@ from .devices.evaporative_humidifier import SwitchbotEvaporativeHumidifier from .devices.fan import SwitchbotFan, SwitchbotStandingFan from .devices.humidifier import SwitchbotHumidifier +from .devices.keypad import SwitchbotKeypad from .devices.keypad_vision import SwitchbotKeypadVision from .devices.light_strip import ( SwitchbotCandleWarmerLamp, @@ -112,6 +113,7 @@ "SwitchbotFan", "SwitchbotGarageDoorOpener", "SwitchbotHumidifier", + "SwitchbotKeypad", "SwitchbotKeypadVision", "SwitchbotLightStrip", "SwitchbotLock", diff --git a/switchbot/devices/keypad.py b/switchbot/devices/keypad.py index 9d48db4f..fdb52cbb 100644 --- a/switchbot/devices/keypad.py +++ b/switchbot/devices/keypad.py @@ -1 +1,19 @@ +"""Keypad device handling.""" + from __future__ import annotations + +from .device import SwitchbotDevice + + +class SwitchbotKeypad(SwitchbotDevice): + """ + Representation of a Switchbot Keypad (WoKeypad) device. + + Passive BLE-only — no commands. Battery and attempt_state come from + advertisement parsing in adv_parsers/keypad.py. + """ + + @property + def attempt_state(self) -> int | None: + """Return the last attempt state from advertisement data.""" + return self._get_adv_value("attempt_state") diff --git a/tests/__init__.py b/tests/__init__.py index b2883c54..054bff3f 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -13,6 +13,19 @@ class AdvTestCase: modelName: SwitchbotModel +KEYPAD_INFO = AdvTestCase( + b"\xeb\x13\x02\xe6#\x0f\x8fd\x00\x00\x00\x00", + b"y\x00d", + { + "battery": 100, + "attempt_state": 143, + }, + "y", + "Keypad", + SwitchbotModel.KEYPAD, +) + + STRIP_LIGHT_3_INFO = AdvTestCase( b'\xc0N0\xe0U\x9a\x85\x9e"\xd0\x00\x00\x00\x00\x00\x00\x12\x91\x00', b"\x00\x00\x00\x00\x10\xd0\xb1", diff --git a/tests/test_keypad.py b/tests/test_keypad.py new file mode 100644 index 00000000..edefe3c5 --- /dev/null +++ b/tests/test_keypad.py @@ -0,0 +1,59 @@ +"""Test keypad device advertisement parsing.""" + +from switchbot import SwitchbotKeypad, SwitchbotModel +from switchbot.adv_parser import parse_advertisement_data + +from . import KEYPAD_INFO +from .test_adv_parser import generate_advertisement_data, generate_ble_device + + +def test_keypad_advertisement_battery() -> None: + """Test that battery is parsed from keypad advertisement data.""" + ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any") + adv_data = generate_advertisement_data( + manufacturer_data={2409: KEYPAD_INFO.manufacturer_data}, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": KEYPAD_INFO.service_data}, + rssi=-80, + ) + advertisement = parse_advertisement_data( + ble_device, adv_data, SwitchbotModel.KEYPAD + ) + device = SwitchbotKeypad(ble_device) + device.update_from_advertisement(advertisement) + + assert device.get_battery_percent() == 100 + + +def test_keypad_advertisement_attempt_state() -> None: + """Test that attempt_state is parsed from keypad advertisement data.""" + ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any") + adv_data = generate_advertisement_data( + manufacturer_data={2409: KEYPAD_INFO.manufacturer_data}, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": KEYPAD_INFO.service_data}, + rssi=-80, + ) + advertisement = parse_advertisement_data( + ble_device, adv_data, SwitchbotModel.KEYPAD + ) + device = SwitchbotKeypad(ble_device) + device.update_from_advertisement(advertisement) + + assert device.attempt_state == 143 + + +def test_keypad_advertisement_battery_none_when_no_data() -> None: + """Test that battery is None when advertisement data is missing.""" + ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any") + adv_data = generate_advertisement_data( + manufacturer_data={}, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": KEYPAD_INFO.service_data}, + rssi=-80, + ) + advertisement = parse_advertisement_data( + ble_device, adv_data, SwitchbotModel.KEYPAD + ) + device = SwitchbotKeypad(ble_device) + device.update_from_advertisement(advertisement) + + assert device.get_battery_percent() is None + assert device.attempt_state is None