]> www.vanbest.org Git - motioneye-debian/commitdiff
added a remote module
authorCalin Crisan <ccrisan@gmail.com>
Sat, 5 Oct 2013 19:54:13 +0000 (22:54 +0300)
committerCalin Crisan <ccrisan@gmail.com>
Sat, 5 Oct 2013 19:54:13 +0000 (22:54 +0300)
doc/todo.txt
src/config.py
src/handlers.py
src/remote.py [new file with mode: 0644]
static/img/apply-progress.gif [new file with mode: 0644]
static/img/progress.gif [deleted file]
static/img/small-progress.gif [new file with mode: 0644]
static/js/main.js
static/js/ui.js

index e243170b5a9cd7ece4bb3e090b944303b4b8d6aa..46666f878248a29f03d982b762ed9af59b5ac51f 100644 (file)
@@ -1,3 +1,7 @@
+-> move/copy list available resolutions code to get_config
+-> bug: adding a remote device does not provide the available resolutions
+-> bug: if updating a remote camera config, local motion will get restarted
 -> make camera frames positions configurable
 -> hide horrible 404 image on cameras
 -> prevent Request closed errors by stopping mjpg clients before stopping motion
index ad7921e2d3dcd8e48ac6bcb68a10cbdada9198e4..89f7f094eeee677e284bf661a2be60f682199fda 100644 (file)
@@ -179,12 +179,13 @@ def get_camera(camera_id, as_lines=False):
     data = _conf_to_dict(lines)
     
     # determine the enabled status
-    main_config = get_main()
-    threads = main_config.get('thread', [])
-    data['@enabled'] = _CAMERA_CONFIG_FILE_NAME % {'id': camera_id} in threads
-    data['@id'] = camera_id
-    
-    _set_default_motion_camera(data)
+    if data['@proto'] == 'v4l2':
+        main_config = get_main()
+        threads = main_config.get('thread', [])
+        data['@enabled'] = _CAMERA_CONFIG_FILE_NAME % {'id': camera_id} in threads
+        data['@id'] = camera_id
+
+        _set_default_motion_camera(data)
     
     return data
 
@@ -192,24 +193,23 @@ def get_camera(camera_id, as_lines=False):
 def set_camera(camera_id, data):
     # TODO use a cache
     
-    _set_default_motion_camera(data)
-    
-    # set the enabled status in main config
-    main_config = get_main()
-    threads = main_config.setdefault('thread', [])
-    config_file_name = _CAMERA_CONFIG_FILE_NAME % {'id': camera_id}
-    if data['@enabled'] and config_file_name not in threads:
-        threads.append(config_file_name)
-            
-    elif not data['@enabled']:
-        threads = [t for t in threads if t != config_file_name]
-
-    main_config['thread'] = threads
-    
-    set_main(main_config)
+    if data.get('@proto') == 'v4l2':
+        _set_default_motion_camera(data)
+        
+        # set the enabled status in main config
+        main_config = get_main()
+        threads = main_config.setdefault('thread', [])
+        config_file_name = _CAMERA_CONFIG_FILE_NAME % {'id': camera_id}
+        if data['@enabled'] and config_file_name not in threads:
+            threads.append(config_file_name)
+                
+        elif not data['@enabled']:
+            threads = [t for t in threads if t != config_file_name]
+    
+        main_config['thread'] = threads
+        
+        set_main(main_config)
 
-    del data['@enabled']
-    
     # read the actual configuration from file
     config_file_path = _CAMERA_CONFIG_FILE_PATH % {'id': camera_id}
     if os.path.isfile(config_file_path):
@@ -248,9 +248,12 @@ def set_camera(camera_id, data):
     return data
 
 
-def add_camera(device):
+def add_camera(device_details):
     # TODO use a cache
     
+    device = device_details.get('device')
+    proto = device_details.get('proto')
+        
     # determine the last camera id
     camera_ids = get_camera_ids()
 
@@ -260,25 +263,24 @@ def add_camera(device):
     
     logging.info('adding new camera with id %(id)s...' % {'id': camera_id})
     
-    # get device type
-    proto = None
-    if device.count('://'):
-        proto, device = device.split('://', 1)
-
     # add the default camera config
     data = OrderedDict()
     data['@name'] = 'Camera' + str(camera_id)
     data['@proto'] = proto
     data['@enabled'] = True
