--- /dev/null
+
+# static files (.css, .js etc) are served at this root url;
+# change this if you run motionEye behind a reverse proxy (e.g. nginx),
+# and you want static files to be served directly by it
+#static_url '/static/'
+
+# path to the configuration directory (must be writable by motionEye)
+conf_path /etc/motioneye
+
+# path to the directory where pid files go (must be writable by motionEye)
+#run_path /var/run
+
+# path to the directory where log files go (must be writable by motionEye)
+#log_path /var/log
+
+# default output path for media files (must be writable by motionEye)
+#media_path /var/lib/motioneye
+
+# path to the motion binary to use (automatically detected by default)
+#motion_binary /usr/bin/motion
+
+# the log level (use quiet, error, warning, info or debug)
+#log_level info
+
+# the IP address to listen on
+# (0.0.0.0 for all interfaces, 127.0.0.1 for localhost)
+#listen 0.0.0.0
+
+# the TCP port to listen on
+#port 8765
+
+# interval in seconds at which motionEye checks the SMB mounts
+#mount_check_interval 300
+
+# interval in seconds at which motionEye checks if motion is running
+#motion_check_interval 10
+
+# interval in seconds at which the janitor is called
+# to remove old pictures and movies
+#cleanup_interval 43200
+
+# interval in seconds at which the thumbnail mechanism runs
+# (set to 0 to disable)
+#thumbnailer_interval 60
+
+# timeout in seconds to wait for response from a remote motionEye server
+#remote_request_timeout 10
+
+# timeout in seconds to wait for mjpg data from the motion daemon
+#mjpg_client_timeout 10
+
+# timeout in seconds after which an idle mjpg client is removed
+# (set to 0 to disable)
+#mjpg_client_idle_timeout 10
+
+# enable SMB shares (requires motionEye to run as root)
+#smb_shares false
+
+# the directory where the SMB mount points will be created
+#smb_mount_root /media
+
+# path to the wpa_supplicant.conf file
+# (enable this to configure wifi settings from the UI)
+#wpa_supplicant_conf /etc/wpa_supplicant.conf
+
+# path to the localtime file
+# (enable this to configure the system time zone from the UI)
+#local_time_file /etc/localtime
+
+# enables shutdown and rebooting after changing system settings
+# (such as wifi settings or time zone)
+#enable_reboot false
+
+# timeout in seconds to use when talking to the SMTP server
+#smtp_timeout 60
+
+# timeout in seconds to wait for zip file creation
+#zip_timeout 500
+
+# enable adding and removing cameras from UI
+#add_remove_cameras true
if device_details['username']:
camera_config['netcam_userpass'] = device_details['username'] + ':' + device_details['password']
- camera_config['netcam_keepalive'] = device_details.get('keep_alive')
+ camera_config['netcam_keepalive'] = device_details.get('keep_alive', False)
camera_config['netcam_tolerant_check'] = True
if device_details.get('camera_index') == 'udp':
def motion_camera_ui_to_dict(ui, old_config=None):
+ import meyectl
import smbctl
old_config = dict(old_config or {})
data['@working_schedule_type'] = ui['working_schedule_type']
# event start
- event_relay_path = os.path.join(settings.PROJECT_PATH, 'eventrelay.py')
- event_relay_path = os.path.abspath(event_relay_path)
-
- on_event_start = ['%(script)s start %%t' % {'script': event_relay_path}]
+ on_event_start = ['%(script)s start %%t' % {'script': meyectl.find_command('relayevent')}]
if ui['email_notifications_enabled']:
send_mail_path = os.path.join(settings.PROJECT_PATH, 'sendmail.py')
send_mail_path = os.path.abspath(send_mail_path)
emails = re.sub('\\s', '', ui['email_notifications_addresses'])
on_event_start.append("%(script)s '%(server)s' '%(port)s' '%(account)s' '%(password)s' '%(tls)s' '%(to)s' 'motion_start' '%%t' '%%Y-%%m-%%dT%%H:%%M:%%S' '%(timespan)s'" % {
- 'script': send_mail_path,
+ 'script': meyectl.find_command('sendmail'),
'server': ui['email_notifications_smtp_server'],
'port': ui['email_notifications_smtp_port'],
'account': ui['email_notifications_smtp_account'],
'timespan': ui['email_notifications_picture_time_span']})
if ui['web_hook_notifications_enabled']:
- web_hook_path = os.path.join(settings.PROJECT_PATH, 'webhook.py')
- web_hook_path = os.path.abspath(web_hook_path)
url = re.sub('\\s', '+', ui['web_hook_notifications_url'])
on_event_start.append("%(script)s '%(method)s' '%(url)s'" % {
- 'script': web_hook_path,
+ 'script': meyectl.find_command('webhook'),
'method': ui['web_hook_notifications_http_method'],
'url': url})
data['on_event_start'] = '; '.join(on_event_start)
# event end
- on_event_end = ['%(script)s stop %%t' % {'script': event_relay_path}]
+ on_event_end = ['%(script)s stop %%t' % {'script': meyectl.find_command('relayevent')}]
data['on_event_end'] = '; '.join(on_event_end)
ui['email_notifications_picture_time_span'] = 0
command_notifications = []
for e in on_event_start:
- if e.count('sendmail.py') and e.count('motion_start'):
+ if e.count('sendmail'):
e = shlex.split(e)
+
if len(e) < 10:
continue
-
+
ui['email_notifications_enabled'] = True
- ui['email_notifications_smtp_server'] = e[1]
- ui['email_notifications_smtp_port'] = e[2]
- ui['email_notifications_smtp_account'] = e[3]
- ui['email_notifications_smtp_password'] = e[4]
- ui['email_notifications_smtp_tls'] = e[5].lower() == 'true'
- ui['email_notifications_addresses'] = e[6]
+ ui['email_notifications_smtp_server'] = e[-10]
+ ui['email_notifications_smtp_port'] = e[-9]
+ ui['email_notifications_smtp_account'] = e[-8]
+ ui['email_notifications_smtp_password'] = e[-7]
+ ui['email_notifications_smtp_tls'] = e[-6].lower() == 'true'
+ ui['email_notifications_addresses'] = e[-5]
try:
- ui['email_notifications_picture_time_span'] = int(e[10])
+ ui['email_notifications_picture_time_span'] = int(e[-1])
except:
ui['email_notifications_picture_time_span'] = 0
- elif e.count('webhook.py'):
+ elif e.count('webhook'):
e = shlex.split(e)
- if len(e) != 3:
+
+ if len(e) < 3:
continue
ui['web_hook_notifications_enabled'] = True
- ui['web_hook_notifications_http_method'] = e[1]
- ui['web_hook_notifications_url'] = e[2]
+ ui['web_hook_notifications_http_method'] = e[-1]
+ ui['web_hook_notifications_url'] = e[-1]
- elif e.count('eventrelay.py'):
+ elif e.count('relayevent') or e.count('eventrelay.py'):
continue # ignore internal relay script
else: # custom command
+++ /dev/null
-#!/usr/bin/env python
-
-# Copyright (c) 2013 Calin Crisan
-# This file is part of motionEye.
-#
-# motionEye is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-import errno
-import json
-import logging
-import os.path
-import sys
-import urllib
-
-sys.path.append(os.path.join(os.path.dirname(sys.argv[0]),'src'))
-
-import settings
-import utils
-
-from motioneye import _configure_settings, _configure_logging
-
-
-_configure_settings()
-_configure_logging(module='eventrelay')
-
-
-def print_usage():
- print 'Usage: eventrelay.py <event> <thread_id>'
-
-
-def get_admin_credentials():
- # this shortcut function is a bit faster than using the config module functions
- config_file_path = os.path.join(settings.CONF_PATH, 'motion.conf')
-
- logging.debug('reading main config from file %(path)s...' % {'path': config_file_path})
-
- lines = None
- try:
- file = open(config_file_path, 'r')
-
- except IOError as e:
- if e.errno == errno.ENOENT: # file does not exist
- logging.info('main config file %(path)s does not exist, using default values' % {'path': config_file_path})
-
- lines = []
-
- else:
- logging.error('could not open main config file %(path)s: %(msg)s' % {
- 'path': config_file_path, 'msg': unicode(e)})
-
- raise
-
- if lines is None:
- try:
- lines = [l[:-1] for l in file.readlines()]
-
- except Exception as e:
- logging.error('could not read main config file %(path)s: %(msg)s' % {
- 'path': config_file_path, 'msg': unicode(e)})
-
- raise
-
- finally:
- file.close()
-
- admin_username = 'admin'
- admin_password = ''
- for line in lines:
- line = line.strip()
- if not line.startswith('#'):
- continue
-
- line = line[1:].strip()
- if line.startswith('@admin_username'):
- parts = line.split(' ', 1)
- admin_username = parts[1] if len(parts) > 1 else ''
-
- continue
-
- if line.startswith('@admin_password'):
- parts = line.split(' ', 1)
- admin_password = parts[1] if len(parts) > 1 else ''
-
- continue
-
- return admin_username, admin_password
-
-
-# def compute_signature(method, uri, body, key):
-# parts = list(urlparse.urlsplit(uri))
-# query = [q for q in urlparse.parse_qsl(parts[3]) if (q[0] != 'signature')]
-# query.sort(key=lambda q: q[0])
-# query = urllib.urlencode(query)
-# parts[0] = parts[1] = ''
-# parts[3] = query
-# uri = urlparse.urlunsplit(parts)
-#
-# return hashlib.sha1('%s:%s:%s:%s' % (method, uri, body or '', key)).hexdigest().lower()
-
-
-if __name__ == '__main__':
- if len(sys.argv) < 3:
- print_usage()
- sys.exit(-1)
-
- event = sys.argv[1]
- thread_id = sys.argv[2]
-
- logging.debug('hello!')
- logging.debug('event = %s' % event)
- logging.debug('thread_id = %s' % thread_id)
-
- admin_username, admin_password = get_admin_credentials()
-
- uri = '/_relay_event/?event=%(event)s&thread_id=%(thread_id)s&_username=%(username)s' % {
- 'username': admin_username,
- 'thread_id': thread_id,
- 'event': event}
-
- signature = utils.compute_signature('POST', uri, '', admin_password)
-
- url = 'http://127.0.0.1:%(port)s' + uri + '&_signature=' + signature
- url = url % {'port': settings.PORT}
-
- try:
- response = urllib.urlopen(url, data='')
- response = json.load(response)
- if response.get('error'):
- raise Exception(response['error'])
-
- logging.debug('event successfully relayed')
-
- except Exception as e:
- logging.error('failed to relay event: %s' % e)
-
- logging.debug('bye!')
--- /dev/null
+#!/usr/bin/env python
+
+# Copyright (c) 2013 Calin Crisan
+# This file is part of motionEye.
+#
+# motionEye is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import argparse
+import logging
+import os.path
+import pipes
+import sys
+
+from tornado.httpclient import AsyncHTTPClient
+
+# make sure motioneye is on python path
+sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+import relayevent
+import sendmail
+import server
+import settings
+import webhook
+
+
+_LOG_FILE = 'motioneye.log'
+
+
+def find_command(command):
+ cmd = os.path.abspath(sys.argv[0])
+ cmd += ' %s ' % command
+ cmd += ' '.join([pipes.quote(arg) for arg in sys.argv[2:]])
+
+ return cmd
+
+
+def load_settings():
+ # parse common comnand line argumentss
+ config_file = None
+ debug = False
+
+ for i in xrange(1, len(sys.argv)):
+ arg = sys.argv[i]
+ next_arg = i < len(sys.argv) - 1 and sys.argv[i + 1]
+ if arg == '-c':
+ config_file = next_arg
+
+ elif arg == '-d':
+ debug = True
+
+ def parse_conf_line(line):
+ line = line.strip()
+ if not line or line.startswith('#'):
+ return
+
+ parts = line.split(' ', 1)
+ if len(parts) != 2:
+ raise Exception('invalid configuration line: %s' % line)
+
+ name, value = parts
+ upper_name = name.upper().replace('-', '_')
+
+ if hasattr(settings, upper_name):
+ curr_value = getattr(settings, upper_name)
+
+ if upper_name == 'LOG_LEVEL':
+ if value == 'quiet':
+ value = 100
+
+ else:
+ value = getattr(logging, value.upper(), logging.DEBUG)
+
+ elif value.lower() == 'true':
+ value = True
+
+ elif value.lower() == 'false':
+ value = False
+
+ elif isinstance(curr_value, int):
+ value = int(value)
+
+ elif isinstance(curr_value, float):
+ value = float(value)
+
+ setattr(settings, upper_name, value)
+
+ else:
+ raise Exception('unknown configuration option: %s' % name)
+
+ if config_file:
+ try:
+ with open(config_file) as f:
+ for line in f:
+ parse_conf_line(line)
+
+ except Exception as e:
+ logging.fatal('failed to read settings from "%s": %s' % (config_file, e))
+ sys.exit(-1)
+
+ else:
+ logging.info('no configuration file given, using built-in defaults')
+
+ if debug:
+ settings.LOG_LEVEL = logging.DEBUG
+
+
+def configure_logging(cmd, log_to_file=False):
+ format = '%(asctime)s: [{cmd}] %(levelname)s: %(message)s'.format(cmd=cmd)
+
+ for h in logging.getLogger().handlers:
+ logging.getLogger().removeHandler(h)
+
+ try:
+ if log_to_file:
+ log_file = os.path.join(settings.LOG_PATH, _LOG_FILE)
+
+ else:
+ log_file = None
+
+ logging.basicConfig(filename=log_file, level=settings.LOG_LEVEL,
+ format=format, datefmt='%Y-%m-%d %H:%M:%S')
+
+ except Exception as e:
+ sys.stderr.write('failed to configure logging: %s\n' % e)
+ sys.exit(-1)
+
+ logging.getLogger('tornado').setLevel(logging.WARN)
+
+
+def configure_tornado():
+ AsyncHTTPClient.configure('tornado.curl_httpclient.CurlAsyncHTTPClient', max_clients=16)
+
+
+def make_arg_parser(command=None):
+ if command:
+ usage = description = epilog = None
+
+ else:
+ usage = '%(prog)s [command] [-c CONFIG_FILE] [-d] [-v] [command options...]\n\n'
+
+ description = 'available commands:\n'
+ description += ' startserver\n'
+ description += ' stopserver\n'
+ description += ' relayevent\n'
+ description += ' sendmail\n'
+ description += ' webhook\n\n'
+
+ epilog = 'type "%(prog)s [command] -h" for help on a specific command\n\n'
+
+ parser = argparse.ArgumentParser(prog='meyectl%s' % ((' ' + command) if command else ''),
+ usage=usage, description=description, epilog=epilog,
+ add_help=False, formatter_class=argparse.RawTextHelpFormatter)
+
+ parser.add_argument('-c', help='use a config file instead of built-in defaults',
+ type=str, dest='config_file')
+ parser.add_argument('-d', help='enable debugging, overriding log level from config file',
+ action='store_true', dest='debug')
+ parser.add_argument('-h', help='print this help and exit',
+ action='help', default=argparse.SUPPRESS)
+ parser.add_argument('-v', help='print program version and exit',
+ action='version', default=argparse.SUPPRESS)
+
+ return parser
+
+
+def print_usage_and_exit(code):
+ parser = make_arg_parser()
+ parser.print_help(sys.stderr)
+
+ sys.exit(code)
+
+
+def print_version_and_exit():
+ import motioneye
+
+ sys.stderr.write('motionEye %s\n' % motioneye.VERSION)
+ sys.exit()
+
+
+def main():
+ for a in sys.argv:
+ if a == '-v':
+ print_version_and_exit()
+
+ if len(sys.argv) < 2 or sys.argv[1] == '-h':
+ print_usage_and_exit(0)
+
+ load_settings()
+
+ command = sys.argv[1]
+ arg_parser = make_arg_parser(command)
+
+ if command in ('startserver', 'stopserver'):
+ server.main(arg_parser, sys.argv[2:], command[:-6])
+
+ elif command == 'sendmail':
+ sendmail.main(arg_parser, sys.argv[2:])
+
+ elif command == 'relayevent':
+ relayevent.main(arg_parser, sys.argv[2:])
+
+ elif command == 'webhook':
+ webhook.main(arg_parser, sys.argv[2:])
+
+ else:
+ sys.stderr.write('unknown command "%s"\n\n' % command)
+ print_usage_and_exit(-1)
+
+
+if __name__ == '__main__':
+ main()
motionctl.start(deferred=True)
MjpgClient.last_erroneous_close_time = now
+
+ # remove the cached picture
+ MjpgClient.last_jpgs.pop(self._camera_id, None)
def _check_error(self):
if self.socket is None:
--- /dev/null
+
+# Copyright (c) 2013 Calin Crisan
+# This file is part of motionEye.
+#
+# motionEye is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import errno
+import json
+import logging
+import os.path
+import sys
+import urllib
+
+sys.path.append(os.path.join(os.path.dirname(sys.argv[0]),'src'))
+
+import settings
+import utils
+
+
+def get_admin_credentials():
+ # this shortcut function is a bit faster than using the config module functions
+ config_file_path = os.path.join(settings.CONF_PATH, 'motion.conf')
+
+ logging.debug('reading main config from file %(path)s...' % {'path': config_file_path})
+
+ lines = None
+ try:
+ file = open(config_file_path, 'r')
+
+ except IOError as e:
+ if e.errno == errno.ENOENT: # file does not exist
+ logging.info('main config file %(path)s does not exist, using default values' % {'path': config_file_path})
+
+ lines = []
+
+ else:
+ logging.error('could not open main config file %(path)s: %(msg)s' % {
+ 'path': config_file_path, 'msg': unicode(e)})
+
+ raise
+
+ if lines is None:
+ try:
+ lines = [l[:-1] for l in file.readlines()]
+
+ except Exception as e:
+ logging.error('could not read main config file %(path)s: %(msg)s' % {
+ 'path': config_file_path, 'msg': unicode(e)})
+
+ raise
+
+ finally:
+ file.close()
+
+ admin_username = 'admin'
+ admin_password = ''
+ for line in lines:
+ line = line.strip()
+ if not line.startswith('#'):
+ continue
+
+ line = line[1:].strip()
+ if line.startswith('@admin_username'):
+ parts = line.split(' ', 1)
+ admin_username = parts[1] if len(parts) > 1 else ''
+
+ continue
+
+ if line.startswith('@admin_password'):
+ parts = line.split(' ', 1)
+ admin_password = parts[1] if len(parts) > 1 else ''
+
+ continue
+
+ return admin_username, admin_password
+
+
+def parse_options(parser, args):
+ parser.add_argument('event', help='the name of the event to relay')
+ parser.add_argument('thread_id', help='the id of the thread')
+
+ return parser.parse_args(args)
+
+
+def main(parser, args):
+ import meyectl
+
+ options = parse_options(parser, args)
+
+ meyectl.configure_logging('relayevent')
+ meyectl.configure_tornado()
+
+ logging.debug('hello!')
+ logging.debug('event = %s' % options.event)
+ logging.debug('thread_id = %s' % options.thread_id)
+
+ admin_username, admin_password = get_admin_credentials()
+
+ uri = '/_relay_event/?event=%(event)s&thread_id=%(thread_id)s&_username=%(username)s' % {
+ 'username': admin_username,
+ 'thread_id': options.thread_id,
+ 'event': options.event}
+
+ signature = utils.compute_signature('POST', uri, '', admin_password)
+
+ url = 'http://127.0.0.1:%(port)s' + uri + '&_signature=' + signature
+ url = url % {'port': settings.PORT}
+
+ try:
+ response = urllib.urlopen(url, data='')
+ response = json.load(response)
+ if response.get('error'):
+ raise Exception(response['error'])
+
+ logging.debug('event successfully relayed')
+
+ except Exception as e:
+ logging.error('failed to relay event: %s' % e)
+
+ logging.debug('bye!')
-#!/usr/bin/env python
# Copyright (c) 2013 Calin Crisan
# This file is part of motionEye.
import re
import smtplib
import socket
-import sys
import time
from email import Encoders
import settings
-from motioneye import _configure_settings, _configure_logging, _configure_signals
-
-_configure_settings()
-_configure_signals()
-_configure_logging(module='sendmail')
-
import config
import mediafiles
import tzctl
def send_mail(server, port, account, password, tls, to, subject, message, files):
- conn = smtplib.SMTP(server, port, timeout=getattr(settings, 'SMTP_TIMEOUT', 60))
+ conn = smtplib.SMTP(server, port, timeout=settings.SMTP_TIMEOUT)
if tls:
conn.starttls()
mediafiles.list_media(camera_config, media_type='picture', callback=on_media_files)
-def print_usage():
- print 'Usage: sendmail.py <server> <port> <account> <password> <tls> <to> <msg_id> <camera_id> <moment> <frames> [timespan]'
-
+def parse_options(parser, args):
+ parser.add_argument('server', help='address of the SMTP server')
+ parser.add_argument('port', help='port for the SMTP connection')
+ parser.add_argument('account', help='SMTP account name (username)')
+ parser.add_argument('password', help='SMTP account password')
+ parser.add_argument('tls', help='"true" to use TLS')
+ parser.add_argument('to', help='the email recipient(s)')
+ parser.add_argument('msg_id', help='the identifier of the message')
+ parser.add_argument('camera_id', help='the id of the camera')
+ parser.add_argument('moment', help='the moment in ISO-8601 format')
+ parser.add_argument('timespan', help='picture collection time span')
-if __name__ == '__main__':
- if len(sys.argv) < 10:
- print_usage()
- sys.exit(-1)
+ return parser.parse_args(args)
- server = sys.argv[1]
- port = int(sys.argv[2])
- account = sys.argv[3]
- password = sys.argv[4]
- tls = sys.argv[5].lower() == 'true'
- to = sys.argv[6]
- msg_id = sys.argv[7]
- camera_id = sys.argv[8]
- moment = sys.argv[9]
- try:
- timespan = int(sys.argv[10])
- except:
- timespan = 0
+def main(parser, args):
+ import meyectl
+
+ options = parse_options(parser, args)
+
+ meyectl.configure_logging('sendmail')
+ meyectl.configure_tornado()
logging.debug('hello!')
- message = messages.get(msg_id)
- subject = subjects.get(msg_id)
- if not message or not subject:
- logging.error('unknown message id')
- sys.exit(-1)
+ options.port = int(options.port)
+ options.tls = options.tls.lower() == 'true'
+ options.timespan = int(options.timespan)
+ message = messages.get(options.msg_id)
+ subject = subjects.get(options.msg_id)
+ options.moment = datetime.datetime.strptime(options.moment, '%Y-%m-%dT%H:%M:%S')
- moment = datetime.datetime.strptime(moment, '%Y-%m-%dT%H:%M:%S')
-
- logging.debug('server = %s' % server)
- logging.debug('port = %s' % port)
- logging.debug('account = %s' % account)
+ logging.debug('server = %s' % options.server)
+ logging.debug('port = %s' % options.port)
+ logging.debug('account = %s' % options.account)
logging.debug('password = ******')
- logging.debug('server = %s' % server)
- logging.debug('tls = %s' % tls)
- logging.debug('to = %s' % to)
- logging.debug('msg_id = %s' % msg_id)
- logging.debug('camera_id = %s' % camera_id)
- logging.debug('moment = %s' % moment.strftime('%Y-%m-%d %H:%M:%S'))
+ logging.debug('server = %s' % options.server)
+ logging.debug('tls = %s' % str(options.tls).lower())
+ logging.debug('to = %s' % options.to)
+ logging.debug('msg_id = %s' % options.msg_id)
+ logging.debug('camera_id = %s' % options.camera_id)
+ logging.debug('moment = %s' % options.moment.strftime('%Y-%m-%d %H:%M:%S'))
logging.debug('smtp timeout = %d' % settings.SMTP_TIMEOUT)
- logging.debug('timespan = %d' % timespan)
+ logging.debug('timespan = %d' % options.timespan)
- if not to:
- logging.info('no email address specified')
- sys.exit(0)
-
- to = [t.strip() for t in re.split('[,;| ]', to)]
+ to = [t.strip() for t in re.split('[,;| ]', options.to)]
to = [t for t in to if t]
io_loop = IOLoop.instance()
def on_message(subject, message, files):
try:
- send_mail(server, port, account, password, tls, to, subject, message, files)
+ send_mail(options.server, options.port, options.account, options.password,
+ options.tls, options.to, subject, message, files)
logging.info('email sent')
-
+
except Exception as e:
logging.error('failed to send mail: %s' % e, exc_info=True)
def ioloop_timeout():
io_loop.stop()
- make_message(subject, message, camera_id, moment, timespan, on_message)
+ make_message(subject, message, options.camera_id, options.moment, options.timespan, on_message)
io_loop.add_timeout(datetime.timedelta(seconds=settings.SMTP_TIMEOUT), ioloop_timeout)
io_loop.start()
- logging.debug('bye!')
\ No newline at end of file
+ logging.debug('bye!')
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+import argparse
+import atexit
import datetime
+import logging
import multiprocessing
-import os.path
+import os
import signal
-import sys
+import sys
+import time
-from tornado.httpclient import AsyncHTTPClient
from tornado.web import Application
import handlers
-import logging
import settings
import template
-def load_settings():
- # TODO use optparse
-# length = len(sys.argv) - 1
-# for i in xrange(length):
-# arg = sys.argv[i + 1]
-#
-# if not arg.startswith('--'):
-# continue
-#
-# next_arg = None
-# if i < length - 1:
-# next_arg = sys.argv[i + 2]
-#
-# name = arg[2:].upper().replace('-', '_')
-#
-# if hasattr(settings, name):
-# curr_value = getattr(settings, name)
-#
-# if next_arg.lower() == 'debug':
-# next_arg = logging.DEBUG
-#
-# elif next_arg.lower() == 'info':
-# next_arg = logging.INFO
-#
-# elif next_arg.lower() == 'warn':
-# next_arg = logging.WARN
-#
-# elif next_arg.lower() == 'error':
-# next_arg = logging.ERROR
-#
-# elif next_arg.lower() == 'fatal':
-# next_arg = logging.FATAL
-#
-# elif next_arg.lower() == 'true':
-# next_arg = True
-#
-# elif next_arg.lower() == 'false':
-# next_arg = False
-#
-# elif isinstance(curr_value, int):
-# next_arg = int(next_arg)
-#
-# elif isinstance(curr_value, float):
-# next_arg = float(next_arg)
-#
-# setattr(settings, name, next_arg)
-#
-# else:
-# return arg[2:]
+_PID_FILE = 'motioneye.pid'
- if not os.path.exists(settings.CONF_PATH):
- logging.fatal('config directory "%s" does not exist' % settings.CONF_PATH)
- sys.exit(-1)
+
+class Daemon(object):
+ def __init__(self, pid_file, run_callback=None):
+ self.pid_file = pid_file
+ self.run_callback = run_callback
+
+ def daemonize(self):
+ # first fork
+ try:
+ if os.fork() > 0: # parent
+ sys.exit(0)
+
+ except OSError, e:
+ sys.stderr.write('fork() failed: %s\n' % e.strerror)
+ sys.exit(-1)
+
+ # separate from parent
+ os.chdir('/')
+ os.setsid()
+ os.umask(0)
+
+ # second fork
+ try:
+ if os.fork() > 0: # parent
+ sys.exit(0)
+
+ except OSError, e:
+ sys.stderr.write('fork() failed: %s\n' % e.strerror)
+ sys.exit(-1)
+
+ # redirect standard file descriptors
+ sys.stdout.flush()
+ sys.stderr.flush()
+ si = file('/dev/null', 'r')
+ so = file('/dev/null', 'a+')
+ se = file('/dev/null', 'a+', 0)
+ os.dup2(si.fileno(), sys.stdin.fileno())
+ os.dup2(so.fileno(), sys.stdout.fileno())
+ os.dup2(se.fileno(), sys.stderr.fileno())
+
+ # pid file
+ atexit.register(self.del_pid)
+ with open(self.pid_file, 'w') as f:
+ f.write('%s\n' % os.getpid())
+
+ def del_pid(self):
+ try:
+ os.remove(self.pid_file)
+
+ except:
+ pass
- if not os.path.exists(settings.RUN_PATH):
- logging.fatal('pid directory "%s" does not exist' % settings.RUN_PATH)
- sys.exit(-1)
+ def running(self):
+ try:
+ with open(self.pid_file) as f:
+ pid = int(f.read().strip())
- if not os.path.exists(settings.LOG_PATH):
- logging.fatal('log directory "%s" does not exist' % settings.LOG_PATH)
- sys.exit(-1)
+ except:
+ return None
- if not os.path.exists(settings.MEDIA_PATH):
- logging.fatal('media directory "%s" does not exist' % settings.MEDIA_PATH)
- sys.exit(-1)
+ try:
+ os.kill(pid, 0)
+ return pid
+
+ except:
+ return None
+
+ def start(self):
+ if self.running():
+ sys.stderr.write('server is already running\n')
+ sys.exit(-1)
+
+ self.daemonize()
+ sys.stdout.write('server started\n')
+ self.run_callback()
+
+ def stop(self):
+ pid = self.running()
+ if not pid:
+ sys.stderr.write('server is not running\n')
+ sys.exit(-1)
+
+ try:
+ os.kill(pid, signal.SIGTERM)
+
+ except Exception as e:
+ sys.stderr.write('failed to terminate server: %s\n' % e)
+
+ for i in xrange(50): # @UnusedVariable
+ try:
+ os.kill(pid, 0)
+ time.sleep(0.1)
+
+ except OSError as e:
+ if str(e).count('No such process'):
+ self.del_pid()
+ sys.stdout.write('server stopped\n')
+ break
+
+ else:
+ sys.stderr.write('failed to terminate server: %s\n' % e)
+ sys.exit(-11)
+
+ else:
+ sys.stderr.write('server failed to stop, killing it\n')
+ try:
+ os.kill(pid, signal.SIGKILL)
+
+ except:
+ pass
+
+
+def _log_request(handler):
+ if handler.get_status() < 400:
+ log_method = logging.debug
+
+ elif handler.get_status() < 500:
+ log_method = logging.warning
+
+ else:
+ log_method = logging.error
+
+ request_time = 1000.0 * handler.request.request_time()
+ log_method("%d %s %.2fms", handler.get_status(),
+ handler._request_summary(), request_time)
+
+application = Application(
+ [
+ (r'^/$', handlers.MainHandler),
+ (r'^/config/main/(?P<op>set|get)/?$', handlers.ConfigHandler),
+ (r'^/config/(?P<camera_id>\d+)/(?P<op>get|set|rem|set_preview)/?$', handlers.ConfigHandler),
+ (r'^/config/(?P<op>add|list|backup|restore)/?$', handlers.ConfigHandler),
+ (r'^/picture/(?P<camera_id>\d+)/(?P<op>current|list|frame)/?$', handlers.PictureHandler),
+ (r'^/picture/(?P<camera_id>\d+)/(?P<op>download|preview|delete)/(?P<filename>.+?)/?$', handlers.PictureHandler),
+ (r'^/picture/(?P<camera_id>\d+)/(?P<op>zipped|timelapse|delete_all)/(?P<group>.+?)/?$', handlers.PictureHandler),
+ (r'^/movie/(?P<camera_id>\d+)/(?P<op>list)/?$', handlers.MovieHandler),
+ (r'^/movie/(?P<camera_id>\d+)/(?P<op>download|preview|delete)/(?P<filename>.+?)/?$', handlers.MovieHandler),
+ (r'^/movie/(?P<camera_id>\d+)/(?P<op>delete_all)/(?P<group>.+?)/?$', handlers.MovieHandler),
+ (r'^/_relay_event/?$', handlers.RelayEventHandler),
+ (r'^/log/(?P<name>\w+)/?$', handlers.LogHandler),
+ (r'^/update/?$', handlers.UpdateHandler),
+ (r'^/power/(?P<op>shutdown|reboot)/?$', handlers.PowerHandler),
+ (r'^/version/?$', handlers.VersionHandler),
+ (r'^/login/?$', handlers.LoginHandler),
+ (r'^.*$', handlers.NotFoundHandler),
+ ],
+ debug=False,
+ log_function=_log_request,
+ static_path=settings.STATIC_PATH,
+ static_url_prefix=settings.STATIC_URL
+)
+
+template.add_context('STATIC_URL', settings.STATIC_URL)
def configure_signals():
signal.signal(signal.SIGCHLD, child_handler)
-def configure_logging(module=None):
- if module:
- format = '%(asctime)s: [{module}] %(levelname)s: %(message)s'.format(module=module)
-
- else:
- format = '%(asctime)s: %(levelname)s: %(message)s'
-
- logging.basicConfig(filename=None, level=settings.LOG_LEVEL,
- format=format, datefmt='%Y-%m-%d %H:%M:%S')
-
- logging.getLogger('tornado').setLevel(logging.WARN)
-
+def test_requirements():
+ if not os.access(settings.CONF_PATH, os.W_OK):
+ logging.fatal('config directory "%s" does not exist or is not writable' % settings.CONF_PATH)
+ sys.exit(-1)
+
+ if not os.access(settings.RUN_PATH, os.W_OK):
+ logging.fatal('pid directory "%s" does not exist or is not writable' % settings.RUN_PATH)
+ sys.exit(-1)
-def configure_tornado():
- AsyncHTTPClient.configure('tornado.curl_httpclient.CurlAsyncHTTPClient', max_clients=16)
+ if not os.access(settings.LOG_PATH, os.W_OK):
+ logging.fatal('log directory "%s" does not exist or is not writable' % settings.LOG_PATH)
+ sys.exit(-1)
+ if not os.access(settings.MEDIA_PATH, os.W_OK):
+ logging.fatal('media directory "%s" does not exist or is not writable' % settings.MEDIA_PATH)
+ sys.exit(-1)
-def test_requirements():
if os.geteuid() != 0:
if settings.SMB_SHARES:
print('SMB_SHARES require root privileges')
logging.info('thumbnailer started')
-def run_server():
+def parse_options(parser, args):
+ parser.add_argument('-b', help='start the server in background (daemonize)',
+ action='store_true', dest='background', default=False)
+
+ return parser.parse_args(args)
+
+
+def run():
import cleanup
import motionctl
+ import motioneye
+ import smbctl
import thumbnailer
import tornado.ioloop
- import smbctl
+
+ configure_signals()
+ test_requirements()
+
+ logging.info('hello! this is motionEye server %s' % motioneye.VERSION)
+
+ if settings.SMB_SHARES:
+
+ stop, start = smbctl.update_mounts() # @UnusedVariable
+ if start:
+ start_motion()
+
+ else:
+ start_motion()
+
+ start_cleanup()
+ start_wsswitch()
+
+ if settings.THUMBNAILER_INTERVAL:
+ start_thumbnailer()
application.listen(settings.PORT, settings.LISTEN)
logging.info('server started')
smbctl.umount_all()
logging.info('SMB shares unmounted')
-
-def log_request(handler):
- if handler.get_status() < 400:
- log_method = logging.debug
-
- elif handler.get_status() < 500:
- log_method = logging.warning
-
- else:
- log_method = logging.error
-
- request_time = 1000.0 * handler.request.request_time()
- log_method("%d %s %.2fms", handler.get_status(),
- handler._request_summary(), request_time)
-
-
-application = Application(
- [
- (r'^/$', handlers.MainHandler),
- (r'^/config/main/(?P<op>set|get)/?$', handlers.ConfigHandler),
- (r'^/config/(?P<camera_id>\d+)/(?P<op>get|set|rem|set_preview)/?$', handlers.ConfigHandler),
- (r'^/config/(?P<op>add|list|backup|restore)/?$', handlers.ConfigHandler),
- (r'^/picture/(?P<camera_id>\d+)/(?P<op>current|list|frame)/?$', handlers.PictureHandler),
- (r'^/picture/(?P<camera_id>\d+)/(?P<op>download|preview|delete)/(?P<filename>.+?)/?$', handlers.PictureHandler),
- (r'^/picture/(?P<camera_id>\d+)/(?P<op>zipped|timelapse|delete_all)/(?P<group>.+?)/?$', handlers.PictureHandler),
- (r'^/movie/(?P<camera_id>\d+)/(?P<op>list)/?$', handlers.MovieHandler),
- (r'^/movie/(?P<camera_id>\d+)/(?P<op>download|preview|delete)/(?P<filename>.+?)/?$', handlers.MovieHandler),
- (r'^/movie/(?P<camera_id>\d+)/(?P<op>delete_all)/(?P<group>.+?)/?$', handlers.MovieHandler),
- (r'^/_relay_event/?$', handlers.RelayEventHandler),
- (r'^/log/(?P<name>\w+)/?$', handlers.LogHandler),
- (r'^/update/?$', handlers.UpdateHandler),
- (r'^/power/(?P<op>shutdown|reboot)/?$', handlers.PowerHandler),
- (r'^/version/?$', handlers.VersionHandler),
- (r'^/login/?$', handlers.LoginHandler),
- (r'^.*$', handlers.NotFoundHandler),
- ],
- debug=False,
- log_function=log_request,
- static_path=settings.STATIC_PATH,
- static_url_prefix=settings.STATIC_URL
-)
-
-template.add_context('STATIC_URL', settings.STATIC_URL)
+ logging.info('bye!')
-def main():
- import motioneye
+def main(parser, args, command):
+ import meyectl
- load_settings()
- configure_signals()
- configure_logging()
- test_requirements()
- configure_tornado()
-
- logging.info('hello! this is motionEye %s' % motioneye.VERSION)
+ options = parse_options(parser, args)
- if settings.SMB_SHARES:
- import smbctl
-
- stop, start = smbctl.update_mounts() # @UnusedVariable
- if start:
- start_motion()
-
- else:
- start_motion()
-
- start_cleanup()
- start_wsswitch()
-
- if settings.THUMBNAILER_INTERVAL:
- start_thumbnailer()
-
- run_server()
+ meyectl.configure_logging('motioneye', options.background)
+ meyectl.configure_tornado()
+
+ if command == 'start':
+ if options.background:
+ daemon = Daemon(
+ pid_file=os.path.join(settings.RUN_PATH, _PID_FILE),
+ run_callback=run)
+ daemon.start()
+
+ else:
+ run()
- logging.info('bye!')
+ elif command == 'stop':
+ daemon = Daemon(pid_file=os.path.join(settings.RUN_PATH, _PID_FILE))
+ daemon.stop()
# the static files directory
STATIC_PATH = os.path.join(PROJECT_PATH, 'static')
-# static files (.css, .js etc) are served at this root url
+# static files (.css, .js etc) are served at this root url;
+# change this if you run motionEye behind a reverse proxy (e.g. nginx),
+# and you want static files to be served directly by it
STATIC_URL = '/static/'
-# path to the config directory; must be writable
+# path to the configuration directory (must be writable by motionEye)
CONF_PATH = [sys.prefix, ''][sys.prefix == '/usr'] + '/etc/motioneye'
-# pid files go here
+# path to the directory where pid files go (must be writable by motionEye)
for d in ['/run', '/var/run', '/tmp', '/var/tmp']:
if os.path.exists(d):
RUN_PATH = d
else:
RUN_PATH = PROJECT_PATH
-# log files go here
+# path to the directory where log files go (must be writable by motionEye)
for d in ['/log', '/var/log', '/tmp', '/var/tmp']:
if os.path.exists(d):
LOG_PATH = d
else:
LOG_PATH = RUN_PATH
-# default output path for media files
+# default output path for media files (must be writable by motionEye)
MEDIA_PATH = RUN_PATH
-# path to motion binary (automatically detected if not set)
+# path to the motion binary to use (automatically detected by default)
MOTION_BINARY = None
-# the log level
+# the log level (use FATAL, ERROR, WARNING, INFO or DEBUG)
LOG_LEVEL = logging.INFO
-# IP addresses to listen on
+# the IP address to listen on
+# (0.0.0.0 for all interfaces, 127.0.0.1 for localhost)
LISTEN = '0.0.0.0'
# the TCP port to listen on
# interval in seconds at which motionEye checks if motion is running
MOTION_CHECK_INTERVAL = 10
-# interval in seconds at which the janitor is called to remove old pictures and movies
+# interval in seconds at which the janitor is called
+# to remove old pictures and movies
CLEANUP_INTERVAL = 43200
-# interval in seconds at which the thumbnail mechanism runs (set to 0 to disable)
+# interval in seconds at which the thumbnail mechanism runs
+# (set to 0 to disable)
THUMBNAILER_INTERVAL = 60
-# timeout in seconds when waiting for response from a remote motionEye server
+# timeout in seconds to wait for response from a remote motionEye server
REMOTE_REQUEST_TIMEOUT = 10
-# timeout in seconds when waiting for mjpg data from the motion daemon
+# timeout in seconds to wait for mjpg data from the motion daemon
MJPG_CLIENT_TIMEOUT = 10
-# timeout in seconds after which an idle mjpg client is removed (set to 0 to disable)
+# timeout in seconds after which an idle mjpg client is removed
+# (set to 0 to disable)
MJPG_CLIENT_IDLE_TIMEOUT = 10
-# enable SMB shares (requires root)
+# enable SMB shares (requires motionEye to run as root)
SMB_SHARES = False
-# the directory where the SMB mounts will be created
+# the directory where the SMB mount points will be created
SMB_MOUNT_ROOT = '/media'
-# path to a wpa_supplicant.conf file if wifi settings UI is desired
+# path to the wpa_supplicant.conf file
+# (enable this to configure wifi settings from the UI)
WPA_SUPPLICANT_CONF = None
-# path to a localtime file if time zone settings UI is desired
+# path to the localtime file
+# (enable this to configure the system time zone from the UI)
LOCAL_TIME_FILE = None
-# enables shutdown and rebooting after changing system settings (such as wifi settings or system updates)
+# enables shutdown and rebooting after changing system settings
+# (such as wifi settings or time zone)
ENABLE_REBOOT = False
-# the timeout in seconds to use when talking to a SMTP server
+# timeout in seconds to use when talking to the SMTP server
SMTP_TIMEOUT = 60
-# the time to wait for zip file creation
+# timeout in seconds to wait for zip file creation
ZIP_TIMEOUT = 500
# enable adding and removing cameras from UI
-#!/usr/bin/env python
# Copyright (c) 2013 Calin Crisan
# This file is part of motionEye.
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
-import sys
import urllib2
import urlparse
import settings
-from motioneye import _configure_settings, _configure_logging
+def parse_options(parser, args):
+ parser.add_argument('method', help='the HTTP method to use')
+ parser.add_argument('url', help='the URL for the request')
-_configure_settings()
-_configure_logging()
+ return parser.parse_args(args)
-def print_usage():
- print 'Usage: webhook.py <method> <url>'
-
-
-if __name__ == '__main__':
- if len(sys.argv) < 3:
- print_usage()
- sys.exit(-1)
+def main(parser, args):
+ import meyectl
- method = sys.argv[1]
- url = sys.argv[2]
+ options = parse_options(parser, args)
+
+ meyectl.configure_logging('webhook')
+ meyectl.configure_tornado()
- logging.debug('method = %s' % method)
- logging.debug('url = %s' % url)
+ logging.debug('hello!')
+ logging.debug('method = %s' % options.method)
+ logging.debug('url = %s' % options.url)
- if method == 'POST':
- parts = urlparse.urlparse(url)
+ if options.method == 'POST':
+ parts = urlparse.urlparse(options.url)
data = parts.query
else:
data = None
- request = urllib2.Request(url, data)
+ request = urllib2.Request(options.url, data)
try:
urllib2.urlopen(request, timeout=settings.REMOTE_REQUEST_TIMEOUT)
logging.debug('webhook successfully called')
except Exception as e:
logging.error('failed to call webhook: %s' % e)
+
+ logging.debug('bye!')