Add support for UniFi Video >= 3.2.0

Unfortunately, Ubiquiti changed their (supposedly versioned) API in
3.2.0 which causes us to have to refer to cameras by id instead of
UUID. The firmware for 3.2.x also changed the on-camera login procedures
and snapshot functionality significantly.

This bumps the requirement for uvcclient to 0.9.0, which supports the
newer API and makes the tweaks necessary to interact properly.
This commit is contained in:
Dan Smith 2016-06-05 10:09:58 -07:00
parent f4594027fd
commit 49de55e75b
3 changed files with 65 additions and 19 deletions

View file

@ -12,7 +12,7 @@ import requests
from homeassistant.components.camera import DOMAIN, Camera
from homeassistant.helpers import validate_config
REQUIREMENTS = ['uvcclient==0.8']
REQUIREMENTS = ['uvcclient==0.9.0']
_LOGGER = logging.getLogger(__name__)
@ -45,13 +45,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.error('Unable to connect to NVR: %s', str(ex))
return False
identifier = nvrconn.server_version >= (3, 2, 0) and 'id' or '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['uuid'])['model']]
cameras = [
camera for camera in cameras
if 'airCam' not in nvrconn.get_camera(camera[identifier])['model']]
add_devices([UnifiVideoCamera(nvrconn,
camera['uuid'],
camera[identifier],
camera['name'])
for camera in cameras])
return True
@ -110,12 +112,17 @@ class UnifiVideoCamera(Camera):
dict(name=self._name))
password = 'ubnt'
if self._nvr.server_version >= (3, 2, 0):
client_cls = uvc_camera.UVCCameraClientV320
else:
client_cls = uvc_camera.UVCCameraClient
camera = None
for addr in addrs:
try:
camera = uvc_camera.UVCCameraClient(addr,
caminfo['username'],
password)
camera = client_cls(addr,
caminfo['username'],
password)
camera.login()
_LOGGER.debug('Logged into UVC camera %(name)s via %(addr)s',
dict(name=self._name, addr=addr))

View file

@ -375,7 +375,7 @@ unifi==1.2.5
urllib3
# homeassistant.components.camera.uvc
uvcclient==0.8
uvcclient==0.9.0
# homeassistant.components.verisure
vsure==0.8.1

View file

