Beispiel #1
0
    def __init__(self, endpoint_url=None, sensor_name=None, *args, **kwargs):
        self.logger = get_logger()
        self.logger.info(f'Setting up remote sensor {sensor_name}')

        # Setup the DB either from kwargs or config.
        self.db = None
        db_type = get_config('db.type', default='file')

        if 'db_type' in kwargs:
            self.logger.info(f"Setting up {kwargs['db_type']} type database")
            db_type = kwargs.get('db_type', db_type)

        self.db = PanDB(db_type=db_type)

        self.sensor_name = sensor_name
        self.sensor = None

        if endpoint_url is None:
            # Get the config for the sensor
            endpoint_url = get_config(f'environment.{sensor_name}.url')
            if endpoint_url is None:
                raise error.PanError(f'No endpoint_url for {sensor_name}')

        if not endpoint_url.startswith('http'):
            endpoint_url = f'http://{endpoint_url}'

        self.endpoint_url = endpoint_url
Beispiel #2
0
def test_config_reset(config_host, config_port):
    # Reset config via url
    url = f'http://{config_host}:{config_port}/reset-config'

    def reset_conf():
        response = requests.post(url,
                                 data=serializers.to_json({'reset': True}),
                                 headers={'Content-Type': 'application/json'})
        assert response.ok

    reset_conf()

    # Check we are at default value.
    assert get_config('location.horizon') == 30 * u.degree

    # Set to new value.
    set_config_return = set_config('location.horizon', 3 * u.degree)
    assert set_config_return == {'location.horizon': 3 * u.degree}

    # Check we have changed.
    assert get_config('location.horizon') == 3 * u.degree

    reset_conf()

    # Check we are at default again.
    assert get_config('location.horizon') == 30 * u.degree
Beispiel #3
0
def horizon_line():
    obstruction_list = get_config('location.obstructions', default=list())
    default_horizon = get_config('location.horizon')

    horizon_line = horizon_utils.Horizon(obstructions=obstruction_list,
                                         default_horizon=default_horizon)
    return horizon_line
Beispiel #4
0
def create_scheduler_from_config(observer=None, *args, **kwargs):
    """ Sets up the scheduler that will be used by the observatory """

    logger = get_logger()

    scheduler_config = get_config('scheduler', default=None)
    logger.info(f'scheduler_config: {scheduler_config!r}')

    if scheduler_config is None or len(scheduler_config) == 0:
        logger.info("No scheduler in config")
        return None

    if not observer:
        logger.debug(f'No Observer provided, creating from config.')
        site_details = create_location_from_config()
        observer = site_details['observer']

    scheduler_type = scheduler_config.get('type', 'dispatch')

    # Read the targets from the file
    fields_file = scheduler_config.get('fields_file', 'simple.yaml')
    fields_path = os.path.join(get_config('directories.targets'), fields_file)
    logger.debug(f'Creating scheduler: {fields_path}')

    if os.path.exists(fields_path):

        try:
            # Load the required module
            module = load_module(f'panoptes.pocs.scheduler.{scheduler_type}')

            obstruction_list = get_config('location.obstructions', default=[])
            default_horizon = get_config('location.horizon',
                                         default=30 * u.degree)

            horizon_line = horizon_utils.Horizon(
                obstructions=obstruction_list,
                default_horizon=default_horizon.value)

            # Simple constraint for now
            constraints = [
                Altitude(horizon=horizon_line),
                MoonAvoidance(),
                Duration(default_horizon, weight=5.)
            ]

            # Create the Scheduler instance
            scheduler = module.Scheduler(observer,
                                         fields_file=fields_path,
                                         constraints=constraints,
                                         *args,
                                         **kwargs)
            logger.debug("Scheduler created")
        except error.NotFound as e:
            raise error.NotFound(msg=e)
    else:
        raise error.NotFound(
            msg=f"Fields file does not exist: fields_file={fields_file!r}")

    return scheduler
Beispiel #5
0
def field_file():
    scheduler_config = get_config('scheduler', default={})

    # Read the targets from the file
    fields_file = scheduler_config.get('fields_file', 'simple.yaml')
    fields_path = os.path.join(get_config('directories.targets'), fields_file)

    return fields_path
