22from asyncio import Future
33
44from asynctest import MagicMock , patch
5+ from onvif .exceptions import ONVIFError
6+ from zeep .exceptions import Fault
57
68from homeassistant import config_entries , data_entry_flow
79from homeassistant .components .onvif import config_flow
1618PASSWORD = "12345"
1719MAC = "aa:bb:cc:dd:ee"
1820
19-
20- def setup_mock_onvif_device (mock_device , two_profiles = False ):
21+ DISCOVERY = [
22+ {
23+ "EPR" : URN ,
24+ config_flow .CONF_NAME : NAME ,
25+ config_flow .CONF_HOST : HOST ,
26+ config_flow .CONF_PORT : PORT ,
27+ "MAC" : MAC ,
28+ },
29+ {
30+ "EPR" : "urn:uuid:987654321" ,
31+ config_flow .CONF_NAME : "TestCamera2" ,
32+ config_flow .CONF_HOST : "5.6.7.8" ,
33+ config_flow .CONF_PORT : PORT ,
34+ "MAC" : "ee:dd:cc:bb:aa" ,
35+ },
36+ ]
37+
38+
39+ def setup_mock_onvif_device (
40+ mock_device , with_h264 = True , two_profiles = False , with_interfaces = True
41+ ):
2142 """Prepare mock ONVIF device."""
2243 devicemgmt = MagicMock ()
2344
@@ -26,12 +47,14 @@ def setup_mock_onvif_device(mock_device, two_profiles=False):
2647 interface .Info .HwAddress = MAC
2748
2849 devicemgmt .GetNetworkInterfaces .return_value = Future ()
29- devicemgmt .GetNetworkInterfaces .return_value .set_result ([interface ])
50+ devicemgmt .GetNetworkInterfaces .return_value .set_result (
51+ [interface ] if with_interfaces else []
52+ )
3053
3154 media_service = MagicMock ()
3255
3356 profile1 = MagicMock ()
34- profile1 .VideoEncoderConfiguration .Encoding = "H264"
57+ profile1 .VideoEncoderConfiguration .Encoding = "H264" if with_h264 else "MJPEG"
3558 profile2 = MagicMock ()
3659 profile2 .VideoEncoderConfiguration .Encoding = "H264" if two_profiles else "MJPEG"
3760
@@ -60,24 +83,35 @@ def mock_constructor(
6083 mock_device .side_effect = mock_constructor
6184
6285
63- def setup_mock_discovery (mock_discovery , with_name = False , with_mac = False ):
86+ def setup_mock_discovery (
87+ mock_discovery , with_name = False , with_mac = False , two_devices = False
88+ ):
6489 """Prepare mock discovery result."""
65- service = MagicMock ()
66- service .getXAddrs = MagicMock (
67- return_value = [f"http://{ HOST } :{ PORT } /onvif/device_service" ]
68- )
69- service .getEPR = MagicMock (return_value = URN )
70- scopes = []
71- if with_name :
72- scope = MagicMock ()
73- scope .getValue = MagicMock (return_value = f"onvif://www.onvif.org/name/{ NAME } " )
74- scopes .append (scope )
75- if with_mac :
76- scope = MagicMock ()
77- scope .getValue = MagicMock (return_value = f"onvif://www.onvif.org/mac/{ MAC } " )
78- scopes .append (scope )
79- service .getScopes = MagicMock (return_value = scopes )
80- mock_discovery .return_value = [service ]
90+ services = []
91+ for item in DISCOVERY :
92+ service = MagicMock ()
93+ service .getXAddrs = MagicMock (
94+ return_value = [
95+ f"http://{ item [config_flow .CONF_HOST ]} :{ item [config_flow .CONF_PORT ]} /onvif/device_service"
96+ ]
97+ )
98+ service .getEPR = MagicMock (return_value = item ["EPR" ])
99+ scopes = []
100+ if with_name :
101+ scope = MagicMock ()
102+ scope .getValue = MagicMock (
103+ return_value = f"onvif://www.onvif.org/name/{ item [config_flow .CONF_NAME ]} "
104+ )
105+ scopes .append (scope )
106+ if with_mac :
107+ scope = MagicMock ()
108+ scope .getValue = MagicMock (
109+ return_value = f"onvif://www.onvif.org/mac/{ item ['MAC' ]} "
110+ )
111+ scopes .append (scope )
112+ service .getScopes = MagicMock (return_value = scopes )
113+ services .append (service )
114+ mock_discovery .return_value = services
81115
82116
83117def setup_mock_camera (mock_camera ):
@@ -93,7 +127,7 @@ def mock_constructor(hass, config):
93127
94128
95129async def setup_onvif_integration (
96- hass , config = None , options = None , entry_id = "1" , source = "user" ,
130+ hass , config = None , options = None , unique_id = MAC , entry_id = "1" , source = "user" ,
97131):
98132 """Create an ONVIF config entry."""
99133 if not config :
@@ -113,7 +147,7 @@ async def setup_onvif_integration(
113147 connection_class = config_entries .CONN_CLASS_LOCAL_PUSH ,
114148 options = options or {},
115149 entry_id = entry_id ,
116- unique_id = MAC ,
150+ unique_id = unique_id ,
117151 )
118152 config_entry .add_to_hass (hass )
119153
@@ -160,7 +194,7 @@ async def test_flow_discovered_devices(hass):
160194
161195 assert result ["type" ] == data_entry_flow .RESULT_TYPE_FORM
162196 assert result ["step_id" ] == "device"
163- assert len (result ["data_schema" ].schema [config_flow .CONF_HOST ].container ) == 2
197+ assert len (result ["data_schema" ].schema [config_flow .CONF_HOST ].container ) == 3
164198
165199 result = await hass .config_entries .flow .async_configure (
166200 result ["flow_id" ], user_input = {config_flow .CONF_HOST : f"{ URN } ({ HOST } )" }
@@ -191,9 +225,60 @@ async def test_flow_discovered_devices(hass):
191225 }
192226
193227
228+ async def test_flow_discovered_devices_ignore_configured_manual_input (hass ):
229+ """Test that config flow discovery ignores configured devices."""
230+ await setup_onvif_integration (hass )
231+
232+ result = await hass .config_entries .flow .async_init (
233+ config_flow .DOMAIN , context = {"source" : "user" }
234+ )
235+
236+ assert result ["type" ] == data_entry_flow .RESULT_TYPE_FORM
237+ assert result ["step_id" ] == "user"
238+
239+ with patch (
240+ "homeassistant.components.onvif.config_flow.get_device"
241+ ) as mock_device , patch (
242+ "homeassistant.components.onvif.config_flow.wsdiscovery"
243+ ) as mock_discovery , patch (
244+ "homeassistant.components.onvif.camera.ONVIFHassCamera"
245+ ) as mock_camera :
246+ setup_mock_onvif_device (mock_device )
247+ setup_mock_discovery (mock_discovery , with_mac = True )
248+ setup_mock_camera (mock_camera )
249+
250+ result = await hass .config_entries .flow .async_configure (
251+ result ["flow_id" ], user_input = {}
252+ )
253+
254+ assert result ["type" ] == data_entry_flow .RESULT_TYPE_FORM
255+ assert result ["step_id" ] == "device"
256+ assert len (result ["data_schema" ].schema [config_flow .CONF_HOST ].container ) == 2
257+
258+ result = await hass .config_entries .flow .async_configure (
259+ result ["flow_id" ],
260+ user_input = {config_flow .CONF_HOST : config_flow .CONF_MANUAL_INPUT },
261+ )
262+
263+ assert result ["type" ] == data_entry_flow .RESULT_TYPE_FORM
264+ assert result ["step_id" ] == "manual_input"
265+
266+
194267async def test_flow_discovery_ignore_existing_and_abort (hass ):
195268 """Test that config flow discovery ignores setup devices."""
196269 await setup_onvif_integration (hass )
270+ await setup_onvif_integration (
271+ hass ,
272+ config = {
273+ config_flow .CONF_NAME : DISCOVERY [1 ]["EPR" ],
274+ config_flow .CONF_HOST : DISCOVERY [1 ][config_flow .CONF_HOST ],
275+ config_flow .CONF_PORT : DISCOVERY [1 ][config_flow .CONF_PORT ],
276+ config_flow .CONF_USERNAME : "" ,
277+ config_flow .CONF_PASSWORD : "" ,
278+ },
279+ unique_id = DISCOVERY [1 ]["MAC" ],
280+ entry_id = "2" ,
281+ )
197282
198283 result = await hass .config_entries .flow .async_init (
199284 config_flow .DOMAIN , context = {"source" : "user" }
@@ -267,8 +352,7 @@ async def test_flow_manual_entry(hass):
267352 setup_mock_camera (mock_camera )
268353
269354 result = await hass .config_entries .flow .async_configure (
270- result ["flow_id" ],
271- user_input = {config_flow .CONF_HOST : config_flow .CONF_MANUAL_INPUT },
355+ result ["flow_id" ], user_input = {},
272356 )
273357
274358 assert result ["type" ] == data_entry_flow .RESULT_TYPE_FORM
@@ -308,6 +392,98 @@ async def test_flow_manual_entry(hass):
308392 }
309393
310394
395+ async def test_flow_import_no_mac (hass ):
396+ """Test that config flow fails when no MAC available."""
397+ with patch ("homeassistant.components.onvif.config_flow.get_device" ) as mock_device :
398+ setup_mock_onvif_device (mock_device , with_interfaces = False )
399+
400+ result = await hass .config_entries .flow .async_init (
401+ config_flow .DOMAIN ,
402+ context = {"source" : config_entries .SOURCE_IMPORT },
403+ data = {
404+ config_flow .CONF_NAME : NAME ,
405+ config_flow .CONF_HOST : HOST ,
406+ config_flow .CONF_PORT : PORT ,
407+ config_flow .CONF_USERNAME : USERNAME ,
408+ config_flow .CONF_PASSWORD : PASSWORD ,
409+ config_flow .CONF_PROFILE : [0 ],
410+ },
411+ )
412+
413+ assert result ["type" ] == data_entry_flow .RESULT_TYPE_ABORT
414+ assert result ["reason" ] == "no_mac"
415+
416+
417+ async def test_flow_import_no_h264 (hass ):
418+ """Test that config flow fails when no MAC available."""
419+ with patch ("homeassistant.components.onvif.config_flow.get_device" ) as mock_device :
420+ setup_mock_onvif_device (mock_device , with_h264 = False )
421+
422+ result = await hass .config_entries .flow .async_init (
423+ config_flow .DOMAIN ,
424+ context = {"source" : config_entries .SOURCE_IMPORT },
425+ data = {
426+ config_flow .CONF_NAME : NAME ,
427+ config_flow .CONF_HOST : HOST ,
428+ config_flow .CONF_PORT : PORT ,
429+ config_flow .CONF_USERNAME : USERNAME ,
430+ config_flow .CONF_PASSWORD : PASSWORD ,
431+ },
432+ )
433+
434+ assert result ["type" ] == data_entry_flow .RESULT_TYPE_ABORT
435+ assert result ["reason" ] == "no_h264"
436+
437+
438+ async def test_flow_import_onvif_api_error (hass ):
439+ """Test that config flow fails when ONVIF API fails."""
440+ with patch ("homeassistant.components.onvif.config_flow.get_device" ) as mock_device :
441+ setup_mock_onvif_device (mock_device )
442+ mock_device .create_devicemgmt_service = MagicMock (
443+ side_effect = ONVIFError ("Could not get device mgmt service" )
444+ )
445+
446+ result = await hass .config_entries .flow .async_init (
447+ config_flow .DOMAIN ,
448+ context = {"source" : config_entries .SOURCE_IMPORT },
449+ data = {
450+ config_flow .CONF_NAME : NAME ,
451+ config_flow .CONF_HOST : HOST ,
452+ config_flow .CONF_PORT : PORT ,
453+ config_flow .CONF_USERNAME : USERNAME ,
454+ config_flow .CONF_PASSWORD : PASSWORD ,
455+ },
456+ )
457+
458+ assert result ["type" ] == data_entry_flow .RESULT_TYPE_ABORT
459+ assert result ["reason" ] == "onvif_error"
460+
461+
462+ async def test_flow_import_onvif_auth_error (hass ):
463+ """Test that config flow fails when ONVIF API fails."""
464+ with patch ("homeassistant.components.onvif.config_flow.get_device" ) as mock_device :
465+ setup_mock_onvif_device (mock_device )
466+ mock_device .create_devicemgmt_service = MagicMock (
467+ side_effect = Fault ("Auth Error" )
468+ )
469+
470+ result = await hass .config_entries .flow .async_init (
471+ config_flow .DOMAIN ,
472+ context = {"source" : config_entries .SOURCE_IMPORT },
473+ data = {
474+ config_flow .CONF_NAME : NAME ,
475+ config_flow .CONF_HOST : HOST ,
476+ config_flow .CONF_PORT : PORT ,
477+ config_flow .CONF_USERNAME : USERNAME ,
478+ config_flow .CONF_PASSWORD : PASSWORD ,
479+ },
480+ )
481+
482+ assert result ["type" ] == data_entry_flow .RESULT_TYPE_FORM
483+ assert result ["step_id" ] == "auth"
484+ assert result ["errors" ]["base" ] == "connection_failed"
485+
486+
311487async def test_option_flow (hass ):
312488 """Test config flow options."""
313489 entry = await setup_onvif_integration (hass )
0 commit comments