Skip to content

Configuration

PANOPTES uses a single YAML file as the source of truth for observatory configuration. The file is human-editable and is the only place a user needs to look when setting up or adjusting their unit.

This page covers the file-based configuration helpers provided by panoptes-utils: typed Pydantic models and a file watcher. For the deprecated HTTP config server, see Config Server (Deprecated).


Getting started

Create a starter config file with:

panoptes-utils config init

This writes ~/.panoptes/config.yaml from the built-in template. Edit it to match your hardware and location, then point the software to it:

export PANOPTES_CONFIG_FILE=~/.panoptes/config.yaml

To write to a different location, use --output:

panoptes-utils config init --output /path/to/my_unit.yaml

Loading config

load_config reads one or more YAML files and returns a plain dict. When called with no arguments it resolves the config file automatically:

  1. $PANOPTES_CONFIG_FILE environment variable (if set)
  2. ~/.panoptes/config.yaml
  3. Empty dict with a warning if neither exists
from panoptes.utils.config import load_config

# Uses $PANOPTES_CONFIG_FILE or ~/.panoptes/config.yaml automatically
config = load_config()
print(config["location"]["latitude"])  # <Quantity 19.54 deg>

# Or supply an explicit path
config = load_config("path/to/config.yaml")

Multiple files are merged in order, with later files overriding earlier ones.

Deprecated: The automatic loading of <name>_local.yaml companion files (load_local=True) is deprecated. Consolidate all overrides into your single user config file instead.


Saving config

save_config writes a config dict to a YAML file. When called with no path it writes to $PANOPTES_CONFIG_FILE or ~/.panoptes/config.yaml:

from panoptes.utils.config import save_config

save_config(config={"name": "My Unit"})

Typed config models

panoptes-utils ships Pydantic v2 models for the config sections it owns. Pass model= to load_config to get a validated instance instead of a raw dict:

from panoptes.utils.config import load_config, UnitConfig

cfg = load_config("path/to/config.yaml", model=UnitConfig)

# Typed access — no dict key errors, IDE autocompletion works
print(cfg.pan_id)                  # 'PAN000'
print(cfg.location.latitude)       # <Quantity 19.54 deg>
print(cfg.location.timezone)       # 'US/Hawaii'
print(cfg.directories.base)        # '/home/panoptes'
print(cfg.earth_location)          # EarthLocation(...)

Models also accept raw strings for unit-bearing fields, so you can construct them directly without going through YAML:

from panoptes.utils.config.models import LocationConfig

loc = LocationConfig(
    latitude="19.54 deg",
    longitude="-155.58 deg",
    elevation="3400 m",
    timezone="US/Hawaii",
)
print(loc.latitude)   # <Quantity 19.54 deg>

Available models

Bases: BaseModel

Geographic location of the observatory.

All angle and distance fields accept either an already-parsed astropy.units.Quantity (as returned by load_config) or a plain string in the form "<value> <unit>" (e.g. "19.54 deg").

Examples:

>>> from astropy import units as u
>>> loc = LocationConfig(
...     latitude="19.54 deg",
...     longitude="-155.58 deg",
...     elevation="3400.0 m",
...     timezone="US/Hawaii",
... )
>>> loc.latitude
<Quantity 19.54 deg>

parse_angle(v) classmethod

Accept string or Quantity for angle fields; must be convertible to degrees.

parse_distance(v) classmethod

Accept string or Quantity for elevation; must be convertible to meters.

Bases: BaseModel

Filesystem directories used by PANOPTES.

The base directory is the root; all other relative paths are resolved against it by parse_config_directories.

Examples:

>>> dirs = DirectoriesConfig(base="/home/panoptes")
>>> dirs.images
'images'

coerce_path(v) classmethod

Accept Path objects as well as strings.

Bases: BaseModel

Configuration for the local database (PanDB / telemetry).

Examples:

>>> db = DatabaseConfig(name="panoptes", type="file")
>>> db.type
'file'

Bases: BaseModel

Top-level configuration shared by all PANOPTES components.