Beispiel #6
0
 def do_load_camera_env_board(self, *arg):
     """ Load the arduino control_board sensors """
     if self._keep_looping:
         print_error('The timer loop is already running.')
         return
     print("Loading camera box environment board sensor")
     endpoint_url = get_config('environment.camera_env_board.url')
     self.camera_env_board = RemoteMonitor(endpoint_url=endpoint_url,
                                           sensor_name='camera_env_board',
                                           db_type=get_config(
                                               'db.type', default='file'))
     self.do_enable_sensor('camera_env_board', delay=10)
Beispiel #7
0
    def do_load_weather(self, *arg):
        """ Load the weather reader """
        if self._keep_looping:
            print_error('The timer loop is already running.')
            return

        print("Loading weather reader endpoint")
        endpoint_url = get_config('environment.weather.url')
        self.weather = RemoteMonitor(endpoint_url=endpoint_url,
                                     sensor_name='weather',
                                     db_type=get_config('db.type',
                                                        default='file'))
        self.do_enable_sensor('weather', delay=60)
Beispiel #8
0
def test_config_client(dynamic_config_server, config_port):
    assert isinstance(get_config(port=config_port), dict)

    assert set_config('location.horizon', 47 * u.degree, port=config_port) == {
        'location.horizon': 47 * u.degree
    }

    # With parsing
    assert get_config('location.horizon', port=config_port) == 47 * u.degree

    # Without parsing
    assert get_config('location.horizon', port=config_port,
                      parse=False) == '47.0 deg'
Beispiel #9
0
def config_getter(context, key, parse=True, default=None):
    """Get an item from the config server by key name, using dotted notation (e.g. 'location.elevation')

    If no key is given, returns the entire config.
    """
    host = context.obj.get('host')
    port = context.obj.get('port')
    try:
        # The nargs=-1 makes this a tuple so we get first entry.
        key = key[0]
    except IndexError:
        key = None
    logger.debug(f'Getting config  key={key!r}')
    try:
        config_entry = get_config(key=key,
                                  host=host,
                                  port=port,
                                  parse=parse,
                                  default=default)
    except Exception as e:
        logger.error(f'Error while trying to get config: {e!r}')
        click.secho(f'Error while trying to get config: {e!r}', fg='red')
    else:
        logger.debug(f'Config server response:  config_entry={config_entry!r}')
        click.echo(config_entry)
Beispiel #10
0
    def do_setup_pocs(self, *arg):
        """Setup and initialize a POCS instance."""
        args, kwargs = string_to_params(*arg)

        simulator = kwargs.get('simulator', list())
        if isinstance(simulator, str):
            simulator = [simulator]

        # Set whatever simulators were passed during setup
        client.set_config('simulator', simulator)
        # Retrieve what was set
        simulators = client.get_config('simulator', default=list())
        if len(simulators):
            print_warning(f'Using simulators: {simulators}')

        if 'POCSTIME' in os.environ:
            print_warning("Clearing POCSTIME variable")
            del os.environ['POCSTIME']

        try:
            mount = create_mount_from_config()
            cameras = create_cameras_from_config()
            scheduler = create_scheduler_from_config()

            observatory = Observatory(mount=mount, cameras=cameras, scheduler=scheduler)
            self.pocs = POCS(observatory)
            self.pocs.initialize()
        except error.PanError as e:
            print_warning('Problem setting up POCS: {}'.format(e))
Beispiel #11
0
def create_mount_simulator(mount_info=None,
                           earth_location=None,
                           *args, **kwargs):
    # Remove mount simulator
    current_simulators = get_config('simulator', default=[])
    logger.warning(f'Current simulators: {current_simulators}')
    with suppress(ValueError):
        current_simulators.remove('mount')

    mount_config = mount_info or {
        'model': 'Mount Simulator',
        'driver': 'simulator',
        'serial': {
            'port': '/dev/FAKE'
        }
    }

    # Set mount device info to simulator
    set_config('mount', mount_config)

    earth_location = earth_location or create_location_from_config()['earth_location']

    logger.debug(f"Loading mount driver: pocs.mount.{mount_config['driver']}")
    try:
        module = load_module(f"panoptes.pocs.mount.{mount_config['driver']}")
    except error.NotFound as e:
        raise error.MountNotFound(f'Error loading mount module: {e!r}')

    mount = module.Mount(earth_location, *args, **kwargs)

    logger.success(f"{mount_config['driver'].title()} mount created")

    return mount
