Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ homeassistant/components/rainmachine/* @bachya
homeassistant/components/random/* @fabaff
homeassistant/components/repetier/* @MTrab
homeassistant/components/rfxtrx/* @danielhiversen
homeassistant/components/ring/* @balloob
homeassistant/components/rmvtransport/* @cgtobi
homeassistant/components/roomba/* @pschmitt
homeassistant/components/saj/* @fredericvl
Expand Down
126 changes: 83 additions & 43 deletions homeassistant/components/ring/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, __version__
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.event import track_time_interval
from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.util.async_ import run_callback_threadsafe

_LOGGER = logging.getLogger(__name__)
Expand All @@ -22,14 +23,13 @@
NOTIFICATION_ID = "ring_notification"
NOTIFICATION_TITLE = "Ring Setup"

DATA_RING_DOORBELLS = "ring_doorbells"
DATA_RING_STICKUP_CAMS = "ring_stickup_cams"
DATA_RING_CHIMES = "ring_chimes"
DATA_HEALTH_DATA_TRACKER = "ring_health_data"
DATA_TRACK_INTERVAL = "ring_track_interval"

DOMAIN = "ring"
DEFAULT_ENTITY_NAMESPACE = "ring"
SIGNAL_UPDATE_RING = "ring_update"
SIGNAL_UPDATE_HEALTH_RING = "ring_health_update"

SCAN_INTERVAL = timedelta(seconds=10)

Expand Down Expand Up @@ -88,51 +88,41 @@ def token_updater(token):
),
).result()

auth = Auth(entry.data["token"], token_updater)
auth = Auth(f"HomeAssistant/{__version__}", entry.data["token"], token_updater)
ring = Ring(auth)

await hass.async_add_executor_job(finish_setup_entry, hass, ring)
await hass.async_add_executor_job(ring.update_data)

hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ring

for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)

return True


def finish_setup_entry(hass, ring):
"""Finish setting up entry."""
devices = ring.devices
hass.data[DATA_RING_CHIMES] = chimes = devices["chimes"]
hass.data[DATA_RING_DOORBELLS] = doorbells = devices["doorbells"]
hass.data[DATA_RING_STICKUP_CAMS] = stickup_cams = devices["stickup_cams"]

ring_devices = chimes + doorbells + stickup_cams

def service_hub_refresh(service):
hub_refresh()

def timer_hub_refresh(event_time):
hub_refresh()

def hub_refresh():
"""Call ring to refresh information."""
_LOGGER.debug("Updating Ring Hub component")

for camera in ring_devices:
_LOGGER.debug("Updating camera %s", camera.name)
camera.update()
if hass.services.has_service(DOMAIN, "update"):
return True

dispatcher_send(hass, SIGNAL_UPDATE_RING)
async def refresh_all(_):
"""Refresh all ring accounts."""
await asyncio.gather(
*[
hass.async_add_executor_job(api.update_data)
for api in hass.data[DOMAIN].values()
]
)
async_dispatcher_send(hass, SIGNAL_UPDATE_RING)

# register service
hass.services.register(DOMAIN, "update", service_hub_refresh)
hass.services.async_register(DOMAIN, "update", refresh_all)

# register scan interval for ring
hass.data[DATA_TRACK_INTERVAL] = track_time_interval(
hass, timer_hub_refresh, SCAN_INTERVAL
hass.data[DATA_TRACK_INTERVAL] = async_track_time_interval(
hass, refresh_all, SCAN_INTERVAL
)
hass.data[DATA_HEALTH_DATA_TRACKER] = HealthDataUpdater(hass)

return True


async def async_unload_entry(hass, entry):
Expand All @@ -148,13 +138,63 @@ async def async_unload_entry(hass, entry):
if not unload_ok:
return False

await hass.async_add_executor_job(hass.data[DATA_TRACK_INTERVAL])
hass.data[DOMAIN].pop(entry.entry_id)

if len(hass.data[DOMAIN]) != 0:
return True

# Last entry unloaded, clean up
hass.data.pop(DATA_TRACK_INTERVAL)()
hass.data.pop(DATA_HEALTH_DATA_TRACKER)
hass.services.async_remove(DOMAIN, "update")

hass.data.pop(DATA_RING_DOORBELLS)
hass.data.pop(DATA_RING_STICKUP_CAMS)
hass.data.pop(DATA_RING_CHIMES)
hass.data.pop(DATA_TRACK_INTERVAL)
return True


class HealthDataUpdater:
"""Data storage for health data."""

def __init__(self, hass):
"""Track devices that need healh data updated."""
Comment thread
balloob marked this conversation as resolved.
Outdated
self.hass = hass
self.devices = {}
self._unsub_interval = None

async def track_device(self, config_entry_id, device):
"""Track a device."""
if not self.devices:
self._unsub_interval = async_track_time_interval(
self.hass, self.refresh_all, SCAN_INTERVAL
)

key = (config_entry_id, device.device_id)

if key not in self.devices:
self.devices[key] = {
"device": device,
"count": 1,
}
else:
self.devices[key]["count"] += 1

await self.hass.async_add_executor_job(device.update_health_data)

@callback
def untrack_device(self, config_entry_id, device):
"""Untrack a device."""
key = (config_entry_id, device.device_id)
self.devices[key]["count"] -= 1

if self.devices[key]["count"] == 0:
self.devices.pop(key)

if not self.devices:
self._unsub_interval()
self._unsub_interval = None

def refresh_all(self, _):
"""Refresh all registered devices."""
for info in self.devices.values():
info["device"].update_health_data()

return unload_ok
dispatcher_send(self.hass, SIGNAL_UPDATE_HEALTH_RING)
82 changes: 52 additions & 30 deletions homeassistant/components/ring/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
"""This component provides HA sensor support for Ring Door Bell/Chimes."""
from datetime import timedelta
from itertools import chain
import logging

from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.const import ATTR_ATTRIBUTION
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect

from . import ATTRIBUTION, DATA_RING_DOORBELLS, DATA_RING_STICKUP_CAMS, DOMAIN
from . import ATTRIBUTION, DOMAIN, SIGNAL_UPDATE_RING

_LOGGER = logging.getLogger(__name__)

Expand All @@ -20,37 +23,62 @@

async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Ring binary sensors from a config entry."""
ring_doorbells = hass.data[DATA_RING_DOORBELLS]
ring_stickup_cams = hass.data[DATA_RING_STICKUP_CAMS]
ring = hass.data[DOMAIN][config_entry.entry_id]
devices = ring.devices()

sensors = []
for device in ring_doorbells: # ring.doorbells is doing I/O
for device in chain(devices["doorbots"], devices["authorized_doorbots"]):
for sensor_type in SENSOR_TYPES:
if "doorbell" in SENSOR_TYPES[sensor_type][1]:
sensors.append(RingBinarySensor(hass, device, sensor_type))
sensors.append(RingBinarySensor(hass, ring, device, sensor_type))

for device in ring_stickup_cams: # ring.stickup_cams is doing I/O
for device in devices["stickup_cams"]:
for sensor_type in SENSOR_TYPES:
if "stickup_cams" in SENSOR_TYPES[sensor_type][1]:
sensors.append(RingBinarySensor(hass, device, sensor_type))
sensors.append(RingBinarySensor(hass, ring, device, sensor_type))

async_add_entities(sensors, True)


class RingBinarySensor(BinarySensorDevice):
"""A binary sensor implementation for Ring device."""

def __init__(self, hass, data, sensor_type):
def __init__(self, hass, ring, device, sensor_type):
Comment thread
balloob marked this conversation as resolved.
Outdated
"""Initialize a sensor for Ring device."""
super().__init__()
Comment thread
balloob marked this conversation as resolved.
Outdated
self._sensor_type = sensor_type
self._data = data
self._ring = ring
self._device = device
self._name = "{0} {1}".format(
self._data.name, SENSOR_TYPES.get(self._sensor_type)[0]
self._device.name, SENSOR_TYPES.get(self._sensor_type)[0]
)
self._device_class = SENSOR_TYPES.get(self._sensor_type)[2]
self._state = None
self._unique_id = f"{self._data.id}-{self._sensor_type}"
self._unique_id = f"{self._device.id}-{self._sensor_type}"
self._disp_disconnect = None

async def async_added_to_hass(self):
"""Register callbacks."""
self._disp_disconnect = async_dispatcher_connect(
self.hass, SIGNAL_UPDATE_RING, self._update_callback
)

async def async_will_remove_from_hass(self):
"""Disconnect callbacks."""
if self._disp_disconnect:
self._disp_disconnect()
self._disp_disconnect = None

@callback
def _update_callback(self):
"""Call update method."""
self.async_schedule_update_ha_state(True)
_LOGGER.debug("Updating Ring binary sensor %s (callback)", self.name)

@property
def should_poll(self):
"""Return False, updates are controlled via the hub."""
return False

@property
def name(self):
Expand All @@ -76,10 +104,10 @@ def unique_id(self):
def device_info(self):
"""Return device info."""
return {
"identifiers": {(DOMAIN, self._data.id)},
"sw_version": self._data.firmware,
"name": self._data.name,
"model": self._data.kind,
"identifiers": {(DOMAIN, self._device.device_id)},
"sw_version": self._device.firmware,
"name": self._device.name,
"model": self._device.model,
"manufacturer": "Ring",
}

Expand All @@ -89,22 +117,16 @@ def device_state_attributes(self):
attrs = {}
attrs[ATTR_ATTRIBUTION] = ATTRIBUTION

attrs["timezone"] = self._data.timezone

if self._data.alert and self._data.alert_expires_at:
attrs["expires_at"] = self._data.alert_expires_at
attrs["state"] = self._data.alert.get("state")
if self._device.alert and self._device.alert_expires_at:
attrs["expires_at"] = self._device.alert_expires_at
attrs["state"] = self._device.alert.get("state")

return attrs

def update(self):
async def async_update(self):
"""Get the latest data and updates the state."""
self._data.check_alerts()

if self._data.alert:
if self._sensor_type == self._data.alert.get(
"kind"
) and self._data.account_id == self._data.alert.get("doorbot_id"):
self._state = True
else:
self._state = False
self._state = any(
alert["kind"] == self._sensor_type
and alert["doorbot_id"] == self._device.id
for alert in self._ring.active_alerts()
)
Loading