-    data['videodevice'] = device
-    
-    # find a suitable resolution
-    for (w, h) in v4l2ctl.list_resolutions(device):
-        if w > 300:
-            data['width'] = w
-            data['height'] = h
-            break
     
+    for k, v in device_details.items():
+        data['@' + k] = v
+    
+    if proto == 'v4l2':
+        data['videodevice'] = device
+        # find a suitable resolution
+        for (w, h) in v4l2ctl.list_resolutions(device): # TODO move/copy this code to handler/get_config
+            if w > 300:
+                data['width'] = w
+                data['height'] = h
+                break
+            
     # write the configuration to file
     set_camera(camera_id, data)
     
@@ -527,7 +529,6 @@ def _set_default_motion_camera(data):
     data.setdefault('quality', 75)
     data.setdefault('@preserve_images', 0)
     
-    data.setdefault('motion_movies', False)
     data.setdefault('ffmpeg_variable_bitrate', 14)
     data.setdefault('movie_filename', '%Y-%m-%d-%H-%M-%S')
     data.setdefault('ffmpeg_cap_new', False)
@@ -537,4 +538,3 @@ def _set_default_motion_camera(data):
     data.setdefault('@motion_notifications_emails', '')
     
     data.setdefault('@working_schedule', '')
-
index 117154a679fb7670db5021fffccf4ab2da4d25f7..a650469d1c69f2ec4910ef4029106e219eba7453 100644 (file)
@@ -7,6 +7,7 @@ from tornado.web import RequestHandler, HTTPError
 import config
 import mjpgclient
 import motionctl
+import remote
 import template
 import v4l2ctl
 
@@ -90,11 +91,29 @@ class ConfigHandler(BaseHandler):
                 raise HTTPError(404, 'no such camera')
             
             camera_config = config.get_camera(camera_id)
+            if camera_config['@proto'] != 'v4l2':
+                try:
+                    remote_data = remote.get_config(
+                            camera_config.get('@host'),
+                            camera_config.get('@port'),
+                            camera_config.get('@username'),
+                            camera_config.get('@password'),
+                            camera_config.get('@remote_camera_id'))
+                    
+                except Exception as e:
+                    return self.finish_json({'error': unicode(e)})       
+        
+                remote_data = self._camera_ui_to_dict(remote_data)
+                
+                camera_config.update(remote_data) 
+            
             ui_config = self._camera_dict_to_ui(camera_config)
-            resolutions = v4l2ctl.list_resolutions(camera_config['videodevice'])
-            resolutions = [(str(w) + 'x' + str(h)) for (w, h) in resolutions]
             
-            ui_config['available_resolutions'] = resolutions
+            if camera_config['@proto'] == 'v4l2':
+                resolutions = v4l2ctl.list_resolutions(camera_config['videodevice'])
+                resolutions = [(str(w) + 'x' + str(h)) for (w, h) in resolutions]
+                ui_config['available_resolutions'] = resolutions
+                
             self.finish_json(ui_config)
             
         else:
@@ -112,7 +131,7 @@ class ConfigHandler(BaseHandler):
             
             raise
         
-        restart = bool(data.get('restart'))
+        restart = bool(data.get('last'))
         if restart and motionctl.running():
             motionctl.stop()
         
@@ -123,9 +142,20 @@ class ConfigHandler(BaseHandler):
                 camera_ids = config.get_camera_ids()
                 if camera_id not in camera_ids:
                     raise HTTPError(404, 'no such camera')
-    
-                data = self._camera_ui_to_dict(data)
-                config.set_camera(camera_id, data)
+                
+                camera_config = config.get_camera(camera_id)
+                if camera_config['@proto'] == 'v4l2':
+                    data = self._camera_ui_to_dict(data)
+                    config.set_camera(camera_id, data)
+                    
+                else:
+                    remote.set_config(
+                            camera_config.get('@host'),
+                            camera_config.get('@port'),
+                            camera_config.get('@username'),
+                            camera_config.get('@password'),
+                            camera_config.get('@remote_camera_id'),
+                            data)
     
             else:
                 logging.debug('setting main config')
