From 11f75d199a78138edec736f25ee09a3f8fece041 Mon Sep 17 00:00:00 2001 From: Calin Crisan Date: Sat, 21 Sep 2013 16:00:49 +0300 Subject: [PATCH] added a configuration mechanism --- .gitignore | 2 + conf/.gitkeep | 1 + doc/todo.txt | 2 + src/config.py | 435 ++++++++++++++++++++++++++++++++++++++++++++++++ src/handlers.py | 12 +- src/server.py | 1 + temp/.gitkeep | 1 - 7 files changed, 451 insertions(+), 3 deletions(-) create mode 100644 conf/.gitkeep create mode 100644 src/config.py delete mode 100644 temp/.gitkeep diff --git a/.gitignore b/.gitignore index d7c6380..6abc30f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ *.bak .project .pydevproject +*.conf +*.json diff --git a/conf/.gitkeep b/conf/.gitkeep new file mode 100644 index 0000000..0538fcf --- /dev/null +++ b/conf/.gitkeep @@ -0,0 +1 @@ +this directory contains config files for motionEye as well as for motion \ No newline at end of file diff --git a/doc/todo.txt b/doc/todo.txt index ee68a78..9cc7c3e 100644 --- a/doc/todo.txt +++ b/doc/todo.txt @@ -1,3 +1,5 @@ +-> config.py functions should make use of exceptions rather than returning None + -> browser compatibility test -> authentication -> proxy for slave motioneyes diff --git a/src/config.py b/src/config.py new file mode 100644 index 0000000..a268f1c --- /dev/null +++ b/src/config.py @@ -0,0 +1,435 @@ + +import errno +import json +import logging +import os.path +import re + +import settings + + +_CONFIG_DIR = 'conf' +_CAMERA_CONFIG_FILE_NAME = 'thread-%(id)s.conf' + +_GENERAL_CONFIG_FILE_PATH = os.path.join(_CONFIG_DIR, 'motion-eye.json') +_MOTION_CONFIG_FILE_PATH = os.path.join(_CONFIG_DIR, 'motion.conf') +_CAMERA_CONFIG_FILE_PATH = os.path.join(_CONFIG_DIR, _CAMERA_CONFIG_FILE_NAME) + + +def get_general(): + config_file_path = os.path.join(settings.PROJECT_PATH, _GENERAL_CONFIG_FILE_PATH) + + logging.info('reading general config from file %(path)s...' % {'path': config_file_path}) + + try: + file = open(config_file_path, 'r') + + except IOError as e: + if e.errno == errno.ENOENT: # file does not exist + logging.info('config file %(path)s does not exist, creating a new default one...' % {'path': config_file_path}) + + return set_general({}) + + else: + logging.error('could not open config file %(path)s: %(msg)s' % { + 'path': config_file_path, 'msg': unicode(e)}) + + return None + + try: + data = json.load(file) + _set_default_general(data) + + return data + + except Exception as e: + logging.error('could not read config file %(path)s: %(msg)s' % { + 'path': config_file_path, 'msg': unicode(e)}) + + finally: + file.close() + + return None + + +def set_general(data): + _set_default_general(data) + + config_file_path = os.path.join(settings.PROJECT_PATH, _GENERAL_CONFIG_FILE_PATH) + + logging.info('writing general config to file %(path)s...' % {'path': config_file_path}) + + try: + file = open(config_file_path, 'w') + + except Exception as e: + logging.error('could not open config file %(path)s for writing: %(msg)s' % { + 'path': config_file_path, 'msg': unicode(e)}) + + return None + + try: + json.dump(file, data) + + except Exception as e: + logging.error('could not write config file %(path)s: %(msg)s' % { + 'path': config_file_path, 'msg': unicode(e)}) + + finally: + file.close() + + return data + + +def get_cameras(): + config_path = os.path.join(settings.PROJECT_PATH, _CONFIG_DIR) + + logging.info('loading cameras from directory %(path)s...' % {'path': config_path}) + + try: + ls = os.listdir(config_path) + + except Exception as e: + logging.error('could not list contents of %(dir)s: %(msg)s' % { + 'dir': config_path, 'msg': unicode(e)}) + + return None + + cameras = {} + + pattern = _CAMERA_CONFIG_FILE_NAME.replace('%(id)s', '(\w+)') + for name in ls: + match = re.match(pattern, name) + if not match: + continue # not a camera config file + + camera_id = match.groups()[0] + + camera_config_path = os.path.join(config_path, name) + + logging.info('reading camera config from %(path)s...' % {'path': camera_config_path}) + + try: + file = open(camera_config_path, 'r') + + except Exception as e: + logging.error('could not open camera config file %(path)s: %(msg)s' % { + 'path': camera_config_path, 'msg': unicode(e)}) + + continue + + try: + lines = [l[:-1] for l in file.readlines()] + + except Exception as e: + logging.error('could not read camera config file %(path)s: %(msg)s' % { + 'path': camera_config_path, 'msg': unicode(e)}) + + continue + + finally: + file.close() + + data = _conf_to_dict(lines) + _set_default_motion_camera(data) + + cameras[camera_id] = data + + logging.info('loaded %(count)d cameras' % {'count': len(cameras)}) + + return cameras + + +def get_camera(camera_id): + config_path = os.path.join(settings.PROJECT_PATH, _CONFIG_DIR) + camera_config_path = os.path.join(config_path, _CAMERA_CONFIG_FILE_NAME % {'id': camera_id}) + + logging.info('reading camera config from %(path)s...' % {'path': camera_config_path}) + + try: + file = open(camera_config_path, 'r') + + except Exception as e: + logging.error('could not open camera config file: %(msg)s' % {'msg': unicode(e)}) + + return None + + try: + lines = [l[:-1] for l in file.readlines()] + + except Exception as e: + logging.error('could not read camera config file %(path)s: %(msg)s' % { + 'path': camera_config_path, 'msg': unicode(e)}) + + return None + + finally: + file.close() + + data = _conf_to_dict(lines) + _set_default_motion_camera(data) + + return data + + +def set_camera(camera_id, data): + config_path = os.path.join(settings.PROJECT_PATH, _CONFIG_DIR) + camera_config_path = os.path.join(config_path, _CAMERA_CONFIG_FILE_NAME % {'id': camera_id}) + + # read the actual configuration from file + + logging.info('reading camera config from %(path)s...' % {'path': camera_config_path}) + + try: + file = open(camera_config_path, 'r') + + except Exception as e: + logging.error('could not open camera config file %(path)s: %(msg)s' % { + 'path': camera_config_path, 'msg': unicode(e)}) + + return None + + try: + lines = [l[:-1] for l in file.readlines()] + + except Exception as e: + logging.error('could not read camera config file %(path)s: %(msg)s' % { + 'path': camera_config_path, 'msg': unicode(e)}) + + return None + + finally: + file.close() + + # write the configuration to file + + logging.info('writing camera config to %(path)s...' % {'path': camera_config_path}) + + try: + file = open(camera_config_path, 'w') + + except Exception as e: + logging.error('could not open camera config file %(path)s for writing: %(msg)s' % { + 'path': camera_config_path, 'msg': unicode(e)}) + + return None + + lines = _dict_to_conf(lines, data) + + try: + file.writelines([l + '\n' for l in lines]) + + except Exception as e: + logging.error('could not write camera config file %(path)s: %(msg)s' % { + 'path': camera_config_path, 'msg': unicode(e)}) + + return None + + finally: + file.close() + + return data + + +def add_camera(): + config_path = os.path.join(settings.PROJECT_PATH, _CONFIG_DIR) + + logging.info('loading cameras from directory %(path)s...' % {'path': config_path}) + + try: + ls = os.listdir(config_path) + + except Exception as e: + logging.error('could not list contents of %(dir)s: %(msg)s' % { + 'dir': config_path, 'msg': unicode(e)}) + + return None + + camera_ids = [] + + pattern = _CAMERA_CONFIG_FILE_NAME.replace('%(id)s', '(\w+)') + for name in ls: + match = re.match(pattern, name) + if not match: + continue # not a camera config file + + camera_id = match.groups()[0] + try: + camera_id = int(camera_id) + + except ValueError: + logging.error('camera id is not an integer: %(id)s' % {'id': camera_id}) + + continue + + camera_ids.append(camera_id) + + logging.debug('found camera with id %(id)s' % {'id': camera_id}) + + last_camera_id = max(camera_ids or [0]) + camera_id = last_camera_id + 1 + + logging.info('adding new camera with id %(id)s...' % {'id': camera_id}) + + # write the configuration to file + + camera_config_path = os.path.join(config_path, _CAMERA_CONFIG_FILE_NAME % {'id': camera_id}) + logging.info('writing camera config to %(path)s...' % {'path': camera_config_path}) + + try: + file = open(camera_config_path, 'w') + + except Exception as e: + logging.error('could not open camera config file %(path)s for writing: %(msg)s' % { + 'path': camera_config_path, 'msg': unicode(e)}) + + return None + + data = {} + _set_default_motion_camera(data) + + lines = _dict_to_conf([], data) + + try: + file.writelines([l + '\n' for l in lines]) + + except Exception as e: + logging.error('could not write camera config file %(path)s: %(msg)s' % { + 'path': camera_config_path, 'msg': unicode(e)}) + + return None + + finally: + file.close() + + return camera_id, data + + +def rem_camera(camera_id): + config_path = os.path.join(settings.PROJECT_PATH, _CONFIG_DIR) + camera_config_path = os.path.join(config_path, _CAMERA_CONFIG_FILE_NAME % {'id': camera_id}) + + logging.info('removing camera config file %(path)s...' % {'path': camera_config_path}) + + try: + os.remove(camera_config_path) + + except Exception as e: + logging.error('could not remove camera config file %(path)s: %(msg)s' % { + 'path': camera_config_path, 'msg': unicode(e)}) + + return None + + +def _value_to_python(value): + value_lower = value.lower() + if value_lower == 'off': + return False + + elif value_lower == 'on': + return True + + try: + return int(value) + + except ValueError: + try: + return float(value) + + except ValueError: + return value + + +def _python_to_value(value): + if value is True: + return 'on' + + elif value is False: + return 'off' + + elif isinstance(value, (int, float)): + return str(value) + + else: + return value + + +def _conf_to_dict(lines): + data = {} + + for line in lines: + line = line.strip() + if len(line) == 0: # empty line + continue + + if line.startswith('#') or line.startswith(';'): # comment line + continue + + parts = line.split(None, 1) + if len(parts) != 2: # invalid line format + continue + + (name, value) = parts + value = value.strip() + + value = data[name] = _value_to_python(value) + + return data + + +def _dict_to_conf(lines, data): + conf_lines = [] + data_copy = dict(data) + + # parse existing lines and replace the values + + for line in lines: + line = line.strip() + if len(line) == 0: # empty line + conf_lines.append(line) + continue + + if line.startswith('#') or line.startswith(';'): # comment line + conf_lines.append(line) + continue + + parts = line.split(None, 1) + if len(parts) != 2: # invalid line format + conf_lines.append(line) + continue + + (name, value) = parts + + new_value = data.get(name) + if new_value is not None: + value = _python_to_value(new_value) + + line = name + ' ' + value + conf_lines.append(line) + + del data_copy[name] + + # add the remaining config values not covered by existing lines + + for (name, value) in data_copy: + line = name + ' ' + value + conf_lines.append(line) + + return conf_lines + + +def _set_default_general(data): + data.set_default('show_advanced', False) + data.set_default('admin_username', 'admin') + data.set_default('admin_password', '') + data.set_default('normal_username', 'user') + data.set_default('storage_device', 'local-disk') + data.set_default('root_directory', '/') + + +def _set_default_motion(data): + pass + + +def _set_default_motion_camera(data): + pass diff --git a/src/handlers.py b/src/handlers.py index cd78db6..dedd2c7 100644 --- a/src/handlers.py +++ b/src/handlers.py @@ -46,10 +46,18 @@ class ConfigHandler(BaseHandler): raise HTTPError(400, 'unknown operation') def get_config(self, camera_id): - logging.debug('getting config for camera %(id)s' % {'camera': camera_id}) + if camera_id: + logging.debug('getting config for camera %(id)s' % {'camera': camera_id}) + + else: + logging.debug('getting general config') def set_config(self, camera_id): - logging.debug('setting config for camera %(id)s' % {'camera': camera_id}) + if camera_id: + logging.debug('setting config for camera %(id)s' % {'camera': camera_id}) + + else: + logging.debug('setting general config') def add_camera(self): logging.debug('adding new camera') diff --git a/src/server.py b/src/server.py index 8d7b86b..19c228b 100644 --- a/src/server.py +++ b/src/server.py @@ -11,6 +11,7 @@ application = Application( (r'^/$', handlers.MainHandler), (r'^/config/(?P\w+)/(?Pget|set|rem)/?$', handlers.ConfigHandler), (r'^/config/(?Padd)/?$', handlers.ConfigHandler), + (r'^/config/general/(?Pset|get)/?$', handlers.ConfigHandler), (r'^/snapshot/(?P\w+)/(?Pcurrent|list)/?$', handlers.SnapshotHandler), (r'^/snapshot/(?P\w+)/(?Pdownload)/(?P.+)/?$', handlers.SnapshotHandler), (r'^/movie/(?P\w+)/(?Plist)/?$', handlers.MovieHandler), diff --git a/temp/.gitkeep b/temp/.gitkeep deleted file mode 100644 index 18d93d2..0000000 --- a/temp/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -this directory contains (at runtime) temporary config files for motion \ No newline at end of file -- 2.39.5