#!/usr/bin/python

# pi-timolo - Raspberry Pi Long Duration Timelapse, Motion Tracking, with Low Light Capability
# written by Claude Pageau Jul-2017 (release 7.x)
# This release uses OpenCV to do Motion Tracking.  It requires updated config.py

progVer = "ver 8.10"
__version__ = "8.10"   # May test for version number at a future time

import datetime
import logging
import os
import sys
import subprocess

mypath = os.path.abspath(__file__)  # Find the full path of this python script
baseDir = os.path.dirname(mypath)   # get the path location only (excluding script name)
baseFileName = os.path.splitext(os.path.basename(mypath))[0]
progName = os.path.basename(__file__)
logFilePath = os.path.join(baseDir, baseFileName + ".log")

print("----------------------------------------------------------------------------------------------")
print("%s %s  written by Claude Pageau" %( progName, progVer ))
print("INFO  - Initializing ....")

# Check for that pi camaera module is installed and enabled
camResult = subprocess.check_output("vcgencmd get_camera", shell=True)
camResult = camResult.decode("utf-8")
camResult = camResult.replace("\n", "")
if (camResult.find("0")) >= 0:   # -1 is zero not found. Cam OK
    print("ERROR - Pi Camera Module Not Found %s" % camResult)
    print("        if supported=0 Enable Camera using command sudo raspi-config")
    print("        if detected=0 Check Pi Camera Module is Installed Correctly")
    print("Exiting %s" % progName)
    quit()
else:
    print("INFO  - Pi Camera Module is Enabled and Connected %s" % camResult )

# Check for variable file to import and error out if not found.
configFilePath = os.path.join(baseDir, "config.py")
if not os.path.exists(configFilePath):
    print("ERROR - Cannot Import Configuration Variables. Missing Configuration File %s" % ( configFilePath ))
    quit()
else:
    # Read Configuration variables from config.py file
    print("INFO  - Importing Configuration Variables from File %s" % ( configFilePath ))
    from config import *

# Setup Logging now that variables are imported from config.py
if logDataToFile:
    print("INFO  - Sending Logging Data to %s  (Console Messages Disabled)" %( logFilePath ))
    logging.basicConfig(level=logging.DEBUG,
                    format='%(asctime)s %(levelname)-8s %(funcName)-10s %(message)s',
                    datefmt='%Y-%m-%d %H:%M:%S',
                    filename=logFilePath,
                    filemode='w')
elif verbose:
    print("INFO  - Logging to Console per Variable verbose=True")
    logging.basicConfig(level=logging.DEBUG,
                    format='%(asctime)s %(levelname)-8s %(funcName)-10s %(message)s',
                    datefmt='%Y-%m-%d %H:%M:%S')
else:
    logging.basicConfig(level=logging.CRITICAL,
                    format='%(asctime)s %(levelname)-8s %(funcName)-10s %(message)s',
                    datefmt='%Y-%m-%d %H:%M:%S')

print("INFO  - Loading Python Libraries Please Wait ...")  # import remaining python libraries

try:
    import cv2
except:
    if (sys.version_info > (2, 9)):
        print("ERROR - python3 Failed to import cv2")
        print("        Try installing opencv for python3")
        print("        google for details regarding installing opencv for python3")
    else:
        print("ERROR - python2 Failed to import cv2")
        print("        Try reinstalling per command")
        print("        sudo apt-get install python-opencv")
    print("INFO  - Exiting %s Due to Error" % progName)
    quit()

import glob
import shutil
import time
import math
from threading import Thread
import numpy as np
from PIL import Image
from PIL import ImageFont
from PIL import ImageDraw
from fractions import Fraction

from picamera.array import PiRGBArray
import picamera
from picamera import PiCamera
import picamera.array

if not (sys.version_info > (3, 0)):  # Bypass pyexiv2 if opencv 3 used
    import pyexiv2

#==================================
#      System Variables
# Should not need to be customized
#==================================

SECONDS2MICRO = 1000000    # Used to convert from seconds to microseconds
nightMaxShut = int(nightMaxShutSec * SECONDS2MICRO)  # default=5 sec IMPORTANT- 6 sec works sometimes but occasionally locks RPI and HARD reboot required to clear
darkAdjust = int((SECONDS2MICRO/5.0) * nightDarkAdjust)
testWidth = 128            # width of rgb image stream used for timelapse day/night changes
testHeight = 80            # height of rgb image stream used for timelapse day/night changes
daymode = False            # default should always be False.
motionPath = os.path.join(baseDir, motionDir)  # Store Motion images
motionNumPath = os.path.join(baseDir, motionPrefix + baseFileName + ".dat")  # dat file to save currentCount
motionStreamStopSec = 0.5  # default= 0.5 seconds  Time to close stream thread
timelapsePath = os.path.join(baseDir, timelapseDir)  # Store Time Lapse images
timelapseNumPath = os.path.join(baseDir, timelapsePrefix + baseFileName + ".dat")  # dat file to save currentCount
lockFilePath = os.path.join(baseDir, baseFileName + ".sync")

# Video Stream Settings for motion detection using opencv motion tracking
CAMERA_WIDTH = 640     # width of video stream
CAMERA_HEIGHT = 480    # height of video stream
bigImage = motionTrackQPBigger  # increase size of motionTrackQuickPic image
bigImageWidth = int(CAMERA_WIDTH * bigImage)
bigImageHeight = int(CAMERA_HEIGHT * bigImage)
CAMERA_FRAMERATE = motionTrackFrameRate  # camera framerate
TRACK_TRIG_LEN = motionTrackTrigLen  # Length of track to trigger speed photo
TRACK_TRIG_LEN_MIN = int(motionTrackTrigLen / 4)
TRACK_TRIG_LEN_MAX = int(CAMERA_HEIGHT / 2)  # Set max over triglen allowed half cam height
TRACK_TIMEOUT = motionTrackTimeOut   # Timeout seconds Stops motion tracking when no activity
MIN_AREA = motionTrackMinArea    # OpenCV Contour sq px area must be greater than this.

BLUR_SIZE = 10              # OpenCV setting for Gaussian difference image blur
THRESHOLD_SENSITIVITY = 20  # OpenCV setting for difference image threshold

#-----------------------------------------------------------------------------------------------
class PiVideoStream:
    def __init__(self, resolution=(CAMERA_WIDTH, CAMERA_HEIGHT), framerate=CAMERA_FRAMERATE, rotation=0, hflip=False, vflip=False):
        # initialize the camera and stream
        try:
           self.camera = PiCamera()
        except:
           print("ERROR - PiCamera Already in Use by Another Process")
           print("INFO  - Exit %s" % progName)
           quit()
        self.camera.resolution = resolution
        self.camera.framerate = framerate
        self.camera.hflip = hflip
        self.camera.vflip = vflip
        self.camera.rotation = rotation
        self.rawCapture = PiRGBArray(self.camera, size=resolution)
        self.stream = self.camera.capture_continuous(self.rawCapture,
            format="bgr", use_video_port=True)
        # initialize the frame and the variable used to indicate
        # if the thread should be stopped
        self.frame = None
        self.stopped = False

    def start(self):
        # start the thread to read frames from the video stream
        t = Thread(target=self.update, args=())
        t.daemon = True
        t.start()
        return self

    def update(self):
        # keep looping infinitely until the thread is stopped
        for f in self.stream:
            # grab the frame from the stream and clear the stream in
            # preparation for the next frame
            self.frame = f.array
            self.rawCapture.truncate(0)

            # if the thread indicator variable is set, stop the thread
            # and resource camera resources
            if self.stopped:
                self.stream.close()
                self.rawCapture.close()
                self.camera.close()
                return

    def read(self):
        # return the frame most recently read
        return self.frame

    def stop(self):
        # indicate that the thread should be stopped
        self.stopped = True

#-----------------------------------------------------------------------------------------------
def userMotionCodeHere():
    # Users can put code here that needs to be run prior to taking motion capture images
    # Eg Notify or activate something.

    # User code goes here

    return