Beispiel #12
0
def test_config_client():
    assert isinstance(get_config(), dict)

    assert get_config('location.horizon') == 30 * u.degree
    assert set_config('location.horizon', 47 * u.degree) == {
        'location.horizon': 47 * u.degree
    }
    assert get_config('location.horizon') == 47 * u.degree

    # Without parsing the result contains the double-quotes since that's what the raw
    # response has.
    assert get_config('location.horizon', parse=False) == '"47.0 deg"'

    assert set_config('location.horizon', 42 * u.degree, parse=False) == {
        'location.horizon': '42.0 deg'
    }
Beispiel #13
0
def pocs(target):
    try:
        del os.environ['POCSTIME']
    except KeyError:
        pass

    config = get_config()

    pocs = POCS(simulator=['weather', 'night', 'camera'],
                run_once=True,
                config=config,
                db='panoptes_testing')

    pocs.observatory.scheduler.fields_list = [
        {
            'name': 'Testing Target',
            'position': target.to_string(style='hmsdms'),
            'priority': '100',
            'exptime': 2,
            'min_nexp': 2,
            'exp_set_size': 2,
        },
    ]

    yield pocs

    pocs.power_down()
Beispiel #14
0
def observer():
    loc = get_config('location')
    location = EarthLocation(lon=loc['longitude'],
                             lat=loc['latitude'],
                             height=loc['elevation'])
    return Observer(location=location,
                    name="Test Observer",
                    timezone=loc['timezone'])
Beispiel #15
0
def get_simulator_names(simulator=None, kwargs=None):
    """Returns the names of the simulators to be used in lieu of hardware drivers.

    Note that returning a list containing 'X' doesn't mean that the config calls for a driver
    of type 'X'; that is up to the code working with the config to create drivers for real or
    simulated hardware.

    This function is intended to be called from `PanBase` or similar, which receives kwargs that
    may include simulator, config or both. For example::

        get_simulator_names(config=self.config, kwargs=kwargs)

        # Or:

        get_simulator_names(simulator=simulator, config=self.config)

    The reason this function doesn't just take **kwargs as its sole arg is that we need to allow
    for the case where the caller is passing in simulator (or config) twice, once on its own,
    and once in the kwargs (which won't be examined). Python doesn't permit a keyword argument
    to be passed in twice.

    >>> from panoptes.pocs.hardware import get_simulator_names
    >>> get_simulator_names()
    []
    >>> get_simulator_names('all')
    ['camera', 'dome', 'mount', 'night', 'power', 'sensors', 'theskyx', 'weather']


    Args:
        simulator (list): An explicit list of names of hardware to be simulated
            (i.e. hardware drivers to be replaced with simulators).
        kwargs: The kwargs passed in to the caller, which is inspected for an arg
            called 'simulator'.

    Returns:
        List of names of the hardware to be simulated.
    """
    empty = dict()

    def extract_simulator(d):
        return (d or empty).get('simulator')

    for v in [
            simulator,
            extract_simulator(kwargs),
            extract_simulator(get_config())
    ]:
        if not v:
            continue
        if isinstance(v, str):
            v = [v]
        if 'all' in v:
            return ALL_NAMES
        else:
            return sorted(v)
    return []
Beispiel #16
0
def test_config_client_bad(dynamic_config_server, config_port, caplog):
    # Bad host will return `None` but also throw error
    assert set_config('foo', 42, host='foobaz') is None
    assert caplog.records[-1].levelname == "INFO"
    assert caplog.records[-1].message.startswith("Problem with set_config")

    # Bad host will return `None` but also throw error
    assert get_config('foo', host='foobaz') is None
    assert caplog.records[-1].levelname == "INFO"
    assert caplog.records[-1].message.startswith("Problem with get_config")
