Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions homeassistant/components/switchbot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
SupportedModels.HYGROMETER_CO2.value: [Platform.SENSOR],
SupportedModels.CONTACT.value: [Platform.BINARY_SENSOR, Platform.SENSOR],
SupportedModels.MOTION.value: [Platform.BINARY_SENSOR, Platform.SENSOR],
SupportedModels.PRESENCE_SENSOR.value: [Platform.BINARY_SENSOR, Platform.SENSOR],
SupportedModels.HUMIDIFIER.value: [Platform.HUMIDIFIER, Platform.SENSOR],
SupportedModels.LOCK.value: [
Platform.BINARY_SENSOR,
Expand Down
59 changes: 40 additions & 19 deletions homeassistant/components/switchbot/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass

from switchbot import SwitchbotModel

from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
Expand All @@ -16,57 +21,67 @@

PARALLEL_UPDATES = 0

BINARY_SENSOR_TYPES: dict[str, BinarySensorEntityDescription] = {
"calibration": BinarySensorEntityDescription(

@dataclass(frozen=True, kw_only=True)
class SwitchbotBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describes Switchbot binary sensor entity."""

device_class_fn: Callable[[SwitchbotModel], BinarySensorDeviceClass] | None = None


BINARY_SENSOR_TYPES: dict[str, SwitchbotBinarySensorEntityDescription] = {
"calibration": SwitchbotBinarySensorEntityDescription(
key="calibration",
translation_key="calibration",
entity_category=EntityCategory.DIAGNOSTIC,
),
"motion_detected": BinarySensorEntityDescription(
"motion_detected": SwitchbotBinarySensorEntityDescription(
key="pir_state",
device_class=BinarySensorDeviceClass.MOTION,
device_class_fn=lambda model: {
SwitchbotModel.PRESENCE_SENSOR: BinarySensorDeviceClass.OCCUPANCY,
}.get(model, BinarySensorDeviceClass.MOTION),
),
"contact_open": BinarySensorEntityDescription(
"contact_open": SwitchbotBinarySensorEntityDescription(
key="contact_open",
name=None,
device_class=BinarySensorDeviceClass.DOOR,
device_class_fn=lambda _: BinarySensorDeviceClass.DOOR,
),
"contact_timeout": BinarySensorEntityDescription(
"contact_timeout": SwitchbotBinarySensorEntityDescription(
key="contact_timeout",
translation_key="door_timeout",
device_class=BinarySensorDeviceClass.PROBLEM,
device_class_fn=lambda _: BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
),
"is_light": BinarySensorEntityDescription(
"is_light": SwitchbotBinarySensorEntityDescription(
key="is_light",
device_class=BinarySensorDeviceClass.LIGHT,
device_class_fn=lambda _: BinarySensorDeviceClass.LIGHT,
),
"door_open": BinarySensorEntityDescription(
"door_open": SwitchbotBinarySensorEntityDescription(
key="door_status",
name=None,
device_class=BinarySensorDeviceClass.DOOR,
device_class_fn=lambda _: BinarySensorDeviceClass.DOOR,
),
"unclosed_alarm": BinarySensorEntityDescription(
"unclosed_alarm": SwitchbotBinarySensorEntityDescription(
key="unclosed_alarm",
translation_key="door_unclosed_alarm",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=BinarySensorDeviceClass.PROBLEM,
device_class_fn=lambda _: BinarySensorDeviceClass.PROBLEM,
),
"unlocked_alarm": BinarySensorEntityDescription(
"unlocked_alarm": SwitchbotBinarySensorEntityDescription(
key="unlocked_alarm",
translation_key="door_unlocked_alarm",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=BinarySensorDeviceClass.PROBLEM,
device_class_fn=lambda _: BinarySensorDeviceClass.PROBLEM,
),
"auto_lock_paused": BinarySensorEntityDescription(
"auto_lock_paused": SwitchbotBinarySensorEntityDescription(
key="auto_lock_paused",
translation_key="door_auto_lock_paused",
entity_category=EntityCategory.DIAGNOSTIC,
),
"leak": BinarySensorEntityDescription(
"leak": SwitchbotBinarySensorEntityDescription(
key="leak",
name=None,
device_class=BinarySensorDeviceClass.MOISTURE,
device_class_fn=lambda _: BinarySensorDeviceClass.MOISTURE,
),
}

Expand All @@ -88,6 +103,8 @@ async def async_setup_entry(
class SwitchBotBinarySensor(SwitchbotEntity, BinarySensorEntity):
"""Representation of a Switchbot binary sensor."""

entity_description: SwitchbotBinarySensorEntityDescription

def __init__(
self,
coordinator: SwitchbotDataUpdateCoordinator,
Expand All @@ -98,6 +115,10 @@ def __init__(
self._sensor = binary_sensor
self._attr_unique_id = f"{coordinator.base_unique_id}-{binary_sensor}"
self.entity_description = BINARY_SENSOR_TYPES[binary_sensor]
if self.entity_description.device_class_fn:
self._attr_device_class = self.entity_description.device_class_fn(
coordinator.model
)

@property
def is_on(self) -> bool:
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/switchbot/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class SupportedModels(StrEnum):
CONTACT = "contact"
PLUG = "plug"
MOTION = "motion"
PRESENCE_SENSOR = "presence_sensor"
HUMIDIFIER = "humidifier"
LOCK = "lock"
LOCK_PRO = "lock_pro"
Expand Down Expand Up @@ -108,6 +109,7 @@ class SupportedModels(StrEnum):
SwitchbotModel.METER_PRO_C: SupportedModels.HYGROMETER_CO2,
SwitchbotModel.CONTACT_SENSOR: SupportedModels.CONTACT,
SwitchbotModel.MOTION_SENSOR: SupportedModels.MOTION,
SwitchbotModel.PRESENCE_SENSOR: SupportedModels.PRESENCE_SENSOR,
SwitchbotModel.LEAK: SupportedModels.LEAK,
SwitchbotModel.REMOTE: SupportedModels.REMOTE,
SwitchbotModel.HUBMINI_MATTER: SupportedModels.HUBMINI_MATTER,
Expand Down
25 changes: 25 additions & 0 deletions tests/components/switchbot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1248,3 +1248,28 @@ def make_advertisement(
connectable=True,
tx_power=-127,
)


PRESENCE_SENSOR_SERVICE_INFO = BluetoothServiceInfoBleak(
name="Presence Sensor",
manufacturer_data={2409: b"\xb0\xe9\xfe\xb8r\x9c\xbe\xcc\x00\x1a\x00\x82"},
service_data={
"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00 d\x00\x10\xcc\xc8",
},
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
address="AA:BB:CC:DD:EE:FF",
rssi=-60,
source="local",
advertisement=generate_advertisement_data(
local_name="Presence Sensor",
manufacturer_data={2409: b"\xb0\xe9\xfe\xb8r\x9c\xbe\xcc\x00\x1a\x00\x82"},
service_data={
"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00 d\x00\x10\xcc\xc8"
},
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
),
device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Presence Sensor"),
time=0,
connectable=False,
tx_power=-127,
)
55 changes: 55 additions & 0 deletions tests/components/switchbot/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
HUBMINI_MATTER_SERVICE_INFO,
LEAK_SERVICE_INFO,
PLUG_MINI_EU_SERVICE_INFO,
PRESENCE_SENSOR_SERVICE_INFO,
RELAY_SWITCH_2PM_SERVICE_INFO,
REMOTE_SERVICE_INFO,
WOHAND_SERVICE_INFO,
Expand Down Expand Up @@ -793,3 +794,57 @@ async def test_climate_panel_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")
async def test_presence_sensor(hass: HomeAssistant) -> None:
"""Test setting up creates the sensors for Presence Sensor."""
await async_setup_component(hass, DOMAIN, {})
inject_bluetooth_service_info(hass, PRESENCE_SENSOR_SERVICE_INFO)

entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_ADDRESS: "AA:BB:CC:DD:EE:FF",
CONF_NAME: "test-name",
CONF_SENSOR_TYPE: "presence_sensor",
},
unique_id="aabbccddeeff",
)
entry.add_to_hass(hass)

assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()

assert len(hass.states.async_all("sensor")) == 3
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"

light_level_sensor = hass.states.get("sensor.test_name_light_level")
light_level_sensor_attrs = light_level_sensor.attributes
assert light_level_sensor
assert light_level_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Light level"

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"

light_sensor = hass.states.get("binary_sensor.test_name_light")
light_sensor_attrs = light_sensor.attributes
assert light_sensor
assert light_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Light"

motion_sensor = hass.states.get("binary_sensor.test_name_motion")
motion_sensor_attrs = motion_sensor.attributes
assert motion_sensor
assert motion_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Motion"

assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
Loading