#-----------------------------------------------------------------------------------------------
def shut2Sec (shutspeed):
    shutspeedSec = shutspeed/float(SECONDS2MICRO)
    shutstring = str("%.4f") % ( shutspeedSec )
    return shutstring

#-----------------------------------------------------------------------------------------------
def showTime():
    rightNow = datetime.datetime.now()
    currentTime = ("%04d-%02d-%02d %02d:%02d:%02d" % (rightNow.year, rightNow.month, rightNow.day,
    rightNow.hour, rightNow.minute, rightNow.second))
    return currentTime

#-----------------------------------------------------------------------------------------------
def showDots(dotcnt):
    if motionDotsOn:
        if motionTrackOn and verbose:
            dotcnt += 1
            if dotcnt > motionDotsMax + 2:
                print("")
                dotcnt = 0
            elif dotcnt > motionDotsMax:
                print("")
                stime = showTime() + " ."
                sys.stdout.write(stime)
                sys.stdout.flush()
                dotcnt = 0
            else:
                sys.stdout.write('.')
                sys.stdout.flush()
        return dotcnt

#-----------------------------------------------------------------------------------------------
def checkConfig():
    if not motionTrackOn and not timelapseOn:
        logging.warning("Both Motion and Timelapse are turned OFF - motionTrackOn=%s timelapseOn=%s", motionTrackOn, timelapseOn)
        sys.exit(2)

#-----------------------------------------------------------------------------------------------
def displayInfo(motioncount, timelapsecount):
    if verbose:
        print("-------------------------------------- Settings ----------------------------------------------")
        print("Config File .. configName=%s  configTitle=%s" % (configName, configTitle))
        print("")
        print("Image Info ... Size=%ix%i  Prefix=%s  VFlip=%s  HFlip=%s  Rotation=%i  Preview=%s"
              % (imageWidth, imageHeight, imageNamePrefix, imageVFlip, imageHFlip, imageRotation, imagePreview))
        print("               JpegQuality=%i 1=highest 40=lowest" % ( imageJpegQuality ))
        print("   Low Light.. nightTwilightThreshold=%i  nightDarkThreshold=%i  nightBlackThreshold=%i"
                           % ( nightTwilightThreshold, nightDarkThreshold, nightBlackThreshold ))
        print("               nightMaxShutSec=%.2f  nightMaxISO=%i  nightDarkAdjust=%.2f  nightSleepSec=%i"
                           % ( nightMaxShutSec, nightMaxISO, nightDarkAdjust, nightSleepSec ))
        print("   No Shots .. noNightShots=%s   noDayShots=%s" % ( noNightShots, noDayShots ))
        if showDateOnImage:
            print("   Img Text .. On=%s  Bottom=%s (False=Top)  WhiteText=%s (False=Black)  showTextWhiteNight=%s"
                         % ( showDateOnImage, showTextBottom, showTextWhite, showTextWhiteNight ))
            print("               showTextFontSize=%i px height" % (showTextFontSize))
        else:
            print("    No Text .. showDateOnImage=%s  Text on Image is Disabled"  % (showDateOnImage))
        print("")
        if motionTrackOn:
            print("Motion Track.. On=%s  Prefix=%s  MinArea=%i sqpx  TrigLen=%i-%i px  TimeOut=%i sec"
                             % (motionTrackOn, motionPrefix, motionTrackMinArea,
                                                      motionTrackTrigLen, TRACK_TRIG_LEN_MAX, motionTrackTimeOut))
            print("               motionTrackInfo=%s   motionDotsOn=%s"  % ( motionTrackInfo, motionDotsOn ))
            print("   Stream .... size=%ix%i  framerate=%i fps  motionStreamStopSec=%.2f  QuickPic=%s" %
                                  ( CAMERA_WIDTH, CAMERA_HEIGHT, motionTrackFrameRate, motionStreamStopSec, motionTrackQuickPic ))
            print("   Img Path .. motionPath=%s  motionCamSleep=%.2f sec" % (motionPath, motionCamSleep))
            print("   Force ..... forceTimer=%i min (If No Motion)"  % (motionForce/60))
            if motionNumOn:
                print("   Num Seq ... motionNumOn=%s  numRecycle=%s  numStart=%i   numMax=%i  current=%s"
                                   % (motionNumOn, motionNumRecycle, motionNumStart, motionNumMax, motioncount))
                print("   Num Path .. motionNumPath=%s " % (motionNumPath))

            else:
                print("   Date-Time.. motionNumOn=%s  Image Numbering is Disabled" % (motionNumOn))
            if motionQuickTLOn:
                print("   Quick TL .. motionQuickTLOn=%s   motionQuickTLTimer=%i sec  motionQuickTLInterval=%i sec (0=fastest)"
                                   % (motionQuickTLOn, motionQuickTLTimer, motionQuickTLInterval))
            else:
                print("   Quick TL .. motionQuickTLOn=%s  Quick Time Lapse Disabled" % (motionQuickTLOn))
            if motionVideoOn:
                print("   Video ..... motionVideoOn=%s   motionVideoTimer=%i sec  motionVideoFPS=%i (superseded by QuickTL)"
                                   % (motionVideoOn, motionVideoTimer, motionVideoFPS))
            else:
                print("   Video ..... motionVideoOn=%s  Motion Video is Disabled" % (motionVideoOn))
            print("   Sub-Dir ... motionSubDirMaxHours=%i (0-off)  motionSubDirMaxFiles=%i (0=off)" %
                                ( motionSubDirMaxHours, motionSubDirMaxFiles ))
            print("   Recent .... motionRecentMax=%i (0=off)  motionRecentDir=%s" %
                                ( motionRecentMax, motionRecentDir ))
        else:
            print("Motion ....... motionTrackOn=%s  Motion Detection is Disabled)" % (motionTrackOn))
        print("")
        if timelapseOn:
            print("Time Lapse ... On=%s  Prefix=%s   Timer=%i sec   timelapseExitSec=%i (0=Continuous)"
                        % (timelapseOn, timelapsePrefix, timelapseTimer, timelapseExitSec))
            print("               timelapseMaxFiles=%i" % ( timelapseMaxFiles ))
            print("   Img Path .. timelapsePath=%s  timelapseCamSleep=%.2f sec" % (timelapsePath, timelapseCamSleep))
            if timelapseNumOn:
                print("   Num Seq ... On=%s  numRecycle=%s  numStart=%i   numMax=%i  current=%s"
                          % (timelapseNumOn, timelapseNumRecycle, timelapseNumStart, timelapseNumMax, timelapsecount))
                print("   Num Path .. numPath=%s" % (timelapseNumPath))
            else:
                print("   Date-Time.. motionNumOn=%s  Numbering Disabled" % (timelapseNumOn))
            print("   Sub-Dir ... timelapseSubDirMaxHours=%i (0=off)  timelapseSubDirMaxFiles=%i (0=off)" %
                                ( timelapseSubDirMaxHours, timelapseSubDirMaxFiles ))
            print("   Recent .... timelapseRecentMax=%i (0=off)  timelapseRecentDir=%s" %
                                ( timelapseRecentMax, timelapseRecentDir ))
            if createLockFile:
                print("")
                print("gdrive Sync .. On=%s  Path=%s  Note: syncs for motion images only." % (createLockFile, lockFilePath))
        else:
            print("Time Lapse ... timelapseOn=%s  Timelapse is Disabled" % (timelapseOn))
        print("")
        if spaceTimerHrs > 0:   # Check if disk mgmnt is enabled
            print("Disk Space  .. Enabled - Manage Target Free Disk Space. Delete Oldest %s Files if Required" % (spaceFileExt))
            print("               Check Every spaceTimerHrs=%i (0=off)  Target spaceFreeMB=%i (min=100 MB)  spaceFileExt=%s" %
                                 (spaceTimerHrs, spaceFreeMB, spaceFileExt))
            print("               Delete Oldest spaceFileExt=%s  spaceMediaDir=%s" %
                                 ( spaceFileExt, spaceMediaDir))
        else:
            print("Disk Space  .. spaceTimerHrs=%i (Disabled) - Manage Target Free Disk Space. Delete Oldest %s Files" %
                                   ( spaceTimerHrs, spaceFileExt))
            print("            .. Check Every spaceTimerHrs=%i (0=Off)  Target spaceFreeMB=%i (min=100 MB)" %
                                             ( spaceTimerHrs, spaceFreeMB))
        print("")
        print("Logging ...... verbose=%s (True=Enabled False=Disabled)" % ( verbose ))
        print("   Log Path .. logDataToFile=%s  logFilePath=%s" % ( logDataToFile, logFilePath ))
        print("------------------------------------ Log Activity --------------------------------------------")
    checkConfig()

