Upgrade Ring to new version (#30666)

* Upgrade Ring to new version

* Move legacy cleanup down

* Fix test
pull/30803/head
Paulus Schoutsen 2020-01-11 16:04:39 -08:00
parent 86bdba2ea2
commit 718f3438ea
16 changed files with 121 additions and 123 deletions

View File

@ -5,8 +5,7 @@ from functools import partial
import logging
from pathlib import Path
from requests.exceptions import ConnectTimeout, HTTPError
from ring_doorbell import Ring
from ring_doorbell import Auth, Ring
import voluptuous as vol
from homeassistant import config_entries
@ -14,6 +13,7 @@ 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
from homeassistant.util.async_ import run_callback_threadsafe
_LOGGER = logging.getLogger(__name__)
@ -28,7 +28,6 @@ DATA_RING_CHIMES = "ring_chimes"
DATA_TRACK_INTERVAL = "ring_track_interval"
DOMAIN = "ring"
DEFAULT_CACHEDB = ".ring_cache.pickle"
DEFAULT_ENTITY_NAMESPACE = "ring"
SIGNAL_UPDATE_RING = "ring_update"
@ -54,6 +53,14 @@ async def async_setup(hass, config):
if DOMAIN not in config:
return True
def legacy_cleanup():
"""Clean up old tokens."""
old_cache = Path(hass.config.path(".ring_cache.pickle"))
if old_cache.is_file():
old_cache.unlink()
await hass.async_add_executor_job(legacy_cleanup)
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
@ -69,30 +76,20 @@ async def async_setup(hass, config):
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.async_create(
"Error: {}<br />"
"You will need to restart hass after fixing."
"".format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID,
)
return False
if not ring.is_connected:
_LOGGER.error("Unable to connect to Ring service")
return False
def token_updater(token):
"""Handle from sync context when token is updated."""
run_callback_threadsafe(
hass.loop,
partial(
hass.config_entries.async_update_entry,
entry,
data={**entry.data, "token": token},
),
).result()
auth = Auth(entry.data["token"], token_updater)
ring = Ring(auth)
await hass.async_add_executor_job(finish_setup_entry, hass, ring)
@ -106,9 +103,10 @@ async def async_setup_entry(hass, entry):
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
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
@ -160,8 +158,3 @@ async def async_unload_entry(hass, entry):
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

@ -5,7 +5,7 @@ import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.const import ATTR_ATTRIBUTION
from . import ATTRIBUTION, DATA_RING_DOORBELLS, DATA_RING_STICKUP_CAMS
from . import ATTRIBUTION, DATA_RING_DOORBELLS, DATA_RING_STICKUP_CAMS, DOMAIN
_LOGGER = logging.getLogger(__name__)
@ -72,14 +72,23 @@ class RingBinarySensor(BinarySensorDevice):
"""Return a unique ID."""
return self._unique_id
@property
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,
"manufacturer": "Ring",
}
@property
def device_state_attributes(self):
"""Return the state attributes."""
attrs = {}
attrs[ATTR_ATTRIBUTION] = ATTRIBUTION
attrs["device_id"] = self._data.id
attrs["firmware"] = self._data.firmware
attrs["timezone"] = self._data.timezone
if self._data.alert and self._data.alert_expires_at:

View File

@ -18,6 +18,7 @@ from . import (
ATTRIBUTION,
DATA_RING_DOORBELLS,
DATA_RING_STICKUP_CAMS,
DOMAIN,
SIGNAL_UPDATE_RING,
)
@ -86,16 +87,23 @@ class RingCam(Camera):
"""Return a unique ID."""
return self._camera.id
@property
def device_info(self):
"""Return device info."""
return {
"identifiers": {(DOMAIN, self._camera.id)},
"sw_version": self._camera.firmware,
"name": self._camera.name,
"model": self._camera.kind,
"manufacturer": "Ring",
}
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {
ATTR_ATTRIBUTION: ATTRIBUTION,
"device_id": self._camera.id,
"firmware": self._camera.firmware,
"kind": self._camera.kind,
"timezone": self._camera.timezone,
"type": self._camera.family,
"video_url": self._video_url,
"last_video_id": self._last_video_id,
}

