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:
$PANOPTES_CONFIG_FILEenvironment variable (if set)~/.panoptes/config.yaml- 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.yamlcompanion 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 |
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. |
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.