#-----------------------------------------------------------------------------------------------
def subDirLatest(directory): # Scan for directories and return most recent
    dirList = [ name for name in os.listdir(directory) if os.path.isdir(os.path.join(directory, name)) ]
    if len(dirList) > 0:
        lastSubDir = sorted(dirList)[-1]
        lastSubDir = os.path.join(directory, lastSubDir)
    else:
        lastSubDir = directory
    return lastSubDir

#-----------------------------------------------------------------------------------------------
def subDirCreate(directory, prefix):
    now = datetime.datetime.now()
    # Specify folder naming
    subDirName = ('%s%d-%02d-%02d-%02d%02d' % (prefix, now.year, now.month, now.day, now.hour, now.minute))
    subDirPath = os.path.join(directory, subDirName)
    if not os.path.exists(subDirPath):
        try:
            os.makedirs(subDirPath)
        except OSError as err:
            logging.error('Cannot Create Directory %s - %s, using default location.', subDirPath, err)
            subDirPath = directory
        else:
            logging.info('Created %s', subDirPath)
    else:
        subDirPath = directory
    return subDirPath

#-----------------------------------------------------------------------------------------------
def subDirCheckMaxFiles(directory, filesMax):  # Count number of files in a folder path
    fileList = glob.glob(directory + '/*jpg')
    count = len(fileList)
    if count > filesMax:
        makeNewDir = True
        dotCount = showDots(motionDotsMax + 2)
        logging.info('Total Files in %s Exceeds %i ' % ( directory, filesMax ))
    else:
        makeNewDir = False
    return makeNewDir

#-----------------------------------------------------------------------------------------------
def subDirCheckMaxHrs(directory, hrsMax, prefix):   # Note to self need to add error checking
    # extract the date-time from the directory name
    dirName = os.path.split(directory)[1]   # split dir path and keep dirName
    dirStr = dirName.replace(prefix,'')   # remove prefix from dirName so just date-time left
    dirDate = datetime.datetime.strptime(dirStr, "%Y-%m-%d-%H:%M")  # convert string to datetime
    rightNow = datetime.datetime.now()   # get datetime now
    diff =  rightNow - dirDate           # get time difference between dates
    days, seconds = diff.days, diff.seconds
    dirAgeHours = days * 24 + seconds // 3600  # convert to hours
    if dirAgeHours > hrsMax:   # See if hours are exceeded
        makeNewDir = True
        dotCount = showDots(motionDotsMax + 2)
        logging.info('MaxHrs %i Exceeds %i for %s' % ( dirAgeHours, hrsMax, directory ))
    else:
        makeNewDir = False
    return makeNewDir

#-----------------------------------------------------------------------------------------------
def subDirChecks(maxHours, maxFiles, directory, prefix):
    # Check if motion SubDir needs to be created
    if maxHours < 1 and maxFiles < 1:  # No Checks required
        # logging.info('No sub-folders Required in %s', directory)
        subDirPath = directory
    else:
        subDirPath = subDirLatest(directory)
        if subDirPath == directory:   # No subDir Found
            logging.info('No sub folders Found in %s' % directory)
            subDirPath = subDirCreate(directory, prefix)
        elif ( maxHours > 0 and maxFiles < 1 ):   # Check MaxHours Folder Age Only
            if subDirCheckMaxHrs(subDirPath, maxHours, prefix):
                subDirPath = subDirCreate(directory, prefix)
        elif ( maxHours < 1 and maxFiles > 0):   # Check Max Files Only
            if subDirCheckMaxFiles(subDirPath, maxFiles):
                subDirPath = subDirCreate(directory, prefix)
        elif maxHours > 0 and maxFiles > 0:   # Check both Max Files and Age
            if subDirCheckMaxHrs(subDirPath, maxHours, prefix):
                if subDirCheckMaxFiles(subDirPath, maxFiles):
                    subDirPath = subDirCreate(directory, prefix)
                else:
                    logging.info('MaxFiles Not Exceeded in %s', subDirPath)
    os.path.abspath(subDirPath)
    return subDirPath

#-----------------------------------------------------------------------------------------------
def checkImagePath():
    # Checks for image folders and creates them if they do not already exist.
    if motionTrackOn:
        if not os.path.isdir(motionPath):
            logging.info("Create Motion Image Folder %s", motionPath)
            os.makedirs(motionPath)
            if os.path.exists(motionNumPath):
               logging.info("Delete Motion dat file %s", motionNumPath)
               os.remove(motionNumPath)
    if timelapseOn:
        if not os.path.isdir(timelapsePath):
            logging.info("Create TimeLapse Image Folder %s", timelapsePath)
            os.makedirs(timelapsePath)
            if os.path.exists(timelapseNumPath):
               logging.info("Delete TimeLapse dat file %s", timelapseNumPath)
               os.remove(timelapseNumPath)

    # Check for Recent Image Folders and create if they do not already exist.
    if motionRecentMax > 0:
        if not os.path.isdir(motionRecentDir):
            logging.info("Create Motion Recent Folder %s", motionRecentDir)
            try:
                os.makedirs(motionRecentDir)
            except OSError as err:
                logging.error('Failed to Create %s - %s', motionRecentDir, err)

    if timelapseRecentMax > 0:
        if not os.path.isdir(timelapseRecentDir):
            logging.info("Create TimeLapse Recent Folder %s", timelapseRecentDir)
            try:
                os.makedirs(timelapseRecentDir)
            except OSError as err:
                logging.error('Failed to Create %s - %s', timelapseRecentDir, err)

#-----------------------------------------------------------------------------------------------
def deleteOldFiles(maxFiles, dirPath, prefix):
    # Delete Oldest files gt or eq to maxfiles that match filename prefix
    try:
        fileList = sorted(glob.glob(os.path.join(dirPath, prefix + '*')), key=os.path.getmtime)
    except OSError as err:
        logging.error('Problem Reading Directory %s - %s', dirPath, err)
    else:
        while len(fileList) >= maxFiles:
            oldest = fileList[0]
            oldestFile = oldest
            try:   # Remove oldest file in recent folder
                fileList.remove(oldest)
                os.remove(oldestFile)
            except OSError as err:
                logging.error('Cannot Remove %s - %s', oldestFile, err)

#-----------------------------------------------------------------------------------------------
def saveRecent(recentMax, recentDir, filename, prefix):
    # save specified most recent files (timelapse and/or motion) in recent subfolder
    deleteOldFiles(recentMax, recentDir, prefix)
    try:    # Copy image file to recent folder
        shutil.copy(filename, recentDir)
    except OSError as err:
        logging.error('Copy from %s to %s - %s', filename, oldestFile, err)

#-----------------------------------------------------------------------------------------------
def filesToDelete(mediaDirPath, extension=imageFormat):
    return sorted(
        (os.path.join(dirname, filename)
        for dirname, dirnames, filenames in os.walk(mediaDirPath)
        for filename in filenames
        if filename.endswith(extension)),
        key=lambda fn: os.stat(fn).st_mtime, reverse=True)

