{"id":30876318,"url":"https://github.com/howdoicomputer/revontulet","last_synced_at":"2026-06-08T16:03:04.310Z","repository":{"id":264279955,"uuid":"892471723","full_name":"howdoicomputer/revontulet","owner":"howdoicomputer","description":"An API that is used to continuously track if one or more satellites are over a set of coordinates.","archived":false,"fork":false,"pushed_at":"2025-02-08T00:48:58.000Z","size":66,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2026-02-19T07:29:41.328Z","etag":null,"topics":["fastapi","hirshel","httpx","python","satellites"],"latest_commit_sha":null,"homepage":"https://revontulet.lol","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/howdoicomputer.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-11-22T07:11:09.000Z","updated_at":"2025-02-08T00:49:01.000Z","dependencies_parsed_at":null,"dependency_job_id":"4f948134-8581-49a1-a4c8-874c5798d9b1","html_url":"https://github.com/howdoicomputer/revontulet","commit_stats":null,"previous_names":["howdoicomputer/revontulet"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/howdoicomputer/revontulet","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/howdoicomputer%2Frevontulet","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/howdoicomputer%2Frevontulet/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/howdoicomputer%2Frevontulet/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/howdoicomputer%2Frevontulet/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/howdoicomputer","download_url":"https://codeload.github.com/howdoicomputer/revontulet/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/howdoicomputer%2Frevontulet/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34069501,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-08T02:00:07.615Z","response_time":111,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["fastapi","hirshel","httpx","python","satellites"],"created_at":"2025-09-08T02:08:48.197Z","updated_at":"2026-06-08T16:03:04.293Z","avatar_url":"https://github.com/howdoicomputer.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Revontulet | About\n\nRevontulet 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.\n\nThis 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.\n\nAPI docs: https://revontulet.lol/docs#/\n\nTo provide a quick example, this curl command hits the `/api/satellites/above/stream` endpoint with these parameters:\n\n| Parameter          | Value                                          | Description                                  |\n|--------------------|------------------------------------------------|----------------------------------------------|\n| `lat`              | `43.6045`                                     | Latitude of the observer.                   |\n| `lng`              | `1.444`                                       | Longitude of the observer.                  |\n| `search_radius`    | `90`                                          | Search radius in degrees around the observer.|\n| `sat_color_pairs`  | `10155,blue,11690,red,12818,yellow,14277,green`| Comma-separated list of NORAD_IDs and colors. |\n| `format`           | `text`                                        | Response format (`text` or `json`).         |\n\n\n``` sh\ncurl -X 'GET' \\\n  'https://revontulet.lol/api/satellites/above/stream?lat=43.6045\u0026lng=1.444\u0026search_radius=90\u0026sat_color_pairs=10155,blue,11690,red,12818,yellow,14277,green\u0026format=text' \\\n  -H 'accept: application/json'\n```\n\nAnd 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).\n\n``` sh\nNORAD_ID_10155: blue, NORAD_ID_11690: red, NORAD_ID_14277: green\nNORAD_ID_10155: blue, NORAD_ID_11690: red, NORAD_ID_14277: green\nNORAD_ID_10155: blue, NORAD_ID_11690: red, NORAD_ID_14277: green\nNORAD_ID_10155: blue, NORAD_ID_11690: red, NORAD_ID_14277: green\nNORAD_ID_10155: blue, NORAD_ID_11690: red, NORAD_ID_14277: green\nNORAD_ID_10155: blue, NORAD_ID_11690: red, NORAD_ID_14277: green\n```\n\n# How it works\n\n## Upstream APIs and caching\n\nThis API sends requests to two upstream APIs: https://sat.terrestre.ar/docs/#/ and https://www.n2yo.com/api/\n\nThere 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\nif 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.\n\n## Typing and Pydantic\n\nRequests 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.\n\n## Testing\n\nThe 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.\n\nAn 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.\n\nFor example,\n\n``` sh\nrevontulet-py3.13 ❯ ./end_to_end_test.sh\n\nThis script will query out satellites over a location, reprocess them into a sat_color_pair, and then feed that back into the API\nin order to create a representation of functionality.\n\nToulouse latitutde: 43.6045\nToulouse longitude: 1.444\nSearch Radius: 15\nAltitude: 0\nCategory ID: 0\nEndpoint: https://revontulet.lol\nResolved these sat_color_pairs: 5680,blue,6302,red,13603,yellow,14607,green\n\nSatellites (with their colors) currently above Toulouse, France:\n\n[{\"norad_id\":5680,\"color\":\"blue\"},{\"norad_id\":6302,\"color\":\"red\"},{\"norad_id\":13603,\"color\":\"yellow\"},{\"norad_id\":14607,\"color\":\"green\"}]\nThe same satellites but in text format:\n\n\"NORAD_ID_5680: blue, NORAD_ID_6302: red, NORAD_ID_13603: yellow, NORAD_ID_14607: green\"⏎\n```\n\n# Routes\n\n## /api/satellite\n\nThis route queries out a single satellite.\n\n``` sh\ncurl -X 'GET' \\\n  'https://revontulet.lol/api/satellite?norad_id=8018\u0026lat=43.6045\u0026lng=1.444\u0026days=1\u0026limit=1\u0026tz=America%2FLos_Angeles' \\\n  -H 'accept: application/json'\n```\n\n``` sh\n[\n  {\n    \"rise\": {\n      \"alt\": \"10.00\",\n      \"az\": \"331.95\",\n      \"az_octant\": \"NW\",\n      \"utc_datetime\": \"2024-11-23 11:34:55.644424+00:00\",\n      \"utc_timestamp\": 1732361695,\n      \"is_sunlit\": true,\n      \"visible\": false,\n      \"client_time\": \"2024-11-23 03:34:55 PST\"\n    },\n    \"culmination\": {\n      \"alt\": \"29.96\",\n      \"az\": \"315.83\",\n      \"az_octant\": \"NW\",\n      \"utc_datetime\": \"2024-11-23 13:26:06.053063+00:00\",\n      \"utc_timestamp\": 1732368366,\n      \"is_sunlit\": true,\n      \"visible\": false,\n      \"client_time\": \"2024-11-23 05:26:06 PST\"\n    },\n    \"set\": {\n      \"alt\": \"10.00\",\n      \"az\": \"296.41\",\n      \"az_octant\": \"NW\",\n      \"utc_datetime\": \"2024-11-23 16:09:15.689282+00:00\",\n      \"utc_timestamp\": 1732378155,\n      \"is_sunlit\": true,\n      \"visible\": false,\n      \"client_time\": \"2024-11-23 08:09:15 PST\"\n    },\n    \"visible\": false,\n    \"norad_id\": 8018\n  }\n]\n```\n\nThe 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.\n\n## /api/satellites\n\nThis endpoint is a proxy for the N2YO API. It exists only to provide base access and to help perform holistic system smoke tests.\n\nFor example, to get the list of satellites over Toulouse, France (with a 15 degree search radius):\n\n``` sh\ncurl -s -X 'GET' \\\n  'https://revontulet.lol/api/satellites?lat=43.6045\u0026lng=1.444\u0026cat=0\u0026alt=0\u0026search_radius=15' \\\n  -H 'accept: application/json'\n```\n\n## /api/satellites/above|stream\n\nThis 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.\n\n``` sh\ncurl -X 'GET' \\\n  'https://revontulet.lol/api/satellites/above?lat=43.6045\u0026lng=1.444\u0026search_radius=90\u0026sat_color_pairs=11690,blue\u0026format=text' \\\n  -H 'accept: application/json'\n```\n\n``` sh\n\"NORAD_ID_11690: blue\"⏎\n```\n\n`/api/satellites/above/stream` does the same using server sent events over an HTTP stream\n\n## /api/satellites/above/profile|stream\n\nThis 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.\n\n``` sh\ncurl -N \"https://revontulet.lol/api/satellites/above/profile/stream?name=golden\u0026format=json\u0026search_radius=90\"\n```\n\n## /metrics\n\nRevontulet exposes Prometheus metrics.\n\n## Profiles\n\nProfiles 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.\n\nFor example, in `config.py` here are the default profiles:\n\n``` sh\nclass Settings(BaseSettings):\n    n2yo_api_key: str = Field(..., description=\"The API key for N2YO\")\n    office_profiles: List[Office] = Field(\n        default=[\n            Office(\n                name=\"toulouse\",\n                lat=\"43.6045\",\n                lng=\"1.444\",\n                sat_color_pairs=[\n                    SatColorPair(norad_id=\"55076\", color=\"blue\"),\n                    SatColorPair(norad_id=\"48915\", color=\"white\"),\n                    SatColorPair(norad_id=\"59126\", color=\"red\"),\n                ],\n            ),\n            Office(\n                name=\"sf\",\n                lat=\"37.7749\",\n                lng=\"-122.4194\",\n                sat_color_pairs=[\n                    SatColorPair(norad_id=\"55076\", color=\"red\"),\n                    SatColorPair(norad_id=\"48915\", color=\"white\"),\n                    SatColorPair(norad_id=\"59126\", color=\"blue\"),\n                ],\n            ),\n            Office(\n                name=\"golden\",\n                lat=\"39.7555\",\n                lng=\"-105.2211\",\n                sat_color_pairs=[\n                    SatColorPair(norad_id=\"55076\", color=\"red\"),\n                    SatColorPair(norad_id=\"48915\", color=\"white\"),\n                    SatColorPair(norad_id=\"59126\", color=\"blue\"),\n                ],\n            ),\n        ],\n        description=\"A list of all office profiles\",\n    )\n```\n\nIf you want to change the defaults then create `config.json` and place a new set of profiles for Revontulet to load.\n\nHere is what that would look like:\n\n``` json\n{\n  \"office_profiles\": [\n    {\n      \"name\": \"berkeley\",\n      \"lat\": \"37.8715\",\n      \"lng\": \"122.2730\",\n      \"sat_color_pairs\": [\n        {\n          \"norad_id\": \"55076\",\n          \"color\": \"yellow\"\n        },\n        {\n          \"norad_id\": \"48915\",\n          \"color\": \"green\"\n        },\n        {\n          \"norad_id\": \"59126\",\n          \"color\": \"purple\"\n        }\n      ]\n    }\n  ]\n}\n```\n\nWhen you use a `config.json`, Revontulet will override ALL of the default profiles. In this case, only the `berkeley` profile would be available.\n\n# Configuration\n\nThere 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.\n\nHowever, 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.\n\n# Development Environment\n\nThere is a `Makefile` that helps ease development. Check out its\ndirectives to see what it can do.\n\n## Requirements\n\n* Poetry\n* Pyenv\n* GNU Make\n\n## Setup\n\nThis project uses Poetry to manage dependencies. Install Poetry and then run `poetry install` in the root directory.\n\n# Shipping a Version\n\nThe Makefile, by default, uses my Dockerhub username and account. You'll need to change it if you want to build your own images.\n\nThe instructions look like this:\n\n``` sh\npoetry export --without-hashes --format=requirements.txt \u003e requirements.txt\ndocker buildx build --platform linux/amd64 . -t howdoicomputer/revontulet:v$(VERSION)\n```\n\n\nThe Makefile uses docker buildx to create multiplatform builds in order to support multiple architectures.\n\n# Running via Docker\n\nThis command will pull down the latest Revontulet container and then bind it to port 8000. Make sure you set your N2YO_API_KEY :)\n\n``` sh\ndocker run -d -p 8000:8000 -e N2YO_API_KEY=$N2YO_API_KEY --name revontulet-api howdoicomputer/revontulet:latest\n```\n\n# Deployment\n\nThe https://revontulet.lol endpoint is hosted on my [homelab](https://howdoicomputer.lol/posts/homelab-1/) - which is a Nomad server that runs in my house. The files for deploying the API are in `deploy/nomad`.\n\nHowever, I did write some Kubernetes/ArgoCD manifests and they live in `deploy/k8s`.\n\nI 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.\n\n---\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhowdoicomputer%2Frevontulet","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhowdoicomputer%2Frevontulet","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhowdoicomputer%2Frevontulet/lists"}