Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
28 changes: 28 additions & 0 deletions homeassistant/components/ring/.translations/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"config": {
"abort": {
"already_configured": "Device is already configured"
},
"error": {
"cannot_connect": "Failed to connect, please try again",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
"step": {
"2fa": {
"data": {
"2fa": "Two-factor code"
},
"title": "Enter two-factor authentication"
},
"user": {
"data": {
"password": "Password",
"username": "Username"
},
"title": "Connect to the device"
}
},
"title": "Ring"
}
}
112 changes: 91 additions & 21 deletions homeassistant/components/ring/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
"""Support for Ring Doorbell/Chimes."""
import asyncio
from datetime import timedelta
from functools import partial
import logging
from pathlib import Path

from requests.exceptions import ConnectTimeout, HTTPError
from ring_doorbell import Ring
import voluptuous as vol

from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME
from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.event import track_time_interval
Expand All @@ -21,6 +25,7 @@
DATA_RING_DOORBELLS = "ring_doorbells"
DATA_RING_STICKUP_CAMS = "ring_stickup_cams"
DATA_RING_CHIMES = "ring_chimes"
DATA_TRACK_INTERVAL = "ring_track_interval"

DOMAIN = "ring"
DEFAULT_CACHEDB = ".ring_cache.pickle"
Expand All @@ -29,41 +34,54 @@

SCAN_INTERVAL = timedelta(seconds=10)

PLATFORMS = ("binary_sensor", "light", "sensor", "switch", "camera")

CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
vol.Optional(DOMAIN): vol.Schema(
{
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period,
}
)
},
extra=vol.ALLOW_EXTRA,
)


def setup(hass, config):
async def async_setup(hass, config):
"""Set up the Ring component."""
conf = config[DOMAIN]
username = conf[CONF_USERNAME]
password = conf[CONF_PASSWORD]
scan_interval = conf[CONF_SCAN_INTERVAL]

try:
cache = hass.config.path(DEFAULT_CACHEDB)
ring = Ring(username=username, password=password, cache_file=cache)
if not ring.is_connected:
return False
hass.data[DATA_RING_CHIMES] = chimes = ring.chimes
hass.data[DATA_RING_DOORBELLS] = doorbells = ring.doorbells
hass.data[DATA_RING_STICKUP_CAMS] = stickup_cams = ring.stickup_cams
if DOMAIN not in config:
return True

hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data={
"username": config[DOMAIN]["username"],
"password": config[DOMAIN]["password"],
},
)
)
return True

ring_devices = chimes + doorbells + stickup_cams

async def async_setup_entry(hass, entry):
"""Set up a config entry."""
cache = hass.config.path(DEFAULT_CACHEDB)
try:
ring = await hass.async_add_executor_job(
partial(
Ring,
username=entry.data["username"],
password="invalid-password",
cache_file=cache,
)
)
except (ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Ring service: %s", str(ex))
hass.components.persistent_notification.create(
hass.components.persistent_notification.async_create(
"Error: {}<br />"
"You will need to restart hass after fixing."
"".format(ex),
Expand All @@ -72,6 +90,28 @@ def setup(hass, config):
)
return False

if not ring.is_connected:
_LOGGER.error("Unable to connect to Ring service")
return False

await hass.async_add_executor_job(finish_setup_entry, hass, 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."""
hass.data[DATA_RING_CHIMES] = chimes = ring.chimes
hass.data[DATA_RING_DOORBELLS] = doorbells = ring.doorbells
hass.data[DATA_RING_STICKUP_CAMS] = stickup_cams = ring.stickup_cams

ring_devices = chimes + doorbells + stickup_cams

def service_hub_refresh(service):
hub_refresh()

Expand All @@ -92,6 +132,36 @@ def hub_refresh():
hass.services.register(DOMAIN, "update", service_hub_refresh)

# register scan interval for ring
track_time_interval(hass, timer_hub_refresh, scan_interval)
hass.data[DATA_TRACK_INTERVAL] = track_time_interval(
hass, timer_hub_refresh, SCAN_INTERVAL
)


async def async_unload_entry(hass, entry):
"""Unload Ring entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
Comment thread
MartinHjelmare marked this conversation as resolved.
for component in PLATFORMS
]
)
)
if not unload_ok:
return False

return True
await hass.async_add_executor_job(hass.data[DATA_TRACK_INTERVAL])

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 unload_ok


async def async_remove_entry(hass, entry):
"""Act when an entry is removed."""
await hass.async_add_executor_job(Path(hass.config.path(DEFAULT_CACHEDB)).unlink)
41 changes: 9 additions & 32 deletions homeassistant/components/ring/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,10 @@
from datetime import timedelta
import logging