@@ -187,13 +217,33 @@ class ConfigHandler(BaseHandler):
 
     def list_cameras(self):
         logging.debug('listing cameras')
+
+        host = self.get_argument('host', None)
+        port = self.get_argument('port', None)
+        username = self.get_argument('username', None)
+        password = self.get_argument('password', None)
+        
+        if host: # remote
+            try:
+                cameras = remote.list_cameras(host, port, username, password)
+                
+            except Exception as e:
+                return self.finish_json({'error': unicode(e)})       
         
-        cameras = []
-        for camera_id in config.get_camera_ids():
-            data = config.get_camera(camera_id)
-            data = self._camera_dict_to_ui(data)
-            data['id'] = camera_id
-            cameras.append(data)
+        else:
+            cameras = []
+            for camera_id in config.get_camera_ids():
+                data = config.get_camera(camera_id)
+                if data['@proto'] == 'v4l2':
+                    data = self._camera_dict_to_ui(data)
+                    
+                else:
+                    data = {
+                        'name': data['@name']
+                    }
+                    
+                data['id'] = camera_id
+                cameras.append(data)
 
         self.finish_json({'cameras': cameras})
     
@@ -213,29 +263,50 @@ class ConfigHandler(BaseHandler):
     def add_camera(self):
         logging.debug('adding new camera')
         
-        device = self.get_argument('device')
-        camera_id, data = config.add_camera(device)
+        try:
+            device_details = json.loads(self.request.body)
+            
+        except Exception as e:
+            logging.error('could not decode json: %(msg)s' % {'msg': unicode(e)})
+            
+            raise
+
+        camera_id, data = config.add_camera(device_details)
         
         data['@id'] = camera_id
-        data['@enabled'] = True
         
-        if motionctl.running():
+        if motionctl.running() and data['@proto'] == 'v4l2':
             motionctl.stop()
         
-        if config.has_enabled_cameras():
+        if config.has_enabled_cameras() and data['@proto'] == 'v4l2':
             motionctl.start()
+            
+        try:
+            remote_data = remote.get_config(
+                    device_details.get('host'),
+                    device_details.get('port'),
+                    device_details.get('username'),
+                    device_details.get('password'),
+                    device_details.get('remote_camera_id'))
+            
+        except Exception as e:
+            return self.finish_json({'error': unicode(e)})       
+
+        remote_data = self._camera_ui_to_dict(remote_data)
+        remote_data.update(data)
         
-        self.finish_json(self._camera_dict_to_ui(data))
+        self.finish_json(self._camera_dict_to_ui(remote_data))
     
     def rem_camera(self, camera_id):
         logging.debug('removing camera %(id)s' % {'id': camera_id})
         
+        local = config.get_camera(camera_id).get('@proto') == 'v4l2'
         config.rem_camera(camera_id)
         
-        if motionctl.running():
+        if motionctl.running() and local:
             motionctl.stop()
         
-        if config.has_enabled_cameras():
+        if config.has_enabled_cameras() and local:
             motionctl.start()
         
     def _main_ui_to_dict(self, ui):
@@ -259,10 +330,6 @@ class ConfigHandler(BaseHandler):
         }
 
     def _camera_ui_to_dict(self, ui):
-        video_device = ui.get('device', '')
-        if video_device.count('://'):
-            video_device = video_device.split('://')[-1]
-            
         if not ui.get('resolution'): # avoid errors for empty resolution setting
             ui['resolution'] = '352x288'
     
@@ -270,7 +337,6 @@ class ConfigHandler(BaseHandler):
             # device
             '@name': ui.get('name', ''),
             '@enabled': ui.get('enabled', False),
-            'videodevice': video_device,
             'lightswitch': int(ui.get('light_switch_detect', False) * 5),
             'auto_brightness': ui.get('auto_brightness', False),
             'brightness': max(1, int(round(int(ui.get('brightness', 0)) * 2.55))),
@@ -396,17 +462,17 @@ class ConfigHandler(BaseHandler):
             # device
             'name': data['@name'],
             'enabled': data['@enabled'],