Beispiel #17
0
 def do_load_camera_board(self, *arg):
     """ Load the arduino camera_board sensors """
     if self._keep_looping:
         print_error('The timer loop is already running.')
         return
     print("Loading camera board sensor")
     self.camera_board = ArduinoSerialMonitor(
         sensor_name='camera_board',
         db_type=get_config('db.type', default='file'))
     self.do_enable_sensor('camera_board', delay=10)
Beispiel #18
0
    def get_devices(self):
        logger.debug(f'Getting camera device connection config for {self}')
        camera_devices = dict()
        for cam_info in get_config('cameras.devices'):
            name = cam_info.get('name') or cam_info.get('model')
            port = cam_info.get('port') or cam_info.get('serial_number')
            camera_devices[name] = port

        logger.trace(f'camera_devices={camera_devices!r}')

        return camera_devices
Beispiel #19
0
def test_create_mount_with_mount_info(config_host, config_port):
    # Pass the mount info directly with nothing in config.
    mount_info = get_config('mount', default=dict())
    mount_info['driver'] = 'simulator'

    # Remove info from config.
    set_config('mount', None)
    set_config('simulator', hardware.get_all_names(without=['mount']))
    assert isinstance(create_mount_from_config(mount_info=mount_info), AbstractMount) is True

    reset_conf(config_host, config_port)
Beispiel #20
0
def test_bad_mount_driver(config_host, config_port):
    # Remove the mount from the list of simulators so it thinks we have a real one.
    simulators = get_config('simulator')
    with suppress(KeyError, AttributeError):
        simulators.pop('mount')
    set_config('simulator', simulators)

    # Set a bad port, which should cause a fail before actual mount creation.
    set_config('mount.serial.driver', 'foobar')
    with pytest.raises(error.MountNotFound):
        create_mount_from_config()
    reset_conf(config_host, config_port)
Beispiel #21
0
def test_update_location_no_init(mount):
    loc = get_config('location')

    location2 = EarthLocation(
        lon=loc['longitude'],
        lat=loc['latitude'],
        height=loc['elevation'] -
               1000 *
               u.meter)

    with pytest.raises(AssertionError):
        mount.location = location2
Beispiel #22
0
def test_config_reset(dynamic_config_server, config_port, config_host):
    # Check we are at default.
    assert get_config('location.horizon', port=config_port) == 30 * u.degree

    # Set to new value.
    set_config_return = set_config('location.horizon',
                                   47 * u.degree,
                                   port=config_port)
    assert set_config_return == {'location.horizon': 47 * u.degree}

    # Check we have changed.
    assert get_config('location.horizon', port=config_port) == 47 * u.degree

    # Reset config
    url = f'http://{config_host}:{config_port}/reset-config'
    response = requests.post(url,
                             data=serializers.to_json({'reset': True}),
                             headers={'Content-Type': 'application/json'})
    assert response.ok

    # Check we are at default again.
    assert get_config('location.horizon', port=config_port) == 30 * u.degree
Beispiel #23
0
def create_dome_simulator(*args, **kwargs):
    dome_config = get_config('dome')

    brand = dome_config['brand']
    driver = dome_config['driver']

    logger.debug(
        f'Creating dome simulator: brand={brand!r}, driver={driver!r}')

    module = load_module(f'panoptes.pocs.dome.{driver}')
    dome = module.Dome(*args, **kwargs)
    logger.info(f'Created dome driver: brand={brand!r}, driver={driver!r}')

    return dome
Beispiel #24
0
def test_config_client_bad(caplog):
    # Bad host will return `None` but also throw error
    assert set_config('foo', 42, host='foobaz') is None
    assert caplog.records[-1].levelname == "WARNING"
    assert caplog.records[-1].message.startswith("Problem with set_config")

    # Bad host will return `None` but also throw error
    assert get_config('foo', host='foobaz') is None
    found_log = False
    for rec in caplog.records[-5:]:
        if rec.levelname == 'WARNING' and rec.message.startswith(
                'Problem with get_config'):
            found_log = True

    assert found_log
Beispiel #25
0
def test_update_location(mount):
    loc = get_config('location')

    mount.initialize()

    location1 = mount.location
    location2 = EarthLocation(
        lon=loc['longitude'],
        lat=loc['latitude'],
        height=loc['elevation'] -
               1000 *
               u.meter)
    mount.location = location2

    assert location1 != location2
    assert mount.location == location2
