Add Ring config flow (#30564)

* Add Ring config flow

* Address comments + migrate platforms to config entry

* Migrate camera too

* Address comments

* Fix order config flows

* setup -> async_setup
This commit is contained in:
Paulus Schoutsen 2020-01-10 21:35:31 +01:00 committed by GitHub
parent 3348f4f6d1
commit 3f29c234b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 435 additions and 146 deletions

View File

@ -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"
}
}

View File

@ -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: {}<br />"
"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)

View File

@ -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):

View File

@ -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: {}<br />"
"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()

View File

@ -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."""

View File

@ -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):

View File

@ -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
}

View File

@ -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):

View File

@ -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"
}
}
}

View File

@ -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):

View File

@ -66,6 +66,7 @@ FLOWS = [
"point",
"ps4",
"rainmachine",
"ring",
"samsungtv",
"sentry",
"simplisafe",

View File

@ -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()

View File

@ -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",

View File

@ -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()

View File

@ -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"}

View File

@ -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()

View File

@ -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()