UVC camera platform handling unavailable NVR or cameras better (#14864)

* fixed tests: using correct camera configuration now and error handling tests must be separated out to ensure that the setup_component call is actually executed

* better error handling during setup; raising PlatformNotReady in likely recoverable cases; added tests
This commit is contained in:
Malte Franken 2018-06-09 15:22:17 +10:00 committed by Sebastian Muszynski
parent d3d9d9ebf2
commit f242418986
2 changed files with 81 additions and 35 deletions

View File

@ -13,6 +13,7 @@ import voluptuous as vol
from homeassistant.const import CONF_PORT from homeassistant.const import CONF_PORT
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.exceptions import PlatformNotReady
REQUIREMENTS = ['uvcclient==0.10.1'] REQUIREMENTS = ['uvcclient==0.10.1']
@ -41,25 +42,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
port = config[CONF_PORT] port = config[CONF_PORT]
from uvcclient import nvr from uvcclient import nvr
nvrconn = nvr.UVCRemote(addr, port, key)
try: try:
# Exceptions may be raised in all method calls to the nvr library.
nvrconn = nvr.UVCRemote(addr, port, key)
cameras = nvrconn.index() cameras = nvrconn.index()
identifier = 'id' if nvrconn.server_version >= (3, 2, 0) else 'uuid'
# Filter out airCam models, which are not supported in the latest
# version of UnifiVideo and which are EOL by Ubiquiti
cameras = [
camera for camera in cameras
if 'airCam' not in nvrconn.get_camera(camera[identifier])['model']]
except nvr.NotAuthorized: except nvr.NotAuthorized:
_LOGGER.error("Authorization failure while connecting to NVR") _LOGGER.error("Authorization failure while connecting to NVR")
return False return False
except nvr.NvrError: except nvr.NvrError as ex:
_LOGGER.error("NVR refuses to talk to me") _LOGGER.error("NVR refuses to talk to me: %s", str(ex))
return False raise PlatformNotReady
except requests.exceptions.ConnectionError as ex: except requests.exceptions.ConnectionError as ex:
_LOGGER.error("Unable to connect to NVR: %s", str(ex)) _LOGGER.error("Unable to connect to NVR: %s", str(ex))
return False raise PlatformNotReady
identifier = 'id' if nvrconn.server_version >= (3, 2, 0) else 'uuid'
# Filter out airCam models, which are not supported in the latest
# version of UnifiVideo and which are EOL by Ubiquiti
cameras = [
camera for camera in cameras
if 'airCam' not in nvrconn.get_camera(camera[identifier])['model']]
add_devices([UnifiVideoCamera(nvrconn, add_devices([UnifiVideoCamera(nvrconn,
camera[identifier], camera[identifier],

View File

@ -7,6 +7,7 @@ import requests
from uvcclient import camera from uvcclient import camera
from uvcclient import nvr from uvcclient import nvr
from homeassistant.exceptions import PlatformNotReady
from homeassistant.setup import setup_component from homeassistant.setup import setup_component
from homeassistant.components.camera import uvc from homeassistant.components.camera import uvc
from tests.common import get_test_home_assistant from tests.common import get_test_home_assistant
@ -34,21 +35,21 @@ class TestUVCSetup(unittest.TestCase):
'port': 123, 'port': 123,
'key': 'secret', 'key': 'secret',
} }
fake_cameras = [ mock_cameras = [
{'uuid': 'one', 'name': 'Front', 'id': 'id1'}, {'uuid': 'one', 'name': 'Front', 'id': 'id1'},
{'uuid': 'two', 'name': 'Back', 'id': 'id2'}, {'uuid': 'two', 'name': 'Back', 'id': 'id2'},
{'uuid': 'three', 'name': 'Old AirCam', 'id': 'id3'}, {'uuid': 'three', 'name': 'Old AirCam', 'id': 'id3'},
] ]
def fake_get_camera(uuid): def mock_get_camera(uuid):
"""Create a fake camera.""" """Create a mock camera."""
if uuid == 'id3': if uuid == 'id3':
return {'model': 'airCam'} return {'model': 'airCam'}
else: else:
return {'model': 'UVC'} return {'model': 'UVC'}
mock_remote.return_value.index.return_value = fake_cameras mock_remote.return_value.index.return_value = mock_cameras
mock_remote.return_value.get_camera.side_effect = fake_get_camera mock_remote.return_value.get_camera.side_effect = mock_get_camera
mock_remote.return_value.server_version = (3, 2, 0) mock_remote.return_value.server_version = (3, 2, 0)
assert setup_component(self.hass, 'camera', {'camera': config}) assert setup_component(self.hass, 'camera', {'camera': config})
@ -71,11 +72,11 @@ class TestUVCSetup(unittest.TestCase):
'nvr': 'foo', 'nvr': 'foo',
'key': 'secret', 'key': 'secret',
} }
fake_cameras = [ mock_cameras = [
{'uuid': 'one', 'name': 'Front', 'id': 'id1'}, {'uuid': 'one', 'name': 'Front', 'id': 'id1'},
{'uuid': 'two', 'name': 'Back', 'id': 'id2'}, {'uuid': 'two', 'name': 'Back', 'id': 'id2'},
] ]
mock_remote.return_value.index.return_value = fake_cameras mock_remote.return_value.index.return_value = mock_cameras
mock_remote.return_value.get_camera.return_value = {'model': 'UVC'} mock_remote.return_value.get_camera.return_value = {'model': 'UVC'}
mock_remote.return_value.server_version = (3, 2, 0) mock_remote.return_value.server_version = (3, 2, 0)
@ -99,11 +100,11 @@ class TestUVCSetup(unittest.TestCase):
'nvr': 'foo', 'nvr': 'foo',
'key': 'secret', 'key': 'secret',
} }
fake_cameras = [ mock_cameras = [
{'uuid': 'one', 'name': 'Front', 'id': 'id1'}, {'uuid': 'one', 'name': 'Front', 'id': 'id1'},
{'uuid': 'two', 'name': 'Back', 'id': 'id2'}, {'uuid': 'two', 'name': 'Back', 'id': 'id2'},
] ]
mock_remote.return_value.index.return_value = fake_cameras mock_remote.return_value.index.return_value = mock_cameras
mock_remote.return_value.get_camera.return_value = {'model': 'UVC'} mock_remote.return_value.get_camera.return_value = {'model': 'UVC'}
mock_remote.return_value.server_version = (3, 1, 3) mock_remote.return_value.server_version = (3, 1, 3)
@ -133,19 +134,62 @@ class TestUVCSetup(unittest.TestCase):
@mock.patch.object(uvc, 'UnifiVideoCamera') @mock.patch.object(uvc, 'UnifiVideoCamera')
@mock.patch('uvcclient.nvr.UVCRemote') @mock.patch('uvcclient.nvr.UVCRemote')
def test_setup_nvr_errors(self, mock_remote, mock_uvc): def setup_nvr_errors_during_indexing(self, error, mock_remote, mock_uvc):
"""Test for NVR errors.""" """Setup test for NVR errors during indexing."""
errors = [nvr.NotAuthorized, nvr.NvrError,
requests.exceptions.ConnectionError]
config = { config = {
'platform': 'uvc', 'platform': 'uvc',
'nvr': 'foo', 'nvr': 'foo',
'key': 'secret', 'key': 'secret',
} }
for error in errors: mock_remote.return_value.index.side_effect = error
mock_remote.return_value.index.side_effect = error assert setup_component(self.hass, 'camera', {'camera': config})
assert setup_component(self.hass, 'camera', config) assert not mock_uvc.called
assert not mock_uvc.called
def test_setup_nvr_error_during_indexing_notauthorized(self):
"""Test for error: nvr.NotAuthorized."""
self.setup_nvr_errors_during_indexing(nvr.NotAuthorized)
def test_setup_nvr_error_during_indexing_nvrerror(self):
"""Test for error: nvr.NvrError."""
self.setup_nvr_errors_during_indexing(nvr.NvrError)
self.assertRaises(PlatformNotReady)
def test_setup_nvr_error_during_indexing_connectionerror(self):
"""Test for error: requests.exceptions.ConnectionError."""
self.setup_nvr_errors_during_indexing(
requests.exceptions.ConnectionError)
self.assertRaises(PlatformNotReady)
@mock.patch.object(uvc, 'UnifiVideoCamera')
@mock.patch('uvcclient.nvr.UVCRemote.__init__')
def setup_nvr_errors_during_initialization(self, error, mock_remote,
mock_uvc):
"""Setup test for NVR errors during initialization."""
config = {
'platform': 'uvc',
'nvr': 'foo',
'key': 'secret',
}
mock_remote.return_value = None
mock_remote.side_effect = error
assert setup_component(self.hass, 'camera', {'camera': config})
assert not mock_remote.index.called
assert not mock_uvc.called
def test_setup_nvr_error_during_initialization_notauthorized(self):
"""Test for error: nvr.NotAuthorized."""
self.setup_nvr_errors_during_initialization(nvr.NotAuthorized)
def test_setup_nvr_error_during_initialization_nvrerror(self):
"""Test for error: nvr.NvrError."""
self.setup_nvr_errors_during_initialization(nvr.NvrError)
self.assertRaises(PlatformNotReady)
def test_setup_nvr_error_during_initialization_connectionerror(self):
"""Test for error: requests.exceptions.ConnectionError."""
self.setup_nvr_errors_during_initialization(
requests.exceptions.ConnectionError)
self.assertRaises(PlatformNotReady)
class TestUVC(unittest.TestCase): class TestUVC(unittest.TestCase):
@ -208,8 +252,8 @@ class TestUVC(unittest.TestCase):
"""Test the login tries.""" """Test the login tries."""
responses = [0] responses = [0]
def fake_login(*a): def mock_login(*a):
"""Fake login.""" """Mock login."""
try: try:
responses.pop(0) responses.pop(0)
raise socket.error raise socket.error
@ -217,7 +261,7 @@ class TestUVC(unittest.TestCase):
pass pass
mock_store.return_value.get_camera_password.return_value = None mock_store.return_value.get_camera_password.return_value = None
mock_camera.return_value.login.side_effect = fake_login mock_camera.return_value.login.side_effect = mock_login
self.uvc._login() self.uvc._login()
self.assertEqual(2, mock_camera.call_count) self.assertEqual(2, mock_camera.call_count)
self.assertEqual('host-b', self.uvc._connect_addr) self.assertEqual('host-b', self.uvc._connect_addr)
@ -263,8 +307,8 @@ class TestUVC(unittest.TestCase):
"""Test the re-authentication.""" """Test the re-authentication."""
responses = [0] responses = [0]
def fake_snapshot(): def mock_snapshot():
"""Fake snapshot.""" """Mock snapshot."""
try: try:
responses.pop() responses.pop()
raise camera.CameraAuthError() raise camera.CameraAuthError()
@ -273,7 +317,7 @@ class TestUVC(unittest.TestCase):
return 'image' return 'image'
self.uvc._camera = mock.MagicMock() self.uvc._camera = mock.MagicMock()
self.uvc._camera.get_snapshot.side_effect = fake_snapshot self.uvc._camera.get_snapshot.side_effect = mock_snapshot
with mock.patch.object(self.uvc, '_login') as mock_login: with mock.patch.object(self.uvc, '_login') as mock_login:
self.assertEqual('image', self.uvc.camera_image()) self.assertEqual('image', self.uvc.camera_image())
self.assertEqual(mock_login.call_count, 1) self.assertEqual(mock_login.call_count, 1)