View File

@ -1,21 +1,19 @@
"""Config flow for Ring integration."""
from functools import partial
import logging
from oauthlib.oauth2 import AccessDeniedError
from ring_doorbell import Ring
from ring_doorbell import Auth
import voluptuous as vol
from homeassistant import config_entries, core, exceptions
from . import DEFAULT_CACHEDB, DOMAIN # pylint: disable=unused-import
from . import 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:
@ -23,21 +21,16 @@ async def validate_input(hass: core.HomeAssistant, data):
raise Require2FA
auth = Auth()
try:
ring = await hass.async_add_executor_job(
partial(
Ring,
username=data["username"],
password=data["password"],
cache_file=cache,
auth_callback=otp_callback,
)
token = await hass.async_add_executor_job(
auth.fetch_token, data["username"], data["password"], otp_callback,
)
except AccessDeniedError:
raise InvalidAuth
if not ring.is_connected:
raise InvalidAuth
return token
class RingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@ -56,12 +49,12 @@ class RingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors = {}
if user_input is not None:
try:
await validate_input(self.hass, user_input)
token = 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"]},
data={"username": user_input["username"], "token": token},
)
except Require2FA:
self.user_pass = user_input

View File

@ -7,7 +7,7 @@ from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
import homeassistant.util.dt as dt_util
from . import DATA_RING_STICKUP_CAMS, SIGNAL_UPDATE_RING
from . import DATA_RING_STICKUP_CAMS, DOMAIN, SIGNAL_UPDATE_RING
_LOGGER = logging.getLogger(__name__)
@ -84,6 +84,17 @@ class RingLight(Light):
"""If the switch is currently on or off."""
return self._light_on
@property
def device_info(self):
"""Return device info."""
return {
"identifiers": {(DOMAIN, self._device.id)},
"sw_version": self._device.firmware,
"name": self._device.name,
"model": self._device.kind,
"manufacturer": "Ring",
}
def _set_light(self, new_state):
"""Update light state, and causes Home Assistant to correctly update."""
self._device.lights = new_state

View File

@ -2,7 +2,7 @@
"domain": "ring",
"name": "Ring",
"documentation": "https://www.home-assistant.io/integrations/ring",
"requirements": ["ring_doorbell==0.2.9"],
"requirements": ["ring_doorbell==0.4.0"],
"dependencies": ["ffmpeg"],
"codeowners": [],
"config_flow": true

View File

@ -12,6 +12,7 @@ from . import (
DATA_RING_CHIMES,
DATA_RING_DOORBELLS,
DATA_RING_STICKUP_CAMS,
DOMAIN,
SIGNAL_UPDATE_RING,
)
@ -108,6 +109,7 @@ class RingSensor(Entity):
self._disp_disconnect = async_dispatcher_connect(
self.hass, SIGNAL_UPDATE_RING, self._update_callback
)
await self.hass.async_add_executor_job(self._data.update)
async def async_will_remove_from_hass(self):
"""Disconnect callbacks."""
@ -140,17 +142,24 @@ class RingSensor(Entity):
"""Return a unique ID."""
return self._unique_id
@property
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,
"manufacturer": "Ring",
}
@property
def device_state_attributes(self):
"""Return the state attributes."""
attrs = {}
attrs[ATTR_ATTRIBUTION] = ATTRIBUTION
attrs["device_id"] = self._data.id
attrs["firmware"] = self._data.firmware
attrs["kind"] = self._data.kind
attrs["timezone"] = self._data.timezone
attrs["type"] = self._data.family
attrs["wifi_name"] = self._data.wifi_name
if self._extra and self._sensor_type.startswith("last_"):

View File