Beispiel #26
0
    def get_config(self, *args, **kwargs):
        """Thin-wrapper around client based get_config that sets default port.

        See `panoptes.utils.config.client.get_config` for more information.

        Args:
            *args: Passed to get_config
            **kwargs: Passed to get_config
        """
        config_value = None
        try:
            config_value = client.get_config(host=self._config_host,
                                             port=self._config_port,
                                             verbose=False,
                                             *args, **kwargs)
        except ConnectionError as e:  # pragma: no cover
            self.logger.warning(f'Cannot connect to config_server from {self.__class__}: {e!r}')

        return config_value
Beispiel #27
0
def main(directory,
         upload=False,
         remove_jpgs=False,
         overwrite=False,
         make_timelapse=False,
         **kwargs):
    """Upload images from the given directory.

    See argparse help string below for details about parameters.
    """

    pan_id = get_config('pan_id', default=None)
    if pan_id is None:
        raise error.GoogleCloudError(
            "Can't upload without a valid pan_id in the config")

    logger.debug("Cleaning observation directory: {}".format(directory))
    try:
        clean_observation_dir(directory,
                              remove_jpgs=remove_jpgs,
                              include_timelapse=make_timelapse,
                              timelapse_overwrite=overwrite,
                              **kwargs)
    except Exception as e:
        raise error.PanError('Cannot clean observation dir: {}'.format(e))

    if upload:
        logger.debug("Uploading to storage bucket")

        try:
            upload_observation_to_bucket(pan_id,
                                         directory,
                                         include_files='*',
                                         exclude_files='upload_manifest.log',
                                         **kwargs)
        except Exception as e:
            logger.error(f'Error in uploading observations: {e!r}')

    return directory
Beispiel #28
0
def create_dome_from_config(*args, **kwargs):
    """If there is a dome specified in the config, create a driver for it.

    A dome needs a config. We assume that there is at most one dome in the config, i.e. we don't
    support two different dome devices, such as might be the case if there are multiple
    independent actuators, for example slit, rotation and vents. Those would need to be handled
    by a single dome driver class.
    """

    dome_config = get_config('dome')

    if dome_config is None:
        logger.info('No dome in config.')
        return None

    brand = dome_config['brand']
    driver = dome_config['driver']

    logger.debug(f'Creating dome: brand={brand!r}, driver={driver!r}')
    module = load_module(f'panoptes.pocs.dome.{driver}')
    dome = module.Dome(*args, **kwargs)
    logger.info(f'Created dome driver: brand={brand}, driver={driver}')

    return dome
Beispiel #29
0
def location():
    loc = get_config('location')
    return EarthLocation(lon=loc['longitude'], lat=loc['latitude'], height=loc['elevation'])