@ -23,14 +23,14 @@ class TestUVCSetup(unittest.TestCase):
'key': 'secret',
}
fake_cameras = [
{'uuid': 'one', 'name': 'Front'},
{'uuid': 'two', 'name': 'Back'},
{'uuid': 'three', 'name': 'Old AirCam'},
{'uuid': 'one', 'name': 'Front', 'id': 'id1'},
{'uuid': 'two', 'name': 'Back', 'id': 'id2'},
{'uuid': 'three', 'name': 'Old AirCam', 'id': 'id3'},
]
def fake_get_camera(uuid):
""""Create a fake camera."""
if uuid == 'three':
if uuid == 'id3':
return {'model': 'airCam'}
else:
return {'model': 'UVC'}
@ -39,13 +39,14 @@ class TestUVCSetup(unittest.TestCase):
add_devices = mock.MagicMock()
mock_remote.return_value.index.return_value = fake_cameras
mock_remote.return_value.get_camera.side_effect = fake_get_camera
mock_remote.return_value.server_version = (3, 2, 0)
self.assertTrue(uvc.setup_platform(hass, config, add_devices))
mock_remote.assert_called_once_with('foo', 123, 'secret')
add_devices.assert_called_once_with([
mock_uvc.return_value, mock_uvc.return_value])
mock_uvc.assert_has_calls([
mock.call(mock_remote.return_value, 'one', 'Front'),
mock.call(mock_remote.return_value, 'two', 'Back'),
mock.call(mock_remote.return_value, 'id1', 'Front'),
mock.call(mock_remote.return_value, 'id2', 'Back'),
])
@mock.patch('uvcclient.nvr.UVCRemote')
@ -57,13 +58,40 @@ class TestUVCSetup(unittest.TestCase):
'key': 'secret',
}
fake_cameras = [
{'uuid': 'one', 'name': 'Front'},
{'uuid': 'two', 'name': 'Back'},
{'uuid': 'one', 'name': 'Front', 'id': 'id1'},
{'uuid': 'two', 'name': 'Back', 'id': 'id2'},
]
hass = mock.MagicMock()
add_devices = mock.MagicMock()
mock_remote.return_value.index.return_value = fake_cameras
mock_remote.return_value.get_camera.return_value = {'model': 'UVC'}
mock_remote.return_value.server_version = (3, 2, 0)
self.assertTrue(uvc.setup_platform(hass, config, add_devices))
mock_remote.assert_called_once_with('foo', 7080, 'secret')
add_devices.assert_called_once_with([
mock_uvc.return_value, mock_uvc.return_value])
mock_uvc.assert_has_calls([
mock.call(mock_remote.return_value, 'id1', 'Front'),
mock.call(mock_remote.return_value, 'id2', 'Back'),
])
@mock.patch('uvcclient.nvr.UVCRemote')
@mock.patch.object(uvc, 'UnifiVideoCamera')
def test_setup_partial_config_v31x(self, mock_uvc, mock_remote):
"""Test the setup with a v3.1.x server."""
config = {
'nvr': 'foo',
'key': 'secret',
}
fake_cameras = [
{'uuid': 'one', 'name': 'Front', 'id': 'id1'},
{'uuid': 'two', 'name': 'Back', 'id': 'id2'},
]
hass = mock.MagicMock()
add_devices = mock.MagicMock()
mock_remote.return_value.index.return_value = fake_cameras
mock_remote.return_value.get_camera.return_value = {'model': 'UVC'}
mock_remote.return_value.server_version = (3, 1, 3)
self.assertTrue(uvc.setup_platform(hass, config, add_devices))
mock_remote.assert_called_once_with('foo', 7080, 'secret')
add_devices.assert_called_once_with([
@ -114,6 +142,7 @@ class TestUVC(unittest.TestCase):
'internalHost': 'host-b',
'username': 'admin',
}
self.nvr.server_version = (3, 2, 0)
def test_properties(self):
""""Test the properties."""
@ -123,7 +152,7 @@ class TestUVC(unittest.TestCase):
self.assertEqual('UVC Fake', self.uvc.model)
@mock.patch('uvcclient.store.get_info_store')
@mock.patch('uvcclient.camera.UVCCameraClient')
@mock.patch('uvcclient.camera.UVCCameraClientV320')
def test_login(self, mock_camera, mock_store):
""""Test the login."""
mock_store.return_value.get_camera_password.return_value = 'seekret'
@ -133,6 +162,16 @@ class TestUVC(unittest.TestCase):
@mock.patch('uvcclient.store.get_info_store')
@mock.patch('uvcclient.camera.UVCCameraClient')
def test_login_v31x(self, mock_camera, mock_store):
"""Test login with v3.1.x server."""
mock_store.return_value.get_camera_password.return_value = 'seekret'
self.nvr.server_version = (3, 1, 3)
self.uvc._login()
mock_camera.assert_called_once_with('host-a', 'admin', 'seekret')
mock_camera.return_value.login.assert_called_once_with()
@mock.patch('uvcclient.store.get_info_store')
@mock.patch('uvcclient.camera.UVCCameraClientV320')
def test_login_no_password(self, mock_camera, mock_store):
""""Test the login with no password."""
mock_store.return_value.get_camera_password.return_value = None
@ -141,7 +180,7 @@ class TestUVC(unittest.TestCase):
mock_camera.return_value.login.assert_called_once_with()
@mock.patch('uvcclient.store.get_info_store')
@mock.patch('uvcclient.camera.UVCCameraClient')
@mock.patch('uvcclient.camera.UVCCameraClientV320')
def test_login_tries_both_addrs_and_caches(self, mock_camera, mock_store):
""""Test the login tries."""
responses = [0]
@ -165,7 +204,7 @@ class TestUVC(unittest.TestCase):
mock_camera.return_value.login.assert_called_once_with()
@mock.patch('uvcclient.store.get_info_store')
@mock.patch('uvcclient.camera.UVCCameraClient')
@mock.patch('uvcclient.camera.UVCCameraClientV320')
def test_login_fails_both_properly(self, mock_camera, mock_store):
""""Test if login fails properly."""
mock_camera.return_value.login.side_effect = socket.error