@ -7,7 +7,7 @@ from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
import homeassistant.util.dt as dt_util
from . import DATA_RING_STICKUP_CAMS, SIGNAL_UPDATE_RING
from . import DATA_RING_STICKUP_CAMS, DOMAIN, SIGNAL_UPDATE_RING
_LOGGER = logging.getLogger(__name__)
@ -76,6 +76,17 @@ class BaseRingSwitch(SwitchDevice):
"""Update controlled via the hub."""
return False
@property
def device_info(self):
"""Return device info."""
return {
"identifiers": {(DOMAIN, self._device.id)},
"sw_version": self._device.firmware,
"name": self._device.name,
"model": self._device.kind,
"manufacturer": "Ring",
}
class SirenSwitch(BaseRingSwitch):
"""Creates a switch to turn the ring cameras siren on and off."""

View File

@ -1753,7 +1753,7 @@ rfk101py==0.0.1
rflink==0.0.50
# homeassistant.components.ring
ring_doorbell==0.2.9
ring_doorbell==0.4.0
# homeassistant.components.fleetgo
ritassist==0.9.2

View File

@ -567,7 +567,7 @@ restrictedpython==5.0
rflink==0.0.50
# homeassistant.components.ring
ring_doorbell==0.2.9
ring_doorbell==0.4.0
# homeassistant.components.yamaha
rxv==0.6.0

View File

@ -9,7 +9,9 @@ from tests.common import MockConfigEntry
async def setup_platform(hass, platform):
"""Set up the ring platform and prerequisites."""
MockConfigEntry(domain=DOMAIN, data={"username": "foo"}).add_to_hass(hass)
MockConfigEntry(domain=DOMAIN, data={"username": "foo", "token": {}}).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

@ -1,21 +1,12 @@
"""Configuration for Ring tests."""
from asynctest import patch
import pytest
import requests_mock
from tests.common import load_fixture
@pytest.fixture(name="ring_mock")
def ring_save_mock():
"""Fixture to mock a ring."""
with patch("ring_doorbell._exists_cache", return_value=False):
with patch("ring_doorbell._save_cache", return_value=True) as save_mock:
yield save_mock
@pytest.fixture(name="requests_mock")
def requests_mock_fixture(ring_mock):
def requests_mock_fixture():
"""Fixture to provide a requests mocker."""
with requests_mock.mock() as mock:
# Note all devices have an id of 987652, but a different device_id.

View File

