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:
parent
f4594027fd
commit
49de55e75b
3 changed files with 65 additions and 19 deletions
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue