text: """Support for August binary sensors."""
  from __future__ import annotations
  
  from collections.abc import Callable
  from dataclasses import dataclass
  from datetime import datetime, timedelta
  import logging
  from typing import cast
  
  from yalexs.activity import (
      ACTION_DOORBELL_CALL_MISSED,
      SOURCE_PUBNUB,
      Activity,
      ActivityType,
  )
  from yalexs.doorbell import DoorbellDetail
  from yalexs.lock import LockDoorStatus
  from yalexs.util import update_lock_detail_from_activity
  
  from homeassistant.components.binary_sensor import (
      BinarySensorDeviceClass,
      BinarySensorEntity,
      BinarySensorEntityDescription,
  )
  from homeassistant.core import callback
  from homeassistant.helpers.entity import EntityCategory
  from homeassistant.helpers.event import async_call_later
  
  from . import AugustData
  from .const import ACTIVITY_UPDATE_INTERVAL, DATA_AUGUST, DOMAIN
  from .entity import AugustEntityMixin
  
  _LOGGER = logging.getLogger(__name__)
  
  TIME_TO_DECLARE_DETECTION = timedelta(seconds=ACTIVITY_UPDATE_INTERVAL.total_seconds())
  TIME_TO_RECHECK_DETECTION = timedelta(
      seconds=ACTIVITY_UPDATE_INTERVAL.total_seconds() * 3
  )
  
  
  def _retrieve_online_state(data: AugustData, detail: DoorbellDetail) -> bool:
      """Get the latest state of the sensor."""
      # The doorbell will go into standby mode when there is no motion
      # for a short while. It will wake by itself when needed so we need
      # to consider is available or we will not report motion or dings
  
      return detail.is_online or detail.is_standby
  
  
  def _retrieve_motion_state(data: AugustData, detail: DoorbellDetail) -> bool:
      latest = data.activity_stream.get_latest_device_activity(
          detail.device_id, {ActivityType.DOORBELL_MOTION}
      )
  
      if latest is None:
          return False
  
      return _activity_time_based_state(latest)
  
  
  def _retrieve_ding_state(data: AugustData, detail: DoorbellDetail) -> bool:
      latest = data.activity_stream.get_latest_device_activity(
          detail.device_id, {ActivityType.DOORBELL_DING}
      )
  
      if latest is None:
          return False
  
      if (
          data.activity_stream.pubnub.connected
          and latest.action == ACTION_DOORBELL_CALL_MISSED
      ):
          return False
  
      return _activity_time_based_state(latest)
  
  
  def _activity_time_based_state(latest: Activity) -> bool:
      """Get the latest state of the sensor."""
      start = latest.activity_start_time
      end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION
      return start <= _native_datetime() <= end
  
  
  def _native_datetime() -> datetime:
      """Return time in the format august uses without timezone."""
      return datetime.now()
  
  
  @dataclass
  class AugustRequiredKeysMixin:
      """Mixin for required keys."""
  
      value_fn: Callable[[AugustData, DoorbellDetail], bool]
      is_time_based: bool
  
  
  @dataclass
  class AugustBinarySensorEntityDescription(
      BinarySensorEntityDescription, AugustRequiredKeysMixin
  ):
      """Describes August binary_sensor entity."""
  
  
  SENSOR_TYPE_DOOR = BinarySensorEntityDescription(
      key="door_open",
      name="Open",
  )
  
  
  SENSOR_TYPES_DOORBELL: tuple[AugustBinarySensorEntityDescription, ...] = (
      AugustBinarySensorEntityDescription(
          key="doorbell_ding",
          name="Ding",
          device_class=BinarySensorDeviceClass.OCCUPANCY,
          value_fn=_retrieve_ding_state,
          is_time_based=True,
      ),
      AugustBinarySensorEntityDescription(
          key="doorbell_motion",
          name="Motion",
          device_class=BinarySensorDeviceClass.MOTION,
          value_fn=_retrieve_motion_state,
          is_time_based=True,
      ),
      AugustBinarySensorEntityDescription(
          key="doorbell_online",
          name="Online",
          device_class=BinarySensorDeviceClass.CONNECTIVITY,
          entity_category=EntityCategory.DIAGNOSTIC,
          value_fn=_retrieve_online_state,
          is_time_based=False,
      ),
  )
  
  
  async def async_setup_entry(hass, config_entry, async_add_entities):
      """Set up the August binary sensors."""
      data = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST]
      entities = []
  
      for door in data.locks:
          detail = data.get_device_detail(door.device_id)
          if not detail.doorsense:
              _LOGGER.debug(
                  "Not adding sensor class door for lock %s because it does not have doorsense",
                  door.device_name,
              )
              continue
  
          _LOGGER.debug("Adding sensor class door for %s", door.device_name)
          entities.append(AugustDoorBinarySensor(data, door, SENSOR_TYPE_DOOR))
  
      for doorbell in data.doorbells:
          for description in SENSOR_TYPES_DOORBELL:
              _LOGGER.debug(
                  "Adding doorbell sensor class %s for %s",
                  description.device_class,
                  doorbell.device_name,
              )
              entities.append(AugustDoorbellBinarySensor(data, doorbell, description))
  
      async_add_entities(entities)
  
  
  class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity):
      """Representation of an August Door binary sensor."""
  
      _attr_device_class = BinarySensorDeviceClass.DOOR
  
      def __init__(self, data, device, description: BinarySensorEntityDescription):
          """Initialize the sensor."""
          super().__init__(data, device)
          self.entity_description = description
          self._data = data
          self._device = device
          self._attr_name = f"{device.device_name} {description.name}"
          self._attr_unique_id = (
              f"{self._device_id}_{cast(str, description.name).lower()}"
          )
          self._update_from_data()
  
      @callback
      def _update_from_data(self):
          """Get the latest state of the sensor and update activity."""
          door_activity = self._data.activity_stream.get_latest_device_activity(
              self._device_id, {ActivityType.DOOR_OPERATION}
          )
  
          if door_activity is not None:
              update_lock_detail_from_activity(self._detail, door_activity)
              # If the source is pubnub the lock must be online since its a live update
              if door_activity.source == SOURCE_PUBNUB:
                  self._detail.set_online(True)
  
          bridge_activity = self._data.activity_stream.get_latest_device_activity(
              self._device_id, {ActivityType.BRIDGE_OPERATION}
          )
  
          if bridge_activity is not None:
              update_lock_detail_from_activity(self._detail, bridge_activity)
          self._attr_available = self._detail.bridge_is_online
          self._attr_is_on = self._detail.door_state == LockDoorStatus.OPEN
  
  
  class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity):
      """Representation of an August binary sensor."""
  
      entity_description: AugustBinarySensorEntityDescription
  
      def __init__(self, data, device, description: AugustBinarySensorEntityDescription):
          """Initialize the sensor."""
          super().__init__(data, device)
          self.entity_description = description
          self._check_for_off_update_listener = None
          self._data = data
          self._attr_name = f"{device.device_name} {description.name}"
          self._attr_unique_id = (
              f"{self._device_id}_{cast(str, description.name).lower()}"
          )
          self._update_from_data()
  
      @callback
      def _update_from_data(self):
          """Get the latest state of the sensor."""
          self._cancel_any_pending_updates()
          self._attr_is_on = self.entity_description.value_fn(self._data, self._detail)
  
          if self.entity_description.is_time_based:
              self._attr_available = _retrieve_online_state(self._data, self._detail)
              self._schedule_update_to_recheck_turn_off_sensor()
          else:
              self._attr_available = True
  
      def _schedule_update_to_recheck_turn_off_sensor(self):
          """Schedule an update to recheck the sensor to see if it is ready to turn off."""
  
          # If the sensor is already off there is nothing to do
          if not self.is_on:
              return
  
          # self.hass is only available after setup is completed
          # and we will recheck in async_added_to_hass
          if not self.hass:
              return
  
          @callback
          def _scheduled_update(now):
              """Timer callback for sensor update."""
              self._check_for_off_update_listener = None
              self._update_from_data()
              if not self.is_on:
                  self.async_write_ha_state()
  
          self._check_for_off_update_listener = async_call_later(
              self.hass, TIME_TO_RECHECK_DETECTION.total_seconds(), _scheduled_update
          )
  
      def _cancel_any_pending_updates(self):
          """Cancel any updates to recheck a sensor to see if it is ready to turn off."""
          if not self._check_for_off_update_listener:
              return
          _LOGGER.debug("%s: canceled pending update", self.entity_id)
          self._check_for_off_update_listener()
          self._check_for_off_update_listener = None
  
      async def async_added_to_hass(self):
          """Call the mixin to subscribe and setup an async_track_point_in_utc_time to turn off the sensor if needed."""
          self._schedule_update_to_recheck_turn_off_sensor()
          await super().async_added_to_hass()
  