Skip to content

Commit a0323e8

Browse files
Add support for switchbot presence sensor (home-assistant#156314)
Co-authored-by: Joostlek <[email protected]>
1 parent e496fb2 commit a0323e8

File tree

5 files changed

+111
-12
lines changed

5 files changed

+111
-12
lines changed

homeassistant/components/switchbot/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
SupportedModels.HYGROMETER_CO2.value: [Platform.SENSOR],
4747
SupportedModels.CONTACT.value: [Platform.BINARY_SENSOR, Platform.SENSOR],
4848
SupportedModels.MOTION.value: [Platform.BINARY_SENSOR, Platform.SENSOR],
49+
SupportedModels.PRESENCE_SENSOR.value: [Platform.BINARY_SENSOR, Platform.SENSOR],
4950
SupportedModels.HUMIDIFIER.value: [Platform.HUMIDIFIER, Platform.SENSOR],
5051
SupportedModels.LOCK.value: [
5152
Platform.BINARY_SENSOR,

homeassistant/components/switchbot/binary_sensor.py

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
from __future__ import annotations
44

5+
from collections.abc import Callable
6+
from dataclasses import dataclass
7+
8+
from switchbot import SwitchbotModel
9+
510
from homeassistant.components.binary_sensor import (
611
BinarySensorDeviceClass,
712
BinarySensorEntity,
@@ -16,54 +21,64 @@
1621

1722
PARALLEL_UPDATES = 0
1823

19-
BINARY_SENSOR_TYPES: dict[str, BinarySensorEntityDescription] = {
20-
"calibration": BinarySensorEntityDescription(
24+
25+
@dataclass(frozen=True, kw_only=True)
26+
class SwitchbotBinarySensorEntityDescription(BinarySensorEntityDescription):
27+
"""Describes Switchbot binary sensor entity."""
28+
29+
device_class_fn: Callable[[SwitchbotModel], BinarySensorDeviceClass] | None = None
30+
31+
32+
BINARY_SENSOR_TYPES: dict[str, SwitchbotBinarySensorEntityDescription] = {
33+
"calibration": SwitchbotBinarySensorEntityDescription(
2134
key="calibration",
2235
translation_key="calibration",
2336
entity_category=EntityCategory.DIAGNOSTIC,
2437
),
25-
"motion_detected": BinarySensorEntityDescription(
38+
"motion_detected": SwitchbotBinarySensorEntityDescription(
2639
key="pir_state",
27-
device_class=BinarySensorDeviceClass.MOTION,
40+
device_class_fn=lambda model: {
41+
SwitchbotModel.PRESENCE_SENSOR: BinarySensorDeviceClass.OCCUPANCY,
42+
}.get(model, BinarySensorDeviceClass.MOTION),
2843
),
29-
"contact_open": BinarySensorEntityDescription(
44+
"contact_open": SwitchbotBinarySensorEntityDescription(
3045
key="contact_open",
3146
name=None,
3247
device_class=BinarySensorDeviceClass.DOOR,
3348
),
34-
"contact_timeout": BinarySensorEntityDescription(
49+
"contact_timeout": SwitchbotBinarySensorEntityDescription(
3550
key="contact_timeout",
3651
translation_key="door_timeout",
3752
device_class=BinarySensorDeviceClass.PROBLEM,
3853
entity_category=EntityCategory.DIAGNOSTIC,
3954
),
40-
"is_light": BinarySensorEntityDescription(
55+
"is_light": SwitchbotBinarySensorEntityDescription(
4156
key="is_light",
4257
device_class=BinarySensorDeviceClass.LIGHT,
4358
),
44-
"door_open": BinarySensorEntityDescription(
59+
"door_open": SwitchbotBinarySensorEntityDescription(
4560
key="door_status",
4661
name=None,
4762
device_class=BinarySensorDeviceClass.DOOR,
4863
),
49-
"unclosed_alarm": BinarySensorEntityDescription(
64+
"unclosed_alarm": SwitchbotBinarySensorEntityDescription(
5065
key="unclosed_alarm",
5166
translation_key="door_unclosed_alarm",
5267
entity_category=EntityCategory.DIAGNOSTIC,
5368
device_class=BinarySensorDeviceClass.PROBLEM,
5469
),
55-
"unlocked_alarm": BinarySensorEntityDescription(
70+
"unlocked_alarm": SwitchbotBinarySensorEntityDescription(
5671
key="unlocked_alarm",
5772
translation_key="door_unlocked_alarm",
5873
entity_category=EntityCategory.DIAGNOSTIC,
5974
device_class=BinarySensorDeviceClass.PROBLEM,
6075
),
61-
"auto_lock_paused": BinarySensorEntityDescription(
76+
"auto_lock_paused": SwitchbotBinarySensorEntityDescription(
6277
key="auto_lock_paused",
6378
translation_key="door_auto_lock_paused",
6479
entity_category=EntityCategory.DIAGNOSTIC,
6580
),
66-
"leak": BinarySensorEntityDescription(
81+
"leak": SwitchbotBinarySensorEntityDescription(
6782
key="leak",
6883
name=None,
6984
device_class=BinarySensorDeviceClass.MOISTURE,
@@ -88,6 +103,8 @@ async def async_setup_entry(
88103
class SwitchBotBinarySensor(SwitchbotEntity, BinarySensorEntity):
89104
"""Representation of a Switchbot binary sensor."""
90105

106+
entity_description: SwitchbotBinarySensorEntityDescription
107+
91108
def __init__(
92109
self,
93110
coordinator: SwitchbotDataUpdateCoordinator,
@@ -98,6 +115,10 @@ def __init__(
98115
self._sensor = binary_sensor
99116
self._attr_unique_id = f"{coordinator.base_unique_id}-{binary_sensor}"
100117
self.entity_description = BINARY_SENSOR_TYPES[binary_sensor]
118+
if self.entity_description.device_class_fn:
119+
self._attr_device_class = self.entity_description.device_class_fn(
120+
coordinator.model
121+
)
101122

102123
@property
103124
def is_on(self) -> bool:

homeassistant/components/switchbot/const.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class SupportedModels(StrEnum):
2626
CONTACT = "contact"
2727
PLUG = "plug"
2828
MOTION = "motion"
29+
PRESENCE_SENSOR = "presence_sensor"
2930
HUMIDIFIER = "humidifier"
3031
LOCK = "lock"
3132
LOCK_PRO = "lock_pro"
@@ -108,6 +109,7 @@ class SupportedModels(StrEnum):
108109
SwitchbotModel.METER_PRO_C: SupportedModels.HYGROMETER_CO2,
109110
SwitchbotModel.CONTACT_SENSOR: SupportedModels.CONTACT,
110111
SwitchbotModel.MOTION_SENSOR: SupportedModels.MOTION,
112+
SwitchbotModel.PRESENCE_SENSOR: SupportedModels.PRESENCE_SENSOR,
111113
SwitchbotModel.LEAK: SupportedModels.LEAK,
112114
SwitchbotModel.REMOTE: SupportedModels.REMOTE,
113115
SwitchbotModel.HUBMINI_MATTER: SupportedModels.HUBMINI_MATTER,

tests/components/switchbot/__init__.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1248,3 +1248,28 @@ def make_advertisement(
12481248
connectable=True,
12491249
tx_power=-127,
12501250
)
1251+
1252+
1253+
PRESENCE_SENSOR_SERVICE_INFO = BluetoothServiceInfoBleak(
1254+
name="Presence Sensor",
1255+
manufacturer_data={2409: b"\xb0\xe9\xfe\xb8r\x9c\xbe\xcc\x00\x1a\x00\x82"},
1256+
service_data={
1257+
"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00 d\x00\x10\xcc\xc8",
1258+
},
1259+
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
1260+
address="AA:BB:CC:DD:EE:FF",
1261+
rssi=-60,
1262+
source="local",
1263+
advertisement=generate_advertisement_data(
1264+
local_name="Presence Sensor",
1265+
manufacturer_data={2409: b"\xb0\xe9\xfe\xb8r\x9c\xbe\xcc\x00\x1a\x00\x82"},
1266+
service_data={
1267+
"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00 d\x00\x10\xcc\xc8"
1268+
},
1269+
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
1270+
),
1271+
device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Presence Sensor"),
1272+
time=0,
1273+
connectable=False,
1274+
tx_power=-127,
1275+
)

tests/components/switchbot/test_sensor.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
HUBMINI_MATTER_SERVICE_INFO,
3232
LEAK_SERVICE_INFO,
3333
PLUG_MINI_EU_SERVICE_INFO,
34+
PRESENCE_SENSOR_SERVICE_INFO,
3435
RELAY_SWITCH_2PM_SERVICE_INFO,
3536
REMOTE_SERVICE_INFO,
3637
WOHAND_SERVICE_INFO,
@@ -793,3 +794,52 @@ async def test_climate_panel_sensor(hass: HomeAssistant) -> None:
793794

794795
assert await hass.config_entries.async_unload(entry.entry_id)
795796
await hass.async_block_till_done()
797+
798+
799+
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
800+
async def test_presence_sensor(hass: HomeAssistant) -> None:
801+
"""Test setting up creates the sensors for Presence Sensor."""
802+
await async_setup_component(hass, DOMAIN, {})
803+
inject_bluetooth_service_info(hass, PRESENCE_SENSOR_SERVICE_INFO)
804+
805+
entry = MockConfigEntry(
806+
domain=DOMAIN,
807+
data={
808+
CONF_ADDRESS: "AA:BB:CC:DD:EE:FF",
809+
CONF_NAME: "test-name",
810+
CONF_SENSOR_TYPE: "presence_sensor",
811+
},
812+
unique_id="aabbccddeeff",
813+
)
814+
entry.add_to_hass(hass)
815+
816+
assert await hass.config_entries.async_setup(entry.entry_id)
817+
await hass.async_block_till_done()
818+
819+
assert len(hass.states.async_all("sensor")) == 3
820+
assert len(hass.states.async_all("binary_sensor")) == 1
821+
822+
battery_sensor = hass.states.get("sensor.test_name_battery")
823+
battery_sensor_attrs = battery_sensor.attributes
824+
assert battery_sensor
825+
assert battery_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Battery"
826+
assert battery_sensor_attrs[ATTR_STATE_CLASS] == "measurement"
827+
828+
light_level_sensor = hass.states.get("sensor.test_name_light_level")
829+
light_level_sensor_attrs = light_level_sensor.attributes
830+
assert light_level_sensor
831+
assert light_level_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Light level"
832+
833+
rssi_sensor = hass.states.get("sensor.test_name_bluetooth_signal")
834+
rssi_sensor_attrs = rssi_sensor.attributes
835+
assert rssi_sensor
836+
assert rssi_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Bluetooth signal"
837+
assert rssi_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "dBm"
838+
839+
occupancy_sensor = hass.states.get("binary_sensor.test_name_occupancy")
840+
occupancy_sensor_attrs = occupancy_sensor.attributes
841+
assert occupancy_sensor
842+
assert occupancy_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Occupancy"
843+
844+
assert await hass.config_entries.async_unload(entry.entry_id)
845+
await hass.async_block_till_done()

0 commit comments

Comments
 (0)