import voluptuous as vol

from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_ENTITY_NAMESPACE,
CONF_MONITORED_CONDITIONS,
)
import homeassistant.helpers.config_validation as cv

from . import (
ATTRIBUTION,
DATA_RING_DOORBELLS,
DATA_RING_STICKUP_CAMS,
DEFAULT_ENTITY_NAMESPACE,
)
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.const import ATTR_ATTRIBUTION

from . import ATTRIBUTION, DATA_RING_DOORBELLS, DATA_RING_STICKUP_CAMS

_LOGGER = logging.getLogger(__name__)

Expand All @@ -29,35 +17,24 @@
"motion": ["Motion", ["doorbell", "stickup_cams"], "motion"],
}

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(
CONF_ENTITY_NAMESPACE, default=DEFAULT_ENTITY_NAMESPACE
): cv.string,
vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): vol.All(
cv.ensure_list, [vol.In(SENSOR_TYPES)]
),
}
)


def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up a sensor for a Ring device."""
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]

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

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

add_entities(sensors, True)
async_add_entities(sensors, True)


class RingBinarySensor(BinarySensorDevice):
Expand Down
66 changes: 20 additions & 46 deletions homeassistant/components/ring/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@

from haffmpeg.camera import CameraMjpeg
from haffmpeg.tools import IMAGE_JPEG, ImageFrame
import voluptuous as vol

from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
from homeassistant.components.camera import Camera
from homeassistant.components.ffmpeg import DATA_FFMPEG
from homeassistant.const import ATTR_ATTRIBUTION
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util import dt as dt_util
Expand All @@ -20,77 +18,57 @@
ATTRIBUTION,
DATA_RING_DOORBELLS,
DATA_RING_STICKUP_CAMS,
NOTIFICATION_ID,
SIGNAL_UPDATE_RING,
)

CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments"

FORCE_REFRESH_INTERVAL = timedelta(minutes=45)

_LOGGER = logging.getLogger(__name__)

NOTIFICATION_TITLE = "Ring Camera Setup"

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string}
)


def setup_platform(hass, config, add_entities, discovery_info=None):
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up a Ring Door Bell and StickUp Camera."""
ring_doorbell = hass.data[DATA_RING_DOORBELLS]
ring_stickup_cams = hass.data[DATA_RING_STICKUP_CAMS]

cams = []
cams_no_plan = []
for camera in ring_doorbell + ring_stickup_cams:
if camera.has_subscription:
cams.append(RingCam(hass, camera, config))
else:
cams_no_plan.append(camera)

# show notification for all cameras without an active subscription
if cams_no_plan:
cameras = str(", ".join([camera.name for camera in cams_no_plan]))

err_msg = (
"""A Ring Protect Plan is required for the"""
""" following cameras: {}.""".format(cameras)
)
if not camera.has_subscription:
continue

_LOGGER.error(err_msg)
hass.components.persistent_notification.create(
"Error: {}<br />"
"You will need to restart hass after fixing."
"".format(err_msg),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID,
)
camera = await hass.async_add_executor_job(RingCam, hass, camera)
cams.append(camera)

add_entities(cams, True)
return True
async_add_entities(cams, True)


class RingCam(Camera):
"""An implementation of a Ring Door Bell camera."""

def __init__(self, hass, camera, device_info):
def __init__(self, hass, camera):
"""Initialize a Ring Door Bell camera."""
super().__init__()
self._camera = camera
self._hass = hass
self._name = self._camera.name
self._ffmpeg = hass.data[DATA_FFMPEG]
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
self._last_video_id = self._camera.last_recording_id
self._video_url = self._camera.recording_url(self._last_video_id)
Comment thread
MartinHjelmare marked this conversation as resolved.
self._utcnow = dt_util.utcnow()
self._expires_at = FORCE_REFRESH_INTERVAL + self._utcnow
self._disp_disconnect = None

async def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect(self.hass, SIGNAL_UPDATE_RING, self._update_callback)
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):
Expand Down Expand Up @@ -131,11 +109,7 @@ async def async_camera_image(self):
return

image = await asyncio.shield(
ffmpeg.get_image(
self._video_url,
output_format=IMAGE_JPEG,
extra_cmd=self._ffmpeg_arguments,
)
ffmpeg.get_image(self._video_url, output_format=IMAGE_JPEG,)
)
return image

Expand All @@ -146,7 +120,7 @@ async def handle_async_mjpeg_stream(self, request):
return

stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
await stream.open_camera(self._video_url, extra_cmd=self._ffmpeg_arguments)
await stream.open_camera(self._video_url)

try:
stream_reader = await stream.get_reader()
Expand Down
Loading