-            'id': data['@id'],
-            'device': data['@proto'] + '://' + data['videodevice'],
-            'light_switch_detect': data['lightswitch'] > 0,
-            'auto_brightness': data['auto_brightness'],
-            'brightness': int(round(int(data['brightness']) / 2.55)),
-            'contrast': int(round(int(data['contrast']) / 2.55)),
-            'saturation': int(round(int(data['saturation']) / 2.55)),
-            'hue': int(round(int(data['hue']) / 2.55)),
-            'resolution': str(data['width']) + 'x' + str(data['height']),
-            'framerate': int(data['framerate']),
-            'rotation': int(data['rotate']),
+            'id': data.get('@id'),
+            'proto': data['@proto'],
+            'light_switch_detect': data.get('lightswitch') > 0,
+            'auto_brightness': data.get('auto_brightness'),
+            'brightness': int(round(int(data.get('brightness')) / 2.55)),
+            'contrast': int(round(int(data.get('contrast')) / 2.55)),
+            'saturation': int(round(int(data.get('saturation')) / 2.55)),
+            'hue': int(round(int(data.get('hue')) / 2.55)),
+            'resolution': str(data.get('width')) + 'x' + str(data.get('height')),
+            'framerate': int(data.get('framerate')),
+            'rotation': int(data.get('rotate')),
             
             # file storage
             'storage_device': data['@storage_device'],
@@ -414,7 +480,7 @@ class ConfigHandler(BaseHandler):
             'network_share_name': data['@network_share_name'],
             'network_username': data['@network_username'],
             'network_password': data['@network_password'],
-            'root_directory': data['target_dir'],
+            'root_directory': data.get('target_dir'),
             
             # text overlay
             'text_overlay': False,
@@ -424,11 +490,11 @@ class ConfigHandler(BaseHandler):
             'custom_right_text': '',
             
             # streaming
-            'vudeo_streaming': not data['webcam_localhost'],
-            'streaming_port': int(data['webcam_port']),
-            'streaming_framerate': int(data['webcam_maxrate']),
-            'streaming_quality': int(data['webcam_quality']),
-            'streaming_motion': int(data['webcam_motion']),
+            'vudeo_streaming': not data.get('webcam_localhost'),
+            'streaming_port': int(data.get('webcam_port')),
+            'streaming_framerate': int(data.get('webcam_maxrate')),
+            'streaming_quality': int(data.get('webcam_quality')),
+            'streaming_motion': int(data.get('webcam_motion')),
             
             # still images
             'still_images': False,
@@ -439,19 +505,19 @@ class ConfigHandler(BaseHandler):
             'preserve_images': data['@preserve_images'],
             
             # motion movies
-            'motion_movies': data['motion_movies'],
-            'movie_quality': int((max(2, data['ffmpeg_variable_bitrate']) - 2) / 0.29),
-            'movie_file_name': data['movie_filename'],
+            'motion_movies': data.get('ffmpeg_cap_new'),
+            'movie_quality': int((max(2, data.get('ffmpeg_variable_bitrate')) - 2) / 0.29),
+            'movie_file_name': data.get('movie_filename'),
             'preserve_movies': data['@preserve_movies'],
 
             # motion detection
             'show_frame_changes': data.get('text_changes') or data.get('locate'),
-            'frame_change_threshold': data['threshold'],
-            'auto_noise_detect': data['noise_tune'],
-            'noise_level': int(int(data['noise_level']) / 2.55),
-            'gap': int(data['gap']),
-            'pre_capture': int(data['pre_capture']),
-            'post_capture': int(data['post_capture']),
+            'frame_change_threshold': data.get('threshold'),
+            'auto_noise_detect': data.get('noise_tune'),
+            'noise_level': int(int(data.get('noise_level')) / 2.55),
+            'gap': int(data.get('gap')),
+            'pre_capture': int(data.get('pre_capture')),
+            'post_capture': int(data.get('post_capture')),
             
             # motion notifications
             'motion_notifications': data['@motion_notifications'],
@@ -468,8 +534,8 @@ class ConfigHandler(BaseHandler):
             'sunday_from': '09:00', 'sunday_to': '17:00'
         }
         
-        text_left = data['text_left']
-        text_right = data['text_right'] 
+        text_left = data.get('text_left')
+        text_right = data.get('text_right') 
         if text_left or text_right:
             ui['text_overlay'] = True
             
