Coordinate all august detail and activity updates (#32249)
* Removing polling from august * Now using subscribers to the detail and activity * Fix hash to list keys * continue to the next house if one fails * Add async_signal_device_id_update * Fix double initial update * Handle self.hass not being available until after async_added_to_hass * Remove not needed await * Fix regression with device name
This commit is contained in:
parent
fefbe02d44
commit
223c01d842
12 changed files with 385 additions and 460 deletions
|
@ -13,51 +13,45 @@ from homeassistant.components.binary_sensor import (
|
|||
BinarySensorDevice,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from .const import (
|
||||
AUGUST_DEVICE_UPDATE,
|
||||
DATA_AUGUST,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
MIN_TIME_BETWEEN_DETAIL_UPDATES,
|
||||
)
|
||||
from .const import DATA_AUGUST, DOMAIN
|
||||
from .entity import AugustEntityMixin
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TIME_TO_DECLARE_DETECTION = timedelta(seconds=60)
|
||||
|
||||
SCAN_INTERVAL = MIN_TIME_BETWEEN_DETAIL_UPDATES
|
||||
|
||||
|
||||
async def _async_retrieve_online_state(data, detail):
|
||||
def _retrieve_online_state(data, detail):
|
||||
"""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
|
||||
|
||||
|
||||
async def _async_retrieve_motion_state(data, detail):
|
||||
def _retrieve_motion_state(data, detail):
|
||||
|
||||
return await _async_activity_time_based_state(
|
||||
return _activity_time_based_state(
|
||||
data,
|
||||
detail.device_id,
|
||||
[ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_DING],
|
||||
)
|
||||
|
||||
|
||||
async def _async_retrieve_ding_state(data, detail):
|
||||
def _retrieve_ding_state(data, detail):
|
||||
|
||||
return await _async_activity_time_based_state(
|
||||
return _activity_time_based_state(
|
||||
data, detail.device_id, [ActivityType.DOORBELL_DING]
|
||||
)
|
||||
|
||||
|
||||
async def _async_activity_time_based_state(data, device_id, activity_types):
|
||||
def _activity_time_based_state(data, device_id, activity_types):
|
||||
"""Get the latest state of the sensor."""
|
||||
latest = data.activity_stream.async_get_latest_device_activity(
|
||||
device_id, activity_types
|
||||
)
|
||||
latest = data.activity_stream.get_latest_device_activity(device_id, activity_types)
|
||||
|
||||
if latest is not None:
|
||||
start = latest.activity_start_time
|
||||
|
@ -69,15 +63,17 @@ async def _async_activity_time_based_state(data, device_id, activity_types):
|
|||
SENSOR_NAME = 0
|
||||
SENSOR_DEVICE_CLASS = 1
|
||||
SENSOR_STATE_PROVIDER = 2
|
||||
SENSOR_STATE_IS_TIME_BASED = 3
|
||||
|
||||
# sensor_type: [name, device_class, async_state_provider]
|
||||
# sensor_type: [name, device_class, state_provider, is_time_based]
|
||||
SENSOR_TYPES_DOORBELL = {
|
||||
"doorbell_ding": ["Ding", DEVICE_CLASS_OCCUPANCY, _async_retrieve_ding_state],
|
||||
"doorbell_motion": ["Motion", DEVICE_CLASS_MOTION, _async_retrieve_motion_state],
|
||||
"doorbell_ding": ["Ding", DEVICE_CLASS_OCCUPANCY, _retrieve_ding_state, True],
|
||||
"doorbell_motion": ["Motion", DEVICE_CLASS_MOTION, _retrieve_motion_state, True],
|
||||
"doorbell_online": [
|
||||
"Online",
|
||||
DEVICE_CLASS_CONNECTIVITY,
|
||||
_async_retrieve_online_state,
|
||||
_retrieve_online_state,
|
||||
False,
|
||||
],
|
||||
}
|
||||
|
||||
|
@ -88,8 +84,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||
devices = []
|
||||
|
||||
for door in data.locks:
|
||||
if not data.lock_has_doorsense(door.device_id):
|
||||
_LOGGER.debug("Not adding sensor class door for lock %s ", door.device_name)
|
||||
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)
|
||||
|
@ -107,19 +107,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||
async_add_entities(devices, True)
|
||||
|
||||
|
||||
class AugustDoorBinarySensor(BinarySensorDevice):
|
||||
class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorDevice):
|
||||
"""Representation of an August Door binary sensor."""
|
||||
|
||||
def __init__(self, data, sensor_type, door):
|
||||
def __init__(self, data, sensor_type, device):
|
||||
"""Initialize the sensor."""
|
||||
self._undo_dispatch_subscription = None
|
||||
super().__init__(data, device)
|
||||
self._data = data
|
||||
self._sensor_type = sensor_type
|
||||
self._door = door
|
||||
self._device = device
|
||||
self._state = None
|
||||
self._available = False
|
||||
self._firmware_version = None
|
||||
self._model = None
|
||||
self._update_from_data()
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
|
@ -139,76 +138,43 @@ class AugustDoorBinarySensor(BinarySensorDevice):
|
|||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor."""
|
||||
return f"{self._door.device_name} Open"
|
||||
return f"{self._device.device_name} Open"
|
||||
|
||||
async def async_update(self):
|
||||
@callback
|
||||
def _update_from_data(self):
|
||||
"""Get the latest state of the sensor and update activity."""
|
||||
door_activity = self._data.activity_stream.async_get_latest_device_activity(
|
||||
self._door.device_id, [ActivityType.DOOR_OPERATION]
|
||||
door_activity = self._data.activity_stream.get_latest_device_activity(
|
||||
self._device_id, [ActivityType.DOOR_OPERATION]
|
||||
)
|
||||
detail = await self._data.async_get_lock_detail(self._door.device_id)
|
||||
detail = self._detail
|
||||
|
||||
if door_activity is not None:
|
||||
update_lock_detail_from_activity(detail, door_activity)
|
||||
|
||||
lock_door_state = None
|
||||
self._available = False
|
||||
if detail is not None:
|
||||
lock_door_state = detail.door_state
|
||||
self._available = detail.bridge_is_online
|
||||
self._firmware_version = detail.firmware_version
|
||||
self._model = detail.model
|
||||
lock_door_state = detail.door_state
|
||||
self._available = detail.bridge_is_online
|
||||
|
||||
self._state = lock_door_state == LockDoorStatus.OPEN
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Get the unique of the door open binary sensor."""
|
||||
return f"{self._door.device_id}_open"
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return the device_info of the device."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self._door.device_id)},
|
||||
"name": self._door.device_name,
|
||||
"manufacturer": DEFAULT_NAME,
|
||||
"sw_version": self._firmware_version,
|
||||
"model": self._model,
|
||||
}
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
|
||||
@callback
|
||||
def update():
|
||||
"""Update the state."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
self._undo_dispatch_subscription = async_dispatcher_connect(
|
||||
self.hass, f"{AUGUST_DEVICE_UPDATE}-{self._door.device_id}", update
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Undo subscription."""
|
||||
if self._undo_dispatch_subscription:
|
||||
self._undo_dispatch_subscription()
|
||||
return f"{self._device_id}_open"
|
||||
|
||||
|
||||
class AugustDoorbellBinarySensor(BinarySensorDevice):
|
||||
class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorDevice):
|
||||
"""Representation of an August binary sensor."""
|
||||
|
||||
def __init__(self, data, sensor_type, doorbell):
|
||||
def __init__(self, data, sensor_type, device):
|
||||
"""Initialize the sensor."""
|
||||
self._undo_dispatch_subscription = None
|
||||
super().__init__(data, device)
|
||||
self._check_for_off_update_listener = None
|
||||
self._data = data
|
||||
self._sensor_type = sensor_type
|
||||
self._doorbell = doorbell
|
||||
self._device = device
|
||||
self._state = None
|
||||
self._available = False
|
||||
self._firmware_version = None
|
||||
self._model = None
|
||||
self._update_from_data()
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
|
@ -228,42 +194,47 @@ class AugustDoorbellBinarySensor(BinarySensorDevice):
|
|||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor."""
|
||||
return f"{self._doorbell.device_name} {SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_NAME]}"
|
||||
return f"{self._device.device_name} {SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_NAME]}"
|
||||
|
||||
async def async_update(self):
|
||||
@property
|
||||
def _state_provider(self):
|
||||
"""Return the state provider for the binary sensor."""
|
||||
return SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_STATE_PROVIDER]
|
||||
|
||||
@property
|
||||
def _is_time_based(self):
|
||||
"""Return true of false if the sensor is time based."""
|
||||
return SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_STATE_IS_TIME_BASED]
|
||||
|
||||
@callback
|
||||
def _update_from_data(self):
|
||||
"""Get the latest state of the sensor."""
|
||||
self._cancel_any_pending_updates()
|
||||
async_state_provider = SENSOR_TYPES_DOORBELL[self._sensor_type][
|
||||
SENSOR_STATE_PROVIDER
|
||||
]
|
||||
detail = await self._data.async_get_doorbell_detail(self._doorbell.device_id)
|
||||
# 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 self.device_class == DEVICE_CLASS_CONNECTIVITY:
|
||||
self._available = True
|
||||
else:
|
||||
self._available = detail is not None and (
|
||||
detail.is_online or detail.is_standby
|
||||
)
|
||||
self._state = self._state_provider(self._data, self._detail)
|
||||
|
||||
self._state = None
|
||||
if detail is not None:
|
||||
self._firmware_version = detail.firmware_version
|
||||
self._model = detail.model
|
||||
self._state = await async_state_provider(self._data, detail)
|
||||
if self._state and self.device_class != DEVICE_CLASS_CONNECTIVITY:
|
||||
self._schedule_update_to_recheck_turn_off_sensor()
|
||||
if self._is_time_based:
|
||||
self._available = _retrieve_online_state(self._data, self._detail)
|
||||
self._schedule_update_to_recheck_turn_off_sensor()
|
||||
else:
|
||||
self._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._state:
|
||||
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."""
|
||||
_LOGGER.debug("%s: executing scheduled update", self.entity_id)
|
||||
self.async_schedule_update_ha_state(True)
|
||||
self._check_for_off_update_listener = None
|
||||
self._update_from_data()
|
||||
|
||||
self._check_for_off_update_listener = async_track_point_in_utc_time(
|
||||
self.hass, _scheduled_update, utcnow() + TIME_TO_DECLARE_DETECTION
|
||||
|
@ -272,41 +243,19 @@ class AugustDoorbellBinarySensor(BinarySensorDevice):
|
|||
def _cancel_any_pending_updates(self):
|
||||
"""Cancel any updates to recheck a sensor to see if it is ready to turn off."""
|
||||
if self._check_for_off_update_listener:
|
||||
_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()
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Get the unique id of the doorbell sensor."""
|
||||
return (
|
||||
f"{self._doorbell.device_id}_"
|
||||
f"{self._device_id}_"
|
||||
f"{SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_NAME].lower()}"
|
||||
)
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return the device_info of the device."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self._doorbell.device_id)},
|
||||
"name": self._doorbell.device_name,
|
||||
"manufacturer": "August",
|
||||
"sw_version": self._firmware_version,
|
||||
"model": self._model,
|
||||
}
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
|
||||
@callback
|
||||
def update():
|
||||
"""Update the state."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
self._undo_dispatch_subscription = async_dispatcher_connect(
|
||||
self.hass, f"{AUGUST_DEVICE_UPDATE}-{self._doorbell.device_id}", update
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Undo subscription."""
|
||||
if self._undo_dispatch_subscription:
|
||||
self._undo_dispatch_subscription()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue