├── __init__.py ├── .gitignore ├── tests ├── __init__.py └── test_module.py ├── utils ├── __init__.py ├── utils.py ├── startup-script │ └── ssdp └── ssdp_server.py ├── actions ├── __init__.py ├── GoogleDriveActionBase.py ├── DeleteMediaFileAction.py ├── UrlInvokeAction.py ├── GoogleDriveCleanupAction.py ├── SmtpEmailNotifyAction.py └── GoogleDriveUploadAction.py ├── detectors ├── __init__.py ├── ArpBasedDetector.py ├── TimeBasedDetector.py └── IpBasedDetector.py ├── objects ├── __init__.py ├── enums │ ├── __init__.py │ ├── trigger_rule.py │ └── event_type.py ├── event_action.py ├── config.py ├── detector_rules.py └── motion_event.py ├── resources ├── test.jpg └── sample-config-files │ └── motion.conf.example ├── create-motion-conf-entries.txt ├── install-motion-notify.sh ├── motion-notify.cfg ├── motion-notify.py ├── README.md └── LICENSE /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.p12 2 | motion-notify-test.cfg -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'adean' 2 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'adean' 2 | -------------------------------------------------------------------------------- /actions/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'adean' 2 | -------------------------------------------------------------------------------- /detectors/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'adean' 2 | -------------------------------------------------------------------------------- /objects/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'adean' 2 | -------------------------------------------------------------------------------- /objects/enums/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'adean' 2 | -------------------------------------------------------------------------------- /resources/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amdean/motion-notify/HEAD/resources/test.jpg -------------------------------------------------------------------------------- /objects/enums/trigger_rule.py: -------------------------------------------------------------------------------- 1 | __author__ = 'adean' 2 | 3 | from enum import Enum 4 | 5 | 6 | class TriggerRule(Enum): 7 | always = 1 8 | if_active = 2 9 | -------------------------------------------------------------------------------- /objects/enums/event_type.py: -------------------------------------------------------------------------------- 1 | __author__ = 'adean' 2 | 3 | from enum import Enum 4 | 5 | 6 | class EventType(Enum): 7 | on_event_start = 1 8 | on_picture_save = 2 9 | on_movie_end = 3 10 | on_cron_trigger = 4 11 | -------------------------------------------------------------------------------- /utils/utils.py: -------------------------------------------------------------------------------- 1 | __author__ = 'adean' 2 | 3 | 4 | class Utils: 5 | @staticmethod 6 | def reflect_class_from_classname(package, classname): 7 | mod = __import__(package + '.' + classname, fromlist=[classname]) 8 | klass = getattr(mod, classname) 9 | return klass() 10 | -------------------------------------------------------------------------------- /objects/event_action.py: -------------------------------------------------------------------------------- 1 | __author__ = 'adean' 2 | 3 | from enums import trigger_rule as trigger_rule_mod 4 | class EventAction(object): 5 | def __init__(self, action_name, trigger_rule_str): 6 | self.action_name = action_name 7 | self.trigger_rule = trigger_rule_mod.TriggerRule[trigger_rule_str] 8 | -------------------------------------------------------------------------------- /create-motion-conf-entries.txt: -------------------------------------------------------------------------------- 1 | on_picture_save /etc/motion-notify/motion-notify.py /etc/motion-notify/motion-notify.cfg %f on_picture_save %s %v %n 2 | on_movie_end /etc/motion-notify/motion-notify.py /etc/motion-notify/motion-notify.cfg %f on_movie_end %s %v %n 3 | on_event_start /etc/motion-notify/motion-notify.py /etc/motion-notify/motion-notify.cfg None on_event_start %s %v None -------------------------------------------------------------------------------- /actions/GoogleDriveActionBase.py: -------------------------------------------------------------------------------- 1 | from pydrive.auth import GoogleAuth 2 | 3 | import httplib2 4 | import logging.handlers 5 | 6 | from oauth2client.service_account import ServiceAccountCredentials 7 | 8 | logger = logging.getLogger('MotionNotify') 9 | 10 | 11 | class GoogleDriveActionBase: 12 | 13 | @staticmethod 14 | def authenticate(config): 15 | logger.debug("GoogleDriveAction starting authentication") 16 | svc_user_id = config.config_obj.get('GoogleDriveUploadAction', 'service_user_email') 17 | svc_scope = "https://www.googleapis.com/auth/drive" 18 | svc_key_file = config.config_obj.get('GoogleDriveUploadAction', 'key_file') 19 | gcredentials = ServiceAccountCredentials.from_p12_keyfile(svc_user_id, svc_key_file, scopes=svc_scope) 20 | gcredentials.authorize(httplib2.Http()) 21 | gauth = GoogleAuth() 22 | gauth.credentials = gcredentials 23 | logger.debug("GoogleDriveUploadAction authentication complete") 24 | return gauth 25 | -------------------------------------------------------------------------------- /actions/DeleteMediaFileAction.py: -------------------------------------------------------------------------------- 1 | __author__ = 'adean' 2 | 3 | import os 4 | import logging.handlers 5 | 6 | logger = logging.getLogger('MotionNotify') 7 | 8 | class DeleteMediaFileAction: 9 | @staticmethod 10 | def do_event_start_action(config, motion_event): 11 | logger.info("Motionevent_id:" + motion_event.event_id.__str__() + " Deleting: %s", motion_event.media_file) 12 | DeleteMediaFileAction.delete_file(motion_event.media_file) 13 | 14 | @staticmethod 15 | def do_event_end_action(config, motion_event): 16 | logger.info("Motionevent_id:" + motion_event.event_id.__str__() + " Deleting: %s", motion_event.media_file) 17 | DeleteMediaFileAction.delete_file(motion_event.media_file) 18 | 19 | @staticmethod 20 | def do_action(config, motion_event): 21 | logger.info("Motionevent_id:" + motion_event.event_id.__str__() + " Deleting: %s", motion_event.media_file) 22 | DeleteMediaFileAction.delete_file(motion_event.media_file) 23 | 24 | @staticmethod 25 | def delete_file(file_path): 26 | os.remove(file_path) 27 | -------------------------------------------------------------------------------- /detectors/ArpBasedDetector.py: -------------------------------------------------------------------------------- 1 | __author__ = 'adean' 2 | 3 | import subprocess 4 | import ConfigParser 5 | import logging 6 | 7 | logger = logging.getLogger('MotionNotify') 8 | 9 | class ArpBasedDetector: 10 | @staticmethod 11 | def detect_presence(config): 12 | logger.debug("ArpBasedDetector: detecting presence") 13 | presence_macs = [] 14 | network = None 15 | 16 | try: 17 | presence_macs = config.config_obj.get('ArpBasedDetector', 'presence_macs').split(',') 18 | network = config.get('ArpBasedDetector', 'network') 19 | except ConfigParser.NoSectionError, ConfigParser.NoOptionError: 20 | pass 21 | if not network or not presence_macs: 22 | return None 23 | logger.info("ArpBasedDetector: Checking for presence via MAC address") 24 | result = subprocess.Popen(['sudo', 'arp-scan', network], stdout=subprocess.PIPE, 25 | stderr=subprocess.STDOUT).stdout.readlines() 26 | logger.info("result %s", result) 27 | for addr in result: 28 | for i in presence_macs: 29 | if i.lower() in addr.lower(): 30 | logger.info('ArpBasedDetector: ARP entry found - someone is home') 31 | return False 32 | logger.info('ArpBasedDetector: No ARP entry found - nobody is home - system is active') 33 | return True 34 | -------------------------------------------------------------------------------- /actions/UrlInvokeAction.py: -------------------------------------------------------------------------------- 1 | """UrlInvokeAction 2 | 3 | An HTTP request is made to the URL's defined in the config file when the event start and event end actions occur. 4 | No action is carried out when the do_action method is called 5 | """ 6 | __author__ = 'adean' 7 | 8 | import logging 9 | import requests 10 | 11 | logger = logging.getLogger('MotionNotify') 12 | 13 | class UrlInvokeAction: 14 | @staticmethod 15 | def do_event_start_action(config, motion_event_obj): 16 | UrlInvokeAction.make_request(config, motion_event_obj, 'event_start_url') 17 | 18 | @staticmethod 19 | def do_event_end_action(config, motion_event_obj): 20 | UrlInvokeAction.make_request(config, motion_event_obj, 'movie_end_url') 21 | 22 | @staticmethod 23 | def do_action(config, motion_event_obj): 24 | logger.debug("Motionevent_id:" + motion_event_obj.event_id + " UrlInvokeAction: Ignoring action") 25 | 26 | 27 | @staticmethod 28 | def make_request(config, motion_event_obj, config_entry): 29 | logger.debug("Motionevent_id:" + motion_event_obj.event_id + " UrlInvokeAction: Making request") 30 | url = config.config_obj.get('UrlInvokeAction', config_entry) 31 | logger.debug("Motionevent_id:" + motion_event_obj.event_id + " UrlInvokeAction: URL: " + url) 32 | r = requests.get(url) 33 | if r.status_code != requests.codes.ok: 34 | logger.error("Motionevent_id:" + motion_event_obj.event_id + " UrlInvokeAction: Request failed..." + r.reason) 35 | else: 36 | logger.info("Motionevent_id:" + motion_event_obj.event_id + " UrlInvokeAction: Request sent successfully") 37 | -------------------------------------------------------------------------------- /install-motion-notify.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Installs motion-notify and all required dependencies. 4 | # Pre-requisites: Run script as root, ensure a motion.motion user exists, ensure that /etc/motion-notify does NOT exist 5 | # Refer to the README for additional steps required for configuring Google Drive, email and etc (those steps aren't covered by this script) 6 | # If you're upgrading motion-notify, move your existing motion-notify folder to a new location first and then copy your creds.p12 file across after running the install script and then update your new config file 7 | 8 | # Update APT and install dependencies 9 | apt-get update 10 | apt-get install python-twisted-web 11 | apt-get install python-pip 12 | pip install --upgrade PyDrive 13 | pip install --upgrade enum34 14 | pip install --upgrade oauth2client 15 | pip install google-api-python-client --upgrade --ignore-installed six 16 | pip install requests 17 | apt-get install python-openssl 18 | 19 | # Install git and clone motion-notify into the destination directory 20 | apt-get install git 21 | git clone https://github.com/amdean/motion-notify.git /etc/motion-notify 22 | chown -R motion.motion /etc/motion-notify 23 | chmod +x /etc/motion-notify/motion-notify.py 24 | chmod +x /etc/motion-notify/utils/ssdp_server.py 25 | mv /etc/motion-notify/utils/startup-script/ssdp /etc/init.d/ssdp 26 | chmod +x /etc/init.d/ssdp 27 | 28 | # Create the log files and lock files and set ownership and permissions 29 | touch /var/tmp/motion-notify.log 30 | chown motion.motion /var/tmp/motion-notify.log 31 | chmod 664 /var/tmp/motion-notify.log 32 | touch /var/tmp/motion-notify.lock.pid 33 | chmod 664 /var/tmp/motion-notify.lock.pid 34 | chown motion.motion /var/tmp/motion-notify.lock.pid -------------------------------------------------------------------------------- /detectors/TimeBasedDetector.py: -------------------------------------------------------------------------------- 1 | __author__ = 'adean' 2 | 3 | from datetime import time 4 | from datetime import datetime 5 | 6 | import logging 7 | 8 | logger = logging.getLogger('MotionNotify') 9 | 10 | class TimeBasedDetector: 11 | @staticmethod 12 | def detect_presence(config): 13 | logger.debug("TimeBasedDetector: detecting presence") 14 | return TimeBasedDetector.check_time_ranges(TimeBasedDetector.get_time_ranges( 15 | config.config_obj.get('TimeBasedDetector', 'time_ranges')), datetime.now().time()) 16 | 17 | @staticmethod 18 | def check_time_ranges(time_ranges, current_time): 19 | for time_range in time_ranges: 20 | if time_range.start_time <= current_time <= time_range.end_time: 21 | logger.info( 22 | 'TimeBasedDetector: System is active due to TimeBasedDetector: ' + time_range.start_time.strftime( 23 | "%H:%M") + " - " + time_range.end_time.strftime("%H:%M")) 24 | return True 25 | logger.info('System is inactive based on TimeBasedDetector') 26 | return False 27 | 28 | @staticmethod 29 | def get_time_ranges(config_entry): 30 | logger.debug("TimeBasedDetector: getting time ranges for config entry: " + config_entry) 31 | time_ranges = [] 32 | time_range_entries = config_entry.split(",") 33 | for time_range_entry in time_range_entries: 34 | time_rangetime_str = time_range_entry.split("-") 35 | time_ranges.append(TimeRange(time_rangetime_str[0], time_rangetime_str[1])) 36 | return time_ranges 37 | 38 | 39 | class TimeRange: 40 | def __init__(self, start_time, end_time): 41 | self.start_time = time(int(start_time.split(":")[0]), int(start_time.split(":")[1])) 42 | self.end_time = time(int(end_time.split(":")[0]), int(end_time.split(":")[1])) 43 | -------------------------------------------------------------------------------- /detectors/IpBasedDetector.py: -------------------------------------------------------------------------------- 1 | __author__ = 'adean' 2 | 3 | import subprocess 4 | import ConfigParser 5 | import logging 6 | 7 | logger = logging.getLogger('MotionNotify') 8 | 9 | class IpBasedDetector: 10 | @staticmethod 11 | def detect_presence(config): 12 | logger.debug("IpBasedDetector detecting presence") 13 | ip_addresses = None 14 | ping_timeout_seconds = 2 15 | ping_timeout_switch = "-w" 16 | try: 17 | ip_addresses = config.config_obj.get('IpBasedDetector', 'ip_addresses') 18 | ping_timeout_seconds = config.config_obj.get('IpBasedDetector', 'ping_timeout_seconds') 19 | ping_timeout_switch = config.config_obj.get('IpBasedDetector', 'ping_timeout_switch') 20 | except ConfigParser.NoSectionError, ConfigParser.NoOptionError: 21 | pass 22 | 23 | if not ip_addresses: 24 | logger.info("No IP addresses configured - skipping IP check") 25 | return True 26 | logger.info("Checking for presence via IP address") 27 | addresses = ip_addresses.split(',') 28 | for address in addresses: 29 | logger.debug("IpBasedDetector checking IP: " + address) 30 | test_string = 'bytes from' 31 | results = subprocess.Popen(['ping', '-c1', ping_timeout_switch + ping_timeout_seconds, address], 32 | stdout=subprocess.PIPE, 33 | stderr=subprocess.STDOUT).stdout.readlines() 34 | logger.info("Nmap result %s", results) 35 | for result in results: 36 | if test_string in result: 37 | logger.info('IpBasedDetector: IP detected - someone is home') 38 | return False 39 | logger.info('IpBasedDetector: IP inactive - nobody is home - system is active') 40 | return True 41 | -------------------------------------------------------------------------------- /utils/startup-script/ssdp: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ### BEGIN INIT INFO 4 | # Provides: myservice 5 | # Required-Start: $remote_fs $syslog 6 | # Required-Stop: $remote_fs $syslog 7 | # Default-Start: 2 3 4 5 8 | # Default-Stop: 0 1 6 9 | # Short-Description: SSDP Service 10 | # Description: "Provides a SSDP service to handle service discovery and status updates" 11 | ### END INIT INFO 12 | 13 | # Change the next 3 lines to suit where you install your script and what you want to call it 14 | DIR=/home/pi/motion-notify/utils 15 | DAEMON=$DIR/ssdp_server.py 16 | DAEMON_NAME=ssdp 17 | 18 | # Add any command line options for your daemon here 19 | DAEMON_OPTS="" 20 | 21 | # This next line determines what user the script runs as. 22 | # Root generally not recommended but necessary if you are using the Raspberry Pi GPIO from Python. 23 | DAEMON_USER=root 24 | 25 | # The process ID of the script when it runs is stored here: 26 | PIDFILE=/var/run/$DAEMON_NAME.pid 27 | 28 | . /lib/lsb/init-functions 29 | 30 | do_start () { 31 | log_daemon_msg "Starting system $DAEMON_NAME daemon" 32 | start-stop-daemon --start --background --pidfile $PIDFILE --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON -- $DAEMON_OPTS 33 | log_end_msg $? 34 | } 35 | do_stop () { 36 | log_daemon_msg "Stopping system $DAEMON_NAME daemon" 37 | start-stop-daemon --stop --pidfile $PIDFILE --retry 10 38 | log_end_msg $? 39 | } 40 | 41 | case "$1" in 42 | 43 | start|stop) 44 | do_${1} 45 | ;; 46 | 47 | restart|reload|force-reload) 48 | do_stop 49 | do_start 50 | ;; 51 | 52 | status) 53 | status_of_proc "$DAEMON_NAME" "$DAEMON" && exit 0 || exit $? 54 | ;; 55 | 56 | *) 57 | echo "Usage: /etc/init.d/$DAEMON_NAME {start|stop|restart|status}" 58 | exit 1 59 | ;; 60 | 61 | esac 62 | exit 0 63 | -------------------------------------------------------------------------------- /objects/config.py: -------------------------------------------------------------------------------- 1 | __author__ = 'adean' 2 | 3 | import ConfigParser 4 | from objects import event_action as event_action_mod 5 | from objects import detector_rules as detector_rules_mod 6 | 7 | 8 | class Config(object): 9 | def __init__(self, config_file): 10 | self.config_obj = ConfigParser.ConfigParser() 11 | self.config_obj.read(config_file) 12 | 13 | self.on_event_start_event_action_list = [] 14 | self.on_picture_save_event_action_list = [] 15 | self.on_movie_end_event_action_list = [] 16 | self.on_cron_trigger_action_list = [] 17 | 18 | self.set_on_event_start_event_action_list(self.config_obj.get('EventActionRules', 'on_event_start')) 19 | self.set_on_picture_save_event_action_list(self.config_obj.get('EventActionRules', 'on_picture_save')) 20 | self.set_on_movie_end_event_action_list(self.config_obj.get('EventActionRules', 'on_movie_end')) 21 | self.set_on_cron_trigger_action_list(self.config_obj.get('EventActionRules', 'on_cron_trigger')) 22 | 23 | self.detector_rule_set = detector_rules_mod.DetectorRuleSet(self.config_obj.get('Detection', 'detector_rules')) 24 | 25 | def set_on_event_start_event_action_list(self, config_entry): 26 | self.on_event_start_event_action_list = self.get_event_actions(config_entry) 27 | 28 | def set_on_picture_save_event_action_list(self, config_entry): 29 | self.on_picture_save_event_action_list = self.get_event_actions(config_entry) 30 | 31 | def set_on_movie_end_event_action_list(self, config_entry): 32 | self.on_movie_end_event_action_list = self.get_event_actions(config_entry) 33 | 34 | def set_on_cron_trigger_action_list(self, config_entry): 35 | self.on_cron_trigger_action_list = self.get_event_actions(config_entry) 36 | 37 | def get_event_actions(self, config_entry): 38 | event_actions = [] 39 | for entry in config_entry.split(','): 40 | elements = entry.split(":") 41 | event_actions.append(event_action_mod.EventAction(elements[0], elements[1])) 42 | return event_actions 43 | -------------------------------------------------------------------------------- /objects/detector_rules.py: -------------------------------------------------------------------------------- 1 | __author__ = 'adean' 2 | 3 | import re 4 | import logging 5 | from utils import utils 6 | 7 | logger = logging.getLogger('MotionNotify') 8 | 9 | 10 | class DetectorRuleSet: 11 | def __init__(self, detector_rules_string): 12 | self.detector_rule_groups = [] 13 | self.set_rule_groups(detector_rules_string) 14 | 15 | def set_rule_groups(self, detector_rules_string): 16 | results = re.findall('\{.*?\}', detector_rules_string) 17 | for result in results: 18 | self.detector_rule_groups.append(DetectorRuleGroup(result)) 19 | 20 | @staticmethod 21 | def get_status_from_detector_group(self, detectors, config): 22 | logger.debug("DetectorRuleSet: Getting status for detector group : get_status_from_detector_group") 23 | for detector in detectors: 24 | logger.debug("DetectorRuleSet: Getting status for detector : " + detector) 25 | klass = utils.Utils.reflect_class_from_classname('detectors', detector) 26 | if klass.detect_presence(config): 27 | logger.debug("DetectorRuleSet: " + detector + " is active") 28 | return True 29 | logger.debug("DetectorRuleSet: " + detector + " is inactive") 30 | return False 31 | 32 | def get_status_for_detector_rule_set(self, config): 33 | logger.debug("DetectorRuleSet: Getting status for detector rule set : get_status_for_detector_rule_set") 34 | is_system_active = False 35 | for detector_groups in self.detector_rule_groups: 36 | is_system_active = is_system_active or DetectorRuleSet.get_status_from_detector_group(self, 37 | detector_groups.detectors, 38 | config) 39 | logger.debug("DetectorRuleSet: System status is : " + is_system_active.__str__()) 40 | return is_system_active 41 | 42 | 43 | class DetectorRuleGroup: 44 | def __init__(self, detectors): 45 | detectors = detectors.strip("{").strip("}").split(",") 46 | self.detectors = detectors 47 | -------------------------------------------------------------------------------- /objects/motion_event.py: -------------------------------------------------------------------------------- 1 | __author__ = 'adean' 2 | 3 | import os 4 | from enums import event_type as event_type_mod 5 | from enums import trigger_rule as trigger_rule_mod 6 | 7 | class MotionEvent(object): 8 | def __init__(self, media_file, event_type, event_time, event_id, file_type): 9 | self.media_file = media_file 10 | self.event_time = event_time 11 | self.event_id = event_id 12 | self.file_type = file_type 13 | self.event_type = event_type 14 | self.upload_url = "" 15 | 16 | def get_event_actions_for_event(self, config): 17 | if self.event_type == event_type_mod.EventType.on_event_start: 18 | return config.on_event_start_event_action_list; 19 | elif self.event_type == event_type_mod.EventType.on_picture_save: 20 | return config.on_picture_save_event_action_list; 21 | elif self.event_type == event_type_mod.EventType.on_movie_end: 22 | return config.on_movie_end_event_action_list; 23 | elif self.event_type == event_type_mod.EventType.on_cron_trigger: 24 | return config.on_cron_trigger_action_list; 25 | 26 | def get_actions_for_event(self, config, is_system_active): 27 | list_of_event_actions = self.get_event_actions_for_event(config); 28 | actions_to_perform = [] 29 | for event_action in list_of_event_actions: 30 | if event_action.trigger_rule == trigger_rule_mod.TriggerRule.always or ( 31 | event_action.trigger_rule == trigger_rule_mod.TriggerRule.if_active and is_system_active): 32 | actions_to_perform.append(event_action.action_name) 33 | return actions_to_perform 34 | 35 | def does_event_require_presence_check(self, config): 36 | list_of_event_actions = self.get_event_actions_for_event(config) 37 | for event_action in list_of_event_actions: 38 | if event_action.trigger_rule == trigger_rule_mod.TriggerRule.if_active: 39 | return True 40 | return False 41 | 42 | def get_mime_type(self): 43 | if self.media_file.endswith(("jpg", "png", "gif", "bmp")): 44 | return "image/" + self.file_type 45 | else: 46 | return "video/" + self.file_type 47 | 48 | def get_upload_filename(self): 49 | return self.event_id.__str__() + "_" + self.event_time.__str__() + os.path.splitext(self.media_file)[ 50 | 1].__str__() 51 | -------------------------------------------------------------------------------- /actions/GoogleDriveCleanupAction.py: -------------------------------------------------------------------------------- 1 | from pydrive.drive import GoogleDrive 2 | import GoogleDriveActionBase 3 | from datetime import datetime, timedelta 4 | 5 | import logging.handlers 6 | 7 | logger = logging.getLogger('MotionNotify') 8 | 9 | class GoogleDriveCleanupAction: 10 | """This action removes MotionNotify files from Google Drive which are older than the retention period in the config file. 11 | Note that this only removes files which have been created with a Google Drive file property of source:MotionNotify 12 | This property was added to the GoogleDriveUploadAction in January 2016 (property is not visible in the Google Drive UI). """ 13 | @staticmethod 14 | def do_event_start_action(config, motion_event): 15 | logger.info("Motionevent_id:" + motion_event.event_id + " GoogleDriveCleanupAction event start") 16 | GoogleDriveCleanupAction.cleanup(config) 17 | 18 | @staticmethod 19 | def do_event_end_action(config, motion_event): 20 | logger.info("Motionevent_id:" + motion_event.event_id + " GoogleDriveCleanupAction event end") 21 | GoogleDriveCleanupAction.cleanup(config) 22 | 23 | @staticmethod 24 | def do_action(config, motion_event): 25 | logger.info("Motionevent_id:" + motion_event.event_id + " GoogleDriveCleanupAction event") 26 | GoogleDriveCleanupAction.cleanup(config) 27 | 28 | @staticmethod 29 | def cleanup(config): 30 | gauth = GoogleDriveActionBase.GoogleDriveActionBase.authenticate(config) 31 | drive = GoogleDrive(gauth) 32 | retain_from_date = datetime.today() - timedelta(days=int(config.config_obj.get('GoogleDriveUploadAction', 'file_retention_days'))) 33 | file_list_len = 1 34 | logger.debug(drive.GetAbout()) 35 | while file_list_len > 0: 36 | file_list = drive.ListFile({'q': "properties has { key='source' and value='MotionNotify' and visibility='PRIVATE'} and modifiedDate<'" 37 | + retain_from_date.strftime("%Y-%m-%d") + "'"}).GetList() 38 | 39 | file_list_len = file_list.__len__() 40 | logger.info("GoogleDriveCleanAction - removing " + file_list_len.__str__() + " files") 41 | 42 | print(file_list.__len__()) 43 | for file1 in file_list: 44 | logger.debug('GoogleDriveUploadAction Removing: title: %s, id: %s, createdDate: %s, parents: %s' % (file1['title'], file1['id'], file1['createdDate'], file1['parents'])) 45 | file1.Delete() -------------------------------------------------------------------------------- /actions/SmtpEmailNotifyAction.py: -------------------------------------------------------------------------------- 1 | __author__ = 'adean' 2 | 3 | import smtplib 4 | import logging 5 | from datetime import datetime 6 | 7 | logger = logging.getLogger('MotionNotify') 8 | 9 | class SmtpEmailNotifyAction: 10 | @staticmethod 11 | def do_event_start_action(config, motion_event_obj): 12 | logger.info("Motionevent_id:" + motion_event_obj.event_id + " SmtpEmailNotifyAction: Sending start event email") 13 | msg = config.config_obj.get('SmtpEmailNotifyAction', 'event_started_message') 14 | msg += '\n\n' + config.config_obj.get('SmtpEmailNotifyAction', 'image_and_video_folder_link') 15 | logger.info("Motionevent_id:" + motion_event_obj.event_id + " SmtpEmailNotifyAction: Initial config success") 16 | SmtpEmailNotifyAction.send_email(config, motion_event_obj, msg) 17 | 18 | @staticmethod 19 | def do_event_end_action(config, motion_event_obj): 20 | logger.info("Motionevent_id:" + motion_event_obj.event_id + " Sending event end email") 21 | msg = config.config_obj.get('SmtpEmailNotifyAction', 'movie_end_message') 22 | msg += '\n\n' + motion_event_obj.upload_url 23 | logger.info("Motionevent_id:" + motion_event_obj.event_id + " SmtpEmailNotifyAction: Initial config success") 24 | SmtpEmailNotifyAction.send_email(config, motion_event_obj, msg) 25 | 26 | @staticmethod 27 | def do_action(config, motion_event_obj): 28 | logger.info("Motionevent_id:" + motion_event_obj.event_id + " Sending email") 29 | SmtpEmailNotifyAction.send_email(config, motion_event_obj, "") 30 | 31 | @staticmethod 32 | def send_email(config, motion_event_obj, msg): 33 | # SMTP account credentials 34 | username = config.config_obj.get('SmtpEmailNotifyAction', 'user') 35 | password = config.config_obj.get('SmtpEmailNotifyAction', 'password') 36 | from_name = config.config_obj.get('SmtpEmailNotifyAction', 'name') 37 | sender = config.config_obj.get('SmtpEmailNotifyAction', 'sender') 38 | 39 | # Recipient email address (could be same as from_addr) 40 | recipient = config.config_obj.get('SmtpEmailNotifyAction', 'recipient') 41 | 42 | # Subject line for email 43 | subject = config.config_obj.get('SmtpEmailNotifyAction', 'subject') 44 | 45 | logger.info("Motionevent_id:" + motion_event_obj.event_id + " SmtpEmailNotifyAction: Full config success") 46 | 47 | senddate = datetime.strftime(datetime.now(), '%Y-%m-%d') 48 | m = "Date: %s\r\nFrom: %s <%s>\r\nTo: %s\r\nSubject: %s\r\nX-Mailer: My-Mail\r\n\r\n" % ( 49 | senddate, from_name, sender, recipient, subject) 50 | server = smtplib.SMTP('smtp.gmail.com:587') 51 | server.starttls() 52 | server.login(username, password) 53 | server.sendmail(sender, recipient, m + msg) 54 | server.quit() 55 | logger.info("Motionevent_id:" + motion_event_obj.event_id + " Email sent") 56 | -------------------------------------------------------------------------------- /motion-notify.cfg: -------------------------------------------------------------------------------- 1 | [SmtpEmailNotifyAction] 2 | # GMail account credentials 3 | name = Your Name 4 | user = youremail@gmail.com 5 | password = yourpassword 6 | sender = youremail@gmail.com 7 | # Recipient email address (could be same as from_addr) 8 | recipient = youremail@provider.com 9 | # Subject line for email 10 | subject = Motion detected 11 | # First line of email message 12 | movie_end_message = Video uploaded 13 | event_started_message = An event has been detected and is being captured. Images are being uploaded to Google Drive. 14 | # The link to the Google Drive folder that will be included in the email at the start of the event. This is only used in the email - it doesn't affect the upload 15 | image_and_video_folder_link = https://drive.google.com/a/xxxxxxxxxxx.com/folderview?id=xxxxxxxxxxxxxxxxx&usp=sharing 16 | 17 | [GoogleDriveUploadAction] 18 | # The folder in Google drive to upload to (from the URL eg. https://drive.google.com/drive/folders/0Bzt4FJyYHjdYhnA3cECdTFW3RWM 19 | folder_name = MOTION 20 | folder = 0Bzt4FJyYHjdYhnA3cECdTFW3RWM 21 | #key file - this is the p12 key file downloaded from developers.google.com 22 | key_file = /etc/motion-notify/creds.p12 23 | service_user_email = xxxxxxxxxxxxxxxxxxxxxxxxxxx@developer.gserviceaccount.com 24 | dateformat = %Y-%m-%d 25 | #The email of the user who will own the file / folder in Google Drive 26 | owner = owner@gmail.com 27 | #Users who need write access to the daily folders in Google Drive (typically just the owner above) 28 | write_users = youremail@gmail.com,writer@gmail.com 29 | #Users who need read access to the daily folders in Google Drive (anyone with whom you want to share the folder - typically this is the SmtpEmailNotifyAction.recipient above) 30 | read_users = reader@gmail.com 31 | # Boolean to enable locking to syncronise uploads so only one is in progress at a time 32 | # Useful when running with limited bandwidth 33 | mutex-enabled = true 34 | file_retention_days=80 35 | 36 | [UrlInvokeAction] 37 | event_start_url=http://localhost:8080/status/status-active 38 | movie_end_url=http://localhost:8080/status/status-inactive 39 | 40 | [TimeBasedDetector] 41 | # Specify times during which the system is always active 42 | time_ranges = 01:00-07:00,12:00-13:00 43 | 44 | [ArpBasedDetector] 45 | # Network to monitor (used by MAC address detection) 46 | network = 192.168.1.0:255.255.255.0 47 | # MAC addresses (comma separated) of the devices to detect home presence and disable emails (e.g., phones) - these will be ignored if you specify IP addresses below 48 | presence_macs = XX:XX:XX:XX:XX,YY:YY:YY:YY:YY 49 | 50 | [IpBasedDetector] 51 | #Space separated list of IP addresses for detection. If these are present on the network the system is inactive. Setup a static IP on your router to keep your IP constant 52 | #The MAC address above will be ignored if you configure an IP here 53 | ip_addresses = 127.0.0.1 54 | #If no ping response is received from each specified IP within this number of seconds the ping command will exit and assume the device is not online 55 | ping_timeout_seconds = 2 56 | #The switch for timeout arguments aren't consistent between different OS's. BSD based is -t, Debian based is -w 57 | ping_timeout_switch = -w 58 | 59 | [Detection] 60 | #List the detectors to be used in order. Group detectors together if you want them all to be checked in order for the system to be considered active. 61 | #eg1. {TimeBasedDetctor,IPBasedDetector},{ArpBasedDetctor} - system is active if both the TimeBasedDetector and IPBasedDetector are active or the ArpBasedDetector is active 62 | #eg2. {TimeBasedDetector},{IpBasedDetector} - system is active if either the TimeBasedDetector or IpBasedDetector are active. IpBasedDetector will only be called if TimeBasedDetector is inactive 63 | detector_rules = {TimeBasedDetector},{IpBasedDetector} 64 | 65 | [EventActionRules] 66 | #List the actions that should be carried out when an event occurs. Format is ActionClass:always or if_active. When using if_active actions will only be called if the detectors indicate that the system is active 67 | on_event_start = SmtpEmailNotifyAction:if_active,UrlInvokeAction:always 68 | on_picture_save = GoogleDriveUploadAction:always,DeleteMediaFileAction:always 69 | on_movie_end = GoogleDriveUploadAction:always,SmtpEmailNotifyAction:if_active,DeleteMediaFileAction:always,UrlInvokeAction:always 70 | on_cron_trigger = GoogleDriveCleanupAction:always -------------------------------------------------------------------------------- /motion-notify.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2 2 | ''' 3 | Created on 18th July 2015 4 | 5 | @author: Andrew Dean 6 | 7 | Motion Notify v1.0 - uploads images and video to Google Drive and sends notification via email. 8 | Detects whether someone is home by checking the local network for an IP address or MAC address and only sends email if nobody is home. 9 | Allows hours to be defined when the system will be active regardless of network presence. 10 | 11 | Sends an email to the user at that start of an event and uploads images throughout the event. 12 | At the end of an event the video is uploaded to Google Drive and a link is emailed to the user. 13 | Files are deleted once they are uploaded. 14 | 15 | Originally based on the Google Drive uploader developed by Jeremy Blythe (http://jeremyblythe.blogspot.com) and pypymotion (https://github.com/7AC/pypymotion) by Wayne Dyck 16 | ''' 17 | 18 | # This file is part of Motion Notify. 19 | # 20 | # Motion Notify is free software: you can redistribute it and/or modify 21 | # it under the terms of the GNU General Public License as published by 22 | # the Free Software Foundation, either version 3 of the License, or 23 | # (at your option) any later version. 24 | # 25 | # Motion Notify is distributed in the hope that it will be useful, 26 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 27 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 28 | # GNU General Public License for more details. 29 | # 30 | # You should have received a copy of the GNU General Public License 31 | # along with Motion Notify. If not, see . 32 | 33 | import os.path 34 | import sys 35 | import logging.handlers 36 | import traceback 37 | from objects import motion_event as motion_event_mod 38 | from objects import config as config_mod 39 | from objects.enums import event_type as event_type_mod 40 | from utils import utils as utils_mod 41 | 42 | logger = logging.getLogger('MotionNotify') 43 | hdlr = logging.handlers.RotatingFileHandler('/var/tmp/motion-notify.log', 44 | maxBytes=1048576, 45 | backupCount=3) 46 | formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') 47 | hdlr.setFormatter(formatter) 48 | logger.addHandler(hdlr) 49 | logger.setLevel(logging.DEBUG) 50 | 51 | 52 | def loggerExceptHook(t, v, tb): 53 | logger.error(traceback.format_exception(t, v, tb)) 54 | 55 | 56 | sys.excepthook = loggerExceptHook 57 | 58 | 59 | class MotionNotify: 60 | def handle_event(self): 61 | logger.debug("Handling Event Actions...") 62 | # Only carry out the presence checks if needed 63 | if self.motion_event_obj.does_event_require_presence_check(self.config_obj): 64 | self.is_system_active = self.config_obj.detector_rule_set.get_status_for_detector_rule_set(self.config_obj) 65 | else: 66 | self.is_system_active = True 67 | 68 | actions_for_event = self.motion_event_obj.get_actions_for_event(self.config_obj, self.is_system_active) 69 | for action in actions_for_event: 70 | logger.info( 71 | "Handling action: " + action + " for event ID " + self.motion_event_obj.event_id + " with event_type " + self.motion_event_obj.event_type.__str__()) 72 | klass = utils_mod.Utils.reflect_class_from_classname('actions', action) 73 | if self.motion_event_obj.event_type == event_type_mod.EventType.on_event_start: 74 | klass.do_event_start_action(self.config_obj, motion_event_obj) 75 | elif self.motion_event_obj.event_type == event_type_mod.EventType.on_picture_save or self.motion_event_obj.event_type == event_type_mod.EventType.on_cron_trigger: 76 | klass.do_action(self.config_obj, motion_event_obj) 77 | elif self.motion_event_obj.event_type == event_type_mod.EventType.on_movie_end: 78 | klass.do_event_end_action(self.config_obj, motion_event_obj) 79 | logger.debug("All events actions handled...") 80 | 81 | def __init__(self, config_obj, motion_event_obj): 82 | logger.debug("Initializing...") 83 | self.config_obj = config_obj 84 | self.motion_event_obj = motion_event_obj 85 | self.is_system_active = False 86 | self.handle_event() 87 | 88 | 89 | if __name__ == '__main__': 90 | logger.info("Motion Notify script started") 91 | try: 92 | 93 | if len(sys.argv) < 6: 94 | exit( 95 | 'Motion Notify - Usage: motion-notify.py {config-file-path} {media-file-path} {event-type on_event_start, on_picture_save, on_movie_end or on_cron_trigger} {timestamp} {event_id} {file_type} ') 96 | 97 | cfg_path = sys.argv[1] 98 | if not os.path.exists(cfg_path): 99 | exit('Config file does not exist [%s]' % cfg_path) 100 | 101 | motion_event_obj = motion_event_mod.MotionEvent(sys.argv[2], event_type_mod.EventType[sys.argv[3]], sys.argv[4], 102 | sys.argv[5], 103 | sys.argv[6]) 104 | 105 | MotionNotify(config_mod.Config(cfg_path), motion_event_obj) 106 | except Exception as e: 107 | logger.error("Initialization error..." + e.__str__()) 108 | exit('Error: [%s]' % e) 109 | -------------------------------------------------------------------------------- /actions/GoogleDriveUploadAction.py: -------------------------------------------------------------------------------- 1 | from pydrive.drive import GoogleDrive 2 | 3 | import logging.handlers 4 | from datetime import datetime 5 | import fcntl, os 6 | import sys 7 | import GoogleDriveActionBase 8 | 9 | LOCK_FILENAME = "/var/tmp/motion-notify.lock.pid" 10 | logger = logging.getLogger('MotionNotify') 11 | 12 | 13 | class GoogleDriveUploadAction: 14 | @staticmethod 15 | def do_event_start_action(config, motion_event): 16 | logger.info("Motionevent_id:" + motion_event.event_id + " GoogleDriveUploadAction event start") 17 | motion_event.upload_url = GoogleDriveUploadAction.upload(config, motion_event) 18 | 19 | @staticmethod 20 | def do_event_end_action(config, motion_event): 21 | logger.info("Motionevent_id:" + motion_event.event_id + " GoogleDriveUploadAction event end") 22 | motion_event.upload_url = GoogleDriveUploadAction.upload(config, motion_event) 23 | 24 | @staticmethod 25 | def do_action(config, motion_event): 26 | logger.info("Motionevent_id:" + motion_event.event_id + " GoogleDriveUploadAction event") 27 | motion_event.upload_url = GoogleDriveUploadAction.upload(config, motion_event) 28 | 29 | @staticmethod 30 | def _get_folder_resource(drive, folder_name, folder_id): 31 | """Find and return the resource whose title matches the given folder.""" 32 | try: 33 | myfile = drive.CreateFile({'id': folder_id}) 34 | logger.debug("Found Parent Folder title: {}, mimeType: {}".format(myfile['title'], myfile['mimeType'])) 35 | return myfile 36 | 37 | # file_list = drive.ListFile({'q': "title='{}' and mimeType contains 'application/vnd.google-apps.folder' and trashed=false".format(folder_name)}).GetList() 38 | # if len(file_list) > 0: 39 | # return file_list[0] 40 | # else: 41 | # return None 42 | except IndexError: 43 | return None 44 | except: 45 | return None 46 | 47 | @staticmethod 48 | def _get_datefolder_resource(drive, formatted_date, parent_id): 49 | """Find and return the resource whose title matches the given folder.""" 50 | try: 51 | file_list = drive.ListFile({ 52 | 'q': "title='{}' and '{}' in parents and mimeType contains 'application/vnd.google-apps.folder' and trashed=false".format( 53 | formatted_date, parent_id)}).GetList() 54 | if len(file_list) > 0: 55 | return file_list[0] 56 | else: 57 | return None 58 | except: 59 | return None 60 | 61 | @staticmethod 62 | def create_permision(user, role): 63 | return { 64 | 'value': user, 65 | 'type': "user", 66 | 'role': role 67 | } 68 | 69 | @staticmethod 70 | def create_subfolder(drive, folder, sfldname, owner, readers, writers): 71 | new_folder = drive.CreateFile({'title': '{}'.format(sfldname), 72 | 'mimeType': 'application/vnd.google-apps.folder'}) 73 | if folder is not None: 74 | new_folder['parents'] = [{"kind": "drive#fileLink", "id": folder['id']}] 75 | 76 | permissions = [] 77 | owner_permission = GoogleDriveUploadAction.create_permision(owner, "owner") 78 | write_permissions = [GoogleDriveUploadAction.create_permision(x, "owner") for x in writers] 79 | read_permissions = [GoogleDriveUploadAction.create_permision(x, "reader") for x in readers] 80 | 81 | permissions.append(owner_permission) 82 | permissions.extend(write_permissions) 83 | permissions.extend(read_permissions) 84 | 85 | logger.debug('Creating Folder {} with permissions {}'.format(sfldname, permissions)) 86 | 87 | new_folder["permissions"] = permissions 88 | new_folder.Upload() 89 | return new_folder 90 | 91 | @staticmethod 92 | def upload(config, motion_event): 93 | logger.debug("GoogleDriveUploadAction starting upload") 94 | mutex_enabled = config.config_obj.get('GoogleDriveUploadAction', 'mutex-enabled') 95 | if mutex_enabled: 96 | f = GoogleDriveUploadAction.lock(motion_event.media_file) 97 | 98 | drive = GoogleDriveUploadAction.setup_drive(config) 99 | 100 | datefolder_resource = GoogleDriveUploadAction.create_folder(drive, config) 101 | 102 | # Create File in Date Folder 103 | gfile = GoogleDriveUploadAction.upload_file(drive, motion_event, datefolder_resource) 104 | 105 | logger.debug( 106 | "Motionevent_id:" + motion_event.event_id + ' GoogleDriveUploadAction Uploaded File {} {}'.format( 107 | motion_event.get_upload_filename(), 108 | gfile[ 109 | 'id'])) 110 | 111 | if mutex_enabled: 112 | GoogleDriveUploadAction.unlock(f, motion_event.media_file) 113 | 114 | return 'https://drive.google.com/file/d/' + gfile['id'] + '/view?usp=sharing' 115 | 116 | @staticmethod 117 | def setup_drive(config): 118 | gauth = GoogleDriveActionBase.GoogleDriveActionBase.authenticate(config) 119 | return GoogleDrive(gauth) 120 | 121 | @staticmethod 122 | def upload_file(drive, motion_event, folder): 123 | # Create File in Date Folder 124 | gfile = drive.CreateFile({'title': motion_event.get_upload_filename(), 'mimeType': motion_event.get_mime_type(), 125 | "parents": [{"kind": "drive#fileLink", "id": folder['id']}], 126 | "properties": [{"key": "source", "value": "MotionNotify"}]}) 127 | gfile.SetContentFile(motion_event.media_file) 128 | gfile.Upload() 129 | return gfile 130 | 131 | @staticmethod 132 | def create_folder(drive, config): 133 | folder_name = config.config_obj.get('GoogleDriveUploadAction', 'folder_name') 134 | folder_id = config.config_obj.get('GoogleDriveUploadAction', 'folder') 135 | 136 | # Get Permissions 137 | owner = config.config_obj.get('GoogleDriveUploadAction', 'owner') 138 | writers = filter(lambda x: len(x) > 0, [x.strip() for x in 139 | config.config_obj.get('GoogleDriveUploadAction', 'write_users').split( 140 | ",")]) 141 | readers = filter(lambda x: len(x) > 0, 142 | [x.strip() for x in config.config_obj.get('GoogleDriveUploadAction', 'read_users').split(",")]) 143 | # Keep Owner Seperate # Remove Just in case 144 | if owner in writers: 145 | writers.remove(owner); 146 | # Check Root Folder Exists 147 | folder_resource = GoogleDriveUploadAction._get_folder_resource(drive, folder_name, folder_id) 148 | if not folder_resource: 149 | # logger.info('Creating Folder {}'.format(folder_name)) 150 | # folder_resource = MotionNotifyGoogleUpload.create_subfolder(drive,None,folder_name,owner,readers, writers) 151 | # 152 | # This seemed to create lots of folders I'd no access to, so changing to an Exception 153 | # Might need some way to set it to the root of the gmail user and not the service user 154 | 155 | logger.error("Could not find the {} folder {}".format(folder_name, folder_id)) 156 | raise Exception("Could not find the {} folder {}".format(folder_name, folder_id)) 157 | 158 | logger.debug('Using Folder {} {}'.format(folder_name, folder_resource['id'])) 159 | 160 | # Check Date Folder Exists & Create / Use as needed 161 | senddate = (datetime.strftime(datetime.now(), config.config_obj.get('GoogleDriveUploadAction', 'dateformat'))) 162 | datefolder_resource = GoogleDriveUploadAction._get_datefolder_resource(drive, senddate, folder_resource['id']) 163 | if not datefolder_resource: 164 | logger.info('Creating Date Folder {}'.format(senddate)) 165 | datefolder_resource = GoogleDriveUploadAction.create_subfolder(drive, folder_resource, senddate, owner, 166 | readers, writers) 167 | 168 | logger.debug('Using Date Folder {} {}'.format(senddate, datefolder_resource['id'])) 169 | return datefolder_resource 170 | 171 | @staticmethod 172 | def lock(media_file_path): 173 | pid = str(os.getpid()) 174 | f = open(LOCK_FILENAME, 'w') 175 | logger.debug("Trying to Get Lock for upload of {}, pid {}".format(media_file_path, pid)) 176 | try: 177 | fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB) 178 | except IOError: 179 | logger.warn("Can't get Lock for upload of {}, pid {}, blocking".format(media_file_path, pid)) 180 | fcntl.flock(f, fcntl.LOCK_EX) 181 | logger.debug("Got Lock for upload of {}, pid {}, blocking".format(media_file_path, pid)) 182 | 183 | try: 184 | f.write("%s\n" % pid) 185 | logger.debug("Writing PID for upload of {}, pid {}, to file {}".format(media_file_path, pid, LOCK_FILENAME)) 186 | except IOError: 187 | logger.error( 188 | "Unable to write PID for upload of {}, pid {}, to file {}".format(media_file_path, pid, LOCK_FILENAME)) 189 | logger.error("Aborting") 190 | sys.exit(1) 191 | return f 192 | 193 | @staticmethod 194 | def unlock(f, media_file_path): 195 | fcntl.flock(f, fcntl.LOCK_UN) 196 | if (logger.isEnabledFor(logging.DEBUG)): 197 | logger.debug("Unlock for upload of {}, pid {}, blocking".format(media_file_path, os.getpid())) 198 | -------------------------------------------------------------------------------- /tests/test_module.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import sys 4 | from uuid import UUID 5 | import uuid 6 | 7 | 8 | from actions.DeleteMediaFileAction import DeleteMediaFileAction 9 | 10 | __author__ = 'adean' 11 | 12 | import unittest 13 | import logging.handlers 14 | from datetime import time 15 | from objects import config as config_mod 16 | from objects import motion_event as motion_event_mod 17 | from objects.enums import event_type as event_type_mod 18 | from objects.enums import trigger_rule as trigger_rule_mod 19 | from utils import utils as utils_mod 20 | from actions import GoogleDriveUploadAction as google_drive_upload_action_mod 21 | from actions import GoogleDriveCleanupAction as google_drive_cleanup_action_mod 22 | from detectors import TimeBasedDetector as time_based_detector_mod 23 | from objects.detector_rules import DetectorRuleSet as detector_rule_set_mod 24 | 25 | 26 | class MotionNotifyTestSuite(unittest.TestCase): 27 | def setUp(self): 28 | self.logger = logging.getLogger('MotionNotify') 29 | if not self.logger.handlers: 30 | self.logger.level = logging.DEBUG 31 | self.logger.addHandler(logging.StreamHandler(sys.stdout)) 32 | 33 | self.config = config_mod.Config("../motion-notify-test.cfg") 34 | self.config.set_on_event_start_event_action_list("SmtpEmailNotifyAction:if_active") 35 | self.config.set_on_picture_save_event_action_list( 36 | "GoogleDriveUploadAction:always,SmtpEmailNotifyAction:if_active") 37 | self.config.set_on_movie_end_event_action_list("SmtpEmailNotifyAction:if_active") 38 | self.config.set_on_cron_trigger_action_list("GoogleDriveCleanupAction:always") 39 | pass 40 | 41 | def test_get_event_action_from_config_entry(self): 42 | event_actions = self.config.on_event_start_event_action_list 43 | self.assertEqual(1, event_actions.__len__()) 44 | self.assertEqual(event_actions[0].action_name, "SmtpEmailNotifyAction") 45 | self.assertEqual(event_actions[0].trigger_rule, trigger_rule_mod.TriggerRule.if_active) 46 | 47 | event_actions = self.config.on_picture_save_event_action_list 48 | self.assertEqual(2, event_actions.__len__()) 49 | self.assertEqual(event_actions[0].action_name, "GoogleDriveUploadAction") 50 | self.assertEqual(event_actions[0].trigger_rule, trigger_rule_mod.TriggerRule.always) 51 | self.assertEqual(event_actions[1].action_name, "SmtpEmailNotifyAction") 52 | self.assertEqual(event_actions[1].trigger_rule, trigger_rule_mod.TriggerRule.if_active) 53 | 54 | event_actions = self.config.on_cron_trigger_action_list 55 | self.assertEqual(1, event_actions.__len__()) 56 | self.assertEqual(event_actions[0].action_name, "GoogleDriveCleanupAction") 57 | self.assertEqual(event_actions[0].trigger_rule, trigger_rule_mod.TriggerRule.always) 58 | 59 | def test_motion_test_event_get_actions_for_event(self): 60 | motion_test_event = motion_event_mod.MotionEvent('', event_type_mod.EventType.on_event_start, 1234567890, 11, 61 | 'jpg') 62 | event_actions = motion_test_event.get_event_actions_for_event(self.config) 63 | self.assertEqual(1, event_actions.__len__()) 64 | 65 | motion_test_event = motion_event_mod.MotionEvent('', event_type_mod.EventType.on_picture_save, 1234567890, 11, 66 | 'jpg') 67 | event_actions = motion_test_event.get_event_actions_for_event(self.config) 68 | self.assertEqual(2, event_actions.__len__()) 69 | 70 | motion_test_event = motion_event_mod.MotionEvent('', event_type_mod.EventType.on_cron_trigger, 1234567890, 11, 71 | 'jpg') 72 | event_actions = motion_test_event.get_event_actions_for_event(self.config) 73 | self.assertEqual(1, event_actions.__len__()) 74 | 75 | def test_get_actions_for_event(self): 76 | motion_test_event = motion_event_mod.MotionEvent('', event_type_mod.EventType.on_event_start, 1234567890, 11, 77 | 'jpg') 78 | list_of_actions = motion_test_event.get_actions_for_event(self.config, True) 79 | self.assertEqual(1, list_of_actions.__len__()) 80 | self.assertIn("SmtpEmailNotifyAction", list_of_actions) 81 | 82 | motion_test_event = motion_event_mod.MotionEvent('', event_type_mod.EventType.on_picture_save, 1234567890, 11, 83 | 'jpg') 84 | list_of_actions = motion_test_event.get_actions_for_event(self.config, True) 85 | self.assertEqual(2, list_of_actions.__len__()) 86 | self.assertIn("GoogleDriveUploadAction", list_of_actions) 87 | self.assertIn("SmtpEmailNotifyAction", list_of_actions) 88 | 89 | motion_test_event = motion_event_mod.MotionEvent('', event_type_mod.EventType.on_movie_end, 1234567890, 11, 90 | 'jpg') 91 | list_of_actions = motion_test_event.get_actions_for_event(self.config, True) 92 | self.assertEqual(1, list_of_actions.__len__()) 93 | self.assertIn("SmtpEmailNotifyAction", list_of_actions) 94 | 95 | # test that if the system is inactive "if_active" events are not triggered 96 | motion_test_event = motion_event_mod.MotionEvent('', event_type_mod.EventType.on_picture_save, 1234567890, 11, 97 | 'jpg') 98 | list_of_actions = motion_test_event.get_actions_for_event(self.config, False) 99 | self.assertEqual(1, list_of_actions.__len__()) 100 | self.assertIn("GoogleDriveUploadAction", list_of_actions) 101 | 102 | def test_reflection(self): 103 | klass = utils_mod.Utils.reflect_class_from_classname('actions', 'GoogleDriveUploadAction') 104 | self.assertIsInstance(klass, google_drive_upload_action_mod.GoogleDriveUploadAction) 105 | 106 | def test_time_based_detector_check_time_ranges(self): 107 | time_ranges = time_based_detector_mod.TimeBasedDetector.get_time_ranges("01:00-07:00,12:00-13:00") 108 | self.assertTrue(time_based_detector_mod.TimeBasedDetector.check_time_ranges(time_ranges, time(05, 12))) 109 | self.assertTrue(time_based_detector_mod.TimeBasedDetector.check_time_ranges(time_ranges, time(12, 12))) 110 | self.assertFalse( 111 | time_based_detector_mod.TimeBasedDetector.check_time_ranges(time_ranges, time(18, 12))) 112 | 113 | def test_detector_rules_get_rule_groups(self): 114 | detector_rule_set = detector_rule_set_mod("{TimeBasedDetector,IPBasedDetector}{ArpBasedDetector}") 115 | self.assertEqual(2, detector_rule_set.detector_rule_groups.__len__()) 116 | self.assertEqual("TimeBasedDetector", detector_rule_set.detector_rule_groups[0].detectors[0]) 117 | self.assertEqual("IPBasedDetector", detector_rule_set.detector_rule_groups[0].detectors[1]) 118 | self.assertEqual("ArpBasedDetector", detector_rule_set.detector_rule_groups[1].detectors[0]) 119 | detector_rule_set = detector_rule_set_mod("{ArpBasedDetector}") 120 | self.assertEqual("ArpBasedDetector", detector_rule_set.detector_rule_groups[0].detectors[0]) 121 | 122 | def test_get_status_from_detector_group(self): 123 | result = self.config.detector_rule_set.get_status_for_detector_rule_set(self.config) 124 | # IpBasedDetector is looking for 127.0.0.1 so this always shows that the system is inactive as someone is home 125 | self.assertFalse(result) 126 | 127 | def test_delete_media_file_action(self): 128 | motion_test_event = motion_event_mod.MotionEvent('/tmp/' + uuid.uuid1().__str__(), 129 | event_type_mod.EventType.on_event_start, 1234567890, 11, 'jpg') 130 | 131 | text_file = open(motion_test_event.media_file, "w") 132 | text_file.write("output") 133 | text_file.close() 134 | self.assertTrue(os.path.isfile(motion_test_event.media_file)) 135 | 136 | DeleteMediaFileAction.do_action(self.config, motion_test_event) 137 | self.assertFalse(os.path.isfile(motion_test_event.media_file)) 138 | 139 | def test_motion_event_get_upload_filename(self): 140 | motion_test_event = motion_event_mod.MotionEvent('/tmp/' + uuid.uuid1().__str__() + '.jpg', 141 | event_type_mod.EventType.on_event_start, 1234567890, 11, 'jpg') 142 | self.assertEqual(motion_test_event.get_upload_filename(), 143 | motion_test_event.event_id.__str__() + "_" + motion_test_event.event_time.__str__() + ".jpg") 144 | 145 | def test_motion_event_get_mime(self): 146 | motion_test_event = motion_event_mod.MotionEvent('/tmp/' + uuid.uuid1().__str__() + '.jpg', 147 | event_type_mod.EventType.on_event_start, 1234567890, 11, 'jpg') 148 | self.assertEqual(motion_test_event.get_mime_type(), "image/jpg") 149 | 150 | def test_upload_file(self): 151 | motion_test_event = motion_event_mod.MotionEvent('../resources/test.jpg', 152 | event_type_mod.EventType.on_event_start, random.random(), 11, 'jpg') 153 | drive = google_drive_upload_action_mod.GoogleDriveUploadAction.setup_drive(self.config) 154 | folder = google_drive_upload_action_mod.GoogleDriveUploadAction.create_folder(drive, self.config) 155 | gfile = google_drive_upload_action_mod.GoogleDriveUploadAction.upload_file(drive, motion_test_event, folder) 156 | print gfile['title'] 157 | list = drive.ListFile({'q': "title = '" + gfile['title'] + "' and '" + folder['id'] + "' in parents" }).GetList() 158 | self.assertEquals(list.__len__(), 1) 159 | 160 | 161 | 162 | if __name__ == '__main__': 163 | unittest.main() 164 | -------------------------------------------------------------------------------- /utils/ssdp_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.7 2 | 3 | """ Computer Vision Camera for SmartThings 4 | 5 | Copyright 2016 Juan Pablo Risso 6 | Modified from code found here: https://www.hackster.io/juano2310/computer-vision-as-motion-sensor-for-smartthings-803341 7 | 8 | Dependencies: python-twisted, cv2, pyimagesearch 9 | 10 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 11 | this file except in compliance with the License. You may obtain a copy of the 12 | License at: 13 | 14 | http://www.apache.org/licenses/LICENSE-2.0 15 | 16 | Unless required by applicable law or agreed to in writing, software distributed 17 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 18 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 19 | specific language governing permissions and limitations under the License. 20 | """ 21 | 22 | import argparse 23 | import logging.handlers 24 | import requests 25 | 26 | from time import time 27 | from twisted.web import server, resource 28 | from twisted.internet import reactor 29 | from twisted.internet.defer import succeed 30 | from twisted.internet.protocol import DatagramProtocol 31 | from twisted.web.iweb import IBodyProducer 32 | from twisted.web._newclient import ResponseFailed 33 | from zope.interface import implements 34 | 35 | logger = logging.getLogger('MotionNotifySSDP') 36 | hdlr = logging.handlers.RotatingFileHandler('/var/tmp/motion-notify-ssdp.log', 37 | maxBytes=1048576, 38 | backupCount=3) 39 | formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') 40 | hdlr.setFormatter(formatter) 41 | logger.addHandler(hdlr) 42 | logger.setLevel(logging.INFO) 43 | 44 | SSDP_PORT = 1900 45 | SSDP_ADDR = '239.255.255.250' 46 | UUID = 'b02c7058-fd87-4397-b8eb-96d64eb912f8' 47 | SEARCH_RESPONSE = 'HTTP/1.1 200 OK\r\nCACHE-CONTROL:max-age=30\r\nEXT:\r\nLOCATION:%s\r\nSERVER:Linux, UPnP/1.0, Pi_Garage/1.0\r\nST:%s\r\nUSN:uuid:%s::%s' 48 | 49 | def determine_ip_for_host(host): 50 | """Determine local IP address used to communicate with a particular host""" 51 | test_sock = DatagramProtocol() 52 | test_sock_listener = reactor.listenUDP(0, test_sock) # pylint: disable=no-member 53 | test_sock.transport.connect(host, 1900) 54 | my_ip = test_sock.transport.getHost().host 55 | test_sock_listener.stopListening() 56 | return my_ip 57 | 58 | class SSDPServer(DatagramProtocol): 59 | """Receive and response to M-SEARCH discovery requests from SmartThings hub""" 60 | 61 | def __init__(self, interface='', status_port=0, device_target=''): 62 | self.interface = interface 63 | self.device_target = device_target 64 | self.status_port = status_port 65 | self.port = reactor.listenMulticast(SSDP_PORT, self, listenMultiple=True) # pylint: disable=no-member 66 | self.port.joinGroup(SSDP_ADDR, interface=interface) 67 | reactor.addSystemEventTrigger('before', 'shutdown', self.stop) # pylint: disable=no-member 68 | 69 | def datagramReceived(self, data, (host, port)): 70 | try: 71 | header, _ = data.split('\r\n\r\n')[:2] 72 | except ValueError: 73 | return 74 | lines = header.split('\r\n') 75 | cmd = lines.pop(0).split(' ') 76 | lines = [x.replace(': ', ':', 1) for x in lines] 77 | lines = [x for x in lines if len(x) > 0] 78 | headers = [x.split(':', 1) for x in lines] 79 | headers = dict([(x[0].lower(), x[1]) for x in headers]) 80 | 81 | logger.debug('SSDP command %s %s - from %s:%d with headers %s', cmd[0], cmd[1], host, port, headers) 82 | 83 | search_target = '' 84 | if 'st' in headers: 85 | search_target = headers['st'] 86 | 87 | if cmd[0] == 'M-SEARCH' and cmd[1] == '*' and search_target in self.device_target: 88 | logger.info('Received %s %s for %s from %s:%d', cmd[0], cmd[1], search_target, host, port) 89 | url = 'http://%s:%d/status' % (determine_ip_for_host(host), self.status_port) 90 | response = SEARCH_RESPONSE % (url, search_target, UUID, self.device_target) 91 | self.port.write(response, (host, port)) 92 | else: 93 | logger.debug('Ignored SSDP command %s %s', cmd[0], cmd[1]) 94 | 95 | def stop(self): 96 | """Leave multicast group and stop listening""" 97 | self.port.leaveGroup(SSDP_ADDR, interface=self.interface) 98 | self.port.stopListening() 99 | 100 | class StatusServer(resource.Resource): 101 | """HTTP server that serves the status of the camera to the 102 | SmartThings hub""" 103 | isLeaf = True 104 | 105 | def __init__(self, device_target, subscription_list, garage_door_status): 106 | self.device_target = device_target 107 | self.subscription_list = subscription_list 108 | self.garage_door_status = garage_door_status 109 | resource.Resource.__init__(self) 110 | 111 | def render_SUBSCRIBE(self, request): # pylint: disable=invalid-name 112 | """Handle subscribe requests from ST hub - hub wants to be notified of 113 | garage door status updates""" 114 | headers = request.getAllHeaders() 115 | logger.debug("SUBSCRIBE: %s", headers) 116 | if 'callback' in headers: 117 | cb_url = headers['callback'][1:-1] 118 | 119 | if not cb_url in self.subscription_list: 120 | self.subscription_list[cb_url] = {} 121 | #reactor.stop() 122 | logger.info('Added subscription %s', cb_url) 123 | self.subscription_list[cb_url]['expiration'] = time() + 24 * 3600 124 | 125 | return "" 126 | 127 | def render_GET(self, request): # pylint: disable=invalid-name 128 | #Handle updates to system status from device 129 | if request.path == '/status/status-active': 130 | self.notify_hubs("status-active") 131 | return "" 132 | elif request.path == '/status/status-inactive': 133 | self.notify_hubs("status-inactive") 134 | return "" 135 | #Handle requests for status from the Smartthings hub 136 | elif request.path == '/status': 137 | msg = '%suuid:%s::%s' % (self.garage_door_status['last_state'], UUID, self.device_target) 138 | logger.info("Polling request from %s for %s - returned %s", 139 | request.getClientIP(), 140 | request.path, 141 | self.garage_door_status['last_state']) 142 | return msg 143 | else: 144 | logger.info("Invalid request from %s for %s", 145 | request.getClientIP(), 146 | request.path) 147 | return "" 148 | 149 | 150 | def notify_hubs(self, status): 151 | logger.info("Notifying...") 152 | """Notify the subscribed SmartThings hubs that a state change has occurred""" 153 | self.garage_door_status['last_state'] = status 154 | for subscription in self.subscription_list: 155 | if self.subscription_list[subscription]['expiration'] > time(): 156 | logger.info("Notifying hub %s", subscription) 157 | msg = '%suuid:%s::%s' % (status, UUID, self.device_target) 158 | body = StringProducer(msg) 159 | logger.info("Notification message %s", msg) 160 | logger.info("Subscription %s", subscription) 161 | headers = {'Content-Type': 'application/x-www-form-urlencoded'} 162 | r = requests.post(subscription, data=body.body, headers=headers) 163 | 164 | def handle_response(self, response): # pylint: disable=no-self-use 165 | """Handle the SmartThings hub returning a status code to the POST. 166 | This is actually unexpected - it typically closes the connection 167 | for POST/PUT without giving a response code.""" 168 | if response.code == 202: 169 | logger.info("Status update accepted") 170 | else: 171 | logger.error("Unexpected response code: %s", response.code) 172 | 173 | def handle_error(self, response): # pylint: disable=no-self-use 174 | """Handle errors generating performing the NOTIFY. There doesn't seem 175 | to be a way to avoid ResponseFailed - the SmartThings Hub 176 | doesn't generate a proper response code for POST or PUT, and if 177 | NOTIFY is used, it ignores the body.""" 178 | if isinstance(response.value, ResponseFailed): 179 | logger.info("Response failed (expected)") 180 | else: 181 | logger.error("Unexpected response: %s", response) 182 | 183 | 184 | class StringProducer(object): 185 | """Writes an in-memory string to a Twisted request""" 186 | implements(IBodyProducer) 187 | 188 | def __init__(self, body): 189 | self.body = body 190 | self.length = len(body) 191 | 192 | def startProducing(self, consumer): # pylint: disable=invalid-name 193 | """Start producing supplied string to the specified consumer""" 194 | consumer.write(self.body) 195 | return succeed(None) 196 | 197 | def pauseProducing(self): # pylint: disable=invalid-name 198 | """Pause producing - no op""" 199 | pass 200 | 201 | def stopProducing(self): # pylint: disable=invalid-name 202 | """ Stop producing - no op""" 203 | pass 204 | 205 | 206 | def main(): 207 | """Main function to handle use from command line""" 208 | 209 | arg_proc = argparse.ArgumentParser(description='Provides camera active/inactive status to a SmartThings hub') 210 | arg_proc.add_argument('--httpport', dest='http_port', help='HTTP port number', default=8082, type=int) 211 | arg_proc.add_argument('--deviceindex', dest='device_index', help='Device index', default=1, type=int) 212 | options = arg_proc.parse_args() 213 | 214 | device_target = 'urn:schemas-upnp-org:device:RPi_Computer_Vision:%d' % (options.device_index) 215 | 216 | subscription_list = {} 217 | camera_status = {'last_state': 'status-inactive'} 218 | 219 | # SSDP server to handle discovery 220 | SSDPServer(status_port=options.http_port, device_target=device_target) 221 | 222 | # HTTP site to handle subscriptions/polling 223 | status_site = server.Site(StatusServer(device_target, subscription_list, camera_status)) 224 | reactor.listenTCP(options.http_port, status_site) # pylint: disable=no-member 225 | 226 | logger.info('Initialization complete') 227 | reactor.run() # pylint: disable=no-member 228 | 229 | if __name__ == "__main__": 230 | main() 231 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #motion-notify 2 | 3 | ## Motion Notify Version 1.0.3 4 | 5 | Motion Notify 1.0 is a major release which is a significant rewrite from the previous 0.3 release. 6 | The latest version provides a platform which is more stable, more configurable and more extensible. 7 | Focus has been made on providing a platform that is easily extended by allowing developers to add new Actions and Detectors. 8 | See the section at the end of the readme if you want to know more about developing additional actions and detectors. 9 | 10 | ## Overview 11 | 12 | Motion Notify is a notification system for Linux Motion providing upload to Google Drive and email notification when you're not home. 13 | 14 | Take an event, carry out some detections to see if you're home and then carry out a series of actions. 15 | 16 | This allows you to carry out the following: 17 | 18 | - Send an email when a motion is detected 19 | - Upload an image to Google Drive while an event is occuring 20 | - Upload a video to Google Drive when the event ends 21 | - Send an email when the event ends with a link to the video 22 | - Make a call to a REST API or HTTP endpoint when an event starts or ends 23 | - Detect whether you're at home by looking for certain IP addresses on your local network and not send alerts if you're home 24 | - Allows you to specify hours when you want to receive alerts even if you're at home 25 | - Only receive alerts when you're not home 26 | - Specify what you want to happen when each type of event occurs 27 | 28 | ## Upgrades 29 | 30 | If you're upgrading from an older version note the following: 31 | 32 | - The arguments used by Motion Notify have changed so you will need to update the entries in /etc/motion/motion.conf 33 | - Additional Python libraries are required so check that you have all the libraries stated in the install / config section this page 34 | - The config file has changed a lot 35 | - Follow through the install guide to ensure that everything works correctly 36 | 37 | ## Events, Detectors and Actions 38 | 39 | Motion Notify works on the concept of Events, Detectors and Actions. 40 | 41 | - An Event is triggered when motion is detected 42 | - Detectors check whether the system is "active" (typically whether you're at home or not) 43 | - Actions are carried out as a result of an event (eg. when an event is triggered an email notification is sent and the image upload to Google Drive 44 | 45 | The framework and configuration is designed to allow developers to easily develop and integrate their own Detectors and Actions 46 | 47 | ### Event Action Rules 48 | 49 | Each "Event Type" has a set of Event Action Rules allowing you to specify a different series of actions to be called at the start of an event than at the end of an event. 50 | 51 | - Motion Notify allows rules to be configured stating what Actions should occur when an Event is triggered, taking into account whether or not the detectors show that the system is "active" 52 | - Event Action Rules are specified in the config file in the format EVENT_TYPE = ACTION_CLASS:ALWAYS_OR_IF_ACTIVE,ANOTHER_ACTION_CLASS:ALWAYS_OR_IF_ACTIVE 53 | - An example rule would be "on_picture_save = GoogleDriveUploadAction:always,DeleteMediaFileAction:always" 54 | - This would mean that when a "on_picture_save" event occurs the file is upload to Google Drive and the local source file is deleted afterwards. Due to the "always" rule these actions will occur regardless of the system status 55 | - Another example would be on_event_start = SmtpEmailNotifyAction:if_active 56 | - This would mean that an email notification would be sent only if the detectors indicate that the system is active (see Detector Rules below) 57 | 58 | Currently support Actions are: 59 | 60 | - DeleteMediaFileAction - deletes the input file that was generated by the event 61 | - GoogleDriveUploadAction - uploads the input file to Google Drive 62 | - SmtpEmailNotifyAction - Sends an email using SMTP 63 | - UrlInvokeAction - Makes an HTTP request to a REST service or website 64 | 65 | Currently support Event Types are passed to Motion Notify via the command which is trigger by Linux Motion (or any other event trigger) 66 | 67 | - on_event_start 68 | - on_picture_save 69 | - on_movie_end 70 | 71 | Actions may be configured to trigger: 72 | 73 | - always (ignore any detector status) 74 | - if_active (only trigger if the detectors indicate that the system is active) 75 | 76 | ### Detection Rules 77 | 78 | - Detection Rules state what detectors should be called in order 79 | - Currently supported detectors are: 80 | - ArpBasedDetector - carries out a network scan for a MAC address (the IpBasedDetector is recommended over this) 81 | - IpBasedDetector - Checks whether you're home by taking a list of IP addresses and pinging them to see if they are active 82 | - TimeBasedDetector - Allows times of the day to be specified stating times at which the system should be considered active 83 | 84 | Detection rules can be specified in groups to indicate whether they all detectors should indicate that the system is active or whether an individual detector can determine that the system is active 85 | - Example 1: detector_rules = {TimeBasedDetector},{IpBasedDetector} would require that either TimeBasedDetector or IpBasedDetector indicate that the system is active 86 | - Example 2: detector_rules = {TimeBasedDetector,IpBasedDetector} would require that both TimeBasedDetector and IpBasedDetector indicate that the system is active 87 | - Example 3: detector_rules = {TimeBasedDetector,IpBasedDetector},{ArpBasedDetector} would require that both TimeBasedDetector and IpBasedDetector indicate that the system is active or ArpBasedDetector indicates that the system is active 88 | 89 | ### IpBasedDetector 90 | 91 | This detector checks whether you're at home by checking the network for the presence of certain devices by IP address. 92 | 93 | It's highly recommended to use this rather than ArpBasedDetector which looks for MAC addresses. If you choose to use ArpBasedDetector you will need to run Motion Notify (and Motion) as root as it uses ARP - this isn't recommended. 94 | 95 | IP detection uses ping so will run as a regular user. 96 | 97 | Specify a comma separated list of IP addresses which will be checked - if any of those addresses are active then the system assumes that you're at home. 98 | 99 | Note that mobile phones often don't retain a constant connection to the wireless network even though they show that they are connected. 100 | They tend to sleep and then just reconnect occasionally to reduce battery life. 101 | This means that you might get a lot of false alarms if you just use a mobile phone IP address. 102 | 103 | Adding lots of devices that are only active when you're at home will reduce false alarms - try things like your Smart TV, desktop PC etc as well as any mobile phones. 104 | It's highly recommended to configure your devices to use static IP's to prevent the IP addresses from changing. 105 | 106 | You can specify the timeout for ping requests in the config file - this forces ping to exit and assume that the device is offline rather than wait a long time for each response. If you're on a poor wireless connection or experiencing slow ping responses this should be increased. 107 | 108 | The switches for ping are not consistent between operating systems. If you're running on a Debian based system such as Raspbian then ping -w2 127.0.0.1 causes a timeout of 2 seconds. On a BSD based system such as OSX you need ping -t2 127.0.0.1 so set the IpBasedDetector.ping_timeout_switch in the config file to -w or -t according to your operating systems ping command. 109 | 110 | ### ArpBasedDetector 111 | 112 | This detector checks the network for the presence of certain MAC addresses. It requires the script and Linux Motion to be run as root so it's use is only recommended when the IpBasedDetector can't be used (such as when static IP addresses are not possible). 113 | 114 | - presence_macs = XX:XX:XX:XX:XX,YY:YY:YY:YY:YY 115 | - Configuration is carried out using a comma separated list of MAC addresses. 116 | 117 | ### TimeBasedDetector 118 | 119 | This detector allows times of the day to be specified at which the system should always be considered active. 120 | 121 | - Example configuration - time_ranges = 01:00-07:00,12:00-13:00 122 | - This indicates that the system is always active between 01:00 and 07:00 and 12:00 and 13:00 so Actions will always be triggered during those hours 123 | 124 | ## Installation 125 | 126 | Install Python Libraries 127 | 128 | ```bash 129 | sudo apt-get update 130 | sudo apt-get install python-pip 131 | sudo pip install PyDrive 132 | sudo pip install enum34 133 | sudo pip install oauth2client (may already be installed) 134 | sudo pip install google-api-python-client (may already be installed) 135 | sudo apt-get install python-openssl (may already be installed) 136 | ``` 137 | 138 | Clone the Git repo into the directory 139 | ```bash 140 | cd /etc/ 141 | sudo apt-get install git 142 | sudo git clone https://github.com/amdean/motion-notify.git 143 | sudo chown -R motion.motion /etc/motion-notify 144 | ``` 145 | 146 | Create the log file and lock file and set the permissions 147 | 148 | ```bash 149 | sudo touch /var/tmp/motion-notify.log 150 | sudo chown motion.motion /var/tmp/motion-notify.log 151 | sudo chmod 664 /var/tmp/motion-notify.log 152 | sudo touch /var/tmp/motion-notify.lock.pid 153 | sudo chmod 664 /var/tmp/motion-notify.lock.pid 154 | sudo chown motion.motion /var/tmp/motion-notify.lock.pid 155 | ``` 156 | 157 | Update Configuration (see Configuration Section) 158 | 159 | Change the File permissions 160 | 161 | ```bash 162 | sudo chown motion.motion /etc/motion-notify/motion-notify.py 163 | sudo chown motion.motion /etc/motion-notify/motion-notify.cfg 164 | sudo chmod 744 /etc/motion-notify/motion-notify.py 165 | sudo chmod 600 /etc/motion-notify/motion-notify.cfg 166 | ``` 167 | 168 | Create the entry in the Motion conf file to trigger the motion-notify script when there is an alert 169 | 170 | ```bash 171 | sudo cat /etc/motion-notify/create-motion-conf-entries.txt >> /etc/motion/motion.conf 172 | rm /etc/motion-notify/create-motion-conf-entries.txt 173 | ``` 174 | 175 | Motion will now send alerts to you when you're devices aren't present on the network 176 | 177 | 178 | ## Configuration 179 | 180 | ### SmtpEmailNotifyAction config 181 | 182 | Enter the following configuration for emails: 183 | 184 | - Google account details into the GMail section of the config file
185 | (this is just to send emails so you could setup another Google account just for sending if you're worried about storing your account password in the clear). 186 | - Email address to send alerts to 187 | - The URL of the folder you created in your Google account (just copy and paste it from the browser). This will be sent in the alert emails so that you can click through to the folder 188 | 189 | ### TimeBasedNotification Config 190 | - The hours that you always want to receive email alerts even when you're home 191 | 192 | ### GoogleDriveUploadAction Config 193 | 194 | Google drive authentication is done using a service account via key authentication. The service account is given access only to the folder you specify in Google Drive. 195 | 196 | - Login to Google Drive and create a folder where images and video will be upload to 197 | - Enter the ID of the folder into the "folder" field in the config file.
198 | If the URL of the folder is https://drive.google.com/drive/folders/0Bzt4FJyYHjdYhnA3cECdTFW3RWM then the ID is 0Bzt4FJyYHjdYhnA3cECdTFW3RWM. 199 | 200 | Next you need to get some account credentials from the Google Developers console - this will allow motion-notify to upload files to Google Drive. 201 | 202 | - Go to https://console.developers.google.com/ 203 | - From the "Select a project" dropdown at the top of the page choose "Create a project" 204 | - Enter "motion-notify" as the project name (or anything else that you want to call it) 205 | 206 | Once the project is created you'll be take to the project dashboard for that project. 207 | 208 | - Go to APIs & auth > APIs > Drive API and click "Enable API" 209 | - Go to APIs & auth > Credentials and choose "Create new Client ID" and select "Service Account" as the application type. 210 | 211 | You'll receive a download containing a JSON file. 212 | 213 | - Generate a new P12 key for the service account you just created using the button underneath the details of the service account. 214 | - Save this file in the /etc/motion-notify directory and rename it to creds.p12. 215 | 216 | The service account has an email address associated with it which will @developer.gserviceaccount.com. 217 | Copy that email address and enter it into the "service_user_email" field in the config file. 218 | 219 | You now need to allow the service account access to your Google Drive folder. 220 | 221 | - Go to the Google Drive folder where you want images and videos to be uploaded 222 | - Click on the share icon 223 | - Enter the email address of your service account and ensure that "Can edit" is selected. 224 | 225 | ### Sub Folder Config 226 | 227 | To Create Date Level Folders update the date format config. By Default this is set to roll each day. (See Permissions) 228 | 229 | ### Permissions Config 230 | 231 | Using the Service User OAuth (p12 file) means that the files are created and owned by the service user
232 | When using folders that means we need to set the folder permission explicitly.
233 | By Default the ```gmail/user``` from config is set as a writer. To add any other users set the ```read_users``` and ```write_users``` values in config. 234 | These values should be the email addresses of the users you wish to permission. 235 | 236 | ## Usage 237 | 238 | Motion Notify is designed to work with Linux Motion but can be used to handle other applications. 239 | 240 | To run Motion Notify call motion-notify.py and pass in the following arguments: 241 | 242 | - Full address of the Motion Notify config file - typically /etc/motion-notify/motion-notify.cfg 243 | - Full address of an image or video file relating to the event (this will be uploaded to Google Drive etc) - such as /tmp/motion/02-20151101111812-00.jpg 244 | - Event type - this must be one of on_event_start, on_picture_save, on_movie_end. Each Event Type has its' own set of Event Action Rules allowing you to specify what happens for each type of event. 245 | - Timestamp - the time of the event 246 | - Event ID - a unique String ID representing the event 247 | 248 | If you're using Motion Notify with Linux Motion motion detection software you need the following lines at the end of your /etc/motion/motion.conf file: 249 | ```bash 250 | on_picture_save /etc/motion-notify/motion-notify.py /etc/motion-notify/motion-notify.cfg %f on_picture_save %s %v %n 251 | on_movie_end /etc/motion-notify/motion-notify.py /etc/motion-notify/motion-notify.cfg %f on_movie_end %s %v %n 252 | on_event_start /etc/motion-notify/motion-notify.py /etc/motion-notify/motion-notify.cfg None on_event_start %s %v None 253 | ``` 254 | 255 | 256 | ## Troubleshooting 257 | 258 | The first three steps to troubleshooting are: 259 | 260 | - Check the log file: tail -fn 100 /var/tmp/motion-notify.log 261 | - Set the log level to DEBUG - in motion-notify.py change logger.setLevel(logging.INFO) to logger.setLevel(logging.DEBUG) 262 | - Call Motion Notify using one of the commands below 263 | 264 | Trigger an "on_event_start" event 265 | ```bash 266 | sudo /etc/motion-notify/motion-notify.py /etc/motion-notify/motion-notify.cfg None on_event_start 123 456 None 267 | ``` 268 | 269 | Trigger an "on_picture_save" event 270 | ```bash 271 | sudo /etc/motion-notify/motion-notify.py /etc/motion-notify/motion-notify.cfg /home/pi/test.jpg on_picture_save 123 456 test.jpg 272 | ``` 273 | 274 | ## Developers 275 | 276 | Motion Notify is designed to be extended. Create your own actions or detectors. 277 | Please contribute to the project by submitting any changes to https://github.com/amdean/motion-notify 278 | 279 | ### Actions 280 | 281 | Simply create a new Python module in the actions package, add the action to the EventActionRules section of the config file and that's it - no need to modify any code in the Motion Notify core. 282 | 283 | Rules: 284 | - Module name and class name must be the same with the same case (this is due to the way that introspection is used to invoke Actions) 285 | - All actions must provide the following static methods: 286 | - def do_event_start_action(config, motion_event): (Invoked when a on_event_start action occurs) 287 | - def do_event_end_action(config, motion_event): (Invoked when a on_movie_end action occurs) 288 | - def do_action(config, motion_event): (Invoked when any other action occurs) 289 | - The DeleteMediaFileAction is a good example of very simple implementation 290 | - The GoogleDriveUploadAction is a good example of a more complex implementation 291 | 292 | ### Detectors 293 | 294 | Simply create a new Python module in the detectors package, add the detector to the detector_rules line of the config file and that's it - no need to modify any code in the Motion Notify core. 295 | - Module name and class name must be the same with the same case (this is due to the way that introspection is used to invoke Detectors) 296 | - All detectors must provide the following static method: 297 | - def detect_presence(config): 298 | - This method returns True if the system should be considered active 299 | - This may seem counter-intuitive as the IpBasedDetector detector returns True if it fails to detect any active IP addresses and False if it finds an active IP. An active IP indicates that someone is home so the system is not active and False is returned. 300 | 301 | 302 | ## TODO 303 | 304 | - Set Owner Of Date Folders to be the real owner and not the service user (Not sure this is possible) 305 | - Add option to put Video and Pics in seperate folders 306 | 307 | 308 | ###SSDP Installation 309 | sudo pip install requests -------------------------------------------------------------------------------- /resources/sample-config-files/motion.conf.example: -------------------------------------------------------------------------------- 1 | #This is a standard motion.conf file that has been modified for to optimize attributes for use with motion-notify 2 | 3 | # Rename this distribution example file to motion.conf 4 | # 5 | # This config file was generated by motion 4.0 6 | 7 | 8 | ############################################################ 9 | # Daemon 10 | ############################################################ 11 | 12 | # Start in daemon (background) mode and release terminal (default: off) 13 | daemon on 14 | #CHANGED 15 | 16 | # File to store the process ID, also called pid file. (default: not defined) 17 | process_id_file /var/run/motion/motion.pid 18 | 19 | ############################################################ 20 | # Basic Setup Mode 21 | ############################################################ 22 | 23 | # Start in Setup-Mode, daemon disabled. (default: off) 24 | setup_mode off 25 | 26 | 27 | # Use a file to save logs messages, if not defined stderr and syslog is used. (default: not defined) 28 | logfile /var/log/motion/motion.log 29 | 30 | # Level of log messages [1..9] (EMG, ALR, CRT, ERR, WRN, NTC, INF, DBG, ALL). (default: 6 / NTC) 31 | log_level 6 32 | 33 | # Filter to log messages by type (COR, STR, ENC, NET, DBL, EVT, TRK, VID, ALL). (default: ALL) 34 | log_type all 35 | 36 | ########################################################### 37 | # Capture device options 38 | ############################################################ 39 | 40 | # Videodevice to be used for capturing (default /dev/video0) 41 | # for FreeBSD default is /dev/bktr0 42 | videodevice /dev/video0 43 | 44 | # v4l2_palette allows one to choose preferable palette to be use by motion 45 | # to capture from those supported by your videodevice. (default: 17) 46 | # E.g. if your videodevice supports both V4L2_PIX_FMT_SBGGR8 and 47 | # V4L2_PIX_FMT_MJPEG then motion will by default use V4L2_PIX_FMT_MJPEG. 48 | # Setting v4l2_palette to 2 forces motion to use V4L2_PIX_FMT_SBGGR8 49 | # instead. 50 | # 51 | # Values : 52 | # V4L2_PIX_FMT_SN9C10X : 0 'S910' 53 | # V4L2_PIX_FMT_SBGGR16 : 1 'BYR2' 54 | # V4L2_PIX_FMT_SBGGR8 : 2 'BA81' 55 | # V4L2_PIX_FMT_SPCA561 : 3 'S561' 56 | # V4L2_PIX_FMT_SGBRG8 : 4 'GBRG' 57 | # V4L2_PIX_FMT_SGRBG8 : 5 'GRBG' 58 | # V4L2_PIX_FMT_PAC207 : 6 'P207' 59 | # V4L2_PIX_FMT_PJPG : 7 'PJPG' 60 | # V4L2_PIX_FMT_MJPEG : 8 'MJPEG' 61 | # V4L2_PIX_FMT_JPEG : 9 'JPEG' 62 | # V4L2_PIX_FMT_RGB24 : 10 'RGB3' 63 | # V4L2_PIX_FMT_SPCA501 : 11 'S501' 64 | # V4L2_PIX_FMT_SPCA505 : 12 'S505' 65 | # V4L2_PIX_FMT_SPCA508 : 13 'S508' 66 | # V4L2_PIX_FMT_UYVY : 14 'UYVY' 67 | # V4L2_PIX_FMT_YUYV : 15 'YUYV' 68 | # V4L2_PIX_FMT_YUV422P : 16 '422P' 69 | # V4L2_PIX_FMT_YUV420 : 17 'YU12' 70 | # 71 | v4l2_palette 17 72 | 73 | # Tuner device to be used for capturing using tuner as source (default /dev/tuner0) 74 | # This is ONLY used for FreeBSD. Leave it commented out for Linux 75 | ; tunerdevice /dev/tuner0 76 | 77 | # The video input to be used (default: -1) 78 | # Should normally be set to 0 or 1 for video/TV cards, and -1 for USB cameras 79 | # Set to 0 for uvideo(4) on OpenBSD 80 | input -1 81 | 82 | # The video norm to use (only for video capture and TV tuner cards) 83 | # Values: 0 (PAL), 1 (NTSC), 2 (SECAM), 3 (PAL NC no colour). Default: 0 (PAL) 84 | norm 0 85 | 86 | # The frequency to set the tuner to (kHz) (only for TV tuner cards) (default: 0) 87 | frequency 0 88 | 89 | # Override the power line frequency for the webcam. (normally not necessary) 90 | # Values: 91 | # -1 : Do not modify device setting 92 | # 0 : Power line frequency Disabled 93 | # 1 : 50hz 94 | # 2 : 60hz 95 | # 3 : Auto 96 | power_line_frequency -1 97 | 98 | # Rotate image this number of degrees. The rotation affects all saved images as 99 | # well as movies. Valid values: 0 (default = no rotation), 90, 180 and 270. 100 | rotate 0 101 | 102 | # Image width (pixels). Valid range: Camera dependent, default: 352 103 | width 1280 104 | #CHANGED 105 | 106 | # Image height (pixels). Valid range: Camera dependent, default: 288 107 | height 720 108 | #CHANGED 109 | 110 | # Maximum number of frames to be captured per second. 111 | # Valid range: 2-100. Default: 100 (almost no limit). 112 | framerate 50 113 | #CHANGED 114 | 115 | # Minimum time in seconds between capturing picture frames from the camera. 116 | # Default: 0 = disabled - the capture rate is given by the camera framerate. 117 | # This option is used when you want to capture images at a rate lower than 2 per second. 118 | minimum_frame_time 0 119 | 120 | # URL to use if you are using a network camera, size will be autodetected (incl http:// ftp:// mjpg:// rtsp:// mjpeg:// or file:///) 121 | # Must be a URL that returns single jpeg pictures or a raw mjpeg stream. A trailing slash may be required for some cameras. 122 | # Default: Not defined 123 | ; netcam_url value 124 | 125 | # Username and password for network camera (only if required). Default: not defined 126 | # Syntax is user:password 127 | ; netcam_userpass value 128 | 129 | # The setting for keep-alive of network socket, should improve performance on compatible net cameras. 130 | # off: The historical implementation using HTTP/1.0, closing the socket after each http request. 131 | # force: Use HTTP/1.0 requests with keep alive header to reuse the same connection. 132 | # on: Use HTTP/1.1 requests that support keep alive as default. 133 | # Default: off 134 | netcam_keepalive off 135 | 136 | # URL to use for a netcam proxy server, if required, e.g. "http://myproxy". 137 | # If a port number other than 80 is needed, use "http://myproxy:1234". 138 | # Default: not defined 139 | ; netcam_proxy value 140 | 141 | # Set less strict jpeg checks for network cameras with a poor/buggy firmware. 142 | # Default: off 143 | netcam_tolerant_check off 144 | 145 | # RTSP connection uses TCP to communicate to the camera. Can prevent image corruption. 146 | # Default: on 147 | rtsp_uses_tcp on 148 | 149 | # Name of camera to use if you are using a camera accessed through OpenMax/MMAL 150 | # Default: Not defined 151 | ; mmalcam_name vc.ril.camera 152 | 153 | # Camera control parameters (see raspivid/raspistill tool documentation) 154 | # Default: Not defined 155 | ; mmalcam_control_params -hf 156 | 157 | # Let motion regulate the brightness of a video device (default: off). 158 | # The auto_brightness feature uses the brightness option as its target value. 159 | # If brightness is zero auto_brightness will adjust to average brightness value 128. 160 | # Only recommended for cameras without auto brightness 161 | auto_brightness off 162 | 163 | # Set the initial brightness of a video device. 164 | # If auto_brightness is enabled, this value defines the average brightness level 165 | # which Motion will try and adjust to. 166 | # Valid range 0-255, default 0 = disabled 167 | brightness 0 168 | 169 | # Set the contrast of a video device. 170 | # Valid range 0-255, default 0 = disabled 171 | contrast 0 172 | 173 | # Set the saturation of a video device. 174 | # Valid range 0-255, default 0 = disabled 175 | saturation 0 176 | 177 | # Set the hue of a video device (NTSC feature). 178 | # Valid range 0-255, default 0 = disabled 179 | hue 0 180 | 181 | 182 | ############################################################ 183 | # Round Robin (multiple inputs on same video device name) 184 | ############################################################ 185 | 186 | # Number of frames to capture in each roundrobin step (default: 1) 187 | roundrobin_frames 1 188 | 189 | # Number of frames to skip before each roundrobin step (default: 1) 190 | roundrobin_skip 1 191 | 192 | # Try to filter out noise generated by roundrobin (default: off) 193 | switchfilter off 194 | 195 | 196 | ############################################################ 197 | # Motion Detection Settings: 198 | ############################################################ 199 | 200 | # Threshold for number of changed pixels in an image that 201 | # triggers motion detection (default: 1500) 202 | threshold 10000 203 | #CHANGED 204 | 205 | # Automatically tune the threshold down if possible (default: off) 206 | threshold_tune off 207 | 208 | # Noise threshold for the motion detection (default: 32) 209 | noise_level 32 210 | 211 | # Automatically tune the noise threshold (default: on) 212 | noise_tune on 213 | 214 | # Despeckle motion image using (e)rode or (d)ilate or (l)abel (Default: not defined) 215 | # Recommended value is EedDl. Any combination (and number of) of E, e, d, and D is valid. 216 | # (l)abeling must only be used once and the 'l' must be the last letter. 217 | # Comment out to disable 218 | despeckle_filter EedDl 219 | 220 | # Detect motion in predefined areas (1 - 9). Areas are numbered like that: 1 2 3 221 | # A script (on_area_detected) is started immediately when motion is 4 5 6 222 | # detected in one of the given areas, but only once during an event. 7 8 9 223 | # One or more areas can be specified with this option. Take care: This option 224 | # does NOT restrict detection to these areas! (Default: not defined) 225 | ; area_detect value 226 | 227 | # PGM file to use as a sensitivity mask. 228 | # Full path name to. (Default: not defined) 229 | ; mask_file value 230 | 231 | # Dynamically create a mask file during operation (default: 0) 232 | # Adjust speed of mask changes from 0 (off) to 10 (fast) 233 | smart_mask_speed 0 234 | 235 | # Ignore sudden massive light intensity changes given as a percentage of the picture 236 | # area that changed intensity. Valid range: 0 - 100 , default: 0 = disabled 237 | lightswitch 50 238 | 239 | # Picture frames must contain motion at least the specified number of frames 240 | # in a row before they are detected as true motion. At the default of 1, all 241 | # motion is detected. Valid range: 1 to thousands, recommended 1-5 242 | minimum_motion_frames 1 243 | 244 | # Specifies the number of pre-captured (buffered) pictures from before motion 245 | # was detected that will be output at motion detection. 246 | # Recommended range: 0 to 5 (default: 0) 247 | # Do not use large values! Large values will cause Motion to skip video frames and 248 | # cause unsmooth movies. To smooth movies use larger values of post_capture instead. 249 | pre_capture 0 250 | 251 | # Number of frames to capture after motion is no longer detected (default: 0) 252 | post_capture 0 253 | 254 | # Event Gap is the seconds of no motion detection that triggers the end of an event. 255 | # An event is defined as a series of motion images taken within a short timeframe. 256 | # Recommended value is 60 seconds (Default). The value -1 is allowed and disables 257 | # events causing all Motion to be written to one single movie file and no pre_capture. 258 | # If set to 0, motion is running in gapless mode. Movies don't have gaps anymore. An 259 | # event ends right after no more motion is detected and post_capture is over. 260 | event_gap 60 261 | 262 | # Maximum length in seconds of a movie 263 | # When value is exceeded a new movie file is created. (Default: 0 = infinite) 264 | max_movie_time 0 265 | 266 | # Always save images even if there was no motion (default: off) 267 | emulate_motion off 268 | 269 | 270 | ############################################################ 271 | # Image File Output 272 | ############################################################ 273 | 274 | # Output 'normal' pictures when motion is detected (default: on) 275 | # Valid values: on, off, first, best, center 276 | # When set to 'first', only the first picture of an event is saved. 277 | # Picture with most motion of an event is saved when set to 'best'. 278 | # Picture with motion nearest center of picture is saved when set to 'center'. 279 | # Can be used as preview shot for the corresponding movie. 280 | output_pictures on 281 | 282 | # Output pictures with only the pixels moving object (ghost images) (default: off) 283 | output_debug_pictures off 284 | 285 | # The quality (in percent) to be used by the jpeg compression (default: 75) 286 | quality 100 287 | #CHANGED 288 | 289 | # Type of output images 290 | # Valid values: jpeg, ppm (default: jpeg) 291 | picture_type jpeg 292 | 293 | ############################################################ 294 | # FFMPEG related options 295 | # Film (movies) file output, and deinterlacing of the video input 296 | # The options movie_filename and timelapse_filename are also used 297 | # by the ffmpeg feature 298 | ############################################################ 299 | 300 | # Use ffmpeg to encode movies in realtime (default: off) 301 | ffmpeg_output_movies on 302 | 303 | # Use ffmpeg to make movies with only the pixels moving 304 | # object (ghost images) (default: off) 305 | ffmpeg_output_debug_movies off 306 | 307 | # Use ffmpeg to encode a timelapse movie 308 | # Default value 0 = off - else save frame every Nth second 309 | ffmpeg_timelapse 0 310 | 311 | # The file rollover mode of the timelapse video 312 | # Valid values: hourly, daily (default), weekly-sunday, weekly-monday, monthly, manual 313 | ffmpeg_timelapse_mode daily 314 | 315 | # Bitrate to be used by the ffmpeg encoder (default: 400000) 316 | # This option is ignored if ffmpeg_variable_bitrate is not 0 (disabled) 317 | ffmpeg_bps 400000 318 | 319 | # Enables and defines variable bitrate for the ffmpeg encoder. 320 | # ffmpeg_bps is ignored if variable bitrate is enabled. 321 | # Valid values: 0 (default) = fixed bitrate defined by ffmpeg_bps, 322 | # or the range 1 - 100 where 1 means worst quality and 100 is best. 323 | ffmpeg_variable_bitrate 0 324 | 325 | # Codec to used by ffmpeg for the video compression. 326 | # Timelapse videos have two options. 327 | # mpg - Creates mpg file with mpeg-2 encoding. 328 | # If motion is shutdown and restarted, new pics will be appended 329 | # to any previously created file with name indicated for timelapse. 330 | # mpeg4 - Creates avi file with the default encoding. 331 | # If motion is shutdown and restarted, new pics will create a 332 | # new file with the name indicated for timelapse. 333 | # Supported formats are: 334 | # mpeg4 or msmpeg4 - gives you files with extension .avi 335 | # msmpeg4 is recommended for use with Windows Media Player because 336 | # it requires no installation of codec on the Windows client. 337 | # swf - gives you a flash film with extension .swf 338 | # flv - gives you a flash video with extension .flv 339 | # ffv1 - FF video codec 1 for Lossless Encoding 340 | # mov - QuickTime 341 | # mp4 - MPEG-4 Part 14 H264 encoding 342 | # mkv - Matroska H264 encoding 343 | # hevc - H.265 / HEVC (High Efficiency Video Coding) 344 | ffmpeg_video_codec mpeg4 345 | 346 | # When creating videos, should frames be duplicated in order 347 | # to keep up with the requested frames per second 348 | # (default: true) 349 | ffmpeg_duplicate_frames true 350 | 351 | ############################################################ 352 | # SDL Window 353 | ############################################################ 354 | 355 | # Number of motion thread to show in SDL Window (default: 0 = disabled) 356 | #sdl_threadnr 0 357 | 358 | ############################################################ 359 | # External pipe to video encoder 360 | # Replacement for FFMPEG builtin encoder for ffmpeg_output_movies only. 361 | # The options movie_filename and timelapse_filename are also used 362 | # by the ffmpeg feature 363 | ############################################################# 364 | 365 | # Bool to enable or disable extpipe (default: off) 366 | use_extpipe off 367 | 368 | # External program (full path and opts) to pipe raw video to 369 | # Generally, use '-' for STDIN... 370 | ;extpipe mencoder -demuxer rawvideo -rawvideo w=%w:h=%h:i420 -ovc x264 -x264encopts bframes=4:frameref=1:subq=1:scenecut=-1:nob_adapt:threads=1:keyint=1000:8x8dct:vbv_bufsize=4000:crf=24:partitions=i8x8,i4x4:vbv_maxrate=800:no-chroma-me -vf denoise3d=16:12:48:4,pp=lb -of avi -o %f.avi - -fps %fps 371 | ;extpipe x264 - --input-res %wx%h --fps %fps --bitrate 2000 --preset ultrafast --quiet -o %f.mp4 372 | ;extpipe mencoder -demuxer rawvideo -rawvideo w=%w:h=%h:fps=%fps -ovc x264 -x264encopts preset=ultrafast -of lavf -o %f.mp4 - -fps %fps 373 | ;extpipe ffmpeg -y -f rawvideo -pix_fmt yuv420p -video_size %wx%h -framerate %fps -i pipe:0 -vcodec libx264 -preset ultrafast -f mp4 %f.mp4 374 | 375 | 376 | ############################################################ 377 | # Snapshots (Traditional Periodic Webcam File Output) 378 | ############################################################ 379 | 380 | # Make automated snapshot every N seconds (default: 0 = disabled) 381 | snapshot_interval 0 382 | 383 | 384 | ############################################################ 385 | # Text Display 386 | # %Y = year, %m = month, %d = date, 387 | # %H = hour, %M = minute, %S = second, %T = HH:MM:SS, 388 | # %v = event, %q = frame number, %t = camera id number, 389 | # %D = changed pixels, %N = noise level, \n = new line, 390 | # %i and %J = width and height of motion area, 391 | # %K and %L = X and Y coordinates of motion center 392 | # %C = value defined by text_event - do not use with text_event! 393 | # You can put quotation marks around the text to allow 394 | # leading spaces 395 | ############################################################ 396 | 397 | # Locate and draw a box around the moving object. 398 | # Valid values: on, off, preview (default: off) 399 | # Set to 'preview' will only draw a box in preview_shot pictures. 400 | locate_motion_mode off 401 | 402 | # Set the look and style of the locate box if enabled. 403 | # Valid values: box, redbox, cross, redcross (default: box) 404 | # Set to 'box' will draw the traditional box. 405 | # Set to 'redbox' will draw a red box. 406 | # Set to 'cross' will draw a little cross to mark center. 407 | # Set to 'redcross' will draw a little red cross to mark center. 408 | locate_motion_style box 409 | 410 | # Draws the timestamp using same options as C function strftime(3) 411 | # Default: %Y-%m-%d\n%T = date in ISO format and time in 24 hour clock 412 | # Text is placed in lower right corner 413 | text_right %Y-%m-%d\n%T-%q 414 | 415 | # Draw a user defined text on the images using same options as C function strftime(3) 416 | # Default: Not defined = no text 417 | # Text is placed in lower left corner 418 | ; text_left CAMERA %t 419 | 420 | # Draw the number of changed pixed on the images (default: off) 421 | # Will normally be set to off except when you setup and adjust the motion settings 422 | # Text is placed in upper right corner 423 | text_changes off 424 | 425 | # This option defines the value of the special event conversion specifier %C 426 | # You can use any conversion specifier in this option except %C. Date and time 427 | # values are from the timestamp of the first image in the current event. 428 | # Default: %Y%m%d%H%M%S 429 | # The idea is that %C can be used filenames and text_left/right for creating 430 | # a unique identifier for each event. 431 | text_event %Y%m%d%H%M%S 432 | 433 | # Draw characters at twice normal size on images. (default: off) 434 | text_double off 435 | 436 | 437 | # Text to include in a JPEG EXIF comment 438 | # May be any text, including conversion specifiers. 439 | # The EXIF timestamp is included independent of this text. 440 | ;exif_text %i%J/%K%L 441 | 442 | ############################################################ 443 | # Target Directories and filenames For Images And Films 444 | # For the options snapshot_, picture_, movie_ and timelapse_filename 445 | # you can use conversion specifiers 446 | # %Y = year, %m = month, %d = date, 447 | # %H = hour, %M = minute, %S = second, 448 | # %v = event, %q = frame number, %t = camera id number, 449 | # %D = changed pixels, %N = noise level, 450 | # %i and %J = width and height of motion area, 451 | # %K and %L = X and Y coordinates of motion center 452 | # %C = value defined by text_event 453 | # Quotation marks round string are allowed. 454 | ############################################################ 455 | 456 | # Target base directory for pictures and films 457 | # Recommended to use absolute path. (Default: current working directory) 458 | target_dir /tmp/motion 459 | #CHANGED 460 | 461 | # File path for snapshots (jpeg or ppm) relative to target_dir 462 | # Default: %v-%Y%m%d%H%M%S-snapshot 463 | # Default value is equivalent to legacy oldlayout option 464 | # For Motion 3.0 compatible mode choose: %Y/%m/%d/%H/%M/%S-snapshot 465 | # File extension .jpg or .ppm is automatically added so do not include this. 466 | # Note: A symbolic link called lastsnap.jpg created in the target_dir will always 467 | # point to the latest snapshot, unless snapshot_filename is exactly 'lastsnap' 468 | snapshot_filename %v-%Y%m%d%H%M%S-snapshot 469 | 470 | # File path for motion triggered images (jpeg or ppm) relative to target_dir 471 | # Default: %v-%Y%m%d%H%M%S-%q 472 | # Default value is equivalent to legacy oldlayout option 473 | # For Motion 3.0 compatible mode choose: %Y/%m/%d/%H/%M/%S-%q 474 | # File extension .jpg or .ppm is automatically added so do not include this 475 | # Set to 'preview' together with best-preview feature enables special naming 476 | # convention for preview shots. See motion guide for details 477 | picture_filename %v-%Y%m%d%H%M%S-%q 478 | 479 | # File path for motion triggered ffmpeg films (movies) relative to target_dir 480 | # Default: %v-%Y%m%d%H%M%S 481 | # File extensions(.mpg .avi) are automatically added so do not include them 482 | movie_filename %v-%Y%m%d%H%M%S 483 | 484 | # File path for timelapse movies relative to target_dir 485 | # Default: %Y%m%d-timelapse 486 | # File extensions(.mpg .avi) are automatically added so do not include them 487 | timelapse_filename %Y%m%d-timelapse 488 | 489 | ############################################################ 490 | # Global Network Options 491 | ############################################################ 492 | # Enable IPv6 (default: off) 493 | ipv6_enabled off 494 | 495 | ############################################################ 496 | # Live Stream Server 497 | ############################################################ 498 | 499 | # The mini-http server listens to this port for requests (default: 0 = disabled) 500 | stream_port 8081 501 | 502 | # Quality of the jpeg (in percent) images produced (default: 50) 503 | stream_quality 75 504 | #CHANGED 505 | 506 | # Output frames at 1 fps when no motion is detected and increase to the 507 | # rate given by stream_maxrate when motion is detected (default: off) 508 | stream_motion on 509 | #CHANGED 510 | 511 | # Maximum framerate for stream streams (default: 1) 512 | stream_maxrate 3 513 | #CHANGED 514 | 515 | # Restrict stream connections to localhost only (default: on) 516 | stream_localhost off 517 | #CHANGED 518 | 519 | # Limits the number of images per connection (default: 0 = unlimited) 520 | # Number can be defined by multiplying actual stream rate by desired number of seconds 521 | # Actual stream rate is the smallest of the numbers framerate and stream_maxrate 522 | stream_limit 0 523 | 524 | # Set the authentication method (default: 0) 525 | # 0 = disabled 526 | # 1 = Basic authentication 527 | # 2 = MD5 digest (the safer authentication) 528 | stream_auth_method 0 529 | 530 | # Authentication for the stream. Syntax username:password 531 | # Default: not defined (Disabled) 532 | ; stream_authentication username:password 533 | 534 | # Percentage to scale the stream image for preview 535 | # Default: 25 536 | ; stream_preview_scale 25 537 | 538 | # Have stream preview image start on a new line 539 | # Default: no 540 | ; stream_preview_newline no 541 | 542 | ############################################################ 543 | # HTTP Based Control 544 | ############################################################ 545 | 546 | # TCP/IP port for the http server to listen on (default: 0 = disabled) 547 | webcontrol_port 8080 548 | 549 | # Restrict control connections to localhost only (default: on) 550 | webcontrol_localhost on 551 | 552 | # Output for http server, select off to choose raw text plain (default: on) 553 | webcontrol_html_output on 554 | 555 | # Authentication for the http based control. Syntax username:password 556 | # Default: not defined (Disabled) 557 | ; webcontrol_authentication username:password 558 | 559 | 560 | ############################################################ 561 | # Tracking (Pan/Tilt) 562 | ############################################################# 563 | 564 | # Type of tracker (0=none (default), 1=stepper, 2=iomojo, 3=pwc, 4=generic, 5=uvcvideo, 6=servo) 565 | # The generic type enables the definition of motion center and motion size to 566 | # be used with the conversion specifiers for options like on_motion_detected 567 | track_type 0 568 | 569 | # Enable auto tracking (default: off) 570 | track_auto off 571 | 572 | # Serial port of motor (default: none) 573 | ;track_port /dev/ttyS0 574 | 575 | # Motor number for x-axis (default: 0) 576 | ;track_motorx 0 577 | 578 | # Set motorx reverse (default: 0) 579 | ;track_motorx_reverse 0 580 | 581 | # Motor number for y-axis (default: 0) 582 | ;track_motory 1 583 | 584 | # Set motory reverse (default: 0) 585 | ;track_motory_reverse 0 586 | 587 | # Maximum value on x-axis (default: 0) 588 | ;track_maxx 200 589 | 590 | # Minimum value on x-axis (default: 0) 591 | ;track_minx 50 592 | 593 | # Maximum value on y-axis (default: 0) 594 | ;track_maxy 200 595 | 596 | # Minimum value on y-axis (default: 0) 597 | ;track_miny 50 598 | 599 | # Center value on x-axis (default: 0) 600 | ;track_homex 128 601 | 602 | # Center value on y-axis (default: 0) 603 | ;track_homey 128 604 | 605 | # ID of an iomojo camera if used (default: 0) 606 | track_iomojo_id 0 607 | 608 | # Angle in degrees the camera moves per step on the X-axis 609 | # with auto-track (default: 10) 610 | # Currently only used with pwc type cameras 611 | track_step_angle_x 10 612 | 613 | # Angle in degrees the camera moves per step on the Y-axis 614 | # with auto-track (default: 10) 615 | # Currently only used with pwc type cameras 616 | track_step_angle_y 10 617 | 618 | # Delay to wait for after tracking movement as number 619 | # of picture frames (default: 10) 620 | track_move_wait 10 621 | 622 | # Speed to set the motor to (stepper motor option) (default: 255) 623 | track_speed 255 624 | 625 | # Number of steps to make (stepper motor option) (default: 40) 626 | track_stepsize 40 627 | 628 | 629 | ############################################################ 630 | # External Commands, Warnings and Logging: 631 | # You can use conversion specifiers for the on_xxxx commands 632 | # %Y = year, %m = month, %d = date, 633 | # %H = hour, %M = minute, %S = second, 634 | # %v = event, %q = frame number, %t = camera id number, 635 | # %D = changed pixels, %N = noise level, 636 | # %i and %J = width and height of motion area, 637 | # %K and %L = X and Y coordinates of motion center 638 | # %C = value defined by text_event 639 | # %f = filename with full path 640 | # %n = number indicating filetype 641 | # Both %f and %n are only defined for on_picture_save, 642 | # on_movie_start and on_movie_end 643 | # Quotation marks round string are allowed. 644 | ############################################################ 645 | 646 | # Do not sound beeps when detecting motion (default: on) 647 | # Note: Motion never beeps when running in daemon mode. 648 | quiet on 649 | 650 | # Command to be executed when an event starts. (default: none) 651 | # An event starts at first motion detected after a period of no motion defined by event_gap 652 | ; on_event_start value 653 | 654 | # Command to be executed when an event ends after a period of no motion 655 | # (default: none). The period of no motion is defined by option event_gap. 656 | ; on_event_end value 657 | 658 | # Command to be executed when a picture (.ppm|.jpg) is saved (default: none) 659 | # To give the filename as an argument to a command append it with %f 660 | ; on_picture_save value 661 | 662 | # Command to be executed when a motion frame is detected (default: none) 663 | ; on_motion_detected value 664 | 665 | # Command to be executed when motion in a predefined area is detected 666 | # Check option 'area_detect'. (default: none) 667 | ; on_area_detected value 668 | 669 | # Command to be executed when a movie file (.mpg|.avi) is created. (default: none) 670 | # To give the filename as an argument to a command append it with %f 671 | ; on_movie_start value 672 | 673 | # Command to be executed when a movie file (.mpg|.avi) is closed. (default: none) 674 | # To give the filename as an argument to a command append it with %f 675 | ; on_movie_end value 676 | 677 | # Command to be executed when a camera can't be opened or if it is lost 678 | # NOTE: There is situations when motion don't detect a lost camera! 679 | # It depends on the driver, some drivers dosn't detect a lost camera at all 680 | # Some hangs the motion thread. Some even hangs the PC! (default: none) 681 | ; on_camera_lost value 682 | 683 | ##################################################################### 684 | # Common Options for database features. 685 | # Options require database options to be active also. 686 | ##################################################################### 687 | 688 | # Log to the database when creating motion triggered picture file (default: on) 689 | ; sql_log_picture on 690 | 691 | # Log to the database when creating a snapshot image file (default: on) 692 | ; sql_log_snapshot on 693 | 694 | # Log to the database when creating motion triggered movie file (default: off) 695 | ; sql_log_movie off 696 | 697 | # Log to the database when creating timelapse movies file (default: off) 698 | ; sql_log_timelapse off 699 | 700 | # SQL query string that is sent to the database 701 | # Use same conversion specifiers has for text features 702 | # Additional special conversion specifiers are 703 | # %n = the number representing the file_type 704 | # %f = filename with full path 705 | # Default value: 706 | # Create tables : 707 | ## 708 | # Mysql 709 | # CREATE TABLE security (camera int, filename char(80) not null, frame int, file_type int, time_stamp timestamp(14), event_time_stamp timestamp(14)); 710 | # 711 | # Postgresql 712 | # CREATE TABLE security (camera int, filename char(80) not null, frame int, file_type int, time_stamp timestamp without time zone, event_time_stamp timestamp without time zone); 713 | # 714 | # insert into security(camera, filename, frame, file_type, time_stamp, text_event) values('%t', '%f', '%q', '%n', '%Y-%m-%d %T', '%C') 715 | ; sql_query insert into security(camera, filename, frame, file_type, time_stamp, event_time_stamp) values('%t', '%f', '%q', '%n', '%Y-%m-%d %T', '%C') 716 | 717 | 718 | ############################################################ 719 | # Database Options 720 | ############################################################ 721 | 722 | # database type : mysql, postgresql, sqlite3 (default : not defined) 723 | ; database_type value 724 | 725 | # database to log to (default: not defined) 726 | # for sqlite3, the full path and name for the database. 727 | ; database_dbname value 728 | 729 | # The host on which the database is located (default: localhost) 730 | ; database_host value 731 | 732 | # User account name for database (default: not defined) 733 | ; database_user value 734 | 735 | # User password for database (default: not defined) 736 | ; database_password value 737 | 738 | # Port on which the database is located 739 | # mysql 3306 , postgresql 5432 (default: not defined) 740 | ; database_port value 741 | 742 | # Database wait time in milliseconds for locked database to 743 | # be unlocked before returning database locked error (default 0) 744 | ; database_busy_timeout 0 745 | 746 | 747 | 748 | ############################################################ 749 | # Video Loopback Device (vloopback project) 750 | ############################################################ 751 | 752 | # Output images to a video4linux loopback device 753 | # The value '-' means next available (default: not defined) 754 | ; video_pipe value 755 | 756 | # Output motion images to a video4linux loopback device 757 | # The value '-' means next available (default: not defined) 758 | ; motion_video_pipe value 759 | 760 | 761 | ############################################################## 762 | # camera config files - One for each camera. 763 | # Except if only one camera - You only need this config file. 764 | # If you have more than one camera you MUST define one camera 765 | # config file for each camera in addition to this config file. 766 | ############################################################## 767 | 768 | # Remember: If you have more than one camera you must have one 769 | # camera file for each camera. E.g. 2 cameras requires 3 files: 770 | # This motion.conf file AND camera1.conf and camera2.conf. 771 | # Only put the options that are unique to each camera in the 772 | # camera config files. 773 | ; camera /etc/motion/camera1.conf 774 | ; camera /etc/motion/camera2.conf 775 | ; camera /etc/motion/camera3.conf 776 | ; camera /etc/motion/camera4.conf 777 | 778 | 779 | ############################################################## 780 | # Camera config directory - One for each camera. 781 | ############################################################## 782 | # 783 | ; camera_dir /etc/motion/conf.d 784 | 785 | on_picture_save /home/pi/motion-notify/motion-notify.py /home/pi/motion-notify/motion-notify.cfg %f on_picture_save %s %v %n 786 | on_movie_end /home/pi/motion-notify/motion-notify.py /home/pi/motion-notify/motion-notify.cfg %f on_movie_end %s %v %n 787 | on_event_start /home/pi/motion-notify/motion-notify.py /home/pi/motion-notify/motion-notify.cfg None on_event_start %s %v None 788 | #CHANGED 789 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | {project} Copyright (C) {year} {fullname} 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------