"""Support for binary sensors through the SmartThings cloud API.""" from collections.abc import Callable from dataclasses import dataclass from typing import override from pysmartthings import Attribute, Capability, Category, SmartThings, Status from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import FullDevice, SmartThingsConfigEntry from .const import INVALID_SWITCH_CATEGORIES, MAIN from .entity import SmartThingsEntity @dataclass(frozen=True, kw_only=True) class SmartThingsBinarySensorEntityDescription(BinarySensorEntityDescription): """Describe a SmartThings binary sensor entity.""" is_on_key: str category_device_class: dict[Category | str, BinarySensorDeviceClass] | None = None category: set[Category] | None = None exists_fn: ( Callable[ [str, dict[str, dict[Capability | str, dict[Attribute | str, Status]]]], bool, ] | None ) = None component_translation_key: dict[str, str] | None = None supported_states_attributes: Attribute | None = None CAPABILITY_TO_SENSORS: dict[ Capability, dict[Attribute, SmartThingsBinarySensorEntityDescription] ] = { Capability.ACCELERATION_SENSOR: { Attribute.ACCELERATION: SmartThingsBinarySensorEntityDescription( key=Attribute.ACCELERATION, translation_key="acceleration", device_class=BinarySensorDeviceClass.MOVING, is_on_key="active", ) }, Capability.CONTACT_SENSOR: { Attribute.CONTACT: SmartThingsBinarySensorEntityDescription( key=Attribute.CONTACT, device_class=BinarySensorDeviceClass.DOOR, is_on_key="open", category_device_class={ Category.GARAGE_DOOR: BinarySensorDeviceClass.GARAGE_DOOR, Category.DOOR: BinarySensorDeviceClass.DOOR, Category.WINDOW: BinarySensorDeviceClass.WINDOW, }, exists_fn=lambda component, status: ( not ("freezer" in status and "cooler" in status) if component == MAIN else True ), component_translation_key={ "freezer": "freezer_door", "cooler": "cooler_door", "cvroom": "cool_select_plus_door", }, ) }, Capability.CUSTOM_DRYER_WRINKLE_PREVENT: { Attribute.OPERATING_STATE: SmartThingsBinarySensorEntityDescription( key=Attribute.OPERATING_STATE, translation_key="dryer_wrinkle_prevent_active", is_on_key="running", entity_category=EntityCategory.DIAGNOSTIC, ) }, Capability.CUSTOM_WATER_FILTER: { Attribute.WATER_FILTER_STATUS: SmartThingsBinarySensorEntityDescription( key=Attribute.WATER_FILTER_STATUS, translation_key="filter_status", device_class=BinarySensorDeviceClass.PROBLEM, is_on_key="replace", ) }, Capability.SAMSUNG_CE_STEAM_CLOSET_KEEP_FRESH_MODE: { Attribute.OPERATING_STATE: SmartThingsBinarySensorEntityDescription( key=Attribute.OPERATING_STATE, translation_key="keep_fresh_mode_active", is_on_key="running", entity_category=EntityCategory.DIAGNOSTIC, ) }, Capability.FILTER_STATUS: { Attribute.FILTER_STATUS: SmartThingsBinarySensorEntityDescription( key=Attribute.FILTER_STATUS, translation_key="filter_status", device_class=BinarySensorDeviceClass.PROBLEM, is_on_key="replace", ) }, Capability.SAMSUNG_CE_KIDS_LOCK: { Attribute.LOCK_STATE: SmartThingsBinarySensorEntityDescription( key=Attribute.LOCK_STATE, translation_key="child_lock", is_on_key="locked", ) }, Capability.MOTION_SENSOR: { Attribute.MOTION: SmartThingsBinarySensorEntityDescription( key=Attribute.MOTION, device_class=BinarySensorDeviceClass.MOTION, is_on_key="active", ) }, Capability.PRESENCE_SENSOR: { Attribute.PRESENCE: SmartThingsBinarySensorEntityDescription( key=Attribute.PRESENCE, device_class=BinarySensorDeviceClass.PRESENCE, is_on_key="present", ) }, Capability.REMOTE_CONTROL_STATUS: { Attribute.REMOTE_CONTROL_ENABLED: SmartThingsBinarySensorEntityDescription( key=Attribute.REMOTE_CONTROL_ENABLED, translation_key="remote_control", is_on_key="true", component_translation_key={ "sub": "sub_remote_control", }, ) }, Capability.SOUND_SENSOR: { Attribute.SOUND: SmartThingsBinarySensorEntityDescription( key=Attribute.SOUND, device_class=BinarySensorDeviceClass.SOUND, is_on_key="detected", ) }, Capability.SWITCH: { Attribute.SWITCH: SmartThingsBinarySensorEntityDescription( key=Attribute.SWITCH, device_class=BinarySensorDeviceClass.POWER, is_on_key="on", category=INVALID_SWITCH_CATEGORIES, ) }, Capability.TAMPER_ALERT: { Attribute.TAMPER: SmartThingsBinarySensorEntityDescription( key=Attribute.TAMPER, device_class=BinarySensorDeviceClass.TAMPER, is_on_key="detected", entity_category=EntityCategory.DIAGNOSTIC, ) }, Capability.WATER_SENSOR: { Attribute.WATER: SmartThingsBinarySensorEntityDescription( key=Attribute.WATER, device_class=BinarySensorDeviceClass.MOISTURE, is_on_key="wet", ) }, Capability.SAMSUNG_CE_DOOR_STATE: { Attribute.DOOR_STATE: SmartThingsBinarySensorEntityDescription( key=Attribute.DOOR_STATE, translation_key="door", device_class=BinarySensorDeviceClass.OPENING, is_on_key="open", ) }, Capability.GAS_DETECTOR: { Attribute.GAS: SmartThingsBinarySensorEntityDescription( key=Attribute.GAS, device_class=BinarySensorDeviceClass.GAS, is_on_key="detected", ) }, Capability.CUSTOM_OVEN_CAVITY_STATUS: { Attribute.OVEN_CAVITY_STATUS: SmartThingsBinarySensorEntityDescription( key=Attribute.OVEN_CAVITY_STATUS, is_on_key="on", component_translation_key={ "cavity-01": "oven_cavity_status", }, ) }, Capability.SAMSUNG_CE_ROBOT_CLEANER_DUST_BAG: { Attribute.STATUS: SmartThingsBinarySensorEntityDescription( key=Attribute.STATUS, is_on_key="full", component_translation_key={ "station": "robot_cleaner_dust_bag", }, exists_fn=lambda component, _: component == "station", supported_states_attributes=Attribute.SUPPORTED_STATUS, ) }, Capability.CUSTOM_COOKTOP_OPERATING_STATE: { Attribute.COOKTOP_OPERATING_STATE: SmartThingsBinarySensorEntityDescription( key=Attribute.COOKTOP_OPERATING_STATE, translation_key="cooktop_operating_state", is_on_key="run", supported_states_attributes=Attribute.SUPPORTED_COOKTOP_OPERATING_STATE, ) }, Capability.SAMSUNG_CE_CLEAN_STATION_STICK_STATUS: { Attribute.STATUS: SmartThingsBinarySensorEntityDescription( key=Attribute.STATUS, component_translation_key={ "station": "stick_cleaner_status", }, exists_fn=lambda component, _: component == "station", is_on_key="attached", ) }, Capability.SAMSUNG_CE_MICROFIBER_FILTER_STATUS: { Attribute.STATUS: SmartThingsBinarySensorEntityDescription( key=Attribute.STATUS, translation_key="microfiber_filter_blockage", is_on_key="blockage", device_class=BinarySensorDeviceClass.PROBLEM, entity_category=EntityCategory.DIAGNOSTIC, ) }, Capability.SAMSUNG_CE_STICK_CLEANER_DUST_BAG: { Attribute.STATUS: SmartThingsBinarySensorEntityDescription( key=Attribute.STATUS, is_on_key="full", component_translation_key={ "station": "stick_cleaner_dust_bag", }, device_class=BinarySensorDeviceClass.PROBLEM, exists_fn=lambda component, _: component == "station", ) }, Capability.SAMSUNG_CE_STICK_CLEANER_STICK_STATUS: { Attribute.STATUS: SmartThingsBinarySensorEntityDescription( key=Attribute.STATUS, is_on_key="charging", device_class=BinarySensorDeviceClass.BATTERY_CHARGING, entity_category=EntityCategory.DIAGNOSTIC, ) }, } def get_main_component_category( device: FullDevice, ) -> Category | str: """Get the main component of a device.""" main = device.device.components[MAIN] return main.user_category or main.manufacturer_category async def async_setup_entry( hass: HomeAssistant, entry: SmartThingsConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Add binary sensors for a config entry.""" entry_data = entry.runtime_data async_add_entities( SmartThingsBinarySensor( entry_data.client, device, description, capability, attribute, component, ) for device in entry_data.devices.values() for capability, attribute_map in CAPABILITY_TO_SENSORS.items() for attribute, description in attribute_map.items() for component in device.status if ( capability in device.status[component] and ( component == MAIN or ( description.component_translation_key is not None and component in description.component_translation_key ) ) and ( description.exists_fn is None or description.exists_fn(component, device.status) ) and ( not description.category or get_main_component_category(device) in description.category ) and ( not description.supported_states_attributes or ( isinstance( options := device.status[component][capability][ description.supported_states_attributes ].value, list, ) and len(options) == 2 ) ) ) ) class SmartThingsBinarySensor(SmartThingsEntity, BinarySensorEntity): """Define a SmartThings Binary Sensor.""" entity_description: SmartThingsBinarySensorEntityDescription def __init__( self, client: SmartThings, device: FullDevice, entity_description: SmartThingsBinarySensorEntityDescription, capability: Capability, attribute: Attribute, component: str, ) -> None: """Init the class.""" super().__init__(client, device, {capability}, component=component) self._attribute = attribute self.capability = capability self.entity_description = entity_description self._attr_unique_id = ( f"{device.device.device_id}_{component}" f"_{capability}_{attribute}_{attribute}" ) if ( entity_description.category_device_class and (category := get_main_component_category(device)) in entity_description.category_device_class ): self._attr_device_class = entity_description.category_device_class[category] self._attr_name = None if ( entity_description.component_translation_key is not None and ( translation_key := entity_description.component_translation_key.get( component ) ) is not None ): self._attr_translation_key = translation_key @property @override def is_on(self) -> bool: """Return true if the binary sensor is on.""" return ( self.get_attribute_value(self.capability, self._attribute) == self.entity_description.is_on_key )