This model covers the sections defined in panoptes-utils. Hardware-specific sections (mount, cameras, etc.) are handled by POCSConfig in the POCS repository.

Extra fields are allowed so that the full config dict (including POCS-specific keys) can be passed without error.

Examples:

>>> from panoptes.utils.config.helpers import load_config
>>> from panoptes.utils.config.models import UnitConfig
>>> cfg = load_config('tests/testing.yaml', model=UnitConfig)
>>> cfg.pan_id
'PAN000'
>>> cfg.location.timezone
'US/Hawaii'

earth_location property

Return an astropy.coordinates.EarthLocation for the site.

Returns:

Type Description
EarthLocation | None

astropy.coordinates.EarthLocation | None: The site location, or None if no location is configured.

Extending for POCS

Hardware-specific config (mount, cameras, scheduler, …) lives in POCS. POCSConfig will extend UnitConfig so the full config is validated in one shot:

# In POCS (future)
from panoptes.utils.config.models import UnitConfig
from pydantic import BaseModel

class MountConfig(BaseModel): ...
class CameraConfig(BaseModel): ...

class POCSConfig(UnitConfig):
    mount: MountConfig | None = None
    cameras: CameraConfig | None = None

Watching the config file

ConfigWatcher monitors a YAML file with watchdog and fires callbacks whenever values change. Use it to let long-running processes react to config edits without restarting.

from panoptes.utils.config import ConfigWatcher

def on_location_change(config: dict) -> None:
    print("Location updated:", config["location"])

# Context manager — starts and stops the watcher automatically
with ConfigWatcher("path/to/config.yaml") as watcher:
    watcher.register("location", on_location_change)  # per top-level key
    watcher.register(None, lambda cfg: print("Config changed"))  # any change

    # ... run your application ...

Or manage the lifecycle manually:

watcher = ConfigWatcher("path/to/config.yaml")
watcher.register("location", on_location_change)
watcher.start()

# ... later ...
watcher.stop()

Multiple callbacks can be registered for the same key. register(None, callback) receives every change regardless of which key changed. Each callback receives the full updated config dict.

API reference

Watches a YAML config file and fires callbacks on changes.

Callbacks are registered per top-level config key. When the file changes, the new config is loaded and each callback whose key has a different value is invoked with the full updated config dict.

Use register(None, callback) to receive all changes regardless of which key changed.

Parameters:

Name Type Description Default
config_file str | Path

Path to the YAML config file to watch.

required
load_local bool

Whether to also load <name>_local.yaml overrides. Defaults to True, matching load_config behaviour.

True

Examples:

>>> import tempfile, pathlib, time
>>> tmp = pathlib.Path(tempfile.mktemp(suffix=".yaml"))
>>> _ = tmp.write_text("name: test\n")
>>> received = []
>>> watcher = ConfigWatcher(tmp)
>>> watcher.register(None, received.append)
>>> watcher.start()
>>> _ = tmp.write_text("name: changed\n")
>>> time.sleep(0.5)
>>> watcher.stop()
>>> tmp.unlink()
>>> received[-1]["name"]
'changed'

register(key, callback)

Register a callback for changes to a top-level config key.

Parameters:

Name Type Description Default
key str | None

The top-level config key to watch (e.g. "location"), or None to be notified of any change.

required
callback Callable[[dict], None]

A callable that receives the full updated config dict.

required

start()

Start watching the config file for changes.

stop()

Stop watching the config file.


Config file format

The config file is plain YAML. Angle and distance values use astropy unit strings:

name: My PANOPTES Unit
pan_id: PAN001

location:
  name: Mauna Loa Observatory
  latitude: 19.54 deg
  longitude: -155.58 deg
  elevation: 3400.0 m
  horizon: 30 deg
  flat_horizon: -6 deg
  focus_horizon: -12 deg
  observe_horizon: -18 deg
  timezone: US/Hawaii
  gmt_offset: -600

directories:
  base: /home/panoptes
  images: images
  data: data

db:
  name: panoptes
  type: file

Site-specific overrides go in config_local.yaml alongside the main file — this file is never committed to version control.