#-----------------------------------------------------------------------------------------------
def freeSpaceUpTo(spaceFreeMB, mediaDir, extension=imageFormat):
    # Walks mediaDir and deletes oldest files until spaceFreeMB is achieved
    # Use with Caution
    mediaDirPath = os.path.abspath(mediaDir)
    if os.path.isdir(mediaDirPath):
        MB2Bytes = 1048576  # Conversion from MB to Bytes
        targetFreeBytes = spaceFreeMB * MB2Bytes
        fileList = filesToDelete(mediaDir, extension)
        totFiles = len(fileList)
        delcnt = 0
        logging.info('Session Started')
        while fileList:
            statv = os.statvfs(mediaDirPath)
            availFreeBytes = statv.f_bfree*statv.f_bsize
            if availFreeBytes >= targetFreeBytes:
                break
            filePath = fileList.pop()
            try:
                os.remove(filePath)
            except OSError as err:
                logging.error('Del Failed %s', filePath)
                logging.error('Error: %s', err)
            else:
                delcnt += 1
                logging.info('Del %s', filePath)
                logging.info('Target=%i MB  Avail=%i MB  Deleted %i of %i Files ',
                                   targetFreeBytes / MB2Bytes, availFreeBytes / MB2Bytes, delcnt, totFiles )
                if delcnt > totFiles / 4:  # Avoid deleting more than 1/4 of files at one time
                    logging.warning('Max Deletions Reached %i of %i', delcnt, totFiles)
                    logging.warning('Deletions Restricted to 1/4 of total files per session.')
                    break
        logging.info('Session Ended')
    else:
        logging.error('Directory Not Found - %s', mediaDirPath)

#-----------------------------------------------------------------------------------------------
def freeDiskSpaceCheck(lastSpaceCheck):
    if spaceTimerHrs > 0:   # Check if disk free space timer hours is enabled
        # See if it is time to do disk clean-up check
        if ((datetime.datetime.now() - lastSpaceCheck).total_seconds() > spaceTimerHrs * 3600):
            lastSpaceCheck = datetime.datetime.now()
            if spaceFreeMB < 100:   # set freeSpaceMB to reasonable value if too low
                diskFreeMB = 100
            else:
                diskFreeMB = spaceFreeMB
            logging.info('spaceTimerHrs=%i  diskFreeMB=%i  spaceMediaDir=%s spaceFileExt=%s',
                           spaceTimerHrs, diskFreeMB, spaceMediaDir, spaceFileExt)
            freeSpaceUpTo(diskFreeMB, spaceMediaDir, spaceFileExt)
    return lastSpaceCheck

#-----------------------------------------------------------------------------------------------
def getCurrentCount(numberpath, numberstart):
    # Create a .dat file to store currentCount or read file if it already Exists
    # Create numberPath file if it does not exist
    if not os.path.exists(numberpath):
        logging.info("Creating New File %s numberstart= %s", numberpath, numberstart)
        open(numberpath, 'w').close()
        f = open(numberpath, 'w+')
        f.write(str(numberstart))
        f.close()
    # Read the numberPath file to get the last sequence number
    with open(numberpath, 'r') as f:
        writeCount = f.read()
        f.closed
        try:
            numbercounter = int(writeCount)
        except ValueError:   # Found Corrupt dat file since cannot convert to integer
            # Try to determine if this is motion or timelapse
            if numberpath.find(motionPrefix) > 0:
                filePath = motionPath + "/*" + imageFormat
                fprefix = motionPath + motionPrefix + imageNamePrefix
            else:
                filePath = timelapsePath + "/*" + imageFormat
                fprefix = timelapsePath + timelapsePrefix + imageNamePrefix
            try:
               # Scan image folder for most recent file and try to extract numbercounter
                newest = max(glob.iglob(filePath), key=os.path.getctime)
                writeCount = newest[len(fprefix)+1:newest.find(imageFormat)]
            except:
                writeCount = numberstart
            try:
                numbercounter = int(writeCount)+1
            except ValueError:
                numbercounter = numberstart
            logging.error("Invalid Data in %s Reset Counter to %s", numberpath, numbercounter)

        f = open(numberpath, 'w+')
        f.write(str(numbercounter))
        f.close()
        f = open(numberpath, 'r')
        writeCount = f.read()
        f.closed
        numbercounter = int(writeCount)
    return numbercounter

#-----------------------------------------------------------------------------------------------
def writeTextToImage(imagename, datetoprint, daymode):
    # function to write date/time stamp directly on top or bottom of images.
    if showTextWhite:
        FOREGROUND = ( 255, 255, 255 )  # rgb settings for white text foreground
        textColour = "White"
    else:
        FOREGROUND = ( 0, 0, 0 )  # rgb settings for black text foreground
        textColour = "Black"
        if showTextWhiteNight and ( not daymode):
            FOREGROUND = ( 255, 255, 255 )  # rgb settings for black text foreground
            textColour = "White"

    img = cv2.imread(imagename)
    height, width, channels = img.shape
    # centre text and compensate for graphics text being wider
    x = int((width/2) - (len(imagename)*2))
    if showTextBottom:
        y = (height - 50)  # show text at bottom of image
    else:
        y = 10  # show text at top of image

    TEXT = imageNamePrefix + datetoprint
    font_path = '/usr/share/fonts/truetype/freefont/FreeSansBold.ttf'
    font = ImageFont.truetype(font_path, showTextFontSize, encoding='unic')
    text = TEXT.decode('utf-8')
    img = Image.open(imagename)
    
    try:  # Read exif data since ImageDraw does not save this metadata bypass if python3
        metadata = pyexiv2.ImageMetadata(imagename)
        metadata.read()
    except:
        pass

    draw = ImageDraw.Draw(img)      
    # draw.text((x, y),"Sample Text",(r,g,b))
    draw.text(( x, y ), text, FOREGROUND, font=font)
    img.save(imagename)
    
    try:   # bypass writing exif data if running under python3.
        metadata.write()    # Write previously saved exif data to image file
    except:
        pass
        
    logging.info("Added %s Text [ %s ]", textColour, datetoprint)
    logging.info("%s" % imagename)

#-----------------------------------------------------------------------------------------------
def postImageProcessing(numberon, counterstart, countermax, counter, recycle, counterpath, filename, daymode):
    # If required process text to display directly on image
    if (not motionVideoOn):
        rightNow = datetime.datetime.now()
        if showDateOnImage:
            dateTimeText = ("%04d%02d%02d_%02d:%02d:%02d"
                         % (rightNow.year, rightNow.month, rightNow.day, rightNow.hour, rightNow.minute, rightNow.second))
            if numberon:
                counterStr = "%i    "  % ( counter )
                imageText =  counterStr + dateTimeText
            else:
                imageText = dateTimeText
            # Now put the imageText on the current image
            writeTextToImage(filename, imageText, daymode)
        if createLockFile and motionTrackOn:
            createSyncLockFile(filename)
    # Process currentCount for next image if number sequence is enabled
    if numberon:
        counter += 1
        if countermax > 0:
            if (counter > counterstart + countermax):
                if recycle:
                    counter = counterstart
                else:
                    logging.info("Exceeded Image Count numberMax=%i" % ( countermax ))
                    logging.info("Exiting %s" % progName)
                    sys.exit(2)
        # write next image counter number to dat file
        writeCount = str(counter)
        if not os.path.exists(counterpath):
            logging.info("Create New Counter File writeCount=%s %s", writeCount, counterpath)
            open(counterpath, 'w').close()
        f = open(counterpath, 'w+')
        f.write(str(writeCount))
        f.close()
        logging.info("Next Counter=%s %s", writeCount, counterpath)
    return counter

#-----------------------------------------------------------------------------------------------
def getVideoName(path, prefix, numberon, counter):
    # build image file names by number sequence or date/time
    if numberon:
        if motionVideoOn or videoRepeatOn:
            filename = os.path.join(path, prefix + str(counter) + ".h264")
    else:
        if motionVideoOn or videoRepeatOn:
            rightNow = datetime.datetime.now()
            filename = ("%s/%s%04d%02d%02d-%02d%02d%02d.h264"
                     % ( path, prefix ,rightNow.year, rightNow.month, rightNow.day, rightNow.hour, rightNow.minute, rightNow.second))
    return filename