Beispiel #30
0
class PanSensorShell(cmd.Cmd):
    """ A simple command loop for the sensors. """
    intro = 'Welcome to PEAS Shell! Type ? for help'
    prompt = 'PEAS > '
    weather = None
    control_board = None
    control_env_board = None
    camera_board = None
    camera_env_board = None
    active_sensors = dict()
    db = PanDB(db_type=get_config('db.type', default='file'))
    _keep_looping = False
    _loop_delay = 60
    _timer = None
    captured_data = list()

    telemetry_relay_lookup = {
        'computer': {
            'pin': 8,
            'board': 'telemetry_board'
        },
        'fan': {
            'pin': 6,
            'board': 'telemetry_board'
        },
        'camera_box': {
            'pin': 7,
            'board': 'telemetry_board'
        },
        'weather': {
            'pin': 5,
            'board': 'telemetry_board'
        },
        'mount': {
            'pin': 4,
            'board': 'telemetry_board'
        },
        'cam_0': {
            'pin': 5,
            'board': 'camera_board'
        },
        'cam_1': {
            'pin': 6,
            'board': 'camera_board'
        },
    }

    # NOTE: These are not pins but zero-based index numbers.
    controlboard_relay_lookup = {
        'computer': {
            'pin': 0,
            'board': 'control_board'
        },
        'mount': {
            'pin': 1,
            'board': 'control_board'
        },
        'camera_box': {
            'pin': 2,
            'board': 'control_board'
        },
        'weather': {
            'pin': 3,
            'board': 'control_board'
        },
        'fan': {
            'pin': 4,
            'board': 'control_board'
        },
    }

    ##################################################################################################
    # Generic Methods
    ##################################################################################################

    def do_status(self, *arg):
        """ Get the entire system status and print it pretty like! """
        if self._keep_looping:
            console.color_print("{:>12s}: ".format('Loop Timer'), "default",
                                "active", "lightgreen")
        else:
            console.color_print("{:>12s}: ".format('Loop Timer'), "default",
                                "inactive", "yellow")

        for sensor_name in ['control_board', 'camera_board', 'weather']:
            if sensor_name in self.active_sensors:
                console.color_print("{:>12s}: ".format(sensor_name.title()),
                                    "default", "active", "lightgreen")
            else:
                console.color_print("{:>12s}: ".format(sensor_name.title()),
                                    "default", "inactive", "yellow")

    def do_last_reading(self, device):
        """ Gets the last reading from the device. """
        if not device:
            print_warning('Usage: last_reading <device>')
            return
        if not hasattr(self, device):
            print_warning('No such sensor: {!r}'.format(device))
            return

        rec = self.db.get_current(device)

        if rec is None:
            print_warning('No reading found for {!r}'.format(device))
            return

        print_info('*' * 80)
        print("{}:".format(device.upper()))
        pprint(rec)
        print_info('*' * 80)

        # Display the age in seconds of the record
        if isinstance(rec.get('date'), datetime.datetime):
            now = current_time(datetime=True).astimezone(utc)
            record_date = rec['date'].astimezone(utc)
            age = (now - record_date).total_seconds()
            if age < 120:
                print_info('{:.1f} seconds old'.format(age))
            else:
                print_info('{:.1f} minutes old'.format(age / 60.0))

    def complete_last_reading(self, text, line, begidx, endidx):
        """Provide completions for sensor names."""
        names = list(self.active_sensors.keys())
        return [name for name in names if name.startswith(text)]

    def do_enable_sensor(self, sensor, delay=None):
        """ Enable the given sensor """
        if delay is None:
            delay = self._loop_delay

        if hasattr(self, sensor) and sensor not in self.active_sensors:
            self.active_sensors[sensor] = {'reader': sensor, 'delay': delay}

    def do_disable_sensor(self, sensor):
        """ Disable the given sensor """
        if hasattr(self, sensor) and sensor in self.active_sensors:
            del self.active_sensors[sensor]

    def do_toggle_debug(self, sensor):
        """ Toggle DEBUG on/off for sensor

        Arguments:
            sensor {str} -- environment, weather
        """
        # TODO(jamessynge): We currently use a single logger, not one per module or sensor.
        # Figure out whether to keep this code and make it work, or get rid of it.
        import logging
        get_level = {
            logging.DEBUG: logging.INFO,
            logging.INFO: logging.DEBUG,
        }

        if hasattr(self, sensor):
            try:
                log = getattr(self, sensor).logger
                log.setLevel(get_level[log.getEffectiveLevel()])
            except Exception:
                print_error("Can't change log level for {}".format(sensor))

    def complete_toggle_debug(self, text, line, begidx, endidx):
        """Provide completions for toggling debug logging."""
        names = list(self.active_sensors.keys())
        return [name for name in names if name.startswith(text)]