diff --git a/src/remote.py b/src/remote.py
new file mode 100644 (file)
index 0000000..a3ec679
--- /dev/null
@@ -0,0 +1,110 @@
+
+import json
+import logging
+import urllib2
+
+
+def _compose_url(host, port, username, password, uri, query=None):
+    url = '%(scheme)s://%(host)s:%(port)s%(uri)s' % {
+            'scheme': 'http',
+            'host': host,
+            'port': port,
+            'uri': uri}
+    
+    if query:
+        url += '?' + '='.join(query.items())
+    
+    return url
+
+
+def list_cameras(host, port, username, password):
+    logging.debug('listing remote cameras on %(host)s:%(port)s' % {
+            'host': host,
+            'port': port})
+    
+    url = _compose_url(host, port, username, password, '/config/list/')
+    request = urllib2.Request(url)
+    
+    try:
+        response = urllib2.urlopen(request)
+    
+    except Exception as e:
+        logging.error('failed to list remote cameras on %(host)s:%(port)s: %(msg)s' % {
+                'host': host,
+                'port': port,
+                'msg': unicode(e)})
+        
+        raise
+    
+    try:
+        response = json.load(response)
+    
+    except Exception as e:
+        logging.error('failed to decode json answer from %(host)s:%(port)s: %(msg)s' % {
+                'host': host,
+                'port': port,
+                'msg': unicode(e)})
+        
+        raise
+    
+    return response['cameras']
+
+
+def get_config(host, port, username, password, camera_id):
+    logging.debug('getting config for remote camera %(id)s on %(host)s:%(port)s' % {
+            'id': camera_id,
+            'host': host,
+            'port': port})
+    
+    url = _compose_url(host, port, username, password, '/config/%(id)s/get/' % {'id': camera_id})
+    request = urllib2.Request(url)
+    
+    try:
+        response = urllib2.urlopen(request)
+    
+    except Exception as e:
+        logging.error('failed to get config for remote camera %(id)s on %(host)s:%(port)s: %(msg)s' % {
+                'id': camera_id,
+                'host': host,
+                'port': port,
+                'msg': unicode(e)})
+        
+        raise
+    
+    try:
+        response = json.load(response)
+    
+    except Exception as e:
+        logging.error('failed to decode json answer from %(host)s:%(port)s: %(msg)s' % {
+                'host': host,
+                'port': port,
+                'msg': unicode(e)})
+        
+        raise
+    
+    return response
+
+
+def set_config(host, port, username, password, camera_id, data):
+    logging.debug('setting config for remote camera %(id)s on %(host)s:%(port)s' % {
+            'id': camera_id,
+            'host': host,
+            'port': port})
+    
+    data = json.dumps(data)
+    
+    url = _compose_url(host, port, username, password, '/config/%(id)s/set/' % {'id': camera_id})
+    request = urllib2.Request(url, data=data)
+    
+    try:
+        urllib2.urlopen(request)
+    
+    except Exception as e:
+        logging.error('failed to set config for remote camera %(id)s on %(host)s:%(port)s: %(msg)s' % {
+                'id': camera_id,
+                'host': host,
+                'port': port,
+                'msg': unicode(e)})
+        
+        raise
+
diff --git a/static/img/apply-progress.gif b/static/img/apply-progress.gif
new file mode 100644 (file)
index 0000000..610f733
Binary files /dev/null and b/static/img/apply-progress.gif differ
diff --git a/static/img/progress.gif b/static/img/progress.gif
deleted file mode 100644 (file)
index 610f733..0000000
Binary files a/static/img/progress.gif and /dev/null differ
diff --git a/static/img/small-progress.gif b/static/img/small-progress.gif
new file mode 100644 (file)
index 0000000..e0be29f
Binary files /dev/null and b/static/img/small-progress.gif differ
index bea6c3df650ad2000c72a1016146cf2cc17d77f9..989c341b6efcaa186bd9c121b9f89a0e344d641e 100644 (file)
@@ -19,7 +19,7 @@ function ajax(method, url, data, callback) {
         }
     };
     
