diff --git a/homeassistant/components/ring/.translations/en.json b/homeassistant/components/ring/.translations/en.json
new file mode 100644
index 00000000000..db4665b6c0a
--- /dev/null
+++ b/homeassistant/components/ring/.translations/en.json
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py
index a68749b2c67..18c753f4dc9 100644
--- a/homeassistant/components/ring/__init__.py
+++ b/homeassistant/components/ring/__init__.py
@@ -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
@@ -21,6 +25,7 @@ NOTIFICATION_TITLE = "Ring Setup"
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"
@@ -29,13 +34,14 @@ SIGNAL_UPDATE_RING = "ring_update"
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,
}
)
},
@@ -43,27 +49,39 @@ CONFIG_SCHEMA = vol.Schema(
)
-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]
+ 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
+
+
+async def async_setup_entry(hass, entry):
+ """Set up a config entry."""
+ cache = hass.config.path(DEFAULT_CACHEDB)
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
-
- ring_devices = chimes + doorbells + stickup_cams
-
+ 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: {}
"
"You will need to restart hass after fixing."
"".format(ex),
@@ -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()
@@ -92,6 +132,36 @@ def setup(hass, config):
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
+ )
- return True
+
+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)
+ for component in PLATFORMS
+ ]
+ )
+ )
+ if not unload_ok:
+ return False
+
+ 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)
diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py
index 86d26ec25b4..0706752ffb2 100644
--- a/homeassistant/components/ring/binary_sensor.py
+++ b/homeassistant/components/ring/binary_sensor.py
@@ -2,22 +2,10 @@
from datetime import timedelta
import logging
-import voluptuous as vol
+from homeassistant.components.binary_sensor import BinarySensorDevice
+from homeassistant.const import ATTR_ATTRIBUTION
-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 . import ATTRIBUTION, DATA_RING_DOORBELLS, DATA_RING_STICKUP_CAMS
_LOGGER = logging.getLogger(__name__)
@@ -29,35 +17,24 @@ SENSOR_TYPES = {
"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):
diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py
index 1d2fe6ff67b..a3b34afa056 100644
--- a/homeassistant/components/ring/camera.py
+++ b/homeassistant/components/ring/camera.py
@@ -5,13 +5,11 @@ import logging
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
@@ -20,77 +18,57 @@ from . import (
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)
+ if not camera.has_subscription:
+ continue
- # show notification for all cameras without an active subscription
- if cams_no_plan:
- cameras = str(", ".join([camera.name for camera in cams_no_plan]))
+ camera = await hass.async_add_executor_job(RingCam, hass, camera)
+ cams.append(camera)
- err_msg = (
- """A Ring Protect Plan is required for the"""
- """ following cameras: {}.""".format(cameras)
- )
-
- _LOGGER.error(err_msg)
- hass.components.persistent_notification.create(
- "Error: {}
"
- "You will need to restart hass after fixing."
- "".format(err_msg),
- title=NOTIFICATION_TITLE,
- notification_id=NOTIFICATION_ID,
- )
-
- 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)
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):
@@ -131,11 +109,7 @@ class RingCam(Camera):
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
@@ -146,7 +120,7 @@ class RingCam(Camera):
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()
diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py
new file mode 100644
index 00000000000..bdb60cc26c5
--- /dev/null
+++ b/homeassistant/components/ring/config_flow.py
@@ -0,0 +1,105 @@
+"""Config flow for Ring integration."""
+from functools import partial
+import logging
+
+from oauthlib.oauth2 import AccessDeniedError
+from ring_doorbell import Ring
+import voluptuous as vol
+
+from homeassistant import config_entries, core, exceptions
+
+from . import DEFAULT_CACHEDB, DOMAIN # pylint: disable=unused-import
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def validate_input(hass: core.HomeAssistant, data):
+ """Validate the user input allows us to connect."""
+ cache = hass.config.path(DEFAULT_CACHEDB)
+
+ def otp_callback():
+ if "2fa" in data:
+ return data["2fa"]
+
+ raise Require2FA
+
+ try:
+ ring = await hass.async_add_executor_job(
+ partial(
+ Ring,
+ username=data["username"],
+ password=data["password"],
+ cache_file=cache,
+ auth_callback=otp_callback,
+ )
+ )
+ except AccessDeniedError:
+ raise InvalidAuth
+
+ if not ring.is_connected:
+ raise InvalidAuth
+
+
+class RingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for Ring."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ user_pass = None
+
+ async def async_step_user(self, user_input=None):
+ """Handle the initial step."""
+ if self._async_current_entries():
+ return self.async_abort(reason="already_configured")
+
+ errors = {}
+ if user_input is not None:
+ try:
+ await validate_input(self.hass, user_input)
+ await self.async_set_unique_id(user_input["username"])
+
+ return self.async_create_entry(
+ title=user_input["username"],
+ data={"username": user_input["username"]},
+ )
+ except Require2FA:
+ self.user_pass = user_input
+
+ return await self.async_step_2fa()
+
+ except InvalidAuth:
+ errors["base"] = "invalid_auth"
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ errors["base"] = "unknown"
+
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema({"username": str, "password": str}),
+ errors=errors,
+ )
+
+ async def async_step_2fa(self, user_input=None):
+ """Handle 2fa step."""
+ if user_input:
+ return await self.async_step_user({**self.user_pass, **user_input})
+
+ return self.async_show_form(
+ step_id="2fa", data_schema=vol.Schema({"2fa": str}),
+ )
+
+ async def async_step_import(self, user_input):
+ """Handle import."""
+ if self._async_current_entries():
+ return self.async_abort(reason="already_configured")
+
+ return await self.async_step_user(user_input)
+
+
+class Require2FA(exceptions.HomeAssistantError):
+ """Error to indicate we require 2FA."""
+
+
+class InvalidAuth(exceptions.HomeAssistantError):
+ """Error to indicate there is invalid auth."""
diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py
index fe048731352..1b360f24f1f 100644
--- a/homeassistant/components/ring/light.py
+++ b/homeassistant/components/ring/light.py
@@ -23,7 +23,7 @@ ON_STATE = "on"
OFF_STATE = "off"
-def setup_platform(hass, config, add_entities, discovery_info=None):
+async def async_setup_entry(hass, config_entry, async_add_entities):
"""Create the lights for the Ring devices."""
cameras = hass.data[DATA_RING_STICKUP_CAMS]
lights = []
@@ -32,7 +32,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
if device.has_capability("light"):
lights.append(RingLight(device))
- add_entities(lights, True)
+ async_add_entities(lights, True)
class RingLight(Light):
@@ -44,10 +44,19 @@ class RingLight(Light):
self._unique_id = self._device.id
self._light_on = False
self._no_updates_until = dt_util.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):
diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json
index 124df7d162b..b8a3c26bd8b 100644
--- a/homeassistant/components/ring/manifest.json
+++ b/homeassistant/components/ring/manifest.json
@@ -4,5 +4,6 @@
"documentation": "https://www.home-assistant.io/integrations/ring",
"requirements": ["ring_doorbell==0.2.9"],
"dependencies": ["ffmpeg"],
- "codeowners": []
+ "codeowners": [],
+ "config_flow": true
}
diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py
index b54c750664e..532f15f94c1 100644
--- a/homeassistant/components/ring/sensor.py
+++ b/homeassistant/components/ring/sensor.py
@@ -1,16 +1,8 @@
"""This component provides HA sensor support for Ring Door Bell/Chimes."""
import logging
-import voluptuous as vol
-
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import (
- ATTR_ATTRIBUTION,
- CONF_ENTITY_NAMESPACE,
- CONF_MONITORED_CONDITIONS,
-)
+from homeassistant.const import ATTR_ATTRIBUTION
from homeassistant.core import callback
-import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.icon import icon_for_battery_level
@@ -20,7 +12,6 @@ from . import (
DATA_RING_CHIMES,
DATA_RING_DOORBELLS,
DATA_RING_STICKUP_CAMS,
- DEFAULT_ENTITY_NAMESPACE,
SIGNAL_UPDATE_RING,
)
@@ -67,19 +58,8 @@ SENSOR_TYPES = {
],
}
-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):
+async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up a sensor for a Ring device."""
ring_chimes = hass.data[DATA_RING_CHIMES]
ring_doorbells = hass.data[DATA_RING_DOORBELLS]
@@ -87,22 +67,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
sensors = []
for device in ring_chimes:
- for sensor_type in config[CONF_MONITORED_CONDITIONS]:
+ for sensor_type in SENSOR_TYPES:
if "chime" in SENSOR_TYPES[sensor_type][1]:
sensors.append(RingSensor(hass, device, sensor_type))
for device in ring_doorbells:
- for sensor_type in config[CONF_MONITORED_CONDITIONS]:
+ for sensor_type in SENSOR_TYPES:
if "doorbell" in SENSOR_TYPES[sensor_type][1]:
sensors.append(RingSensor(hass, device, sensor_type))
for device in ring_stickup_cams:
- 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(RingSensor(hass, device, sensor_type))
- add_entities(sensors, True)
- return True
+ async_add_entities(sensors, True)
class RingSensor(Entity):
@@ -122,10 +101,19 @@ class RingSensor(Entity):
self._state = None
self._tz = str(hass.config.time_zone)
self._unique_id = f"{self._data.id}-{self._sensor_type}"
+ 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):
diff --git a/homeassistant/components/ring/strings.json b/homeassistant/components/ring/strings.json
new file mode 100644
index 00000000000..6dff7c00ba6
--- /dev/null
+++ b/homeassistant/components/ring/strings.json
@@ -0,0 +1,27 @@
+{
+ "config": {
+ "title": "Ring",
+ "step": {
+ "user": {
+ "title": "Sign-in with Ring account",
+ "data": {
+ "username": "Username",
+ "password": "Password"
+ }
+ },
+ "2fa": {
+ "title": "Two-factor authentication",
+ "data": {
+ "2fa": "Two-factor code"
+ }
+ }
+ },
+ "error": {
+ "invalid_auth": "Invalid authentication",
+ "unknown": "Unexpected error"
+ },
+ "abort": {
+ "already_configured": "Device is already configured"
+ }
+ }
+}
diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py
index 86f5c65d87c..51c9e64377b 100644
--- a/homeassistant/components/ring/switch.py
+++ b/homeassistant/components/ring/switch.py
@@ -22,7 +22,7 @@ SIREN_ICON = "mdi:alarm-bell"
SKIP_UPDATES_DELAY = timedelta(seconds=5)
-def setup_platform(hass, config, add_entities, discovery_info=None):
+async def async_setup_entry(hass, config_entry, async_add_entities):
"""Create the switches for the Ring devices."""
cameras = hass.data[DATA_RING_STICKUP_CAMS]
switches = []
@@ -30,7 +30,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
if device.has_capability("siren"):
switches.append(SirenSwitch(device))
- add_entities(switches, True)
+ async_add_entities(switches, True)
class BaseRingSwitch(SwitchDevice):
@@ -41,10 +41,19 @@ class BaseRingSwitch(SwitchDevice):
self._device = device
self._device_type = device_type
self._unique_id = f"{self._device.id}-{self._device_type}"
+ 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):
diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py
index 6f3f0e714f6..dcae6fd065e 100644
--- a/homeassistant/generated/config_flows.py
+++ b/homeassistant/generated/config_flows.py
@@ -66,6 +66,7 @@ FLOWS = [
"point",
"ps4",
"rainmachine",
+ "ring",
"sentry",
"simplisafe",
"smartthings",
diff --git a/tests/components/ring/common.py b/tests/components/ring/common.py
index e5042a935d6..1afc597415e 100644
--- a/tests/components/ring/common.py
+++ b/tests/components/ring/common.py
@@ -1,14 +1,15 @@
"""Common methods used across the tests for ring devices."""
+from unittest.mock import patch
+
from homeassistant.components.ring import DOMAIN
-from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME
from homeassistant.setup import async_setup_component
+from tests.common import MockConfigEntry
+
async def setup_platform(hass, platform):
"""Set up the ring platform and prerequisites."""
- config = {
- DOMAIN: {CONF_USERNAME: "foo", CONF_PASSWORD: "bar", CONF_SCAN_INTERVAL: 1000},
- platform: {"platform": DOMAIN},
- }
- assert await async_setup_component(hass, platform, config)
+ MockConfigEntry(domain=DOMAIN, data={"username": "foo"}).add_to_hass(hass)
+ with patch("homeassistant.components.ring.PLATFORMS", [platform]):
+ assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
diff --git a/tests/components/ring/conftest.py b/tests/components/ring/conftest.py
index b61840769a2..e4b516496e7 100644
--- a/tests/components/ring/conftest.py
+++ b/tests/components/ring/conftest.py
@@ -36,6 +36,10 @@ def requests_mock_fixture(ring_mock):
"https://api.ring.com/clients_api/ring_devices",
text=load_fixture("ring_devices.json"),
)
+ mock.get(
+ "https://api.ring.com/clients_api/dings/active",
+ text=load_fixture("ring_ding_active.json"),
+ )
# Mocks the response for getting the history of a device
mock.get(
"https://api.ring.com/clients_api/doorbots/987652/history",
diff --git a/tests/components/ring/test_binary_sensor.py b/tests/components/ring/test_binary_sensor.py
index c0b538b8eff..5a04017f54b 100644
--- a/tests/components/ring/test_binary_sensor.py
+++ b/tests/components/ring/test_binary_sensor.py
@@ -1,13 +1,20 @@
"""The tests for the Ring binary sensor platform."""
+from asyncio import run_coroutine_threadsafe
import os
import unittest
+from unittest.mock import patch
import requests_mock
from homeassistant.components import ring as base_ring
from homeassistant.components.ring import binary_sensor as ring
-from tests.common import get_test_config_dir, get_test_home_assistant, load_fixture
+from tests.common import (
+ get_test_config_dir,
+ get_test_home_assistant,
+ load_fixture,
+ mock_storage,
+)
from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG
@@ -68,8 +75,17 @@ class TestRingBinarySensorSetup(unittest.TestCase):
text=load_fixture("ring_chime_health_attrs.json"),
)
- base_ring.setup(self.hass, VALID_CONFIG)
- ring.setup_platform(self.hass, self.config, self.add_entities, None)
+ with mock_storage(), patch("homeassistant.components.ring.PLATFORMS", []):
+ run_coroutine_threadsafe(
+ base_ring.async_setup(self.hass, VALID_CONFIG), self.hass.loop
+ ).result()
+ run_coroutine_threadsafe(
+ self.hass.async_block_till_done(), self.hass.loop
+ ).result()
+ run_coroutine_threadsafe(
+ ring.async_setup_entry(self.hass, None, self.add_entities),
+ self.hass.loop,
+ ).result()
for device in self.DEVICES:
device.update()
diff --git a/tests/components/ring/test_config_flow.py b/tests/components/ring/test_config_flow.py
new file mode 100644
index 00000000000..46925069c31
--- /dev/null
+++ b/tests/components/ring/test_config_flow.py
@@ -0,0 +1,58 @@
+"""Test the Ring config flow."""
+from unittest.mock import Mock, patch
+
+from homeassistant import config_entries, setup
+from homeassistant.components.ring import DOMAIN
+from homeassistant.components.ring.config_flow import InvalidAuth
+
+from tests.common import mock_coro
+
+
+async def test_form(hass):
+ """Test we get the form."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == "form"
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.ring.config_flow.Ring",
+ return_value=Mock(is_connected=True),
+ ), patch(
+ "homeassistant.components.ring.async_setup", return_value=mock_coro(True)
+ ) as mock_setup, patch(
+ "homeassistant.components.ring.async_setup_entry", return_value=mock_coro(True),
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"username": "hello@home-assistant.io", "password": "test-password"},
+ )
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "hello@home-assistant.io"
+ assert result2["data"] == {
+ "username": "hello@home-assistant.io",
+ }
+ await hass.async_block_till_done()
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_form_invalid_auth(hass):
+ """Test we handle invalid auth."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.ring.config_flow.Ring", side_effect=InvalidAuth,
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"username": "hello@home-assistant.io", "password": "test-password"},
+ )
+
+ assert result2["type"] == "form"
+ assert result2["errors"] == {"base": "invalid_auth"}
diff --git a/tests/components/ring/test_init.py b/tests/components/ring/test_init.py
index 4d3fede89a9..cfc19da78bf 100644
--- a/tests/components/ring/test_init.py
+++ b/tests/components/ring/test_init.py
@@ -1,4 +1,5 @@
"""The tests for the Ring component."""
+from asyncio import run_coroutine_threadsafe
from copy import deepcopy
from datetime import timedelta
import os
@@ -59,7 +60,10 @@ class TestRing(unittest.TestCase):
"https://api.ring.com/clients_api/doorbots/987652/health",
text=load_fixture("ring_doorboot_health_attrs.json"),
)
- response = ring.setup(self.hass, self.config)
+ response = run_coroutine_threadsafe(
+ ring.async_setup(self.hass, self.config), self.hass.loop
+ ).result()
+
assert response
@requests_mock.Mocker()
diff --git a/tests/components/ring/test_sensor.py b/tests/components/ring/test_sensor.py
index dd9d36f80a1..0102020e3c2 100644
--- a/tests/components/ring/test_sensor.py
+++ b/tests/components/ring/test_sensor.py
@@ -1,6 +1,8 @@
"""The tests for the Ring sensor platform."""
+from asyncio import run_coroutine_threadsafe
import os
import unittest
+from unittest.mock import patch
import requests_mock
@@ -8,7 +10,12 @@ from homeassistant.components import ring as base_ring
import homeassistant.components.ring.sensor as ring
from homeassistant.helpers.icon import icon_for_battery_level
-from tests.common import get_test_config_dir, get_test_home_assistant, load_fixture
+from tests.common import (
+ get_test_config_dir,
+ get_test_home_assistant,
+ load_fixture,
+ mock_storage,
+)
from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG
@@ -76,8 +83,18 @@ class TestRingSensorSetup(unittest.TestCase):
"https://api.ring.com/clients_api/chimes/999999/health",
text=load_fixture("ring_chime_health_attrs.json"),
)
- base_ring.setup(self.hass, VALID_CONFIG)
- ring.setup_platform(self.hass, self.config, self.add_entities, None)
+
+ with mock_storage(), patch("homeassistant.components.ring.PLATFORMS", []):
+ run_coroutine_threadsafe(
+ base_ring.async_setup(self.hass, VALID_CONFIG), self.hass.loop
+ ).result()
+ run_coroutine_threadsafe(
+ self.hass.async_block_till_done(), self.hass.loop
+ ).result()
+ run_coroutine_threadsafe(
+ ring.async_setup_entry(self.hass, None, self.add_entities),
+ self.hass.loop,
+ ).result()
for device in self.DEVICES:
device.update()