#-----------------------------------------------------------------------------------------------
def getImageName(path, prefix, numberon, counter):
    # build image file names by number sequence or date/time
    if numberon:
        filename = os.path.join(path, prefix + str(counter) + imageFormat)
    else:
        rightNow = datetime.datetime.now()
        filename = ("%s/%s%04d%02d%02d-%02d%02d%02d%s"
                 % ( path, prefix ,rightNow.year, rightNow.month, rightNow.day,
                    rightNow.hour, rightNow.minute, rightNow.second, imageFormat))
    return filename


#-----------------------------------------------------------------------------------------------
def takeTrackQuickPic(image, filename):
    big_image = cv2.resize(image, (bigImageWidth, bigImageHeight))
    cv2.imwrite(filename, big_image)
    logging.info("Saved %ix%i Image to %s", bigImageWidth, bigImageHeight, filename)

#-----------------------------------------------------------------------------------------------
def takeDayImage(filename, cam_sleep_time):
    # Take a Day image using exp=auto and awb=auto
    with picamera.PiCamera() as camera:
        camera.resolution = (imageWidth, imageHeight)
        camera.framerate = 80
        camera.vflip = imageVFlip
        camera.hflip = imageHFlip
        camera.rotation = imageRotation # Valid values 0, 90, 180, 270
        # Day Automatic Mode
        camera.exposure_mode = 'auto'
        camera.awb_mode = 'auto'
        time.sleep(cam_sleep_time)   # use motion or TL camera sleep to get AWB
        if imagePreview:
            camera.start_preview()
        if imageFormat == ".jpg" :
            camera.capture(filename, use_video_port=useVideoPort, format='jpeg',quality=imageJpegQuality)
        else:
            camera.capture(filename, use_video_port=useVideoPort)
        camera.close()
    logging.info("camSleepSec=%.2f exp=auto awb=auto Size=%ix%i "
              % ( cam_sleep_time, imageWidth, imageHeight ))
    if not showDateOnImage:   # showDateOnImage displays FilePath so avoid showing twice
        logging.info("FilePath  %s" % (filename))

#-----------------------------------------------------------------------------------------------
def getShut(pxAve):
    px = pxAve + 1  # avoid division by zero
    offset = nightMaxShut - ((nightMaxShut / float(nightDarkThreshold) * px))
    brightness = offset * (1/float(nightDarkAdjust))
    shut = (nightMaxShut * (1 / float(px))) + brightness # hyperbolic curve + brightness adjust
    return int(shut)

#-----------------------------------------------------------------------------------------------
def takeNightImage(filename):
    # Take low light Twilight or Night image
    dayStream = getStreamImage(True)  # Get a day image stream to calc pixAve below
    with picamera.PiCamera() as camera:
        time.sleep(1.5)  # Wait for camera to warm up to reduce green tint images
        camera.resolution = (imageWidth, imageHeight)
        camera.vflip = imageVFlip
        camera.hflip = imageHFlip
        camera.rotation = imageRotation # valid values 0, 90, 180, 270
        dayPixAve = getStreamPixAve(dayStream)
        # Format common settings string
        settings = ("camSleepSec=%i MaxISO=%i Size=%ix%i"
                 % (nightSleepSec, nightMaxISO, imageWidth, imageHeight))

        if dayPixAve >= nightDarkThreshold:  # Twilight so use variable framerate_range
            logging.info("TwilightThresh=%i/%i shutSec=range %s"
                      % ( dayPixAve, nightTwilightThreshold, settings ))
            camera.framerate_range = (Fraction(1, 6), Fraction(30, 1))
            time.sleep(2) # Give camera time to measure AWB
            camera.iso = nightMaxISO
        else:
            camera.framerate = Fraction(1, 6) # Set the framerate to a fixed value
            time.sleep(1)  # short wait to allow framerate to settle
            if dayPixAve <= nightBlackThreshold:  # Black (Very Low Light) so Use Max Settings
                camShut = nightMaxShut
                logging.info("BlackThresh=%i/%i shutSec=%s %s"
                          % ( dayPixAve, nightBlackThreshold, shut2Sec(camShut), settings ))
            else:
                # Dark so calculate camShut exposure time based on dayPixAve light curve + brightness
                camShut = getShut(dayPixAve)
                if camShut > nightMaxShut:
                    camShut = nightMaxShut
                logging.info("DarkThresh=%i/%i shutSec=%s %s"
                          % ( dayPixAve, nightDarkThreshold, shut2Sec(camShut), settings ))
            camera.shutter_speed = camShut  # Set the shutter for long exposure
            camera.iso = nightMaxISO   # Set the ISO to a fixed value for long exposure
            time.sleep(nightSleepSec)  # Give camera a long time to calc Night Settings
        if imageFormat == ".jpg" :
            camera.capture(filename,format='jpeg',quality=imageJpegQuality)
        else:
            camera.capture(filename)
        camera.close()
    if not showDateOnImage:  # showDateOnImage displays FilePath so avoid showing twice
        logging.info("FilePath %s" % filename)

#-----------------------------------------------------------------------------------------------
def takeQuickTimeLapse(moPath, imagePrefix, motionNumOn, motionNumCount, daymode, motionNumPath):
    logging.info("motion Quick Time Lapse for %i sec every %i sec" % (motionQuickTLTimer, motionQuickTLInterval))

    checkTimeLapseTimer = datetime.datetime.now()
    keepTakingImages = True
    filename = getImageName(moPath, imagePrefix, motionNumOn, motionNumCount)
    while keepTakingImages:
        yield filename
        rightNow = datetime.datetime.now()
        timelapseDiff = (rightNow - checkTimeLapseTimer).total_seconds()
        if timelapseDiff > motionQuickTLTimer:
            keepTakingImages=False
        else:
            motionNumCount = postImageProcessing(motionNumOn, motionNumStart, motionNumMax, motionNumCount, motionNumRecycle, motionNumPath, filename, daymode)
            filename = getImageName(moPath, imagePrefix, motionNumOn, motionNumCount)
            time.sleep(motionQuickTLInterval)

#-----------------------------------------------------------------------------------------------
def takeVideo(filename, duration, fps=30):
    # Take a short motion video if required
    logging.info("Start: Size %ix%i for %i sec to %s" % (imageWidth, imageHeight, duration, filename))
    if motionVideoOn or videoRepeatOn:
        with picamera.PiCamera() as camera:
            camera.resolution = (imageWidth, imageHeight)
            camera.vflip = imageVFlip
            camera.hflip = imageHFlip
            camera.rotation = imageRotation # You can also use imageVFlip and imageHFlip variables
            camera.framerate = fps
            if showDateOnImage:
                rightNow = datetime.datetime.now()
                dateTimeText = (" Started at %04d-%02d-%02d %02d:%02d:%02d "
                             % (rightNow.year, rightNow.month, rightNow.day, rightNow.hour, rightNow.minute, rightNow.second))
                camera.annotate_text_size = showTextFontSize
                camera.annotate_foreground = picamera.Color('black')
                camera.annotate_background = picamera.Color('white')
                camera.annotate_text = dateTimeText
            camera.start_recording(filename)
            camera.wait_recording(duration)
            camera.stop_recording()
            camera.close()
        # This creates a subprocess that runs convid.sh with the filename as a parameter
        try:
            convid = "%s/convid.sh %s" % ( baseDir, filename )
            proc = subprocess.Popen(convid, shell=True, stdin=None, stdout=None,
                                                        stderr=None, close_fds=True)
        except IOError:
            logging.error("subprocess %s failed" %s ( convid ))
        createSyncLockFile(filename)