-    if (data && typeof data === 'object') {
+    if (data && method === 'POST' && typeof data === 'object') {
         options['contentType'] = 'application/json';
         options['data'] = JSON.stringify(options['data']);
     }
@@ -266,7 +266,11 @@ function initUI() {
         
         runConfirmDialog('Remove device ' + deviceName + '?', function () {
             showProgress();
-            ajax('POST', '/config/' + cameraId + '/rem/', null, function () {
+            ajax('POST', '/config/' + cameraId + '/rem/', null, function (data) {
+                if (data == null || data.error) {
+                    return; // TODO handle error
+                }
+                
                 hideApply();
                 fetchCurrentConfig();
             });
@@ -406,8 +410,8 @@ function updateConfigUi() {
         }
     });
     
-    /* re-validate all the input validators */
-    $('div.settings').find('input.validator').each(function () {
+    /* re-validate all the validators */
+    $('div.settings').find('.validator').each(function () {
         this.validate();
     });
     
@@ -425,6 +429,11 @@ function updateConfigUi() {
 }
 
 function configUiValid() {
+    /* re-validate all the validators */
+    $('div.settings').find('.validator').each(function () {
+        this.validate();
+    });
+    
     var valid = true;
     $('div.settings input, select').each(function () {
         if (this.invalid) {
@@ -659,7 +668,7 @@ function showProgress() {
         return; /* progress already visible */
     }
     
-    applyButton.html('<img class="apply-progress" src="' + staticUrl + 'img/progress.gif">');
+    applyButton.html('<img class="apply-progress" src="' + staticUrl + 'img/apply-progress.gif">');
     applyButton.css('display', 'inline-block');
     applyButton.animate({'opacity': '1'}, 100);
     applyButton.addClass('progress');
@@ -737,9 +746,13 @@ function doApply() {
     for (var i = 0; i < configs.length; i++) {
         var config = configs[i];
         if (i === configs.length - 1) {
-            config.config['restart'] = true;
+            config.config['last'] = true;
         }
-        ajax('POST', '/config/' + config.key + '/set/', config.config, function () {
+        ajax('POST', '/config/' + config.key + '/set/', config.config, function (data) {
+            if (data == null || data.error) {
+                return; // TODO handle error
+            }
+            
             finishedCount++;
             testReady();
         });
@@ -759,10 +772,18 @@ function doApply() {
 function fetchCurrentConfig() {
     /* fetch the main configuration */
     ajax('GET', '/config/main/get/', null, function (data) {
+        if (data == null || data.error) {
+            return; // TODO handle error
+        }
+        
         dict2MainUi(data);
 
         /* fetch the camera list */
         ajax('GET', '/config/list/', null, function (data) {
+            if (data == null || data.error) {
+                return; // TODO handle error
+            }
+            
             var i, cameras = data.cameras;
             var videoDeviceSelect = $('#videoDeviceSelect');
             videoDeviceSelect.html('');
@@ -791,6 +812,10 @@ function fetchCurrentCameraConfig() {
     var cameraId = $('#videoDeviceSelect').val();
     if (cameraId != null) {
         ajax('GET', '/config/' + cameraId + '/get/', null, function (data) {
+            if (data == null || data.error) {
+                return; // TODO handle error
+            }
+            
             dict2CameraUi(data);
         });
     }
@@ -832,7 +857,11 @@ function pushPreview() {
         'hue': hue
     };
     
-    ajax('POST', '/config/' + cameraId + '/set_preview/', data);
+    ajax('POST', '/config/' + cameraId + '/set_preview/', data, function (data) {
+        if (data == null || data.error) {
+            return; // TODO handle error
+        }
+    });
 }
 
 
@@ -874,6 +903,11 @@ function runAddCameraDialog() {
                     '<td class="dialog-item-value"><input type="password" class="styled" id="passwordEntry" placeholder="password..."></td>' +
                     '<td><span class="help-mark" title="the remote administrator\'s password">?</span></td>' +
                 '</tr>' +
+                '<tr class="remote">' +
+                    '<td class="dialog-item-label"><span class="dialog-item-label">Camera</span></td>' +
+                    '<td class="dialog-item-value"><select class="styled" id="cameraSelect"></select></td>' +
+                    '<td><span class="help-mark" title="the remote camera you wish to add to motionEye">?</span></td>' +
+                '</tr>' +
             '</table>');
     
     /* collect ui widgets */
@@ -882,15 +916,19 @@ function runAddCameraDialog() {
     var portEntry = content.find('#portEntry');
     var usernameEntry = content.find('#usernameEntry');
     var passwordEntry = content.find('#passwordEntry');
+    var cameraSelect = content.find('#cameraSelect');
     
     /* make validators */
     makeTextValidator(hostEntry, true);
     makeNumberValidator(portEntry, 1, 65535, false, false, true);
     makeTextValidator(usernameEntry, true);
+    makeTextValidator(deviceSelect, true);
+    makeComboValidator(cameraSelect, true);
     
     /* ui interaction */
     content.find('tr.remote').css('display', 'none');
-    var updateUi = function () {
+    
+    function updateUi() {
         if (deviceSelect.val() === 'remote') {
             content.find('tr.remote').css('display', 'table-row');
         }
@@ -899,14 +937,88 @@ function runAddCameraDialog() {
         }
         
         updateModalDialogPosition();
-    };
+        cameraSelect.html('');
+
+        /* re-validate all the validators */
+        content.find('.validator').each(function () {
+            this.validate();
+        });
+        
+        if (uiValid() && deviceSelect.val() == 'remote') {
+            fetchRemoteCameras();
+        }
+    }
     
-    deviceSelect.change(updateUi).change();
+    function uiValid(includeCameraSelect) {
+        /* re-validate all the validators */
+        content.find('.validator').each(function () {
+            this.validate();
+        });
+        
+        var valid = true;
+        var query = content.find('input, select');
+        if (!includeCameraSelect) {
+            query = query.not('#cameraSelect');
+        }
+        query.each(function () {
+            if (this.invalid) {
+                valid = false;
+                return false;
+            }
+        });
+        
+        return valid;
+    }
+    
+    function fetchRemoteCameras() {
+        var progress = $('<div style="text-align: center; margin: 2px;"><img src="' + staticUrl + 'img/small-progress.gif"></div>');
+        
+        cameraSelect.hide();
+        cameraSelect.before(progress);
+        cameraSelect.parent().find('div').remove(); /* remove any previous progress div */
+        
+        var data = {
+            host: hostEntry.val(),
+            port: portEntry.val(),
+            username: usernameEntry.val(),
+            password: passwordEntry.val()
+        };
+        
+        ajax('GET', '/config/list/', data, function (data) {
+            if (data == null || data.error) {
+                return; // TODO handle error
+            }
+            
+            cameraSelect.html('');
+            progress.remove();
+            
+            if (data.error || !data.cameras) {
+                return;
+            }
+
+            data.cameras.forEach(function (info) {
+                cameraSelect.append('<option value="' + info.id + '">' + info.name + '</option>');
+            });
+            
+            cameraSelect.show();
+        });
+    }
+    
+    deviceSelect.change(updateUi);
+    hostEntry.change(updateUi);
+    portEntry.change(updateUi);
+    usernameEntry.change(updateUi);
+    passwordEntry.change(updateUi);
+    updateUi();
     
     showModalDialog('<div class="modal-progress"></div>');
 
     /* fetch the available devices */
     ajax('GET', '/config/list_devices/', null, function (data) {
+        if (data == null || data.error) {
+            return; // TODO handle error
+        }
+        
         /* add available devices */
         data.devices.forEach(function (device) {
             if (!device.configured) {
@@ -916,23 +1028,39 @@ function runAddCameraDialog() {
         
         deviceSelect.append('<option value="remote">Remote device...</option>');
         
+        updateUi();
+        
         runModalDialog({
             title: 'Add Camera...',
             closeButton: true,
             buttons: 'okcancel',
             content: content,
             onOk: function () {
-                var fullDevice;
+                if (!uiValid(true)) {
+                    return false;
+                }
+                
+                var data = {};
                 if (deviceSelect.val() == 'remote') {
-                    fullDevice = 'http://' + usernameEntry.val() + ':' + passwordEntry.val() + '@' + hostEntry + ':' + portEntry;
+                    data.proto = 'http';
+                    data.host = hostEntry.val();
+                    data.port = portEntry.val();
+                    data.username = usernameEntry.val();
+                    data.password = passwordEntry.val();
+                    data.remote_camera_id = cameraSelect.val();
                 }
                 else {
-                    fullDevice = 'v4l2://' + deviceSelect.val();
+                    data.proto = 'v4l2';
+                    data.device = deviceSelect.val();
                 }
-                    
+
                 showProgress();
-                
-                ajax('POST', '/config/add/?device=' + fullDevice, null, function (data) {
+
+                ajax('POST', '/config/add/', data, function (data) {
+                    if (data == null || data.error) {
+                        return; // TODO handle error
+                    }
+                    
                     hideApply();
                     var addCameraOption = $('#videoDeviceSelect').find('option[value=add]');
                     addCameraOption.before('<option value="' + data.id + '">' + data.name + '</option>');
@@ -1068,6 +1196,10 @@ function recreateCameraFrames(cameras) {
     }
     else {
         ajax('GET', '/config/list/', null, function (data) {
+            if (data == null || data.error) {
+                return; // TODO handle error
+            }
+            
             updateCameras(data.cameras);
         });
     }
@@ -1081,9 +1213,17 @@ function doCloseCamera(cameraId) {
     remCameraFrameUi(cameraId);
     showProgress();
     ajax('GET', '/config/' + cameraId + '/get/', null, function (data) {
+        if (data == null || data.error) {
+            return; // TODO handle error
+        }
+        
         data['enabled'] = false;
-        data['restart'] = true;
-        ajax('POST', '/config/' + cameraId + '/set/', data, function () {
+        data['last'] = true;
+        ajax('POST', '/config/' + cameraId + '/set/', data, function (data) {
+            if (data == null || data.error) {
+                return; // TODO handle error
+            }
+            
             endProgress();
             
             /* if the current camera in the settings panel is the closed camera,
index 2b28bf65a87e12298a452c747a9a7786be6fa40c..1b7ffc63661296d3f834bcfd1aacd744ad11fedf 100644 (file)
@@ -271,6 +271,48 @@ function makeTextValidator($input, required) {
     $input[0].validate = validate;
 }
 
+function makeComboValidator($select, required) {
+    if (required == null) {
+        required = true;
+    }
+    
+    function isValid(strVal) {
+        if (!$select.parents('tr:eq(0)').is(':visible')) {
+            return true; /* an invisible element is considered always valid */
+        }
+        
+        if (strVal.length === 0 && required) {
+            return false;
+        }
+
+        return true;
+    }
+    
+    var msg = 'this field is required';
+    
+    function validate() {
+        var strVal = $select.val() || '';
+        if (isValid(strVal)) {
+            $select.attr('title', '');
+            $select.removeClass('error');
+            $select[0].invalid = false;
+        }
+        else {
+            $select.attr('title', msg);
+            $select.addClass('error');
+            $select[0].invalid = true;
+        }
+    }
+    
+    $select.keyup(validate);
+    $select.blur(validate);
+    $select.change(validate).change();
+    
+    $select.addClass('validator');
+    $select.addClass('combo-validator');
+    $select[0].validate = validate;
+}
+
 function makeNumberValidator($input, minVal, maxVal, floating, sign, required) {
     if (minVal == null) {
         minVal = -Infinity;
@@ -524,7 +566,6 @@ function makeModalDialogButtons(buttonsInfo) {
     buttonsInfo.forEach(function (info) {
         var buttonDiv = $('<div class="button dialog mouse-effect"></div>');
         
-        buttonDiv.click(hideModalDialog); /* every button closes the dialog */
         buttonDiv.attr('tabIndex', '0'); /* make button focusable */
         buttonDiv.html(info.caption);
         
@@ -533,9 +574,21 @@ function makeModalDialogButtons(buttonsInfo) {
         }
         
         if (info.click) {
-            buttonDiv.click(info.click);
+            var oldClick = info.click;
+            info.click = function () {
+                if (oldClick() == false) {
+                    return;
+                }
+                
+                hideModalDialog();
+            };
+        }
+        else {
+            info.click = hideModalDialog; /* every button closes the dialog */
         }
         
+        buttonDiv.click(info.click);
+
         var td = $('<td></td>');
         td.append(buttonDiv);
         tr.append(td);
@@ -650,7 +703,9 @@ function runModalDialog(options) {
         switch (e.which) {
             case 13:
                 if (defaultClick) {
-                    defaultClick();
+                    if (defaultClick() == false) {
+                        return;
+                    };
                 }
                 /* intentionally no break */