##################################################################################################
# Load Methods
##################################################################################################

    def do_load_all(self, *arg):
        """Load the weather and environment sensors."""
        if self._keep_looping:
            print_error('The timer loop is already running.')
            return
        self.do_load_weather()
        self.do_load_control_board()
        self.do_load_camera_board()

    def do_load_control_board(self, *arg):
        """ Load the arduino control_board sensors """
        if self._keep_looping:
            print_error('The timer loop is already running.')
            return
        print("Loading control board sensor")
        self.control_board = ArduinoSerialMonitor(sensor_name='control_board',
                                                  db_type=get_config(
                                                      'db.type',
                                                      default='file'))
        self.do_enable_sensor('control_board', delay=10)

    def do_load_camera_board(self, *arg):
        """ Load the arduino camera_board sensors """
        if self._keep_looping:
            print_error('The timer loop is already running.')
            return
        print("Loading camera board sensor")
        self.camera_board = ArduinoSerialMonitor(
            sensor_name='camera_board',
            db_type=get_config('db.type', default='file'))
        self.do_enable_sensor('camera_board', delay=10)

    def do_load_control_env_board(self, *arg):
        """ Load the arduino control_board sensors """
        if self._keep_looping:
            print_error('The timer loop is already running.')
            return
        print("Loading control box environment board sensor")
        endpoint_url = get_config('environment.control_env_board.url')
        self.control_env_board = RemoteMonitor(endpoint_url=endpoint_url,
                                               sensor_name='control_env_board',
                                               db_type=get_config(
                                                   'db.type', default='file'))
        self.do_enable_sensor('control_env_board', delay=10)

    def do_load_camera_env_board(self, *arg):
        """ Load the arduino control_board sensors """
        if self._keep_looping:
            print_error('The timer loop is already running.')
            return
        print("Loading camera box environment board sensor")
        endpoint_url = get_config('environment.camera_env_board.url')
        self.camera_env_board = RemoteMonitor(endpoint_url=endpoint_url,
                                              sensor_name='camera_env_board',
                                              db_type=get_config(
                                                  'db.type', default='file'))
        self.do_enable_sensor('camera_env_board', delay=10)

    def do_load_weather(self, *arg):
        """ Load the weather reader """
        if self._keep_looping:
            print_error('The timer loop is already running.')
            return

        print("Loading weather reader endpoint")
        endpoint_url = get_config('environment.weather.url')
        self.weather = RemoteMonitor(endpoint_url=endpoint_url,
                                     sensor_name='weather',
                                     db_type=get_config('db.type',
                                                        default='file'))
        self.do_enable_sensor('weather', delay=60)

##################################################################################################
# Relay Methods
##################################################################################################

    def do_turn_off_relay(self, *arg):
        """Turn on relay.

        The argument should be the name of the relay, i.e. on of:

            * fan
            * mount
            * weather
            * camera_box

        The names must correspond to the entries in the lookup tables above.
        """
        relay = arg[0]

        if hasattr(self, 'control_board'):
            relay_lookup = self.controlboard_relay_lookup
        else:
            relay_lookup = self.telemetry_relay_lookup

        try:
            relay_info = relay_lookup[relay]
            serial_connection = self.control_board.serial_readers[
                relay_info['board']]['reader']

            serial_connection.ser.reset_input_buffer()
            serial_connection.write("{},0\n".format(relay_info['pin']))
        except Exception as e:
            print_warning(f"Problem turning relay off {relay} {e!r}")
            print_warning(e)

    def do_turn_on_relay(self, *arg):
        """Turn off relay.

        The argument should be the name of the relay, i.e. on of:

            * fan
            * mount
            * weather
            * camera_box

        The names must correspond to the entries in the lookup tables above.
    """
        relay = arg[0]

        if hasattr(self, 'control_board'):
            relay_lookup = self.controlboard_relay_lookup
        else:
            relay_lookup = self.telemetry_relay_lookup

        try:
            relay_info = relay_lookup[relay]
            serial_connection = self.control_board.serial_readers[
                relay_info['board']]['reader']

            serial_connection.ser.reset_input_buffer()
            serial_connection.write("{},1\n".format(relay_info['pin']))
        except Exception as e:
            print_warning(f"Problem turning relay off {relay} {e!r}")
            print_warning(e)

    def complete_turn_off_relay(self, text, line, begidx, endidx):
        """Provide completions for relay names."""
        if hasattr(self, 'control_board'):
            names = ['camera_box', 'fan', 'mount', 'weather']
        else:
            names = ['cam_0', 'cam_1', 'camera_box', 'fan', 'mount', 'weather']
        return [name for name in names if name.startswith(text)]

    def complete_turn_on_relay(self, text, line, begidx, endidx):
        """Provide completions for relay names."""
        if hasattr(self, 'control_board'):
            names = ['camera_box', 'fan', 'mount', 'weather']
        else:
            names = ['cam_0', 'cam_1', 'camera_box', 'fan', 'mount', 'weather']
        return [name for name in names if name.startswith(text)]

    def do_toggle_computer(self, *arg):
        """Toggle the computer relay off and then on again after 30 seconds.

        Note:

            The time delay is set on the arduino and is blocking.
        """
        relay = 'computer'

        if hasattr(self, 'control_board'):
            relay_lookup = self.controlboard_relay_lookup
        else:
            relay_lookup = self.telemetry_relay_lookup

        try:
            relay_info = relay_lookup[relay]
            serial_connection = self.control_board.serial_readers[
                relay_info['board']]['reader']

            serial_connection.ser.reset_input_buffer()
            serial_connection.write("{},9\n".format(relay_info['pin']))
        except Exception as e:
            print_warning(f"Problem toggling computer: {e!r}")
            print_warning(e)

