import json
import logging
import os.path
-import re
import settings
def get_general():
+ # TODO use a cache
+
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})
logging.error('could not open config file %(path)s: %(msg)s' % {
'path': config_file_path, 'msg': unicode(e)})
- return None
+ raise
try:
data = json.load(file)
logging.error('could not read config file %(path)s: %(msg)s' % {
'path': config_file_path, 'msg': unicode(e)})
+ raise
+
finally:
file.close()
- return None
-
def set_general(data):
+ # TODO use a cache
+
_set_default_general(data)
config_file_path = os.path.join(settings.PROJECT_PATH, _GENERAL_CONFIG_FILE_PATH)
logging.error('could not open config file %(path)s for writing: %(msg)s' % {
'path': config_file_path, 'msg': unicode(e)})
- return None
+ raise
try:
- json.dump(file, data)
+ json.dump(data, file)
except Exception as e:
logging.error('could not write config file %(path)s: %(msg)s' % {
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):
+ # TODO use a cache
+
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})
except Exception as e:
logging.error('could not open camera config file: %(msg)s' % {'msg': unicode(e)})
- return None
+ raise
try:
lines = [l[:-1] for l in file.readlines()]
logging.error('could not read camera config file %(path)s: %(msg)s' % {
'path': camera_config_path, 'msg': unicode(e)})
- return None
+ raise
finally:
file.close()
- data = _conf_to_dict(lines)
- _set_default_motion_camera(data)
-
- return data
+ return _conf_to_dict(lines)
def set_camera(camera_id, data):
+ # TODO use a cache
+
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.error('could not open camera config file %(path)s: %(msg)s' % {
'path': camera_config_path, 'msg': unicode(e)})
- return None
+ raise
try:
lines = [l[:-1] for l in file.readlines()]
logging.error('could not read camera config file %(path)s: %(msg)s' % {
'path': camera_config_path, 'msg': unicode(e)})
- return None
+ raise
finally:
file.close()
logging.error('could not open camera config file %(path)s for writing: %(msg)s' % {
'path': camera_config_path, 'msg': unicode(e)})
- return None
+ raise
lines = _dict_to_conf(lines, data)
logging.error('could not write camera config file %(path)s: %(msg)s' % {
'path': camera_config_path, 'msg': unicode(e)})
- return None
+ raise
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
+def add_camera(device):
+ # TODO use a cache
- 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})
+ # determine the last camera id
+ cameras = get_general().get('cameras', {})
+ camera_ids = [int(k) for k in cameras.iterkeys()]
+
last_camera_id = max(camera_ids or [0])
camera_id = last_camera_id + 1
# write the configuration to file
+ 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('writing camera config to %(path)s...' % {'path': camera_config_path})
logging.error('could not open camera config file %(path)s for writing: %(msg)s' % {
'path': camera_config_path, 'msg': unicode(e)})
- return None
+ raise
- data = {}
- _set_default_motion_camera(data)
+ # add the default camera config
+ ui = camera_dict_to_ui(camera_id, {})
+ data = camera_ui_to_dict(camera_id, ui)
lines = _dict_to_conf([], data)
logging.error('could not write camera config file %(path)s: %(msg)s' % {
'path': camera_config_path, 'msg': unicode(e)})
- return None
+ raise
finally:
file.close()
- return camera_id, data
+ # add the camera to the general config
+
+ cameras[camera_id] = {
+ 'name': 'camera' + str(camera_id),
+ 'device': device,
+ 'enabled': True
+ }
+
+ general_config = get_general()
+ general_config['cameras'] = cameras
+
+ set_general(general_config)
+
+ return camera_id, cameras[camera_id]['name'], data
def rem_camera(camera_id):
+ # TODO use a cache
+
+ # TODO remove the camera from general config as well
+
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.error('could not remove camera config file %(path)s: %(msg)s' % {
'path': camera_config_path, 'msg': unicode(e)})
- return None
+ raise
+
+
+def camera_ui_to_dict(camera_id, ui):
+ cameras = get_general().get('cameras', {})
+ camera_info = cameras.get(camera_id, {})
+ camera_name = camera_info.get('name', '(unknown)')
+
+ data = {
+ # device
+ 'lightswitch': int(ui.get('light_switch_detect', False) * 5),
+ 'auto_brightness': ui.get('auto_brightness', False),
+ 'brightness': int(int(ui.get('brightness', 0)) * 2.55),
+ 'contrast': int(int(ui.get('contrast', 0)) * 2.55),
+ 'saturation': int(int(ui.get('saturation', 0)) * 2.55),
+ 'hue': int(int(ui.get('hue', 0))),
+ 'width': int(ui.get('resolution', '352x288').split('x')[0]),
+ 'height': int(ui.get('resolution', '352x288').split('x')[1]),
+ 'framerate': int(ui.get('framerate', 1)),
+
+ # text overlay
+ 'text_left': '',
+ 'text_right': '',
+
+ # streaming
+ 'webcam_localhost': not ui.get('video_streaming', True),
+ 'webcam_port': int(ui.get('streaming_port', 8080)),
+ 'webcam_maxrate': int(ui.get('streaming_framerate', 1)),
+ 'webcam_quality': max(1, int(ui.get('streaming_quality', 50))),
+
+ # still images
+ 'output_normal': False,
+ 'output_all': False,
+ 'output_motion': False,
+ 'snapshot_interval': 0,
+ 'jpeg_filename': '',
+ 'snapshot_filename': '',
+ # TODO preserve images
+
+ # movies
+ 'ffmpeg_variable_bitrate': 0,
+ 'ffmpeg_video_codec': 'mpeg4',
+ 'ffmpeg_cap_new': True,
+ 'movie_filename': '',
+ # TODO preserve movies
+
+ # motion detection
+ 'text_changes': ui.get('show_frame_changes', False),
+ 'locate': ui.get('show_frame_changes', False),
+ 'threshold': ui.get('frame_change_threshold', 1500),
+ 'noise_tune': ui.get('auto_noise_detect', True),
+ 'noise_level': max(1, int(int(ui.get('noise_level', 8)) * 2.55)),
+ 'gap': int(ui.get('gap', 60)),
+ 'pre_capture': int(ui.get('pre_capture', 0)),
+ 'post_capture': int(ui.get('post_capture', 0)),
+
+ # TODO notifications
+ }
+
+ if ui.get('text_overlay', False):
+ left_text = ui.get('left_text', 'camera-name')
+ if left_text == 'camera-name':
+ data['text_left'] = camera_name
+
+ elif left_text == 'timestamp':
+ data['text_left'] = '%Y-%m-%d\n%T'
+
+ else:
+ data['text_left'] = ui.get('custom_left_text', '')
+
+ right_text = ui.get('right_text', 'timestamp')
+ if right_text == 'camera-name':
+ data['text_right'] = camera_name
+
+ elif right_text == 'timestamp':
+ data['text_right'] = '%Y-%m-%d\n%T'
+
+ else:
+ data['text_right'] = ui.get('custom_right_text', '')
+
+ if ui.get('still_images', False):
+ capture_mode = ui.get('capture_mode', 'motion-triggered')
+ if capture_mode == 'motion-triggered':
+ data['output_normal'] = True
+ data['jpeg_filename'] = ui.get('image_file_name', '%Y-%m-%d-%H-%M-%S-%q')
+
+ elif capture_mode == 'interval-snapshots':
+ data['snapshot_interval'] = int(ui.get('snapshot_interval'), 300)
+ data['snapshot_filename'] = ui.get('image_file_name', '%Y-%m-%d-%H-%M-%S-%q')
+
+ elif capture_mode == 'all-frames':
+ data['output_all'] = True
+ data['jpeg_filename'] = ui.get('image_file_name', '%Y-%m-%d-%H-%M-%S')
+
+ data['quality'] = max(1, int(ui.get('image_quality', 75)))
+
+ if ui.get('motion_movies', False):
+ data['ffmpeg_variable_bitrate'] = 2 + int((100 - int(ui.get('movie_quality', 50))) * 0.29)
+ data['movie_filename'] = ui.get('movie_file_name', '%Y-%m-%d-%H-%M-%S-%q')
+
+ return data
+
+
+def camera_dict_to_ui(camera_id, data):
+ # this is where the default values come from
+
+ cameras = get_general().get('cameras', {})
+ camera_info = cameras.get(camera_id, {})
+ camera_name = camera_info.get('name', '(unknown)')
+
+ ui = {
+ # device
+ 'light_switch_detect': data.get('lightswitch', 0) > 0,
+ 'auto_brightness': data.get('auto_brightness', False),
+ 'brightness': int(int(data.get('brightness', 0)) / 2.55),
+ 'contrast': int(int(data.get('contrast', 0)) / 2.55),
+ 'saturation': int(int(data.get('saturation', 0)) / 2.55),
+ 'hue': int(int(data.get('hue', 0))),
+ 'resolution': str(data.get('width', 352)) + 'x' + str(data.get('height', 288)),
+ 'framerate': int(data.get('framerate', 1)),
+
+ # text overlay
+ 'text_overlay': False,
+ 'left_text': 'camera-name',
+ 'right_text': 'timestamp',
+
+ # streaming
+ 'vudeo_streaming': not data.get('webcam_localhost', False),
+ 'streaming_port': int(data.get('webcam_port', 8080)),
+ 'streaming_framerate': int(data.get('webcam_maxrate', 1)),
+ 'streaming_quality': int(data.get('webcam_quality', 50)),
+
+ # still images
+ 'still_images': False,
+ 'capture_mode': 'motion-triggered',
+ 'image_file_name': '%Y-%m-%d-%H-%M-%S',
+ 'image_quality': 75,
+ # TODO preserve images
+
+ # motion movies
+ 'motion_movies': False,
+ 'movie_quality': 50,
+ 'movie_file_name': '%Y-%m-%d-%H-%M-%S-%q',
+ # TODO preserve movies
+
+ # motion detection
+ 'show_frame_changes': data.get('text_changes') or data.get('locate'),
+ 'frame_change_threshold': data.get('threshold', 1500),
+ 'auto_noise_detect': data.get('noise_tune', True),
+ 'noise_level': int(int(data.get('noise_level', 32)) / 2.55),
+ 'gap': int(data.get('gap', 60)),
+ 'pre_capture': int(data.get('pre_capture', 0)),
+ 'post_capture': int(data.get('post_capture', 0)),
+
+ # TODO notifications
+ }
+
+ text_left = data.get('text_left', '')
+ text_right = data.get('text_right', '')
+ if text_left or text_right:
+ ui['text_overlay'] = True
+
+ if text_left == camera_name:
+ ui['left_text'] = 'camera-name'
+
+ elif text_left == '%Y-%m-%d\n%T':
+ ui['left_text'] = 'timestamp'
+
+ else:
+ ui['left_text'] = 'custom-text'
+ ui['custom_left_text'] = text_left
+
+ if text_right == camera_name:
+ ui['right_text'] = 'camera-name'
+
+ elif text_right == '%Y-%m-%d\n%T':
+ ui['right_text'] = 'timestamp'
+
+ else:
+ ui['right_text'] = 'custom-text'
+ ui['custom_right_text'] = text_right
+
+ output_all = data.get('output_all')
+ output_normal = data.get('output_normal')
+ jpeg_filename = data.get('jpeg_filename')
+ snapshot_interval = data.get('snapshot_interval')
+ snapshot_filename = data.get('snapshpt_filename')
+
+ if (((output_all or output_normal) and jpeg_filename) or
+ (snapshot_interval and snapshot_filename)):
+
+ ui['still_images'] = True
+
+ if output_all:
+ ui['capture_mode'] = 'all-frames'
+ ui['image_file_name'] = jpeg_filename
+
+ elif data.get('snapshot_interval'):
+ ui['capture-mode'] = 'interval-snapshots'
+ ui['image_file_name'] = snapshot_filename
+
+ elif data.get('output_normal'):
+ ui['capture-mode'] = 'motion-triggered'
+ ui['image_file_name'] = jpeg_filename
+
+ ui['image_quality'] = ui.get('quality', 75)
+
+ movie_filename = data.get('movie_filename')
+ if movie_filename:
+ ui['motion_movies'] = True
+ ui['movie_quality'] = int((max(2, data.get('ffmpeg_variable_bitrate', 14)) - 2) / 0.29)
+ ui['movie_file_name'] = movie_filename
+
+ return data
def _value_to_python(value):
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', '/')
+ data.setdefault('general_enabled', True)
+ data.setdefault('show_advanced', False)
+ data.setdefault('admin_username', 'admin')
+ data.setdefault('admin_password', '')
+ data.setdefault('normal_username', 'user')
+ data.setdefault('storage_device', 'local-disk')
+ data.setdefault('root_directory', '/')
+ data.setdefault('cameras', {})
def _set_default_motion(data):
- pass
-
+ pass # TODO
-def _set_default_motion_camera(data):
- pass