#-----------------------------------------------------------------------------------------------
def createSyncLockFile(imagefilename):
    # If required create a lock file to indicate file(s) to process
    if createLockFile:
        if not os.path.exists(lockFilePath):
            open(lockFilePath, 'w').close()
            logging.info("Create gdrive sync.sh Lock File %s", lockFilePath)
        rightNow = datetime.datetime.now()
        now = ("%04d%02d%02d-%02d%02d%02d"
            % ( rightNow.year, rightNow.month, rightNow.day, rightNow.hour, rightNow.minute, rightNow.second ))
        filecontents = now + " createSyncLockFile - "  + imagefilename + " Ready to sync using sudo ./sync.sh command."
        f = open(lockFilePath, 'w+')
        f.write(filecontents)
        f.close()

#-----------------------------------------------------------------------------------------------
def trackPoint(grayimage1, grayimage2):
    movementCenterPoint = []   # initialize list of movementCenterPoints
    biggestArea = MIN_AREA
    # Get differences between the two greyed images
    differenceimage = cv2.absdiff( grayimage1, grayimage2 )
    # Blur difference image to enhance motion vectors
    differenceimage = cv2.blur( differenceimage,(BLUR_SIZE,BLUR_SIZE ))
    # Get threshold of blurred difference image based on THRESHOLD_SENSITIVITY variable
    retval, thresholdimage = cv2.threshold( differenceimage, THRESHOLD_SENSITIVITY, 255, cv2.THRESH_BINARY )
    try:
        thresholdimage, contours, hierarchy = cv2.findContours( thresholdimage, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE )
    except:
        contours, hierarchy = cv2.findContours( thresholdimage, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE )

    if contours:
        movement = False
        for c in contours:
            cArea = cv2.contourArea(c)
            if cArea > biggestArea:
                biggestArea = cArea
                ( x, y, w, h ) = cv2.boundingRect(c)
                cx = int(x + w/2)   # x centerpoint of contour
                cy = int(y + h/2)   # y centerpoint of contour
                movementCenterPoint = [cx,cy]
    return movementCenterPoint

#-----------------------------------------------------------------------------------------------
def trackDistance(mPoint1, mPoint2):
    x1, y1 = mPoint1
    x2, y2 = mPoint2
    trackLen = abs(math.hypot(x2 - x1, y2 - y1))
    return trackLen

#-----------------------------------------------------------------------------------------------
def getStreamImage(isDay):
    # Capture an image stream to memory based on daymode
    with picamera.PiCamera() as camera:
        camera.resolution = (testWidth, testHeight)
        with picamera.array.PiRGBArray(camera) as stream:
            if isDay:
                camera.exposure_mode = 'auto'
                camera.awb_mode = 'auto'
                time.sleep(motionCamSleep)   # sleep so camera can get AWB
            else:
                # use variable framerate_range for Low Light motion image stream
                camera.framerate_range = (Fraction(1, 6), Fraction(30, 1))
                time.sleep(2) # Give camera time to measure AWB
                camera.iso = nightMaxISO
            camera.capture(stream, format='rgb', use_video_port=useVideoPort)
            camera.close()
            return stream.array

#-----------------------------------------------------------------------------------------------
def getStreamPixAve(streamData):
    # Calculate the average pixel values for the specified stream (used for determining day/night or twilight conditions)
    pixAverage = int(np.average(streamData[...,1]))
    return pixAverage

#-----------------------------------------------------------------------------------------------
def checkIfDay(currentDayMode, image):
    # Try to determine if it is day, night or twilight.
    dayPixAverage = 0
    if currentDayMode:
        dayPixAverage = getStreamPixAve(image)
    else:
        dayStream = getStreamImage(True)
        dayPixAverage = getStreamPixAve(dayStream)

    if dayPixAverage > nightTwilightThreshold:
        currentDayMode = True
    else:
        currentDayMode = False
#   logging.info("daymode=%s dayPixAverage=%i" % (currentDayMode, dayPixAverage))
    return currentDayMode

#-----------------------------------------------------------------------------------------------
def checkIfDayStream(currentDayMode, image):
    # Try to determine if it is day, night or twilight.
    dayPixAverage = 0
    dayPixAverage = getStreamPixAve(image)

    if dayPixAverage > nightTwilightThreshold:
        currentDayMode = True
    else:
        currentDayMode = False
#   logging.info("daymode=%s dayPixAverage=%i" % (currentDayMode, dayPixAverage))
    return currentDayMode

#-----------------------------------------------------------------------------------------------
def timeToSleep(currentDayMode):
    if noNightShots:
       if currentDayMode:
          sleepMode=False
       else:
          sleepMode=True
    elif noDayShots:
        if currentDayMode:
           sleepMode=True
        else:
           sleepMode=False
    else:
        sleepMode=False
    return sleepMode

#-----------------------------------------------------------------------------------------------
def checkForTimelapse (timelapseStart):
    # Check if timelapse timer has expired
    rightNow = datetime.datetime.now()
    timeDiff = ( rightNow - timelapseStart).total_seconds()
    if timeDiff > timelapseTimer:
        timelapseStart = rightNow
        timelapseFound = True
    else:
        timelapseFound = False
    return timelapseFound

#-----------------------------------------------------------------------------------------------
def dataLogger():
    # Replace main() with this function to log day/night pixAve to a file.
    # Note variable logDataToFile must be set to True in config.py
    # You may want to delete pi-timolo.log to clear old data.
    print("dataLogger - One Moment Please ....")
    while True:
        dayStream = getStreamImage(True)
        dayPixAverage = getStreamPixAve(dayStream)
        nightStream = getStreamImage(False)
        nightPixAverage = getStreamPixAve(nightStream)
        logging.info("nightPixAverage=%i dayPixAverage=%i nightTwilightThreshold=%i nightDarkThreshold=%i "
                  % (nightPixAverage, dayPixAverage, nightTwilightThreshold, nightDarkThreshold))
        time.sleep(1)
    return