##################################################################################################
# Start/Stop Methods
##################################################################################################

    def do_start(self, *arg):
        """ Runs all the `active_sensors`. Blocking loop for now """
        if self._keep_looping:
            print_error('The timer loop is already running.')
            return

        self._keep_looping = True

        print_info("Starting sensors")

        self._loop()

    def do_stop(self, *arg):
        """ Stop the loop and cancel next call """
        # NOTE: We don't yet have a way to clear _timer.
        if not self._keep_looping and not self._timer:
            print_error('The timer loop is not running.')
            return

        print_info("Stopping loop")

        self._keep_looping = False

        if self._timer:
            self._timer.cancel()

    def do_change_delay(self, *arg):
        """Change the timing between reads from the named sensor."""
        # NOTE: For at least the Arduinos, we should not need a delay and a timer, but
        # simply a separate thread, reading from the board as data is available.
        # We might use a delay to deal with the case where the device is off-line
        # but we want to periodically check if it becomes available.
        parts = None
        if len(arg) == 1:
            parts = arg[0].split()
        if parts is None or len(parts) != 2:
            print_error('Expected a sensor name and a delay, not "{}"'.format(
                ' '.join(arg)))
            return
        sensor_name, delay = parts
        try:
            delay = float(delay)
            if delay <= 0:
                raise ValueError()
        except ValueError:
            print_warning("Not a positive number: {!r}".format(delay))
            return
        try:
            print_info("Changing sensor {} to a {} second delay".format(
                sensor_name, delay))
            self.active_sensors[sensor_name]['delay'] = delay
        except KeyError:
            print_warning("Sensor not active: {!r}".format(sensor_name))

##################################################################################################
# Shell Methods
##################################################################################################

    def do_shell(self, line):
        """ Run a raw shell command. Can also prepend '!'. """
        print("Shell command:", line)

        output = os.popen(line).read()

        print_info("Shell output: ", output)

        self.last_output = output

    def emptyline(self):
        self.do_status()

    def do_exit(self, *arg):
        """ Exits PEAS Shell """
        print("Shutting down")
        if self._timer or self._keep_looping:
            self.do_stop()

        print(
            "Please be patient and allow for process to finish. Thanks! Bye!")
        return True

##################################################################################################
# Private Methods
##################################################################################################

    def _capture_data(self, sensor_name):
        # We are missing a Mutex here for accessing these from active_sensors and
        # self.
        if sensor_name in self.active_sensors:
            sensor = getattr(self, sensor_name)
            try:
                sensor.capture(store_result=True)
            except Exception as e:
                print_warning(f'Problem storing captured data: {e!r}')

            self._setup_timer(sensor_name,
                              delay=self.active_sensors[sensor_name]['delay'])

    def _loop(self, *arg):
        for sensor_name in self.active_sensors.keys():
            self._capture_data(sensor_name)

    def _setup_timer(self, sensor_name, delay=None):
        if self._keep_looping and len(self.active_sensors) > 0:

            if not delay:
                delay = self._loop_delay

            # WARNING: It appears we have a single _timer attribute, but we create
            # one Timer for each active sensor (i.e. environment and weather).
            self._timer = Timer(delay,
                                self._capture_data,
                                args=(sensor_name, ))

            self._timer.start()