@ -1,6 +1,5 @@
"""The tests for the Ring binary sensor platform."""
from asyncio import run_coroutine_threadsafe
import os
import unittest
from unittest.mock import patch
@ -9,12 +8,7 @@ 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,
mock_storage,
)
from tests.common import get_test_home_assistant, load_fixture, mock_storage
from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG
@ -28,15 +22,9 @@ class TestRingBinarySensorSetup(unittest.TestCase):
for device in devices:
self.DEVICES.append(device)
def cleanup(self):
"""Cleanup any data created from the tests."""
if os.path.isfile(self.cache):
os.remove(self.cache)
def setUp(self):
"""Initialize values for this testcase class."""
self.hass = get_test_home_assistant()
self.cache = get_test_config_dir(base_ring.DEFAULT_CACHEDB)
self.config = {
"username": "foo",
"password": "bar",
@ -46,7 +34,6 @@ class TestRingBinarySensorSetup(unittest.TestCase):
def tearDown(self):
"""Stop everything that was started."""
self.hass.stop()
self.cleanup()
@requests_mock.Mocker()
def test_binary_sensor(self, mock):

View File

@ -18,8 +18,10 @@ async def test_form(hass):
assert result["errors"] == {}
with patch(
"homeassistant.components.ring.config_flow.Ring",
return_value=Mock(is_connected=True),
"homeassistant.components.ring.config_flow.Auth",
return_value=Mock(
fetch_token=Mock(return_value={"access_token": "mock-token"})
),
), patch(
"homeassistant.components.ring.async_setup", return_value=mock_coro(True)
) as mock_setup, patch(
@ -34,6 +36,7 @@ async def test_form(hass):
assert result2["title"] == "hello@home-assistant.io"
assert result2["data"] == {
"username": "hello@home-assistant.io",
"token": {"access_token": "mock-token"},
}
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
@ -47,7 +50,8 @@ async def test_form_invalid_auth(hass):
)
with patch(
"homeassistant.components.ring.config_flow.Ring", side_effect=InvalidAuth,
"homeassistant.components.ring.config_flow.Auth.fetch_token",
side_effect=InvalidAuth,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],

View File

@ -2,7 +2,6 @@
from asyncio import run_coroutine_threadsafe
from copy import deepcopy
from datetime import timedelta
import os
import unittest
import requests_mock
@ -10,7 +9,7 @@ import requests_mock
from homeassistant import setup
import homeassistant.components.ring as ring
from tests.common import get_test_config_dir, get_test_home_assistant, load_fixture
from tests.common import get_test_home_assistant, load_fixture
ATTRIBUTION = "Data provided by Ring.com"
@ -22,21 +21,14 @@ VALID_CONFIG = {
class TestRing(unittest.TestCase):
"""Tests the Ring component."""
def cleanup(self):
"""Cleanup any data created from the tests."""
if os.path.isfile(self.cache):
os.remove(self.cache)
def setUp(self):
"""Initialize values for this test case class."""
self.hass = get_test_home_assistant()
self.cache = get_test_config_dir(ring.DEFAULT_CACHEDB)
self.config = VALID_CONFIG
def tearDown(self): # pylint: disable=invalid-name
"""Stop everything that was started."""
self.hass.stop()
self.cleanup()
@requests_mock.Mocker()
def test_setup(self, mock):

View File

@ -1,6 +1,5 @@
"""The tests for the Ring sensor platform."""
from asyncio import run_coroutine_threadsafe
import os
import unittest
from unittest.mock import patch
@ -10,12 +9,7 @@ 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,
mock_storage,
)
from tests.common import get_test_home_assistant, load_fixture, mock_storage
from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG
@ -29,15 +23,9 @@ class TestRingSensorSetup(unittest.TestCase):
for device in devices:
self.DEVICES.append(device)
def cleanup(self):
"""Cleanup any data created from the tests."""
if os.path.isfile(self.cache):
os.remove(self.cache)
def setUp(self):
"""Initialize values for this testcase class."""
self.hass = get_test_home_assistant()
self.cache = get_test_config_dir(base_ring.DEFAULT_CACHEDB)
self.config = {
"username": "foo",
"password": "bar",
@ -55,7 +43,6 @@ class TestRingSensorSetup(unittest.TestCase):
def tearDown(self):
"""Stop everything that was started."""
self.hass.stop()
self.cleanup()
@requests_mock.Mocker()
def test_sensor(self, mock):
@ -97,6 +84,13 @@ class TestRingSensorSetup(unittest.TestCase):
).result()
for device in self.DEVICES:
# Mimick add to hass
device.hass = self.hass
run_coroutine_threadsafe(
device.async_added_to_hass(), self.hass.loop,
).result()
# Entity update data from ring data
device.update()
if device.name == "Front Battery":
expected_icon = icon_for_battery_level(
@ -104,18 +98,12 @@ class TestRingSensorSetup(unittest.TestCase):
)
assert device.icon == expected_icon
assert 80 == device.state
assert "hp_cam_v1" == device.device_state_attributes["kind"]
assert "stickup_cams" == device.device_state_attributes["type"]
if device.name == "Front Door Battery":
assert 100 == device.state
assert "lpd_v1" == device.device_state_attributes["kind"]
assert "chimes" != device.device_state_attributes["type"]
if device.name == "Downstairs Volume":
assert 2 == device.state
assert "1.2.3" == device.device_state_attributes["firmware"]
assert "ring_mock_wifi" == device.device_state_attributes["wifi_name"]
assert "mdi:bell-ring" == device.icon
assert "chimes" == device.device_state_attributes["type"]
if device.name == "Front Door Last Activity":
assert not device.device_state_attributes["answered"]
assert "America/New_York" == device.device_state_attributes["timezone"]