diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 04f69548a4244e..c0f076c5f4d989 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -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, diff --git a/homeassistant/components/switchbot/binary_sensor.py b/homeassistant/components/switchbot/binary_sensor.py index fc87ad2e497528..144089ab7778b9 100644 --- a/homeassistant/components/switchbot/binary_sensor.py +++ b/homeassistant/components/switchbot/binary_sensor.py @@ -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, @@ -16,54 +21,64 @@ 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, ), - "contact_timeout": BinarySensorEntityDescription( + "contact_timeout": SwitchbotBinarySensorEntityDescription( key="contact_timeout", translation_key="door_timeout", device_class=BinarySensorDeviceClass.PROBLEM, entity_category=EntityCategory.DIAGNOSTIC, ), - "is_light": BinarySensorEntityDescription( + "is_light": SwitchbotBinarySensorEntityDescription( key="is_light", device_class=BinarySensorDeviceClass.LIGHT, ), - "door_open": BinarySensorEntityDescription( + "door_open": SwitchbotBinarySensorEntityDescription( key="door_status", name=None, device_class=BinarySensorDeviceClass.DOOR, ), - "unclosed_alarm": BinarySensorEntityDescription( + "unclosed_alarm": SwitchbotBinarySensorEntityDescription( key="unclosed_alarm", translation_key="door_unclosed_alarm", entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.PROBLEM, ), - "unlocked_alarm": BinarySensorEntityDescription( + "unlocked_alarm": SwitchbotBinarySensorEntityDescription( key="unlocked_alarm", translation_key="door_unlocked_alarm", entity_category=EntityCategory.DIAGNOSTIC, device_class=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, @@ -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, @@ -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: diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index b2677fee38c970..0bc49ebad7bb3d 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -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" @@ -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, diff --git a/tests/components/switchbot/__init__.py b/tests/components/switchbot/__init__.py index 16550d955e97a3..f229f961452f20 100644 --- a/tests/components/switchbot/__init__.py +++ b/tests/components/switchbot/__init__.py @@ -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, +) diff --git a/tests/components/switchbot/test_sensor.py b/tests/components/switchbot/test_sensor.py index b213f496c5b881..b1f9c15ae50ff4 100644 --- a/tests/components/switchbot/test_sensor.py +++ b/tests/components/switchbot/test_sensor.py @@ -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, @@ -793,3 +794,52 @@ 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")) == 1 + + 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" + + occupancy_sensor = hass.states.get("binary_sensor.test_name_occupancy") + occupancy_sensor_attrs = occupancy_sensor.attributes + assert occupancy_sensor + assert occupancy_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Occupancy" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done()