core/homeassistant/components/august/binary_sensor.py

229 lines
8.0 KiB
Python
Raw Normal View History

"""Support for August binary sensors."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime, timedelta
2024-06-19 14:21:04 +00:00
from functools import partial
Add doorsense sensor for August 3rd Gen Smart Lock Pro (#17299) * Add doorsense sensor for August 3rd Gen Smart Lock Pro Add a binary sensor to August for the August 3rd Gen Smart Lock Pro doorsense sensor. This is a re-do from PR 17116 https://github.com/home-assistant/home-assistant/pull/17116 that I closed due to rebase issue on my end. * Changed to use snjoetw provided code Going through the py-august I found that snjoetw had provided updated versions for the august component (august.py and binary_sensor/august.py) to include DoorSense sensor. Changed what I did to to what snjoetw provided instead as he split it into 2 classes; much cleaner I think. I modified his coding with: Fixes that were done to the August component and not part of the coding snjoetw provided. Added the debug logging improvement I had done in the code. Note, fix I committed earlier for lock atribute (lock/august.py) is thus still the same. * Reverted change from add_device to add_entities Missed an item when merging snjoetw's code with current. Fixed. * Updated call from add_devices to add_entities as well Updated the call from add_devices to add_entities. * Fixed permissions on files Fixed permissions on components/august.py and binary_snesor/august.py * Changed if/else to if/continue Changed logic so that if the door sensor state is unknown during initalization the debug log is written and then continue the loop instead of using if/else logic. * Added available property for Door Sensor Added the available property for the Door Sensor and setting it to False if a status unknown is received. * Updated setting self._available Changed line for setting self._available to what Martin provided. Much more efficient to read. :-)
2018-10-19 07:37:02 +00:00
import logging
2024-06-19 19:11:36 +00:00
from yalexs.activity import ACTION_DOORBELL_CALL_MISSED, Activity, ActivityType
from yalexs.doorbell import DoorbellDetail
from yalexs.lock import LockDetail, LockDoorStatus
from yalexs.manager.const import ACTIVITY_UPDATE_INTERVAL
from yalexs.util import update_lock_detail_from_activity
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_call_later
2024-05-02 12:39:02 +00:00
from . import AugustConfigEntry, AugustData
from .entity import AugustDescriptionEntity
Add doorsense sensor for August 3rd Gen Smart Lock Pro (#17299) * Add doorsense sensor for August 3rd Gen Smart Lock Pro Add a binary sensor to August for the August 3rd Gen Smart Lock Pro doorsense sensor. This is a re-do from PR 17116 https://github.com/home-assistant/home-assistant/pull/17116 that I closed due to rebase issue on my end. * Changed to use snjoetw provided code Going through the py-august I found that snjoetw had provided updated versions for the august component (august.py and binary_sensor/august.py) to include DoorSense sensor. Changed what I did to to what snjoetw provided instead as he split it into 2 classes; much cleaner I think. I modified his coding with: Fixes that were done to the August component and not part of the coding snjoetw provided. Added the debug logging improvement I had done in the code. Note, fix I committed earlier for lock atribute (lock/august.py) is thus still the same. * Reverted change from add_device to add_entities Missed an item when merging snjoetw's code with current. Fixed. * Updated call from add_devices to add_entities as well Updated the call from add_devices to add_entities. * Fixed permissions on files Fixed permissions on components/august.py and binary_snesor/august.py * Changed if/else to if/continue Changed logic so that if the door sensor state is unknown during initalization the debug log is written and then continue the loop instead of using if/else logic. * Added available property for Door Sensor Added the available property for the Door Sensor and setting it to False if a status unknown is received. * Updated setting self._available Changed line for setting self._available to what Martin provided. Much more efficient to read. :-)
2018-10-19 07:37:02 +00:00
_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 | LockDetail
) -> 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
if isinstance(detail, DoorbellDetail):
return detail.is_online or detail.is_standby
return detail.bridge_is_online
2024-06-19 14:21:04 +00:00
def _retrieve_time_based_state(
activities: set[ActivityType], data: AugustData, detail: DoorbellDetail
) -> bool:
"""Get the latest state of the sensor."""
stream = data.activity_stream
2024-06-19 14:21:04 +00:00
if latest := stream.get_latest_device_activity(detail.device_id, activities):
return _activity_time_based_state(latest)
return False
_RING_ACTIVITIES = {ActivityType.DOORBELL_DING}
def _retrieve_ding_state(data: AugustData, detail: DoorbellDetail | LockDetail) -> bool:
stream = data.activity_stream
latest = stream.get_latest_device_activity(detail.device_id, _RING_ACTIVITIES)
if latest is None or (
data.push_updates_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(frozen=True, kw_only=True)
class AugustDoorbellBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describes August binary_sensor entity."""
value_fn: Callable[[AugustData, DoorbellDetail], bool]
is_time_based: bool
SENSOR_TYPE_DOOR = BinarySensorEntityDescription(
key="open",
device_class=BinarySensorDeviceClass.DOOR,
)
SENSOR_TYPES_VIDEO_DOORBELL = (
AugustDoorbellBinarySensorEntityDescription(
key="motion",
device_class=BinarySensorDeviceClass.MOTION,
2024-06-19 14:21:04 +00:00
value_fn=partial(_retrieve_time_based_state, {ActivityType.DOORBELL_MOTION}),
is_time_based=True,
),
AugustDoorbellBinarySensorEntityDescription(
key="image capture",
translation_key="image_capture",
2024-06-19 14:21:04 +00:00
value_fn=partial(
_retrieve_time_based_state, {ActivityType.DOORBELL_IMAGE_CAPTURE}
),
is_time_based=True,
),
AugustDoorbellBinarySensorEntityDescription(
key="online",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=_retrieve_online_state,
is_time_based=False,
),
)
SENSOR_TYPES_DOORBELL: tuple[AugustDoorbellBinarySensorEntityDescription, ...] = (
AugustDoorbellBinarySensorEntityDescription(
key="ding",
device_class=BinarySensorDeviceClass.OCCUPANCY,
value_fn=_retrieve_ding_state,
is_time_based=True,
),
)
async def async_setup_entry(
hass: HomeAssistant,
2024-05-02 12:39:02 +00:00
config_entry: AugustConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the August binary sensors."""
2024-05-02 12:39:02 +00:00
data = config_entry.runtime_data
entities: list[BinarySensorEntity] = []
for lock in data.locks:
detail = data.get_device_detail(lock.device_id)
if detail.doorsense:
entities.append(AugustDoorBinarySensor(data, lock, SENSOR_TYPE_DOOR))
Add doorsense sensor for August 3rd Gen Smart Lock Pro (#17299) * Add doorsense sensor for August 3rd Gen Smart Lock Pro Add a binary sensor to August for the August 3rd Gen Smart Lock Pro doorsense sensor. This is a re-do from PR 17116 https://github.com/home-assistant/home-assistant/pull/17116 that I closed due to rebase issue on my end. * Changed to use snjoetw provided code Going through the py-august I found that snjoetw had provided updated versions for the august component (august.py and binary_sensor/august.py) to include DoorSense sensor. Changed what I did to to what snjoetw provided instead as he split it into 2 classes; much cleaner I think. I modified his coding with: Fixes that were done to the August component and not part of the coding snjoetw provided. Added the debug logging improvement I had done in the code. Note, fix I committed earlier for lock atribute (lock/august.py) is thus still the same. * Reverted change from add_device to add_entities Missed an item when merging snjoetw's code with current. Fixed. * Updated call from add_devices to add_entities as well Updated the call from add_devices to add_entities. * Fixed permissions on files Fixed permissions on components/august.py and binary_snesor/august.py * Changed if/else to if/continue Changed logic so that if the door sensor state is unknown during initalization the debug log is written and then continue the loop instead of using if/else logic. * Added available property for Door Sensor Added the available property for the Door Sensor and setting it to False if a status unknown is received. * Updated setting self._available Changed line for setting self._available to what Martin provided. Much more efficient to read. :-)
2018-10-19 07:37:02 +00:00
if detail.doorbell:
entities.extend(
AugustDoorbellBinarySensor(data, lock, description)
for description in SENSOR_TYPES_DOORBELL
)
for doorbell in data.doorbells:
entities.extend(
AugustDoorbellBinarySensor(data, doorbell, description)
for description in SENSOR_TYPES_DOORBELL + SENSOR_TYPES_VIDEO_DOORBELL
)
2021-04-25 09:32:34 +00:00
async_add_entities(entities)
class AugustDoorBinarySensor(AugustDescriptionEntity, BinarySensorEntity):
Add doorsense sensor for August 3rd Gen Smart Lock Pro (#17299) * Add doorsense sensor for August 3rd Gen Smart Lock Pro Add a binary sensor to August for the August 3rd Gen Smart Lock Pro doorsense sensor. This is a re-do from PR 17116 https://github.com/home-assistant/home-assistant/pull/17116 that I closed due to rebase issue on my end. * Changed to use snjoetw provided code Going through the py-august I found that snjoetw had provided updated versions for the august component (august.py and binary_sensor/august.py) to include DoorSense sensor. Changed what I did to to what snjoetw provided instead as he split it into 2 classes; much cleaner I think. I modified his coding with: Fixes that were done to the August component and not part of the coding snjoetw provided. Added the debug logging improvement I had done in the code. Note, fix I committed earlier for lock atribute (lock/august.py) is thus still the same. * Reverted change from add_device to add_entities Missed an item when merging snjoetw's code with current. Fixed. * Updated call from add_devices to add_entities as well Updated the call from add_devices to add_entities. * Fixed permissions on files Fixed permissions on components/august.py and binary_snesor/august.py * Changed if/else to if/continue Changed logic so that if the door sensor state is unknown during initalization the debug log is written and then continue the loop instead of using if/else logic. * Added available property for Door Sensor Added the available property for the Door Sensor and setting it to False if a status unknown is received. * Updated setting self._available Changed line for setting self._available to what Martin provided. Much more efficient to read. :-)
2018-10-19 07:37:02 +00:00
"""Representation of an August Door binary sensor."""
_attr_device_class = BinarySensorDeviceClass.DOOR
description: BinarySensorEntityDescription
Add doorsense sensor for August 3rd Gen Smart Lock Pro (#17299) * Add doorsense sensor for August 3rd Gen Smart Lock Pro Add a binary sensor to August for the August 3rd Gen Smart Lock Pro doorsense sensor. This is a re-do from PR 17116 https://github.com/home-assistant/home-assistant/pull/17116 that I closed due to rebase issue on my end. * Changed to use snjoetw provided code Going through the py-august I found that snjoetw had provided updated versions for the august component (august.py and binary_sensor/august.py) to include DoorSense sensor. Changed what I did to to what snjoetw provided instead as he split it into 2 classes; much cleaner I think. I modified his coding with: Fixes that were done to the August component and not part of the coding snjoetw provided. Added the debug logging improvement I had done in the code. Note, fix I committed earlier for lock atribute (lock/august.py) is thus still the same. * Reverted change from add_device to add_entities Missed an item when merging snjoetw's code with current. Fixed. * Updated call from add_devices to add_entities as well Updated the call from add_devices to add_entities. * Fixed permissions on files Fixed permissions on components/august.py and binary_snesor/august.py * Changed if/else to if/continue Changed logic so that if the door sensor state is unknown during initalization the debug log is written and then continue the loop instead of using if/else logic. * Added available property for Door Sensor Added the available property for the Door Sensor and setting it to False if a status unknown is received. * Updated setting self._available Changed line for setting self._available to what Martin provided. Much more efficient to read. :-)
2018-10-19 07:37:02 +00:00
@callback
2024-01-18 22:13:08 +00:00
def _update_from_data(self) -> None:
"""Get the latest state of the sensor and update activity."""
2024-06-19 14:21:04 +00:00
if door_activity := self._get_latest({ActivityType.DOOR_OPERATION}):
update_lock_detail_from_activity(self._detail, door_activity)
2024-06-19 19:11:36 +00:00
if door_activity.was_pushed:
2021-04-25 09:32:34 +00:00
self._detail.set_online(True)
2024-06-19 14:21:04 +00:00
if bridge_activity := self._get_latest({ActivityType.BRIDGE_OPERATION}):
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(AugustDescriptionEntity, BinarySensorEntity):
"""Representation of an August binary sensor."""
entity_description: AugustDoorbellBinarySensorEntityDescription
_check_for_off_update_listener: Callable[[], None] | None = None
@callback
2024-01-18 22:13:08 +00:00
def _update_from_data(self) -> None:
"""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
2024-06-18 20:43:16 +00:00
@callback
def _async_scheduled_update(self, now: datetime) -> None:
"""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()
2024-01-18 22:13:08 +00:00
def _schedule_update_to_recheck_turn_off_sensor(self) -> None:
"""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._check_for_off_update_listener = async_call_later(
2024-06-18 20:43:16 +00:00
self.hass, TIME_TO_RECHECK_DETECTION, self._async_scheduled_update
)
2024-01-18 22:13:08 +00:00
def _cancel_any_pending_updates(self) -> None:
"""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
2023-02-17 18:15:56 +00:00
async def async_will_remove_from_hass(self) -> None:
"""When removing cancel any scheduled updates."""
self._cancel_any_pending_updates()
await super().async_will_remove_from_hass()