Starlink Satellite SMS Notifications with Python, Kubernetes and Twilio
Tags: twilio, kubernetes, docker
Published: 2020-08-23 15:50:54

---
title: Starlink Satellite SMS Notifications with Python, Kubernetes and Twilio
published: true
date: 2020-08-22 01:41:13 UTC
tags: twilio,kubernetes,docker
canonical_url: https://www.twilio.com/blog/starlink-satellite-sms-notifications-python-kubernetes-twilio
cover_image: https://cdn-images-1.medium.com/max/1024/1*Pbn1u7N4c6TANTzn-LJDqQ.jpeg
---

### Set up a CronJob to check for satellites

SpaceX is launching thousands of [Starlink](https://www.starlink.com/) satellites to assemble a giant interconnected constellation in space. If you look up at just the right time, you might be lucky enough to spot some.

But how can you know ahead of time when a satellite is going to pass overhead?

You don’t have to count on luck to see these tiny silver ants parading across the night sky. This tutorial shows you how to set up a scheduled job to check if a satellite approaches and send an SMS alert.

### Tutorial Requirements

- Python version 3
- A personal phone number to receive SMS notifications
- [A freeTwilio account](http://www.twilio.com/referral/geBKT7) and a [Twilio phone number](https://www.twilio.com/user/account/phone-numbers/incoming) to send SMS notifications
- A free [Docker Hub](https://hub.docker.com/) account, [Docker Desktop](https://docs.docker.com/desktop/) (for Mac or Windows) or [Docker Engine](https://docs.docker.com/engine/install/) (for Linux) to build and share containerized applications
- A free [KubeSail account](https://kubesail.com/) to deploy your code to a Kubernetes cluster

### Set up the Python virtual environment

Make a new project directory, and change into the directory from the command line.

```bash
$ mkdir starlink-alert
$ cd starlink-alert
```

Create a virtual environment called venv. Activate the virtual environment, and then install the required Python packages inside the virtual environment. If you’re on Unix or Mac operating systems, enter these commands in a terminal.

```bash
$ python3 -m venv venv
$ source venv/bin/activate
(venv) $ pip install twilio python-dotenv datetime pytz skyfield
```

If you’re on a Windows machine, enter these commands in a command prompt window.

```powershell
$ python -m venv venv
$ venv\Scripts\activate
(venv) $ pip install twilio python-dotenv datetime pytz skyfield
```

The Python packages we’re using are:

- [Twilio’s Python Library](https://github.com/twilio/twilio-python) — a Python module for communicating with the Twilio API
- [python-dotenv](https://pypi.org/project/python-dotenv/) — to manage environment variables
- [datetime](https://docs.python.org/3/library/datetime.html) and [pytz](http://pytz.sourceforge.net/) — to handle dates, times, and timezones
- [skyfield](https://rhodesmill.org/skyfield/) — to compute positions for the satellites in orbit around the Earth

### Write the Python script

The purpose of the script is to check if a visible satellite is approaching a given city location. If so, your Twilio phone number sends an SMS alert to your personal phone number.

Copy the following code into a file called `tracker.py`:

```python
import os
import math
from dotenv import load_dotenv
load_dotenv()
from skyfield.api import Topos, load
from datetime import timedelta
from pytz import timezone
from twilio.rest import Client

# load the satellite dataset from Celestrak
starlink_url = 'https://celestrak.com/NORAD/elements/starlink.txt'
starlinks = load.tle_file(starlink_url)
print ('Loaded', len(starlinks), 'satellites')

# update city location and timezone
location = Topos('37.7749 N', '122.4194 W')
tz = timezone('US/Pacific')

# establish time window of opportunity
ts = load.timescale()
t0 = ts.now()
t1 = ts.from_datetime(t0.utc_datetime()+ timedelta(hours=2))

# loop through satellites to find next sighting
first_sighting = {}
for satellite in starlinks:

   # filter out farthest satellites and NaN elevation
   elevation = satellite.at(t0).subpoint().elevation.km
   isNan = math.isnan(elevation)
   if elevation > 400 or isNan: continue
   print ('considering: {} at {}km'.format(
       satellite.name,
       round(elevation)
   ))

   # find and loop through rise / set events
   t, events = satellite.find_events(location, t0, t1, altitude_degrees=30.0)
   for ti, event in zip(t, events):

       # check if satellite visible to a ground observer
       eph = load('de421.bsp')
       sunlit = satellite.at(t1).is_sunlit(eph)
       if not sunlit: continue

       # filter by moment of greatest altitude - culminate
       name = ('rise above 30°', 'culminate', 'set below 30°')[event]
       if (name != 'culminate'): continue

       # find earliest time for next sighting
       if (not first_sighting) or (ti.utc < first_sighting['time']):
           first_sighting['time_object'] = ti
           first_sighting['time'] = ti.utc
           first_sighting['satellite'] = satellite

if (first_sighting): 

   # create body for SMS  
   next_sighting = ('next sighting: {} {}'.format(
       first_sighting['satellite'].name,
       first_sighting['time_object'].astimezone(tz).strftime('%Y-%m-%d %H:%M')
   ))

   # send SMS via Twilio if upcoming sighting
   account_sid = os.environ.get('TWILIO_ACCOUNT_SID')
   auth_token = os.environ.get('TWILIO_AUTH_TOKEN')
   client = Client(account_sid, auth_token)

   message = client.messages.create(
       body=next_sighting,
       from_=os.environ.get('TWILIO_PHONE_NUMBER'),
       to=os.environ.get('MY_PHONE_NUMBER')
   )

   print ('Message sent:', message.sid, next_sighting)

else:

   print ('No upcoming sightings')
```

Update variables `location` and `tz` on rows 16 to 17 with your own geographic coordinates and timezone. You can find your latitude and longitude [here](https://www.latlong.net/). See all the timezones by running the following code in a Python shell. Once you find the timezone that applies to your location, exit the Python shell.

```bash
(venv) $ python
>>> import pytz
>>> pytz.all_timezones
>>> exit()
```

The `tracker.py` script relies on a Python package called Skyfield to handle some of the complex space computations. The script loads the satellite dataset, looping through the satellites to find the next good sighting. Skyfield figures out when a satellite rises and sets near you and sees if the satellite is sunlit for maximum visibility. If there are any sightings predicted within the next two hours, Twilio sends the details with an SMS to your phone number.

![](https://cdn-images-1.medium.com/max/1024/1*MGeuijv4jRv4_Mh1QjeHtg.jpeg)<figcaption>The Starlink 3 train rides across the early morning sky, photo by <a href="https://unsplash.com/@forestkatsch?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Forest Katsch</a> on <a href="https://unsplash.com/s/photos/starlink?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></figcaption>

To enable the Twilio SMS, store your credentials and other configuration information in a file called `.env` (notice the dot in front of this filename).

```
TWILIO_ACCOUNT_SID=<your-account-sid>
TWILIO_AUTH_TOKEN=<your-auth-token>
TWILIO_PHONE_NUMBER=<your-twilio-phone-number>
MY_PHONE_NUMBER=<your-personal-phone-number>
```

To find your Twilio Account SID and Auth Token, log into the Twilio Console, then click on “Settings” in the sidebar and scroll down to “API Credentials”. You will also need your Twilio phone number to send the SMS.

Save your updates to the `.env` file, and then run the script from the command line. If you’re running the script during the day, you might determine a satellite is passing overhead, but not be able to see it very well in the daylight. Once you set up a CronJob, you can specify when to run the script for maximum satellite visibility, like at dusk or dawn.

```bash
(venv) $ python tracker.py
```

After you try the script and confirm that it is working, create a requirements file to store the dependencies, so that we can install them in the next step.

```bash
(venv) $ pip freeze > requirements.txt
```

Now, let’s create a Docker image to run the script.

### Create a Docker image and push to Docker Hub

Make sure Docker is installed and running on your machine. Add the following code in a file called `Dockerfile` (notice there’s no file extension) that describes how to build our image.

```Dockerfile
# creates a layer from the python:3 Docker image
FROM python:3

# copy and install dependencies
COPY requirements.txt /
RUN pip install -r requirements.txt

# add script
COPY tracker.py /

# define the command to run the script
CMD ["python", "./tracker.py"]
```

Next we are going to build and run a Docker image for the Python script from the terminal. In the following commands, the image we are building is called `python-starlink` and the container is called `starlink_test`. Since we don’t want to include our environment variables in the container for security reasons, we run the container using the `--env-file` flag in the command line. If you’re on a Linux operating system, precede these `docker` commands with `sudo` or [create a docker group](https://docs.docker.com/engine/install/linux-postinstall/).

```bash
$ docker build --tag python-starlink .
$ docker run --name starlink_test --env-file=.env --rm python-starlink
```

Once you verify the container runs successfully, share the Docker image on the [Docker Hub](https://hub.docker.com/) registry so it can be accessed and run from the cloud. Log into your Docker Hub account from the command line with docker login. Then create a repository and push your image to Docker Hub using the following commands:

```bash
$ docker tag python-starlink <Your Docker ID>/python-starlink:latest
$ docker push <Your Docker ID>/python-starlink:latest
```

In the above commands, `Your Docker ID` is the username you have registered with Docker Hub.

In the next step, let’s schedule a Kubernetes CronJob to run the Python script.

### Deploy a CronJob on Kubernetes

Make sure you are logged into [KubeSail](https://kubesail.com/). KubeSail has public YAML examples. I made a CronJob template called [`satellites`](https://kubesail.com/template/loopDelicious/satellites) that has two kinds of Kubernetes resources:

- [`Secrets`](https://kubernetes.io/docs/concepts/configuration/secret/) to securely store environment variables
- [`CronJob`](https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/) to complete a task on a schedule

Go to the [`satellites`](https://kubesail.com/template/loopDelicious/satellites) template. Under “Required Variables”, replace the default `DOCKER-IMAGE` with the base image you just pushed to Docker Hub. Then input the remaining environment variables.

![](https://cdn-images-1.medium.com/max/500/0*go-NQPVPTXVK9Qw-.png)

If you want to inspect the YAML, click “Edit Yaml” to expand the code editor and review the underlying configuration for each resource. This YAML describes the resource `CronJob`.

```yml
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: starlink
  labels:
    app: starlink
spec:
  schedule: "0/30 19-21 * * *"
  jobTemplate:
    spec:
      template:
        spec:
           restartPolicy: Never
          containers:
            - name: starlink-tracker
              image: "{{DOCKER-IMAGE|joycelin79/python-starlink:latest}}"
              envFrom:
                - secretRef:
                    name: starlink-secret
              imagePullPolicy: Always
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 1
```

This YAML describes the resource `Secret` to store our environment variables.

```yml
apiVersion: v1
kind: Secret
metadata:
  name: starlink-secret
  labels:
    app: starlink
stringData:
  TWILIO_ACCOUNT_SID: "{{TWILIO-ACCOUNT-SID}}"
  TWILIO_AUTH_TOKEN: "{{TWILIO-ACCOUNT-SID}}"
  TWILIO_PHONE_NUMBER: "{{TWILIO-PHONE-NUMBER}}"
  MY_PHONE_NUMBER: "{{MY-PHONE-NUMBER}}"
```

When you’re ready, hit “Launch Template” to deploy a cluster to your Kubernetes namespace on KubeSail.

In the sidebar under “Resources”, you can see all the resources running in this Kubernetes context including the scheduled jobs.

![](https://cdn-images-1.medium.com/max/500/0*qjlbF7Kobw1RdQBA.png)

This is also where you can update the deployment. For example, update your environment variables in the `Secret` resource. To trigger a job every minute while you’re testing, update the YAML configuration for the `CronJob` resource and set the `schedule` to `"* * * * *"`(surrounded in double quotes) and **Apply** your changes.

To edit the underlying Python script, save the changes locally in `tracker.py`. Then build and push the updated image to Docker Hub like we did before. The Kubernetes CronJob in KubeSail automatically pulls the latest version in Docker Hub because we specified `imagePullPolicy: Always` in our container configuration.

And that’s it!

### Conclusion

Creating an SMS notification is pretty straightforward. There’s a bunch of ways to do this — and now you know how to schedule a CronJob on Kubernetes.

For further exploration, you can:

- **Adjust the criteria:** Each batch of Starlink satellites spreads out over time traveling to a higher elevation. If you want to increase the sensitivity of your tracker to see more satellites, adjust the elevation in your script. Or update the cron schedule to run [when satellites are most visible from the ground](https://www.space.com/6870-spot-satellites.html) — at dusk and dawn.
- **Change the deployment** : Set up a different Kubernetes context in KubeSail to deploy the CronJob to a cluster running locally or on another cloud provider. Or experiment with [other CronJob options in Kubernetes](https://kubernetes.io/docs/tasks/job/automated-tasks-with-cron-jobs/).
- **Track something else** : If satellites aren’t your thing, swap it out for [a new dataset of celestial bodies](https://celestrak.com/).

Let me know if you spot something out of this world!

* * *