Revontulet is an API that uses server sent events to create a notification bus to communicate when one or more of a group of satellites passes over a set of coordinates.
This project was originally a take home exercise that I took a bit too far and now I'm using it to build out lights in my office to track when ISS passes over my house.
API docs: https://revontulet.lol/docs#/
To provide a quick example, this curl command hits the /api/satellites/above/stream
endpoint with these parameters:
Parameter | Value | Description |
---|---|---|
lat |
43.6045 |
Latitude of the observer. |
lng |
1.444 |
Longitude of the observer. |
search_radius |
90 |
Search radius in degrees around the observer. |
sat_color_pairs |
10155,blue,11690,red,12818,yellow,14277,green |
Comma-separated list of NORAD_IDs and colors. |
format |
text |
Response format (text or json ). |
curl -X 'GET' \
'https://revontulet.lol/api/satellites/above/stream?lat=43.6045&lng=1.444&search_radius=90&sat_color_pairs=10155,blue,11690,red,12818,yellow,14277,green&format=text' \
-H 'accept: application/json'
And we get back this response as an HTTP stream on a 10 second timer (notice that yellow is missing; it wasn't flying over those coordinates at the time).
NORAD_ID_10155: blue, NORAD_ID_11690: red, NORAD_ID_14277: green
NORAD_ID_10155: blue, NORAD_ID_11690: red, NORAD_ID_14277: green
NORAD_ID_10155: blue, NORAD_ID_11690: red, NORAD_ID_14277: green
NORAD_ID_10155: blue, NORAD_ID_11690: red, NORAD_ID_14277: green
NORAD_ID_10155: blue, NORAD_ID_11690: red, NORAD_ID_14277: green
NORAD_ID_10155: blue, NORAD_ID_11690: red, NORAD_ID_14277: green
This API sends requests to two upstream APIs: https://sat.terrestre.ar/docs/#/ and https://www.n2yo.com/api/
There are home built API clients for both in libs/
that use httpx to asynchronously fetch API results and then cache them on a 10 second TTL
if the responses are 200s. The reason why they're writen using httpx is because requests
is a synchronous client that will block FastAPI's (the underlying API framework) event loop. There were some open source clients that already existed for the above APIs but they were synchronous in nature so I had to write my own.
Requests to the routes themselves use those clients to fetch data and then filter and annotate the results. The codebase makes extensive use of Pydantic models to provide documented, typed, and validated interfaces for both the API routes and internal functions.
The external routes are tested using pytest with mocks and stubbed out data. The mocks and stubs allows our unit tests to be ran without the Internet or secrets.
An end-to-end test exists in scripts/end_to_end_test.sh
that is convenient for me to use. It will curl /api/satellites
to resolve a set of satellites currently over a coordinate pair and then create a sat_color_pair
string to feed into the /api/satellites/above
endpoint to resolve satellites and their color data for a location.
For example,
revontulet-py3.13 ❯ ./end_to_end_test.sh
This script will query out satellites over a location, reprocess them into a sat_color_pair, and then feed that back into the API
in order to create a representation of functionality.
Toulouse latitutde: 43.6045
Toulouse longitude: 1.444
Search Radius: 15
Altitude: 0
Category ID: 0
Endpoint: https://revontulet.lol
Resolved these sat_color_pairs: 5680,blue,6302,red,13603,yellow,14607,green
Satellites (with their colors) currently above Toulouse, France:
[{"norad_id":5680,"color":"blue"},{"norad_id":6302,"color":"red"},{"norad_id":13603,"color":"yellow"},{"norad_id":14607,"color":"green"}]
The same satellites but in text format:
"NORAD_ID_5680: blue, NORAD_ID_6302: red, NORAD_ID_13603: yellow, NORAD_ID_14607: green"⏎
This route queries out a single satellite.
curl -X 'GET' \
'https://revontulet.lol/api/satellite?norad_id=8018&lat=43.6045&lng=1.444&days=1&limit=1&tz=America%2FLos_Angeles' \
-H 'accept: application/json'
[
{
"rise": {
"alt": "10.00",
"az": "331.95",
"az_octant": "NW",
"utc_datetime": "2024-11-23 11:34:55.644424+00:00",
"utc_timestamp": 1732361695,
"is_sunlit": true,
"visible": false,
"client_time": "2024-11-23 03:34:55 PST"
},
"culmination": {
"alt": "29.96",
"az": "315.83",
"az_octant": "NW",
"utc_datetime": "2024-11-23 13:26:06.053063+00:00",
"utc_timestamp": 1732368366,
"is_sunlit": true,
"visible": false,
"client_time": "2024-11-23 05:26:06 PST"
},
"set": {
"alt": "10.00",
"az": "296.41",
"az_octant": "NW",
"utc_datetime": "2024-11-23 16:09:15.689282+00:00",
"utc_timestamp": 1732378155,
"is_sunlit": true,
"visible": false,
"client_time": "2024-11-23 08:09:15 PST"
},
"visible": false,
"norad_id": 8018
}
]
The response is similar to the response given by the Terrestare Pass API except that you can pass in a timezone to get a localized client_time field.
This endpoint is a proxy for the N2YO API. It exists only to provide base access and to help perform holistic system smoke tests.
For example, to get the list of satellites over Toulouse, France (with a 15 degree search radius):
curl -s -X 'GET' \
'https://revontulet.lol/api/satellites?lat=43.6045&lng=1.444&cat=0&alt=0&search_radius=15' \
-H 'accept: application/json'
This endpoint is the meat and potatoes of the app. It takes in a coordinate pair, a list of sat_color_pairs, and a search_radius in order to create a diffed list of satellites that passed over a set of coordinates.
curl -X 'GET' \
'https://revontulet.lol/api/satellites/above?lat=43.6045&lng=1.444&search_radius=90&sat_color_pairs=11690,blue&format=text' \
-H 'accept: application/json'
"NORAD_ID_11690: blue"⏎
/api/satellites/above/stream
does the same using server sent events over an HTTP stream
This route is the same as above but uses profiles for predeclared locations. For example, in config.py
you will see entries for Toulouse, France, Golden, CO, and San Francisco, CA. Each location has a set of sat_color_pairs. Instead of setting in HTTP query parameters that specifies lat/lng coordinates, sat_color_pairs, etc, a client can lean on a profile for a simpler request.
curl -N "https://revontulet.lol/api/satellites/above/profile/stream?name=golden&format=json&search_radius=90"
Revontulet exposes Prometheus metrics.
Profiles are a way for Revontulet to help clients be even more thin. They are preconfigured locations that already include lat/lng coordinates and sat_color_pairs.
For example, in config.py
here are the default profiles:
class Settings(BaseSettings):
n2yo_api_key: str = Field(..., description="The API key for N2YO")
office_profiles: List[Office] = Field(
default=[
Office(
name="toulouse",
lat="43.6045",
lng="1.444",
sat_color_pairs=[
SatColorPair(norad_id="55076", color="blue"),
SatColorPair(norad_id="48915", color="white"),
SatColorPair(norad_id="59126", color="red"),
],
),
Office(
name="sf",
lat="37.7749",
lng="-122.4194",
sat_color_pairs=[
SatColorPair(norad_id="55076", color="red"),
SatColorPair(norad_id="48915", color="white"),
SatColorPair(norad_id="59126", color="blue"),
],
),
Office(
name="golden",
lat="39.7555",
lng="-105.2211",
sat_color_pairs=[
SatColorPair(norad_id="55076", color="red"),
SatColorPair(norad_id="48915", color="white"),
SatColorPair(norad_id="59126", color="blue"),
],
),
],
description="A list of all office profiles",
)
If you want to change the defaults then create config.json
and place a new set of profiles for Revontulet to load.
Here is what that would look like:
{
"office_profiles": [
{
"name": "berkeley",
"lat": "37.8715",
"lng": "122.2730",
"sat_color_pairs": [
{
"norad_id": "55076",
"color": "yellow"
},
{
"norad_id": "48915",
"color": "green"
},
{
"norad_id": "59126",
"color": "purple"
}
]
}
]
}
When you use a config.json
, Revontulet will override ALL of the default profiles. In this case, only the berkeley
profile would be available.
There is really only one configuration value required and that's an N2YO_API_KEY
. There is an sample-envrc
in this repository that you can modify and then cp sample-envrc ./.envrc
to use direnv to autoload that key for development.
However, you are able to override profiles in a config.json
file. Revontulet is able to start without that configuration file. Additionally, you can specify a n2yo_api_key
value in that configuration file but it will take backseat to the environment variable equivalent.
There is a Makefile
that helps ease development. Check out its
directives to see what it can do.
- Poetry
- Pyenv
- GNU Make
This project uses Poetry to manage dependencies. Install Poetry and then run poetry install
in the root directory.
The Makefile, by default, uses my Dockerhub username and account. You'll need to change it if you want to build your own images.
The instructions look like this:
poetry export --without-hashes --format=requirements.txt > requirements.txt
docker buildx build --platform linux/amd64 . -t howdoicomputer/revontulet:v$(VERSION)
The Makefile uses docker buildx to create multiplatform builds in order to support multiple architectures.
This command will pull down the latest Revontulet container and then bind it to port 8000. Make sure you set your N2YO_API_KEY :)
docker run -d -p 8000:8000 -e N2YO_API_KEY=$N2YO_API_KEY --name revontulet-api howdoicomputer/revontulet:latest
The https://revontulet.lol endpoint is hosted on my homelab - which is a Nomad server that runs in my house. The files for deploying the API are in deploy/nomad
.
However, I did write some Kubernetes/ArgoCD manifests and they live in deploy/k8s
.
I thought about deploying this to GKE (I have a bunch of Terraform to do that) but my GCP free credits are running thin and my homelab is incredibly easy to deploy to.