Пример #1
0
    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)
Пример #2
0
 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()
Пример #3
0
    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
Пример #4
0
    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))
Пример #5
0
    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)
Пример #6
0
    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
Пример #7
0
    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
Пример #8
0
    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)
Пример #9
0
 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)
Пример #10
0
    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
Пример #11
0
    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)
Пример #12
0
    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
Пример #13
0
 def __init__(self, options):
     self.cli_options = self.get_argparser().parse_args(options)
     log.debug("Parsed CLI options: %s", self.cli_options)
Пример #14
0
 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)