From 570b2169cd19b45cd192fb2af0c2a04ed54ad7d7 Mon Sep 17 00:00:00 2001 From: Italo Lombardi Date: Wed, 6 May 2026 11:28:27 +0200 Subject: [PATCH 01/10] Add SwitchbotKeypad device class for classic Keypad The classic Keypad (WoKeypad) is a passive BLE-only device paired with SwitchBot Lock. The adv parser already exposes battery and attempt_state from advertisement data. This adds the device class and exports it so integrations can reference it explicitly. --- switchbot/__init__.py | 2 ++ switchbot/devices/keypad.py | 8 +++++ tests/__init__.py | 13 +++++++++ tests/test_keypad.py | 58 +++++++++++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+) create mode 100644 tests/test_keypad.py 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..bb3571f9 100644 --- a/switchbot/devices/keypad.py +++ b/switchbot/devices/keypad.py @@ -1 +1,9 @@ +"""Keypad device handling.""" + from __future__ import annotations + +from .device import SwitchbotDevice + + +class SwitchbotKeypad(SwitchbotDevice): + """Representation of a Switchbot Keypad device.""" 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..e3040315 --- /dev/null +++ b/tests/test_keypad.py @@ -0,0 +1,58 @@ +"""Test keypad device advertisement parsing.""" + +from switchbot import SwitchBotAdvertisement, SwitchbotKeypad +from switchbot.models import SwitchBotAdvertisement + +from . import KEYPAD_INFO +from .test_adv_parser import AdvTestCase, generate_ble_device + + +def make_advertisement_data(ble_device, adv_info: AdvTestCase, init_data: dict | None = None): + """Set advertisement data with defaults.""" + if init_data is None: + init_data = {} + + return SwitchBotAdvertisement( + address="aa:bb:cc:dd:ee:ff", + data={ + "rawAdvData": adv_info.service_data, + "data": adv_info.data | init_data, + "isEncrypted": False, + "model": adv_info.model, + "modelFriendlyName": adv_info.modelFriendlyName, + "modelName": adv_info.modelName, + } + | init_data, + device=ble_device, + rssi=-80, + active=True, + ) + + +def test_keypad_advertisement_battery() -> None: + """Test that battery is parsed from keypad advertisement.""" + ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any") + device = SwitchbotKeypad(ble_device) + device.update_from_advertisement(make_advertisement_data(ble_device, KEYPAD_INFO)) + + assert device.parsed_data["battery"] == 100 + + +def test_keypad_advertisement_attempt_state() -> None: + """Test that attempt_state is parsed from keypad advertisement.""" + ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any") + device = SwitchbotKeypad(ble_device) + device.update_from_advertisement(make_advertisement_data(ble_device, KEYPAD_INFO)) + + assert device.parsed_data["attempt_state"] == 143 + + +def test_keypad_advertisement_battery_none() -> None: + """Test that battery is None when advertisement data is missing.""" + ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any") + device = SwitchbotKeypad(ble_device) + device.update_from_advertisement( + make_advertisement_data(ble_device, KEYPAD_INFO, {"battery": None}) + ) + + assert device.parsed_data["battery"] is None From 7430fd3fd735c65dc057217d5ecf025f90038adb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 09:33:32 +0000 Subject: [PATCH 02/10] chore(pre-commit.ci): auto fixes --- tests/test_keypad.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_keypad.py b/tests/test_keypad.py index e3040315..c24164ab 100644 --- a/tests/test_keypad.py +++ b/tests/test_keypad.py @@ -7,7 +7,9 @@ from .test_adv_parser import AdvTestCase, generate_ble_device -def make_advertisement_data(ble_device, adv_info: AdvTestCase, init_data: dict | None = None): +def make_advertisement_data( + ble_device, adv_info: AdvTestCase, init_data: dict | None = None +): """Set advertisement data with defaults.""" if init_data is None: init_data = {} From 72f509b50daded5da5e543a13cc21b48f8995818 Mon Sep 17 00:00:00 2001 From: Italo Lombardi Date: Wed, 6 May 2026 11:37:05 +0200 Subject: [PATCH 03/10] fix: remove duplicate SwitchBotAdvertisement import in test_keypad --- tests/test_keypad.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_keypad.py b/tests/test_keypad.py index c24164ab..777e6a37 100644 --- a/tests/test_keypad.py +++ b/tests/test_keypad.py @@ -1,6 +1,6 @@ """Test keypad device advertisement parsing.""" -from switchbot import SwitchBotAdvertisement, SwitchbotKeypad +from switchbot import SwitchbotKeypad from switchbot.models import SwitchBotAdvertisement from . import KEYPAD_INFO From e2174d0da161b87118e2ea77eae36a6e48096db8 Mon Sep 17 00:00:00 2001 From: Italo Lombardi Date: Thu, 14 May 2026 23:21:35 +0200 Subject: [PATCH 04/10] test(keypad): drive tests through full parse pipeline Tests previously injected pre-parsed values, bypassing the parser. Now exercises parse_advertisement_data end-to-end so byte-layout regressions in adv_parsers/keypad.py are caught. - Assert get_battery_percent() instead of raw dict access - None test exercises parser's own None branch (empty mfr_data) - Extract make_advertisement_data to tests/__init__.py; remove duplicate in test_keypad_vision.py - Expand SwitchbotKeypad docstring: passive-only, no commands --- switchbot/devices/keypad.py | 6 +++- tests/__init__.py | 27 +++++++++++++- tests/test_keypad.py | 70 ++++++++++++++++--------------------- tests/test_keypad_vision.py | 28 +-------------- 4 files changed, 63 insertions(+), 68 deletions(-) diff --git a/switchbot/devices/keypad.py b/switchbot/devices/keypad.py index bb3571f9..26d80a48 100644 --- a/switchbot/devices/keypad.py +++ b/switchbot/devices/keypad.py @@ -6,4 +6,8 @@ class SwitchbotKeypad(SwitchbotDevice): - """Representation of a Switchbot Keypad device.""" + """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. + """ diff --git a/tests/__init__.py b/tests/__init__.py index 054bff3f..6cb529a6 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,6 +1,8 @@ from dataclasses import dataclass -from switchbot import SwitchbotModel +from bleak.backends.device import BLEDevice + +from switchbot import SwitchBotAdvertisement, SwitchbotModel @dataclass @@ -13,6 +15,29 @@ class AdvTestCase: modelName: SwitchbotModel +def make_advertisement_data( + ble_device: BLEDevice, adv_info: AdvTestCase, init_data: dict | None = None +) -> SwitchBotAdvertisement: + """Build a SwitchBotAdvertisement from an AdvTestCase fixture.""" + if init_data is None: + init_data = {} + + return SwitchBotAdvertisement( + address="aa:bb:cc:dd:ee:ff", + data={ + "rawAdvData": adv_info.service_data, + "data": adv_info.data | init_data, + "isEncrypted": False, + "model": adv_info.model, + "modelFriendlyName": adv_info.modelFriendlyName, + "modelName": adv_info.modelName, + }, + device=ble_device, + rssi=-80, + active=True, + ) + + KEYPAD_INFO = AdvTestCase( b"\xeb\x13\x02\xe6#\x0f\x8fd\x00\x00\x00\x00", b"y\x00d", diff --git a/tests/test_keypad.py b/tests/test_keypad.py index 777e6a37..6f2fae2d 100644 --- a/tests/test_keypad.py +++ b/tests/test_keypad.py @@ -1,60 +1,52 @@ """Test keypad device advertisement parsing.""" -from switchbot import SwitchbotKeypad -from switchbot.models import SwitchBotAdvertisement - -from . import KEYPAD_INFO -from .test_adv_parser import AdvTestCase, generate_ble_device - - -def make_advertisement_data( - ble_device, adv_info: AdvTestCase, init_data: dict | None = None -): - """Set advertisement data with defaults.""" - if init_data is None: - init_data = {} - - return SwitchBotAdvertisement( - address="aa:bb:cc:dd:ee:ff", - data={ - "rawAdvData": adv_info.service_data, - "data": adv_info.data | init_data, - "isEncrypted": False, - "model": adv_info.model, - "modelFriendlyName": adv_info.modelFriendlyName, - "modelName": adv_info.modelName, - } - | init_data, - device=ble_device, - rssi=-80, - active=True, - ) +from switchbot import SwitchbotKeypad, SwitchbotModel +from switchbot.adv_parser import parse_advertisement_data + +from . import KEYPAD_INFO, make_advertisement_data +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.""" + """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(make_advertisement_data(ble_device, KEYPAD_INFO)) + device.update_from_advertisement(advertisement) - assert device.parsed_data["battery"] == 100 + assert device.get_battery_percent() == 100 def test_keypad_advertisement_attempt_state() -> None: - """Test that attempt_state is parsed from keypad advertisement.""" + """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(make_advertisement_data(ble_device, KEYPAD_INFO)) + device.update_from_advertisement(advertisement) assert device.parsed_data["attempt_state"] == 143 -def test_keypad_advertisement_battery_none() -> None: +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") - device = SwitchbotKeypad(ble_device) - device.update_from_advertisement( - make_advertisement_data(ble_device, KEYPAD_INFO, {"battery": None}) + 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.parsed_data["battery"] is None + assert device.get_battery_percent() is None diff --git a/tests/test_keypad_vision.py b/tests/test_keypad_vision.py index a6d7b922..51183674 100644 --- a/tests/test_keypad_vision.py +++ b/tests/test_keypad_vision.py @@ -3,16 +3,14 @@ from unittest.mock import AsyncMock, patch import pytest -from bleak.backends.device import BLEDevice -from switchbot import SwitchBotAdvertisement from switchbot.devices.device import SwitchbotEncryptedDevice from switchbot.devices.keypad_vision import ( COMMAND_GET_PASSWORD_COUNT, SwitchbotKeypadVision, ) -from . import KEYPAD_VISION_INFO, KEYPAD_VISION_PRO_INFO +from . import KEYPAD_VISION_INFO, KEYPAD_VISION_PRO_INFO, make_advertisement_data from .test_adv_parser import AdvTestCase, generate_ble_device @@ -34,30 +32,6 @@ def create_device_for_command_testing( return device -def make_advertisement_data( - ble_device: BLEDevice, adv_info: AdvTestCase, init_data: dict | None = None -): - """Set advertisement data with defaults.""" - if init_data is None: - init_data = {} - - return SwitchBotAdvertisement( - address="aa:bb:cc:dd:ee:ff", - data={ - "rawAdvData": adv_info.service_data, - "data": adv_info.data | init_data, - "isEncrypted": False, - "model": adv_info.model, - "modelFriendlyName": adv_info.modelFriendlyName, - "modelName": adv_info.modelName, - } - | init_data, - device=ble_device, - rssi=-80, - active=True, - ) - - @pytest.mark.asyncio @pytest.mark.parametrize( ("adv_info"), From 0d4085c974ff689c435f51d20fbe3a9841c0ebf5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 21:21:52 +0000 Subject: [PATCH 05/10] chore(pre-commit.ci): auto fixes --- switchbot/devices/keypad.py | 3 ++- tests/test_keypad.py | 14 ++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/switchbot/devices/keypad.py b/switchbot/devices/keypad.py index 26d80a48..30f4910a 100644 --- a/switchbot/devices/keypad.py +++ b/switchbot/devices/keypad.py @@ -6,7 +6,8 @@ class SwitchbotKeypad(SwitchbotDevice): - """Representation of a Switchbot Keypad (WoKeypad) device. + """ + 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. diff --git a/tests/test_keypad.py b/tests/test_keypad.py index 6f2fae2d..f6520da3 100644 --- a/tests/test_keypad.py +++ b/tests/test_keypad.py @@ -3,7 +3,7 @@ from switchbot import SwitchbotKeypad, SwitchbotModel from switchbot.adv_parser import parse_advertisement_data -from . import KEYPAD_INFO, make_advertisement_data +from . import KEYPAD_INFO from .test_adv_parser import generate_advertisement_data, generate_ble_device @@ -15,7 +15,9 @@ def test_keypad_advertisement_battery() -> None: service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": KEYPAD_INFO.service_data}, rssi=-80, ) - advertisement = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.KEYPAD) + advertisement = parse_advertisement_data( + ble_device, adv_data, SwitchbotModel.KEYPAD + ) device = SwitchbotKeypad(ble_device) device.update_from_advertisement(advertisement) @@ -30,7 +32,9 @@ def test_keypad_advertisement_attempt_state() -> None: service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": KEYPAD_INFO.service_data}, rssi=-80, ) - advertisement = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.KEYPAD) + advertisement = parse_advertisement_data( + ble_device, adv_data, SwitchbotModel.KEYPAD + ) device = SwitchbotKeypad(ble_device) device.update_from_advertisement(advertisement) @@ -45,7 +49,9 @@ def test_keypad_advertisement_battery_none_when_no_data() -> None: service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": KEYPAD_INFO.service_data}, rssi=-80, ) - advertisement = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.KEYPAD) + advertisement = parse_advertisement_data( + ble_device, adv_data, SwitchbotModel.KEYPAD + ) device = SwitchbotKeypad(ble_device) device.update_from_advertisement(advertisement) From d0167e8084e171583e60b95274e69e3515ee03ce Mon Sep 17 00:00:00 2001 From: Italo Lombardi Date: Thu, 14 May 2026 23:24:51 +0200 Subject: [PATCH 06/10] revert: restore test_keypad_vision.py to original state File was not in scope for this PR. Revert changes to it and remove the shared make_advertisement_data helper from tests/__init__.py that was added only to support that refactor. --- tests/__init__.py | 27 +-------------------------- tests/test_keypad_vision.py | 28 +++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 6cb529a6..054bff3f 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,8 +1,6 @@ from dataclasses import dataclass -from bleak.backends.device import BLEDevice - -from switchbot import SwitchBotAdvertisement, SwitchbotModel +from switchbot import SwitchbotModel @dataclass @@ -15,29 +13,6 @@ class AdvTestCase: modelName: SwitchbotModel -def make_advertisement_data( - ble_device: BLEDevice, adv_info: AdvTestCase, init_data: dict | None = None -) -> SwitchBotAdvertisement: - """Build a SwitchBotAdvertisement from an AdvTestCase fixture.""" - if init_data is None: - init_data = {} - - return SwitchBotAdvertisement( - address="aa:bb:cc:dd:ee:ff", - data={ - "rawAdvData": adv_info.service_data, - "data": adv_info.data | init_data, - "isEncrypted": False, - "model": adv_info.model, - "modelFriendlyName": adv_info.modelFriendlyName, - "modelName": adv_info.modelName, - }, - device=ble_device, - rssi=-80, - active=True, - ) - - KEYPAD_INFO = AdvTestCase( b"\xeb\x13\x02\xe6#\x0f\x8fd\x00\x00\x00\x00", b"y\x00d", diff --git a/tests/test_keypad_vision.py b/tests/test_keypad_vision.py index 51183674..a6d7b922 100644 --- a/tests/test_keypad_vision.py +++ b/tests/test_keypad_vision.py @@ -3,14 +3,16 @@ from unittest.mock import AsyncMock, patch import pytest +from bleak.backends.device import BLEDevice +from switchbot import SwitchBotAdvertisement from switchbot.devices.device import SwitchbotEncryptedDevice from switchbot.devices.keypad_vision import ( COMMAND_GET_PASSWORD_COUNT, SwitchbotKeypadVision, ) -from . import KEYPAD_VISION_INFO, KEYPAD_VISION_PRO_INFO, make_advertisement_data +from . import KEYPAD_VISION_INFO, KEYPAD_VISION_PRO_INFO from .test_adv_parser import AdvTestCase, generate_ble_device @@ -32,6 +34,30 @@ def create_device_for_command_testing( return device +def make_advertisement_data( + ble_device: BLEDevice, adv_info: AdvTestCase, init_data: dict | None = None +): + """Set advertisement data with defaults.""" + if init_data is None: + init_data = {} + + return SwitchBotAdvertisement( + address="aa:bb:cc:dd:ee:ff", + data={ + "rawAdvData": adv_info.service_data, + "data": adv_info.data | init_data, + "isEncrypted": False, + "model": adv_info.model, + "modelFriendlyName": adv_info.modelFriendlyName, + "modelName": adv_info.modelName, + } + | init_data, + device=ble_device, + rssi=-80, + active=True, + ) + + @pytest.mark.asyncio @pytest.mark.parametrize( ("adv_info"), From b503a705754a6e2cb37d4dbf3fcce6a1697a370f Mon Sep 17 00:00:00 2001 From: Italo Lombardi Date: Fri, 15 May 2026 10:10:04 +0200 Subject: [PATCH 07/10] test(keypad): assert attempt_state is None when mfr_data missing Completes coverage of the None-handling branch in process_wokeypad: both battery and attempt_state are None when manufacturer data absent. --- tests/test_keypad.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_keypad.py b/tests/test_keypad.py index f6520da3..63e08bde 100644 --- a/tests/test_keypad.py +++ b/tests/test_keypad.py @@ -56,3 +56,4 @@ def test_keypad_advertisement_battery_none_when_no_data() -> None: device.update_from_advertisement(advertisement) assert device.get_battery_percent() is None + assert device.parsed_data["attempt_state"] is None From 57af600caad4abaad261afb4bc7f2a5fc1cf5a79 Mon Sep 17 00:00:00 2001 From: Italo Lombardi Date: Sat, 23 May 2026 00:00:53 +0100 Subject: [PATCH 08/10] Add attempt_state property to SwitchbotKeypad Exposes attempt_state via _get_adv_value, consistent with how get_battery_percent is implemented in the base class. --- switchbot/devices/keypad.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/switchbot/devices/keypad.py b/switchbot/devices/keypad.py index 30f4910a..16721600 100644 --- a/switchbot/devices/keypad.py +++ b/switchbot/devices/keypad.py @@ -6,9 +6,13 @@ class SwitchbotKeypad(SwitchbotDevice): - """ - Representation of a Switchbot Keypad (WoKeypad) device. + """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") From 60c32315c6ba63a610b72ac69178e48003e66525 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 23:01:13 +0000 Subject: [PATCH 09/10] chore(pre-commit.ci): auto fixes --- switchbot/devices/keypad.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/switchbot/devices/keypad.py b/switchbot/devices/keypad.py index 16721600..fdb52cbb 100644 --- a/switchbot/devices/keypad.py +++ b/switchbot/devices/keypad.py @@ -6,7 +6,8 @@ class SwitchbotKeypad(SwitchbotDevice): - """Representation of a Switchbot Keypad (WoKeypad) device. + """ + 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. From 7d7129733a54c31a671b46284b6a1804f0c2c14c Mon Sep 17 00:00:00 2001 From: Italo Lombardi Date: Sat, 23 May 2026 00:03:29 +0100 Subject: [PATCH 10/10] Use attempt_state property in tests for coverage Replace raw parsed_data dict access with device.attempt_state property so the new property is exercised by the test suite. --- tests/test_keypad.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_keypad.py b/tests/test_keypad.py index 63e08bde..edefe3c5 100644 --- a/tests/test_keypad.py +++ b/tests/test_keypad.py @@ -38,7 +38,7 @@ def test_keypad_advertisement_attempt_state() -> None: device = SwitchbotKeypad(ble_device) device.update_from_advertisement(advertisement) - assert device.parsed_data["attempt_state"] == 143 + assert device.attempt_state == 143 def test_keypad_advertisement_battery_none_when_no_data() -> None: @@ -56,4 +56,4 @@ def test_keypad_advertisement_battery_none_when_no_data() -> None: device.update_from_advertisement(advertisement) assert device.get_battery_percent() is None - assert device.parsed_data["attempt_state"] is None + assert device.attempt_state is None