#-----------------------------------------------------------------------------------------------
def timolo():
    # Main program initialization and logic loop
    dotCount = 0   # Counter for showDots() display if not motion found (shows system is working)
    checkImagePath()
    timelapseNumCount = 0
    motionNumCount = 0
    tlstr = ""  # Used to display if timelapse is selected
    mostr = ""  # Used to display if motion is selected
    moCnt = "non"
    tlCnt = "non"
    daymode = False       # Used to keep track of night and day based on dayPixAve
    forceMotion = False   # Used for forcing a motion image if no motion for motionForce time exceeded
    motionFound = False

    if spaceTimerHrs > 0:
        lastSpaceCheck = datetime.datetime.now()

    if timelapseOn:
        tlstr = "TimeLapse"
        # Check if timelapse subDirs reqd and create one if non exists
        tlPath = subDirChecks( timelapseSubDirMaxHours, timelapseSubDirMaxFiles,
                               timelapseDir, timelapsePrefix)
        if timelapseNumOn:
            timelapseNumCount = getCurrentCount(timelapseNumPath, timelapseNumStart)
            tlCnt = str(timelapseNumCount)

    if motionTrackOn:
        mostr = "Motion Detect"
        # Check if motion subDirs reqd and create one if required if non exists
        moPath = subDirChecks( motionSubDirMaxHours, motionSubDirMaxFiles,
                               motionDir, motionPrefix)
        if motionNumOn:
            motionNumCount = getCurrentCount(motionNumPath, motionNumStart)
            moCnt = str(motionNumCount)

        trackLen = 0.0
        trackTimeout = time.time()
        trackTimer = TRACK_TIMEOUT
        startPos = []
        startTrack = False
        logging.info("Start PiVideoStream ....")
        vs = PiVideoStream().start()
        vs.camera.rotation = imageRotation
        vs.camera.hflip = imageHFlip
        vs.camera.vflip = imageVFlip
        time.sleep(3)
        image1 = vs.read()
        image2 = vs.read()
        grayimage1 = cv2.cvtColor(image1, cv2.COLOR_BGR2GRAY)
        logging.info("Motion Tracking is On")
        daymode = checkIfDayStream(daymode, image2)
    else:
        image1 = getStreamImage(True).astype(float)  #All functions should still work with float instead of int - just takes more memory
        image2 = getStreamImage(daymode)  # initialise image2 to use in main loop
        daymode = checkIfDay(daymode, image1)

    logging.info("daymode=%s  motionDotsOn=%s " % ( daymode, motionDotsOn ))

    if timelapseOn and motionTrackOn:
        tlstr = " and " + tlstr

    if videoRepeatOn:
        mostr = "Video Repeat"
        tlstr = ""
    displayInfo(moCnt, tlCnt)  # Display config.py settings

    timelapseStart = datetime.datetime.now()
    timelapseExitStart = timelapseStart
    checkMotionTimer = timelapseStart

    if logDataToFile:
        print("")
        print("logDataToFile=%s Logging to Console Disabled." % ( logDataToFile))
        print("Sending Console Messages to %s" % (logFilePath))
        print("Entering Loop for %s%s" % (mostr, tlstr))
    else:
        logging.info("Entering Loop for %s%s  Ready ..." % (mostr, tlstr))

    dotCount = showDots(motionDotsMax)  # reset motion dots
    # Start main program loop here.  Use Ctl-C to exit if run from terminal session.
    while True:
        motionFound = False
        forceMotion = False
        if spaceTimerHrs > 0:  # if required check free disk space and delete older files (jpg)
            lastSpaceCheck = freeDiskSpaceCheck(lastSpaceCheck)

        # use image2 to check daymode as image1 may be average that changes slowly, and image1 may not be updated
        if motionTrackOn:
            if daymode != checkIfDayStream(daymode, image2):
                daymode = not daymode
                image2 = vs.read()
                image1 = image2
            else:
                image2 = vs.read()
        else:
            if daymode != checkIfDay(daymode, image2):  # if daymode has changed, reset background, to avoid false motion trigger
                daymode = not daymode
                image2 = getStreamImage(daymode)  #get new stream
                image1 = image2.astype(float)      #reset background
            else:
                image2 = getStreamImage(daymode)  # This gets the second stream of motion analysis
        rightNow = datetime.datetime.now()   # refresh rightNow time
        if not timeToSleep(daymode):  # Don't take images if noNightShots or noDayShots settings are valid
            if timelapseOn:
                takeTimeLapse = checkForTimelapse(timelapseStart)
                if takeTimeLapse and timelapseExitSec > 0:
                    timelapseStart = datetime.datetime.now()  # Reset timelapse timer
                    if ( datetime.datetime.now() - timelapseExitStart ).total_seconds() > timelapseExitSec:
                        print("")
                        logging.info("timelapseExitSec=%i Exceeded: Suppressing Further Timelapse Images" % ( timelapseExitSec ))
                        logging.info("To Reset: Restart pi-timolo.py to restart timelapseExitSec Timer.")
                        takeTimeLapse = False  # Suppress further timelapse images
                if (takeTimeLapse and timelapseNumOn and (not timelapseNumRecycle)):
                    timelapseStart = datetime.datetime.now()  # Reset timelapse timer
                    if timelapseNumMax > 0 and timelapseNumCount >= (timelapseNumStart + timelapseNumMax):
                        print("")
                        logging.info("timelapseNumRecycle=%s and Counter=%i Exceeded: Surpressing Further Timelapse Images"
                              % ( timelapseNumRecycle, timelapseNumStart + timelapseNumMax  ))
                        logging.info("To Reset: Delete File %s and Restart pi-timolo.py" % timelapseNumPath )
                        takeTimeLapse = False  # Suppress further timelapse images
                if takeTimeLapse:
                    if motionDotsOn and motionTrackOn:
                        dotCount = showDots(motionDotsMax + 2)  # reset motion dots
                    else:
                        print("")
                    logging.info("Scheduled Time Lapse Image - daymode=%s", daymode)
                    imagePrefix = timelapsePrefix + imageNamePrefix
                    filename = getImageName(tlPath, imagePrefix, timelapseNumOn, timelapseNumCount)
                    if motionTrackOn:
                        logging.info("Stop PiVideoStream ...")
                        vs.stop()
                        time.sleep(motionStreamStopSec)
                    timelapseStart = datetime.datetime.now()  # reset time lapse timer
                    if daymode:
                        takeDayImage(filename, timelapseCamSleep)
                    else:
                        takeNightImage(filename)
                    timelapseNumCount = postImageProcessing(timelapseNumOn, timelapseNumStart, timelapseNumMax,
                                                            timelapseNumCount, timelapseNumRecycle,
                                                            timelapseNumPath, filename, daymode)
                    if timelapseRecentMax > 0:
                        saveRecent(timelapseRecentMax, timelapseRecentDir, filename, imagePrefix)

                    if timelapseMaxFiles > 0:
                        deleteOldFiles(timelapseMaxFiles, timelapseDir, imagePrefix)

                    dotCount = showDots(motionDotsMax)
                    if motionTrackOn:
                        logging.info("Restart PiVideoStream ....")
                        vs = PiVideoStream().start()
                        vs.camera.rotation = imageRotation
                        vs.camera.hflip = imageHFlip
                        vs.camera.vflip = imageVFlip
                        time.sleep(2)
                    tlPath = subDirChecks( timelapseSubDirMaxHours, timelapseSubDirMaxFiles, timelapseDir, timelapsePrefix)

            if motionTrackOn:
                # IMPORTANT - Night motion detection may not work very well due to long exposure times and low light
                image2 = vs.read()
                grayimage2 = cv2.cvtColor(image2, cv2.COLOR_BGR2GRAY)
                movePoint1 = trackPoint(grayimage1, grayimage2)
                grayimage1 = grayimage2
                if movePoint1 and not startTrack:
                    startTrack = True
                    trackTimeout = time.time()
                    startPos = movePoint1
                image2 = vs.read()
                grayimage2 = cv2.cvtColor(image2, cv2.COLOR_BGR2GRAY)
                movePoint2 = trackPoint(grayimage1, grayimage2)
                if movePoint2 and startTrack:   # Two sets of movement required
                    trackLen = trackDistance(startPos, movePoint2)
                    if trackLen > TRACK_TRIG_LEN_MIN:  # wait until track well started
                        trackTimeout = time.time()  # Reset tracking timer object moved
                        if motionTrackInfo:
                            logging.info("Track Start(%i,%i)  Now(%i,%i) trackLen=%.2f px",
                               startPos[0], startPos[1], movePoint2[0], movePoint2[1], trackLen)

                    # Track length triggered
                    if trackLen > TRACK_TRIG_LEN:
                        if trackLen > TRACK_TRIG_LEN_MAX:  # reduce chance of two objects at different postions
                            motionFound = False
                            if motionTrackInfo:
                                logging.info("TrackLen %.2f px Exceeded %i px Max Trig Len Allowed.",
                                                   trackLen, TRACK_TRIG_LEN_MAX)
                        else:
                            motionFound = True
                            logging.info("Motion Triggered Start(%i,%i)  End(%i,%i) trackLen=%.2f px",
                               startPos[0], startPos[1], movePoint2[0], movePoint2[1], trackLen)
                        image1 = vs.read()
                        image2 = image1
                        grayimage1 = cv2.cvtColor(image1, cv2.COLOR_BGR2GRAY)
                        grayimage2 = grayimage1
                        startTrack = False
                        startPos = []
                        trackLen = 0.0

                # Track timed out
                if ((time.time() - trackTimeout > trackTimer) and startTrack):
                    image1 = vs.read()
                    image2 = image1
                    grayimage1 = cv2.cvtColor(image1, cv2.COLOR_BGR2GRAY)
                    grayimage2 = grayimage1
                    if motionTrackInfo:
                        logging.info("Track Timer %i sec Exceeded.  Reset Track", trackTimer)
                    startTrack = False
                    startPos = []
                    trackLen = 0.0

                rightNow = datetime.datetime.now()
                timeDiff = (rightNow - checkMotionTimer).total_seconds()
                if motionForce > 0 and timeDiff > motionForce:
                    image1 = vs.read()
                    image2 = image1
                    grayimage1 = cv2.cvtColor(image1, cv2.COLOR_BGR2GRAY)
                    grayimage2 = grayimage1
                    dotCount = showDots(motionDotsMax + 2)      # New Line
                    logging.info("No Motion Detected for %s minutes. Taking Forced Motion Image.", (motionForce / 60))
                    checkMotionTimer = rightNow
                    forceMotion = True

                if motionFound or forceMotion:
                    imagePrefix = motionPrefix + imageNamePrefix
                    if motionTrackQuickPic:  # Do not stop PiVideoStream
                        filename = getImageName(moPath, imagePrefix, motionNumOn, motionNumCount)
                        takeTrackQuickPic(image2, filename)
                        motionNumCount = postImageProcessing(motionNumOn, motionNumStart, motionNumMax,
                                                             motionNumCount, motionNumRecycle, motionNumPath,
                                                             filename, daymode)
                    else:
                        if motionTrackOn:
                            logging.info("Stop PiVideoStream ...")
                            vs.stop()
                            time.sleep(motionStreamStopSec)
                        checkMotionTimer = rightNow
                        if forceMotion:
                            forceMotion = False

                        # check if motion Quick Time Lapse option is On.  This option supersedes motionVideoOn
                        if motionQuickTLOn and daymode:
                            filename = getImageName(moPath, imagePrefix, motionNumOn, motionNumCount)
                            with picamera.PiCamera() as camera:
                                camera.resolution = (imageWidth, imageHeight)
                                camera.vflip = imageVFlip
                                camera.hflip = imageHFlip
                                camera.rotation = imageRotation # valid values 0, 90, 180, 270
                                time.sleep(motionCamSleep)
                                # This uses yield to loop through time lapse sequence but does not seem to be faster due to writing images
                                camera.capture_sequence(takeQuickTimeLapse(moPath, imagePrefix, motionNumOn, motionNumCount, daymode, motionNumPath))
                                camera.close()
                                motionNumCount = getCurrentCount(motionNumPath, motionNumStart)
                        else:
                            if motionVideoOn:
                                filename = getVideoName(motionPath, imagePrefix, motionNumOn, motionNumCount)
                                takeVideo(filename, motionVideoTimer, motionVideoFPS)
                            else:
                                filename = getImageName(moPath, imagePrefix, motionNumOn, motionNumCount)
                                if daymode:
                                    takeDayImage(filename, motionCamSleep)
                                else:
                                    takeNightImage(filename)
                            motionNumCount = postImageProcessing(motionNumOn, motionNumStart, motionNumMax,
                                                                 motionNumCount, motionNumRecycle, motionNumPath,
                                                                 filename, daymode)
                            if motionRecentMax > 0:
                                if not motionVideoOn:   # prevent h264 video files from being copied to recent
                                    saveRecent(motionRecentMax, motionRecentDir, filename, imagePrefix)

                        if motionTrackOn:
                            logging.info("Restart PiVideoStream ....")
                            vs = PiVideoStream().start()
                            vs.camera.rotation = imageRotation
                            vs.camera.hflip = imageHFlip
                            vs.camera.vflip = imageVFlip
                            time.sleep(2)
                            image1 = vs.read()
                            image2 = image1
                            grayimage1 = cv2.cvtColor(image1, cv2.COLOR_BGR2GRAY)
                            grayimage2 = grayimage1
                            trackLen = 0.0
                            trackTimeout = time.time()
                            startPos = []
                            startTrack = False
                            forceMotion = False

                    moPath = subDirChecks( motionSubDirMaxHours, motionSubDirMaxFiles, motionDir, motionPrefix)

                    if motionFound:
                        # =========================================================================
                        # Put your user code in userMotionCodeHere() function at top of this script
                        # =========================================================================
                        userMotionCodeHere()
                        dotCount = showDots(motionDotsMax)
                else:
                    dotCount = showDots(dotCount)  # show progress dots when no motion found

