def store_file(self, file, remove_original=True): filename = os.path.basename(file) upload_path = os.path.join(self._store_path, filename) log.debug("Uploading file '%s' into Dropbox as '%s'", filename, upload_path) with open(file, "rb") as f: try: self._dropbox.files_upload(f.read(), upload_path, mode=WriteMode('overwrite')) except ApiError as err: if err.error.is_path() and err.error.get_path( ).reason.is_insufficient_space(): err_msg = "Cannot back up; insufficient space." elif err.user_message_text: err_msg = err.user_message_text else: err_msg = err raise DataSaveError(err_msg) except Exception as err: raise DataSaveError(err) if remove_original: log.debug("Removing the original file %s", file) os.remove(file)
def __init__(self, options): self.cli_options = self.get_argparser().parse_args(options) log.debug("Parsed CLI options: %s", self.cli_options) self.timelapse_config_list = TimelapseConfig.parse_configs_from_file( self.cli_options.config) self.scheduler = AsyncIOScheduler() self.active_cameras_sn = set()
def should_run_now(self, time_now=None): """ Function which determines whether the timelapse job should be run NOW? :param time_now: Time determining what it means NOW. :return: True if yes, False otherwise. """ if time_now is None: time_now = datetime.datetime.now() def time_in_range(start, end, now): """ Returns True if 'now' is in the range of 'start' and 'end'. False otherwise """ if start <= end: return start <= now <= end else: return start <= now or now <= end # First check day of the week if time_now.weekday() not in self.week_days: log.debug("%s: not configured to run on this week day %d", self, time_now.weekday()) return False # Now check the time of day if not time_in_range(self.since_tod, self.till_tod, time_now.time()): log.debug("%s: not configured to run at this time %s", self, time_now.time()) return False return True
def store_tmp_file_in_datastore(config, tmp_file): datastores = config.datastore for datastore in datastores: datastore_type = datastore[TimelapseConfig.DATASTORE_TYPE] try: if datastore_type == TimelapseConfig.DATASTORE_TYPE_DROPBOX: ds = DropboxDataStore( datastore[TimelapseConfig.DATASTORE_DROPBOX_TOKEN], datastore[TimelapseConfig.DATASTORE_STORE_PATH], datastore.get( TimelapseConfig.DATASTORE_DROPBOX_TIMEOUT, None)) elif datastore_type == TimelapseConfig.DATASTORE_TYPE_FILESYSTEM: ds = FilesystemDataStore( datastore[TimelapseConfig.DATASTORE_STORE_PATH]) else: raise NotImplementedError("Unexpected datastore type '%s'", datastore_type) except DatastoreError as err: log.error( "Failed to initialize datastore '%s' due to error: %s", datastore_type, err) continue log.debug("Storing temporary file '%s' using data store '%s'", tmp_file, ds) try: ds.store_file(tmp_file, False) except DataSaveError as err: log.warning( "Failed to store file '%s' using datastore '%s' due to error: %s", tmp_file, datastore_type, err) continue shutil.rmtree(os.path.dirname(tmp_file))
def store_file(self, file, remove_original=True): filename = os.path.basename(file) move_path = os.path.join(self._store_path, filename) if remove_original: log.debug("Removing the original file %s", file) shutil.move(file, move_path) else: shutil.copyfile(file, move_path)
def camera_summary_get_serial_number(camera_summary): """ Extracts serial number from provided camera summary text. :param camera_summary: :return: """ # extract Serial Number match = re.search(r'Serial Number: (.*)\n', camera_summary) if match: serial_number = match.group(1) log.debug('Extracted Serial Number: %s', serial_number) else: log.error('No Serial Number found in the summary') serial_number = None return serial_number
def find_timelapser_configuration(): config_file_name = 'timelapser.yaml' paths = [ # configuration in CWD os.path.join(os.getcwd(), config_file_name), # configuration in user's home os.path.expanduser(os.path.join('~', config_file_name)), # system-wide configuration os.path.join('etc', config_file_name) ] for path in paths: if os.path.isfile(path): log.debug("Most preferred config file is '%s'", path) return path # TODO: probably return an Exception? we should probably use some default values in case no configurtation was specified. return None
def __init__(self, token, store_path, timeout=None): if timeout is None: timeout = self.DEFAULT_TIMEOUT self._store_path = store_path try: self._dropbox = dropbox.Dropbox(token, timeout=timeout) user_account = self._dropbox.users_get_current_account() except AuthError: raise DatastoreError( "Invalid Dropbox access token. Try re-generating access token from the app console on \ the web") except Exception as err: raise DatastoreError( "Failed to initialize Dropbox datastore due to error: {}". format(err)) else: log.debug("Successfully logged into Dropbox as user '%s'", user_account.name)
def take_picture_job(self, config, camera, eventloop): log.info("Taking picture in %s ...", threading.current_thread()) tmp_store_dir = tempfile.mkdtemp() try: picture = camera.take_picture() tmp_store_location = os.path.join(tmp_store_dir, os.path.basename(picture)) camera.download_picture(picture, tmp_store_location, config.keep_on_camera) except CameraDeviceError as err: # there is some problem with the Camera, remove its whole jobstore log.warning("Error occurred while taking picture on %s(%s)", camera.name, camera.serial_number) log.debug(err) shutil.rmtree(tmp_store_dir) self._scheduler_remove_jobstore(camera.serial_number) else: log.info("Temporarily stored taken picture in %s", tmp_store_location) # TODO: it may make sense to use camera_sn in the store path if the configuration is not bound to a specific camera, thus there can be multiple cameras storing pictures into the same folder eventloop.run_in_executor(None, self.store_tmp_file_in_datastore, config, tmp_store_location)
def get_next_fire_time(self, previous_fire_time, now): """ Returns the next datetime to fire on, If no such datetime can be calculated, returns None. """ # TODO: Take "now" parameter into account when calculating the next run. Especially make sure that "next_time > now" # The job is being scheduled for the first time if not previous_fire_time: previous_fire_time = now delta = datetime.timedelta(seconds=self._timelapse_config.frequency) next_time = previous_fire_time + delta # modify the time until it fits the criteria if not self._timelapse_config.should_run_now(next_time): # There was an error, that made the next_time be scheduled for the same day, but in the past, because the current day # fit the configured weekdays but it was past till_tod. This happened when since_tod < till_tod. In this case we need # to jump one day into the future, but before since_tod, so using 00:00.00! if self._timelapse_config.since_tod < self._timelapse_config.till_tod < next_time.time( ): next_time = datetime.datetime.combine( next_time.date() + datetime.timedelta(days=1), datetime.time(tzinfo=next_time.tzinfo)) # first get through the day of week while next_time.weekday() not in self._timelapse_config.week_days: next_time = datetime.datetime.combine( next_time.date() + datetime.timedelta(days=1), next_time.timetz()) # now fix the time next_time = datetime.datetime.combine( next_time.date(), self._timelapse_config.since_tod, tzinfo=next_time.tzinfo) log.debug("Next job scheduled for %s", next_time.strftime("%c")) return next_time
def refresh_timelapses_job(self): refresh_period = 5 loop = asyncio.get_event_loop() available_cameras = CameraDevice.get_available_cameras() if len(available_cameras) == 0: for removed_camera_sn in self.active_cameras_sn: log.debug("Removing jobs for camera sn: %s", removed_camera_sn) self.scheduler.remove_jobstore(removed_camera_sn) self.active_cameras_sn.clear() self.scheduler.remove_all_jobs() loop.call_later(refresh_period, self.refresh_timelapses_job) return active_cameras_map = {c.serial_number: c for c in available_cameras} new_active_cameras_sn = [c.serial_number for c in available_cameras] # remove jobs and job stores for every removed camera removed_cameras_sn = self.active_cameras_sn - set( new_active_cameras_sn) for removed_camera_sn in removed_cameras_sn: self._scheduler_remove_jobstore(removed_camera_sn) new_cameras_sn = set(new_active_cameras_sn) - self.active_cameras_sn # Go through all configuration and add timelapse jobs for any new cameras that fit them for config in self.timelapse_config_list: camera_sn = config.camera_sn # the config is bound to specific device if camera_sn: if camera_sn in new_cameras_sn: camera_device = active_cameras_map[camera_sn] self._scheduler_add_job(config, camera_device) log.debug("Added timelapse job for camera sn: %s", camera_sn) # configuration is not bound to specific device else: for camera_sn in new_cameras_sn: camera_device = active_cameras_map[camera_sn] self._scheduler_add_job(config, camera_device) log.debug("Added timelapse job for camera sn: %s", camera_sn) loop.call_later(refresh_period, self.refresh_timelapses_job)
def parse_configs_from_file(path=None): """ Parse Timelapse Configurations from a passed YAML config file. :param path: Path to the configuration YAML file :return: list of TimelapseConfig objects. """ # If no specific configuration was specified, just try to look for some if path is None: path = TimelapseConfig.find_timelapser_configuration() # didn't find any configuration file in default locations if path is None: log.info("Didn't find any configuration file.") parsed_configs = None else: log.debug("Using timelapser configuration file '%s'", path) with open(path) as config_file: configuration = yaml.safe_load(config_file) log.debug("Configuration loaded from YMAL file: %s", str(configuration)) parsed_configs = configuration.get("timelapse_configuration", None) configurations = list() if parsed_configs is not None: for config in parsed_configs: configurations.append(TimelapseConfig(config)) log.debug("Parsed Timelapse Config: %s", str(configurations[-1])) else: # no confurations found, go just with default one configurations.append(TimelapseConfig()) log.info( "Didn't find any explicit timelapse configuration. Using default values." ) return configurations
def __init__(self, options): self.cli_options = self.get_argparser().parse_args(options) log.debug("Parsed CLI options: %s", self.cli_options)
def _scheduler_remove_jobstore(self, jobstore): log.debug("Removing jobs for camera sn: %s", jobstore) self.scheduler.remove_jobstore(jobstore) self.active_cameras_sn.remove(jobstore)