#-----------------------------------------------------------------------------------------------
def videoRepeat():
    if not os.path.isdir(videoPath):     # Check if folder exist and create if required
        logging.info("Create videoRepeat Folder %s", videoPath)
        os.makedirs(videoPath)
    print("------------------------------------------------------------------------------------------")
    print("VideoRepeat . videoRepeatOn=%s" % videoRepeatOn)
    print("   Info ..... Size=%ix%i  videoPrefix=%s  videoDuration=%i seconds  videoFPS=%i" %
                       ( imageWidth, imageHeight, videoPrefix, videoDuration, videoFPS ))
    print("   Vid Path . videoPath=%s" % videoPath)
    print("   Timer .... videoTimer=%i minutes  0=Continuous" % ( videoTimer ))
    print("   Num Seq .. videoNumOn=%s  videoNumRecycle=%s  videoNumStart=%i  videoNumMax=%i 0=Continuous" %
                         ( videoNumOn, videoNumRecycle, videoNumStart, videoNumMax ))
    print("------------------------------------------------------------------------------------------")
    print("WARNING: videoRepeatOn=%s Suppresses TimeLapse and Motion Settings." % videoRepeatOn)

    videoStartTime = datetime.datetime.now()
    lastSpaceCheck = datetime.datetime.now()
    videoCount = 0
    videoNumCounter = videoNumStart
    keepRecording = True
    while keepRecording:
        # if required check free disk space and delete older files
        #  Set variables spaceFileExt='mp4' and spaceMediaDir= to appropriate folder path
        if spaceTimerHrs > 0:
            lastSpaceCheck = freeDiskSpaceCheck(lastSpaceCheck)
        filename = getVideoName(videoPath, videoPrefix, videoNumOn, videoNumCounter )
        takeVideo(filename, videoDuration, videoFPS)
        timeUsed = (datetime.datetime.now() - videoStartTime).total_seconds()
        timeRemaining = ( videoTimer*60 - timeUsed ) / 60.0
        videoCount += 1
        if videoNumOn:
            videoNumCounter += 1
            if videoNumMax > 0:
                if videoNumCounter - videoNumStart > videoNumMax:
                    if videoNumRecycle:
                        videoNumCounter = videoNumStart
                        logging.info("Restart Numbering: videoNumRecycle=%s and videoNumMax=%i Exceeded",
                                             videoNumRecycle, videoNumMax)
                    else:
                        keepRecording = False
                        logging.info("Exit since videoNumRecycle=%s and videoNumMax=%i Exceeded  %i Videos Recorded",
                                             videoNumRecycle, videoNumMax, videoCount)
                logging.info("Recorded %i of %i Videos" % ( videoCount, videoNumMax))
            else:
                logging.info("Recorded %i Videos  videoNumMax=%i 0=Continuous" % (videoCount, videoNumMax))
        else:
            logging.info("Progress: %i Videos Recorded in Folder %s", videoCount, videoPath)

        if videoTimer > 0:
            if timeUsed > videoTimer * 60:
                keepRecording = False
                logging.info("Exit since videoTimer=%i minutes Exceeded", videoTimer)
            else:
                logging.info("Remaining Time %.1f of %i minutes", timeRemaining, videoTimer)
        else:
            videoStartTime = datetime.datetime.now()
    logging.info("Exit: %i Videos Recorded in Folder %s", videoCount, videoPath)

#-----------------------------------------------------------------------------------------------
if __name__ == '__main__':

    # Test if the pi camera is already in use
    print("INFO  - Testing if Pi Camera in Use")
    ts = PiVideoStream().start()
    time.sleep(3)
    ts.stop()
    time.sleep(1)
    print("INFO  - Pi Camera is Available.")
    print("INFO  - Starting pi-timolo per %s Settings" % configFilePath)
    if not verbose:
        print("INFO  - Note: Logging Disabled per Variable verbose=False")

    try:
        if debug:
            dataLogger()
        elif videoRepeatOn:
            videoRepeat()
        else:
            timolo()
    except KeyboardInterrupt:
        print("")
        print("+++++++++++++++++++++++++++++++++++")
        print("User Pressed Keyboard ctrl-c")
        print("%s %s - Exiting" % (progName, progVer))
        print("+++++++++++++++++++++++++++++++++++")
        print("")
        quit(0)