# Planefence and Plane-Alert

At the end, your receiver tells you when an unusual aircraft is in range. You get a message in a chat app with the aircraft type, its operator, a photo and a picture of your own map. You also get a log of every aircraft that passes low over your house.

Spurious Emissions · Rev A · checked 2026-10-10 · https://spurious-emissions.net/guides/plane-alerts/

Some links are affiliate links: if you buy through them, I may earn a commission at no extra cost to you. As an Amazon Associate I earn from qualifying purchases.

## 1. What this guide builds

Two programs read the aircraft data that your ADS-B receiver already decodes. The first program keeps a log of aircraft that pass low and close. The second program compares every aircraft with a public watchlist and sends you a message for each match.

This guide adds no antenna and no radio. It starts where the guide [Receive ADS-B](/guides/receive-adsb/) ends: a receiver that works, and a map with aircraft on it.

| Item | In this guide |
|---|---|
| Signal | ADS-B, the position messages that aircraft send |
| Frequency | 1090 MHz |
| Antenna | The 1090 MHz antenna you already have |
| Radio | The USB radio you already have |
| Decoder | The decoder you already have (readsb, in the ultrafeeder container) |
| New programs | planefence and plane-alert (one container), and a screenshot helper (one container) |
| Computer | The computer that runs the decoder, or another computer on the same network |

```
 your ADS-B receiver (already built)                 new in this guide
 -----------------------------------                 -----------------
 antenna -- radio -- ultrafeeder --+-- 30003/tcp ---- planefence container
                     (decoder      |   (text stream)    |-- planefence:  fence log (web page)
                      and map)     |                    `-- plane-alert: watchlist + emergency codes
                                   |                              |
                                   |-- the map, at 80 --- screenshot container
                                   |                              |  (map picture)
                                   |                              v
                                   |                     Discord channel (webhook)
                                   |
                                   `-- aircraft.json ---- Home Assistant (optional)
                                                                  |
                                                                  v
                                                          alert on a phone
```

Two words that this guide uses:

- **Fence.** A cylinder of sky around your antenna: a radius and a maximum altitude. Planefence logs each aircraft that enters it.
- **Watchlist.** A list of aircraft addresses. Every aircraft sends a 24-bit address. Plane-alert looks for each address in the list.

## 2. What you need

You buy nothing new if your receiver computer has memory to spare. A part marked ◆ is what I use. A blank box is for your pencil. Prices are US dollars, as listed by the named seller on 2026-10-07.

### What the guide needs from your receiver

| ☐ | Item | Why | How to check |
|---|---|---|---|
| ☐ | A text stream of aircraft data in SBS format at `30003/tcp` | Planefence and plane-alert read only this stream | Section 4 |
| ☐ | A tar1090 map that a browser can open | The screenshot helper takes its picture from this map | Open the map in a browser |
| ☐ | The file `aircraft.json` over HTTP | The optional Home Assistant rule reads it | Section 4 |
| ☐ | Docker with `docker compose` | Both new programs are Docker containers | `docker compose version` |

SBS format (also named BaseStation format) is one line of plain text for each aircraft message. It is not the same as Beast format, the binary stream that feeder programs read at `30005/tcp`. A Docker container is a program that is packed with everything it needs, and that runs apart from the rest of the computer.

The ultrafeeder container from the guide [Receive ADS-B](/guides/receive-adsb/) gives all three items with no change. For other decoders, read section 4.

### Hardware

| ☐ | Part | Job | Price | Note |
|---|---|---|---|---|
| ☐ | ◆ [FlightAware Pro Stick Plus](https://flightaware.com/adsb/prostick/) | The ADS-B radio, already in your receiver | $45.99, FlightAware store | Any radio that your decoder supports is correct |
| ☐ | ◆ [Raspberry Pi 4](https://www.amazon.com/dp/B07TC2BK1X?tag=spuriousemiss-20) | The computer at my antenna cable. It runs the radio and the decoder | See listing | My layout uses two computers[^layout] |
| ☐ | ◆ [ZimaBlade](https://www.zimaspace.com/products/blade-personal-nas) | A small server. It runs ultrafeeder, planefence and the screenshot helper | See listing | Any 64-bit computer that runs Docker is correct |
| ☐ | [Raspberry Pi 5](https://www.amazon.com/dp/B0CK2FCG1K?tag=spuriousemiss-20), 8 GB | One computer for the receiver and the alerts | $175 list (4 GB: $110) | The lowest-cost path if you start new |

### Software and services

| ☐ | Item | Job | Cost |
|---|---|---|---|
| ☐ | ◆ [docker-planefence](https://github.com/sdr-enthusiasts/docker-planefence) | Planefence and plane-alert in one container | Free, GPL-3.0 |
| ☐ | ◆ [plane-alert-db](https://github.com/sdr-enthusiasts/plane-alert-db) | The community watchlist | Free, Open Database License |
| ☐ | ◆ [browser-screenshot-service](https://github.com/kx1t/browser-screenshot-service) | The screenshot helper: it opens your map in a hidden browser and takes a picture | Free. GitHub shows no standard licence for it |
| ☐ | ◆ [Discord](https://discord.com/) account and a server of your own | Receives the alert messages | Free |
| ☐ | ◆ [Home Assistant](https://www.home-assistant.io/) with its phone app | Optional: the low-aircraft alert on a phone | Free |

### Lowest-cost path

| Path | What you run | What you give up |
|---|---|---|
| Lowest cost | Only the planefence container, on the computer that already runs your receiver. Alerts with the photo from the watchlist | The picture of your own map. The alert on a phone for low aircraft |
| What I use ◆ | The planefence container, the screenshot helper, and Home Assistant on a second computer | Nothing, but it needs more memory |

The screenshot helper is the heavy part. On my station it used 0.66 GB of memory. Its image is a download of about 350 MB (the project's compose file says so). On a Raspberry Pi with 4 GB that also runs the receiver, memory is tight, and I did not test it.

## 3. Before you start

**Skills.** You can log in to your receiver computer with SSH. You can edit a text file there with `nano`. You can start a container with `docker compose up -d`.

**Time.** About one hour for sections 4 to 10. About one more hour for the Home Assistant rule. Then wait one day before you tune.

**Accounts.** A Discord account. Nothing else.

**Values to choose first.** Write these down. The steps use them as placeholders.

| Placeholder | Meaning | Example form |
|---|---|---|
| `<LAT>` `<LON>` | Antenna position, decimal degrees | `xx.xxxxx` |
| `<TZ>` | Time zone, TZ database name | `America/Los_Angeles` |
| `<NAME>` | A station name with no personal detail | `my-station` |
| `<RADIUS>` | Fence radius, in nautical miles | `2.0` |
| `<MAXALT>` | Fence height, in feet | `5000` |
| `<WEBHOOK>` | The address that Discord gives you in section 9 | a long `https://discord.com/api/webhooks/...` address |
| `<LOW_FT>` | Altitude for the phone alert, in feet | `1500` |
| `<DEVICE>` | The name of your phone in Home Assistant | from the Home Assistant phone app |

In this guide the receiver computer has the name `vhf`, and the project folder is `/opt/station`. Use your own names if they are different.

**The law, in short.** I am not a lawyer. This is what I read, for the United States. This guide only receives. ADS-B is a broadcast with no encryption. US law (47 U.S.C. section 605) limits what you may tell others about radio messages that are not meant for the public. It makes exceptions, one of them for messages about aircraft in distress. Other countries have other rules. The watchlist has its own licence: read it before you republish the list.

**Manners.** An alert tells you that an aircraft is in range. It does not tell you why. An emergency code is a matter for air traffic control. Do not call anyone about it.

## 4. Check the data stream from your receiver

Do this before you install anything. Planefence fails without the stream at `30003/tcp`.

### If your decoder is the ultrafeeder container

1. Log in to `vhf` with SSH.
2. Read three lines of the stream.

   ```bash
   docker exec ultrafeeder timeout 5 bash -c 'head -3 < /dev/tcp/localhost/30003'
   ```

   Result: three lines of text that start with `MSG,`. If the sky is empty, the command prints nothing and stops after 5 seconds. Try again when the map shows aircraft.

3. Read the aircraft file.

   ```bash
   curl -s http://localhost:8080/data/aircraft.json | head -c 300
   ```

   Result: text that starts with `{ "now" :` and contains `"hex":` entries. `8080` is the map address from the ultrafeeder compose file. Use yours if it is different.

4. Open the map in a browser: `http://vhf:8080/`. Result: your map, with aircraft.

### If your decoder is another program

The planefence README lists these decoders: ultrafeeder, tar1090, readsb, dump1090 and dump1090-fa.

- For a decoder that does not run in a container, the README says to turn on SBS output at `30003/tcp`. For readsb the option is `--net-sbs-port=30003`.
- For the older `readsb-protobuf` container, the README gives two environment lines: `READSB_NET_SBS_OUTPUT_PORT=30003` and `READSB_EXTRA_ARGS=--net-beast-reduce-interval 2 --net-sbs-reduce`.
- The screenshot helper sends settings that only a tar1090 map understands. The project says they may not work with a PiAware or SkyAware map.

I did not run planefence against any of these. My own layout puts ultrafeeder between the radio computer and planefence.[^layout]

### Aircraft found by multilateration

Multilateration (MLAT) finds an aircraft that sends no position. Several receivers measure the arrival time of the same message. By default, ultrafeeder keeps those aircraft out of the SBS stream. To include them, add one line to the `environment:` list of the `ultrafeeder` service.

```yaml
      - READSB_FORWARD_MLAT_SBS=true
```

Then run `docker compose up -d`. Result: planefence and plane-alert also see aircraft that your station places by multilateration. I did not check if my own station sets this line.

## 5. Start the two containers

1. Open the compose file that holds your `ultrafeeder` service.

   ```bash
   cd /opt/station && nano docker-compose.yml
   ```

2. Add these two services below the last service. Keep the two spaces at the start of the lines `planefence:` and `screenshot:`.

   ```yaml
     planefence:
       image: ghcr.io/sdr-enthusiasts/docker-planefence
       container_name: planefence
       hostname: planefence
       restart: unless-stopped
       ports:
         - 8088:80
         - 9999:9999
       environment:
         - TZ=<TZ>
       volumes:
         - /opt/station/planefence/config:/usr/share/planefence/persist
         - /opt/station/planefence/html:/usr/share/planefence/html
       tmpfs:
         - /run:rw,nosuid,nodev,exec,relatime,size=512M
         - /tmp:rw,nosuid,nodev,noexec,relatime,size=128M

     screenshot:
       image: ghcr.io/kx1t/screenshot
       container_name: screenshot
       hostname: screenshot
       restart: unless-stopped
       shm_size: 1gb
       environment:
         - MAP_ARGS=zoom=7&hideSidebar&hideButtons&mapDim=0.2&monochromeMarkers=ff0000&outlineColor=505050&iconScale=1.5&enableLabels&extendedLabels=2&trackLabels&screenshot&rangerings=0
         - LOAD_SLEEP_TIME=25
         - BASE_URL=http://ultrafeeder
         - MAXTIME=60
         - CANVAS_SIZE=800,1200
   ```

   The host name of the second container must be `screenshot`. Planefence looks for that name. For the lowest-cost path, leave out the `screenshot` service.

3. Save the file. Start the containers.

   ```bash
   docker compose up -d
   ```

   Result: Docker downloads two images and starts them. The first start can take a minute or more.

4. Read the log of planefence.

   ```bash
   docker logs -f planefence
   ```

   Result: warnings that `planefence.config` is not available. That is correct at this point. Press Ctrl-C to stop the log.

5. Make the settings file from the template that the container wrote.

   ```bash
   cd /opt/station/planefence/config
   cp planefence.config.RENAME-and-EDIT-me planefence.config
   ```

   Result: a file `planefence.config` with every setting and a comment for each one.

> **Caution.** The settings page at `9999` and the log page at `8088` have no password that I found in the README. Anyone who opens them sees your position and your fence. Do not make them reachable from the internet.

Planefence also has a settings page in the browser, at `http://vhf:9999/`. The README recommends it. It writes the same file. This guide edits the file, because a file is easier to show in print.

### If planefence runs on another computer

Containers in one compose file find each other by name. On another computer they cannot. Then do these three things:

1. In the compose file of the receiver, add `- 30003:30003` to the `ports:` list of `ultrafeeder`. Run `docker compose up -d` there.
2. In section 6, set `PF_SOCK30003HOST` to the name or the address of the receiver computer.
3. In the `screenshot` service, set `BASE_URL` to the full address of your map, for example `http://vhf:8080`.

## 6. Set the fence

1. Open the settings file.

   ```bash
   nano /opt/station/planefence/config/planefence.config
   ```

2. Find each of these lines and set its value. Do not rename a setting.

   ```ini
   FEEDER_LAT=<LAT>
   FEEDER_LON=<LON>
   PF_SOCK30003HOST=ultrafeeder
   PF_MAXDIST=<RADIUS>
   PF_MAXALT=<MAXALT>
   PF_DISTUNIT=nauticalmile
   PF_ALTUNIT=feet
   PF_NAME="<NAME>"
   ```

   This file spells the longitude `FEEDER_LON`. The ultrafeeder files spell it `FEEDER_LONG` or `READSB_LON`.

3. Save the file. Start the container again with the new settings.

   ```bash
   cd /opt/station && docker compose up -d planefence --force-recreate
   ```

4. Wait 2 minutes. Open the log page: `http://vhf:8088/`.

   Result: a page with a table. The top of the page shows the time of the last update and the number of messages received. The README says the page updates about every 80 seconds. A row appears when an aircraft enters the fence.

### How to choose the fence

| Setting | My value ◆ | What it does |
|---|---|---|
| `PF_MAXDIST` | `2.0` | The radius of the fence, in the unit of `PF_DISTUNIT`. The template value is also 2.0 |
| `PF_MAXALT` | `5000` | The top of the fence, in the unit of `PF_ALTUNIT`. The template value is also 5000 |
| `PF_ELEVATION` | not changed | Your height above sea level. If it is more than 0, planefence reports height above ground |
| `PF_FUDGELOC` | not changed (`3`) | How far the page rounds your position. `2` rounds to 0.01 degree, `0` to whole degrees |
| `PF_DELETEAFTER` | not changed (`90`) | Days that the log stays. `0` keeps it for all time, and the disk can fill |
| `PF_IGNOREDUPES` | not changed (`ON`) | Shows only the first pass of one aircraft and flight each day |

Start with the template values. A larger fence makes a longer log, and nothing else. To keep one frequent aircraft out of the log, put its address in a file `planefence-ignore.txt` in the same folder, one entry on each line.

> **Caution.** The README says this file is a filter pattern. A wide pattern such as `.*` removes every aircraft from the log.

## 7. Choose the watchlist

Plane-alert asks a different question from planefence. Planefence asks where an aircraft is. Plane-alert asks which aircraft it is.

The watchlist is [plane-alert-db](https://github.com/sdr-enthusiasts/plane-alert-db), a community project that volunteers keep. On 2026-10-10 its README counted about 17,259 aircraft in 54 categories. Its military list held 9,856 of them, which is more than half. Other categories are government aircraft, police aircraft, air ambulances, aerial firefighters, and historic aircraft. The list also has the aircraft of famous people (its category "Don't you know who I am?", 81 aircraft), of heads of state (among others "Dictator Alert", 315, and "Royal Aircraft", 9) and of sports teams ("Football", 22).

The project publishes the list as several files:

| File | Content | Aircraft on 2026-10-10 |
|---|---|---|
| `plane-alert-db.csv` | The main list: all categories | 17,259 |
| `plane-alert-db-images.csv` | The main list, with up to 4 photo links for each aircraft | the same aircraft |
| `plane-alert-mil.csv` | Military only | 9,856 |
| `plane-alert-gov.csv` | Governments and government agencies | 1,764 |
| `plane-alert-pol.csv` | Police forces | 996 |
| `plane-alert-civ.csv` | Civilian aircraft, with the historic and distinctive ones | 4,643 |
| `plane-alert-pia.csv` | Aircraft that fly under a privacy address | 94 |

Each of the four smaller lists also has an `-images` version. The project calls the photo links experimental and says not to depend on them.

### The privacy address

In the United States, an aircraft owner can ask the FAA for an alternate, temporary address. The FAA does not assign that address to the owner in the public registry. The program has the name Privacy ICAO Address (PIA). The small list `plane-alert-pia.csv` collects addresses of that kind.

Plane-alert reads its lists first to last. The first list that holds an address gives the alert its label. I put the privacy list first. So an aircraft on both lists gets the label that says it flies under a privacy address.

### Set the list

1. Open `planefence.config` again.
2. Set this line. It is one long line with one comma and no space.

   ```ini
   PF_ALERTLIST=https://raw.githubusercontent.com/sdr-enthusiasts/plane-alert-db/main/plane-alert-pia.csv,https://raw.githubusercontent.com/sdr-enthusiasts/plane-alert-db/main/plane-alert-db-images.csv
   ```

3. Make sure that this line says `ON`.

   ```ini
   PLANEALERT=ON
   ```

4. Save the file. Run `docker compose up -d planefence --force-recreate` in `/opt/station`.
5. Open the alert page: `http://vhf:8088/plane-alert`.

   Result: the alert page. It is empty until the first aircraft on the watchlist comes in range. My station gets about twenty matches a day, so I expect the first one within hours.

For a smaller set of alerts, use the smaller files. For example, use only `plane-alert-mil-images.csv` and `plane-alert-pol-images.csv`. The setting takes up to 10 files or addresses.

### How the list updates

The container downloads every list again every 6 hours. You do nothing. The interval is fixed in the container's own script (`LOOPTIME=6h`). I found no setting for it. Volunteers change the list often: the last change that I saw was on the check date of this guide.

### What plane-alert covers

The setting `PF_PARANGE` is the radius in which plane-alert works. The template value is `999999`, which means every aircraft that your station hears. So plane-alert does not use the fence. Leave it as it is.

## 8. The emergency codes

A pilot sets a four-digit code on the transponder. The crew calls it a squawk code. Three codes report an emergency. Plane-alert sends an alert for any aircraft that sends one of them, on the watchlist or not.

| Code | Meaning | Source |
|---|---|---|
| 7500 | The aircraft is hijacked | FAA Aeronautical Information Manual, 6-3-4 |
| 7600 | The radio has failed | FAA Aeronautical Information Manual, 6-4-2 |
| 7700 | A general emergency | FAA Aeronautical Information Manual, 6-2-2 |
| 7400 | An unmanned aircraft has lost its control link | The planefence template lists the code. I did not confirm the meaning in an FAA document |

1. Find this line in `planefence.config`. The template already holds this value.

   ```ini
   PF_PA_SQUAWKS=7400,7500,7600,7700
   ```

2. Leave it as it is, or remove a code that you do not want. An empty value turns the function off. An `x` stands for any one digit, for example `7x00`.

Result: an emergency alert has the same form as a watchlist alert. In Discord its color bar is always red.

Expect few of these. Expect false ones too. The FAA manual tells pilots to change codes with care, because a wrong digit on the way causes a short false alarm.

## 9. Send alerts to Discord

Discord is a chat app. A webhook is an address that a program can post a message to. Each webhook belongs to one channel.

> **Your position can become public.** An alert shows which aircraft are in range of you, with a picture of your map. A stream of alerts in a shared channel shows your region. Use a private server of your own. Choose a `<NAME>` with no town and no street in it. Section 10 shows how to keep your station mark out of the picture.

> **Caution.** Anyone who has the webhook address can post to your channel. Keep it out of screenshots and out of public files. `planefence.config` holds it in plain text: do not publish that file.

### Make the webhook

1. In Discord, create a server of your own, or open one that you own.
2. Right-click the channel that you want the alerts in. Click "Edit Channel".
3. Open the page "Integrations". Click "Create Webhook".
4. Give the webhook a name, for example "plane-alert".
5. Click "Copy Webhook URL". This is `<WEBHOOK>`.

### Set planefence

1. Open `planefence.config`.
2. Set these lines.

   ```ini
   PA_DISCORD=ON
   PA_DISCORD_WEBHOOKS=<WEBHOOK>
   PF_DISCORD=OFF
   DISCORD_FEEDER_NAME=<NAME>
   DISCORD_MEDIA=screenshot+photo
   PF_SCREENSHOTURL=http://screenshot:5042
   ```

3. Save the file. Run `docker compose up -d planefence --force-recreate` in `/opt/station`.

Result: the next match on the watchlist posts a message to your channel. Nothing arrives until then.

What the settings do:

| Setting | Value | What it does |
|---|---|---|
| `PA_DISCORD` | `ON` | Sends a message for each plane-alert match |
| `PF_DISCORD` | `OFF` | Sends no message for a fence crossing. With `ON`, every low aircraft near your house sends one |
| `DISCORD_FEEDER_NAME` | `<NAME>` | Appears in every alert |
| `DISCORD_MEDIA` | `screenshot+photo` | The map picture is the large image, and the photo is the small one. Other values: `photo+screenshot`, `screenshot`, `photo`, or empty for no image |
| `PA_DISCORD_COLOR` | not changed | The color bar of the message. Red is kept for emergencies |

For the lowest-cost path, set `DISCORD_MEDIA=photo`.

### What an alert looks like

This is an invented example. It shows the parts, not a real aircraft.

```
 PLANE-ALERT                                    example
 ------------------------------------------------------
 Type        twin-engine transport aircraft
 Operator    an air force
 Category    Other Air Forces
 Seen by     <NAME>
 [ large image: your map, centered on the aircraft ]
 [ small image: a photo, from a link in the watchlist ]
```

On my station I get about twenty alerts on a normal day. Most are for military aircraft. Your number depends on what flies in your region.

## 10. Add the map picture

The screenshot helper is the second container from section 5. When plane-alert finds a match, it asks the helper for a picture. The helper opens your tar1090 map in a hidden browser, centered on that aircraft. It waits, takes the picture, and returns it.

1. Find the address of an aircraft that is on your map now.

   ```bash
   curl -s http://localhost:8080/data/aircraft.json | head -c 300
   ```

   Result: text with `"hex":"......"` entries. Copy the six characters of one entry. This is `<hex>`.

2. Ask the helper for a picture of that aircraft, from inside the planefence container.

   ```bash
   docker exec planefence curl -s -o /tmp/t.png http://screenshot:5042/snap/<hex>
   docker exec planefence ls -l /tmp/t.png
   ```

   Result: after about 30 seconds, a file of many kilobytes. A file of a few bytes is a picture of one pixel. The helper returns that for an address that is not valid.

3. Copy the picture out and look at it.

   ```bash
   docker cp planefence:/tmp/t.png /tmp/t.png
   ```

   Then copy `/tmp/t.png` from `vhf` to your own computer, for example with `scp vhf:/tmp/t.png .`, and open it.

   Result: a dim map with the aircraft in red and its track.

The settings of the helper, in the `screenshot` service of the compose file:

| Setting | My value ◆ | What it does |
|---|---|---|
| `BASE_URL` | `http://ultrafeeder` | The address of your tar1090 map, as the helper reaches it |
| `MAP_ARGS` | see section 5 | Options that the helper adds to the map address. `zoom=` takes 1 to 20: a lower number shows a wider area |
| `LOAD_SLEEP_TIME` | `25` | Seconds that the helper waits before it takes the picture |
| `MAXTIME` | `60` | Seconds until the helper gives up |
| `CANVAS_SIZE` | `800,1200` | The size of the picture in pixels. The project's compose file writes it with a comma. Its README writes `width`x`height` |
| `shm_size` | `1gb` | Shared memory for the hidden browser |

In `MAP_ARGS`, the option `screenshot` stops map updates after the first load, and `rangerings=0` turns the range rings off. Both are in the project's compose file.

### Keep your station mark out of the picture

The picture is centered on the aircraft, not on you. But a tar1090 map can draw a mark at the receiver. To hide it, set this line in the `environment:` list of `ultrafeeder`.

```yaml
      - READSB_RX_LOCATION_ACCURACY=1
```

`1` rounds the position on your own map to two decimal places. `0` removes it. The tracking networks still receive the exact position. After the change, take a test picture again and look at it.

## 11. Optional: an alert on a phone for a low aircraft

Planefence can send a message for each fence crossing. I keep that off, because low aircraft cross my fence all day. For a message, I use a stricter rule in Home Assistant, the home automation software. Once a minute it finds the lowest aircraft that my station hears. If that aircraft is below 1,500 feet, my phone gets an alert. One tap opens a map of that aircraft. Then the rule stays quiet for 15 minutes.

This section needs a Home Assistant system that already runs, with its phone app set up for notifications. The guide does not install Home Assistant.

> **Caution.** This rule has no distance test. It looks at every aircraft that your station hears. If an airport is in range of your receiver, each landing can cause an alert. Then use a lower `<LOW_FT>`, or use the fence log only.

### Read the aircraft file

1. Open `configuration.yaml` of Home Assistant in its file editor.
2. Add this sensor. If the file already has a `sensor:` line, add only the lines below it.

   ```yaml
   sensor:
     - platform: rest
       name: "ADSB Aircraft Count"
       resource: "http://vhf:8080/data/aircraft.json"
       scan_interval: 60
       value_template: "{{ value_json.aircraft | length }}"
       unit_of_measurement: "aircraft"
       json_attributes:
         - aircraft
   ```

   Home Assistant must reach that address. If it cannot find the name `vhf`, use the fixed address of the receiver computer.

3. Add these two template sensors. If the file already has a `template:` line, add only the lines below it.

   ```yaml
   template:
     - sensor:
         - name: "ADSB Lowest Altitude"
           unit_of_measurement: "ft"
           state_class: measurement
           state: >
             {% set ac = state_attr('sensor.adsb_aircraft_count', 'aircraft') or [] %}
             {% set alts = ac | selectattr('alt_baro', 'defined') | selectattr('alt_baro', 'number') | map(attribute='alt_baro') | list %}
             {% if alts | length > 0 %}{{ alts | min }}{% else %}{{ none }}{% endif %}
           attributes:
             hex: >
               {% set ac = state_attr('sensor.adsb_aircraft_count', 'aircraft') or [] %}
               {% set airborne = ac | selectattr('alt_baro', 'defined') | selectattr('alt_baro', 'number') | list %}
               {% if airborne | length > 0 %}{{ (airborne | sort(attribute='alt_baro') | first).hex }}{% else %}none{% endif %}
             aircraft_type: >
               {% set ac = state_attr('sensor.adsb_aircraft_count', 'aircraft') or [] %}
               {% set airborne = ac | selectattr('alt_baro', 'defined') | selectattr('alt_baro', 'number') | list %}
               {% if airborne | length > 0 %}{{ (airborne | sort(attribute='alt_baro') | first).desc | default((airborne | sort(attribute='alt_baro') | first).t | default('unknown', true), true) }}{% else %}none{% endif %}

         - name: "ADSB Emergency Squawk"
           state: >
             {% set ac = state_attr('sensor.adsb_aircraft_count', 'aircraft') or [] %}
             {% set em = ac | selectattr('squawk', 'defined') | selectattr('squawk', 'in', ['7500', '7600', '7700']) | list %}
             {% if em | length > 0 %}{{ (em | first).squawk }}{% else %}clear{% endif %}
   ```

4. In Home Assistant, open Developer tools, then the YAML page. Click "Check configuration". If it reports no error, restart Home Assistant.
5. Open Developer tools, then States. Search for `adsb`.

   Result: `sensor.adsb_aircraft_count` shows a number. `sensor.adsb_lowest_altitude` shows an altitude in feet. `sensor.adsb_emergency_squawk` shows `clear`.

How it works: `alt_baro` is the barometric altitude in feet. An aircraft on the ground has the text `ground` in that field, and the test `number` leaves it out. The aircraft type comes from the fields `desc` and `t`. Those fields are in the file only if ultrafeeder has `TAR1090_ENABLE_AC_DB=true`.

### Make the two rules

1. Open `automations.yaml`, or the file where you keep automations.
2. Add these two automations. Put the name of your phone in place of `<DEVICE>`, and your altitude in place of `<LOW_FT>`.

   ```yaml
   - id: alert_adsb_low_aircraft
     alias: "Alert: low aircraft"
     triggers:
       - trigger: numeric_state
         entity_id: sensor.adsb_lowest_altitude
         below: <LOW_FT>
     conditions:
       - condition: template
         value_template: >
           {{ this.attributes.last_triggered is none or
              (now() - this.attributes.last_triggered).total_seconds() > 900 }}
     actions:
       - action: notify.mobile_app_<DEVICE>
         data:
           title: "Low aircraft"
           message: >
             {{ state_attr('sensor.adsb_lowest_altitude', 'aircraft_type') }}
             at {{ states('sensor.adsb_lowest_altitude') }} ft. Tap for the map.
           data:
             url: "https://globe.adsbexchange.com/?icao={{ state_attr('sensor.adsb_lowest_altitude', 'hex') }}"
             clickAction: "https://globe.adsbexchange.com/?icao={{ state_attr('sensor.adsb_lowest_altitude', 'hex') }}"
     mode: single

   - id: alert_adsb_emergency_squawk
     alias: "Alert: aircraft emergency code"
     triggers:
       - trigger: state
         entity_id: sensor.adsb_emergency_squawk
         from: "clear"
     conditions:
       - condition: template
         value_template: "{{ trigger.to_state.state in ['7500', '7600', '7700'] }}"
     actions:
       - action: notify.mobile_app_<DEVICE>
         data:
           title: "Aircraft emergency code"
           message: >
             An aircraft in range sends code {{ states('sensor.adsb_emergency_squawk') }}
             ({{ {'7500': 'hijack', '7600': 'radio failure', '7700': 'general emergency'}[states('sensor.adsb_emergency_squawk')] }}).
     mode: single
   ```

3. Open Developer tools, then the YAML page. Click "Check configuration". Then click "Automations" to load the file again.
4. Test the first rule. Open Settings, Automations, and the rule "Alert: low aircraft". Open its menu and click "Run actions".

   Result: your phone gets the alert. One tap opens the public map on that aircraft.

Facts to know:

- **`url` and `clickAction`.** The first is for an iPhone and the second is for an Android phone. Both hold the same address.
- **The map in the alert is a public tracker.** Your phone can open it when you are not at home. To open your own map, use `http://vhf:8080/?icao=...` in its place. That works only on your home network.
- **900 seconds** is the 15 minutes of quiet. Change the number to change the time.
- **The rule starts when the lowest altitude goes below the limit.** If a second low aircraft comes while the first is still low, the rule does not start again.
- **Once a minute is slow.** A fast, low aircraft can pass between two readings. `scan_interval` sets the time in seconds.

## 12. Run it unattended

Both containers have `restart: unless-stopped`. They start again after a power cut. The watchlist updates by itself. Three jobs remain for you.

### Update the containers

```bash
cd /opt/station
docker compose pull planefence screenshot
docker compose up -d planefence screenshot --force-recreate
```

Result: Docker downloads new images if there are any, and starts the containers again. Your settings and your log stay, because they are in `/opt/station/planefence/`. New settings appear in the file `planefence.config.RENAME-and-EDIT-me` and on the settings page. Your own `planefence.config` does not change.

### Keep a copy of the settings

Copy `/opt/station/planefence/config/planefence.config` to a safe place after each change. It holds your position and your webhook address: keep the copy private. The settings page also keeps old versions in the folder `config-backups`.

### Remove alerts that you do not want

If one kind of aircraft fills the channel, exclude it. The setting takes aircraft type codes, addresses, registrations, or any text in the watchlist entry. Put commas between the entries.

```ini
PA_EXCLUSIONS=tex2
```

This example is from the README. It removes every aircraft of one trainer type. Text is not case sensitive. Put a `\` before a space.

After the change, read the container log.

```bash
docker logs planefence 2>&1 | grep -i "excluded"
```

Result: one line for each entry, with the number of watchlist entries that it removes, then a total. If the total is thousands and you meant one aircraft, your entry is too wide.

## 13. Check that it works

Do these checks in order.

1. **The containers run.**

   ```bash
   docker ps --filter name=planefence --filter name=screenshot
   ```

   Pass: two lines, both with "Up" in the status column.

2. **Planefence reads the stream.** Open `http://vhf:8088/`. Pass: the time of the last update is less than 2 minutes old, and the count of messages is above zero. Wait 2 minutes after a start.

3. **The fence logs.** Wait until an aircraft passes inside your fence. Pass: a new row in the table. Near an airport that takes minutes. Far from one, it can take a day.

4. **The watchlist is loaded.** Open `http://vhf:8088/plane-alert`. Pass: the page opens. A row appears with the first match. Wait up to one day.

5. **The helper takes a picture.** Do the test in section 10. Pass: a picture with a map that has loaded, and the aircraft in it.

6. **Discord receives.** Wait for the first match. Pass: a message in your channel with the type, the operator, the category, a photo and the map picture. Wait up to one day.

7. **The phone alert works.** Do step 4 of "Make the two rules" in section 11. Pass: the alert on your phone, and the map after one tap.

Then let it run for one week, and count the alerts for each day. That number is your baseline. Change one setting at a time, and compare with it.

## 14. If it does not work

| Symptom | Likely cause | What to do |
|---|---|---|
| The log says "We cannot reach" your stream host at `30003` | `PF_SOCK30003HOST` is wrong, or the decoder gives no SBS stream | Do section 4 again. In one compose file, use the container name `ultrafeeder`. On another computer, publish `30003:30003` and use the computer's name or address |
| The log repeats that `planefence.config` is not available | The template was not copied | Do step 5 of section 5 |
| The log page shows messages, but the table stays empty | No aircraft entered the fence, or the position is wrong | Check `FEEDER_LAT` and `FEEDER_LON`. Check the sign of the longitude: west is negative. Make `PF_MAXDIST` larger for one day |
| A change to the position or the stream host has no effect | Those settings need a new start | `docker compose up -d planefence --force-recreate` |
| The alert page stays empty for days | The watchlist address has a mistake, or `PLANEALERT` is `OFF` | Compare `PF_ALERTLIST` with section 7, character by character. Read `docker logs planefence` |
| Aircraft with no position never cause an alert or a log row | Multilateration results are not in the SBS stream | Section 4, "Aircraft found by multilateration" |
| No message in Discord, but rows on the alert page | `PA_DISCORD` is `OFF`, or the webhook address is wrong or was deleted | Check both lines. Make a new webhook and paste it again |
| A message for every low aircraft | `PF_DISCORD=ON` | Set it to `OFF` |
| The alert has a photo but no map picture | The `screenshot` container does not run, or its host name is not `screenshot` | `docker ps`. Check `hostname: screenshot` and `PF_SCREENSHOTURL=http://screenshot:5042` |
| The alert picture is a grey rectangle | The helper took the picture before the map had loaded | Raise `LOAD_SLEEP_TIME`. I use 25 |
| The alert picture is a map with no aircraft in it | The view is too narrow, or the aircraft left your range before the picture | Lower the `zoom=` number in `MAP_ARGS`. I use 7. The second cause has no cure |
| The test picture is a file of a few bytes | The address was not valid | Take a `<hex>` from `aircraft.json` that is on the map now |
| The helper's log shows time-outs | The computer is slow or short of memory | Raise `MAXTIME`. Check memory with `free -h`. The helper used 0.66 GB on my station |
| Too many alerts for one kind of aircraft | A training base or a busy operator is in range | `PA_EXCLUSIONS`, section 12 |
| The Home Assistant sensors show `unknown` | Home Assistant cannot reach `aircraft.json` | Open the address from step 2 of section 11 in a browser on the same network. Use the fixed address, not the name |
| The phone alert has no aircraft type | The file has no `desc` or `t` field | Set `TAR1090_ENABLE_AC_DB=true` for ultrafeeder |
| The phone alert comes for aircraft far away | The rule has no distance test | Lower `<LOW_FT>`, or turn the rule off |

## 15. What I did wrong

My first setup had two faults. Both were in the map picture.

1. **The picture was zoomed in too far.** One real alert arrived with no aircraft in the picture. I changed the zoom from 9 to 7. Each step makes the view twice as wide, so two steps show four times the width. Start at `zoom=7`.
2. **The picture was taken too early.** The map had not loaded, so the picture was a grey rectangle. A wide view needs more map tiles. I changed `LOAD_SLEEP_TIME` from 15 to 25 seconds.

One limit remains. An aircraft at the edge of my range can leave before the picture is taken. The longer wait makes that a little more likely. I accept it.

One thing I did right from the start: no message for a fence crossing. The log is enough for that. A channel that fills with ordinary aircraft is a channel that you stop reading.

## 16. Not run and not confirmed

- No step in this guide was run on a new, empty computer as written. The settings are the ones on my station, checked against each project's documents on 2026-10-10.
- The check command for the stream in section 4 (`docker exec ultrafeeder ... /dev/tcp/localhost/30003`). I wrote it for this guide and did not run it. It needs `bash` and `timeout` in the ultrafeeder container.
- The form of the SBS lines (`MSG,`) and of the start of `aircraft.json`. I did not read them from my station for this guide.
- That a later change of the lowest aircraft does not start the Home Assistant rule again while the altitude stays below the limit. That is how I read the trigger's document. I did not test it.
- Planefence with a decoder other than ultrafeeder. The options in section 4 are from the planefence README.
- The setting `READSB_FORWARD_MLAT_SBS`. It is from the README of planefence and of ultrafeeder. I did not check it on my station.
- Planefence and the receiver on one Raspberry Pi with 4 GB. I measured the memory of the screenshot helper only.
- The lowest-cost path with no `screenshot` service and `DISCORD_MEDIA=photo`. The setting is in the template. I did not run it.
- The copy of the test picture with `docker cp` in section 10. My station runs the first two commands of that test only.
- That the settings page at `9999` and the log page at `8088` have no login. I found none in the README. I did not look for one in the program.
- `PA_EXCLUSIONS`, `planefence-ignore.txt`, `PF_ELEVATION` and `PF_FUDGELOC`. They are from the README and the template. My station does not change them.
- The Home Assistant files in section 11. My station runs the same two rules, written in the older spelling (`trigger:`, `platform:`, `service:`) and sent to all my devices at once. I wrote the newer spelling from the Home Assistant documents of 2026-10-10 and did not run it. My own rules also show the registration and the callsign, and a second button for the local map.
- That a REST sensor can hold the full aircraft list as one attribute on a busy station. It works on mine. Home Assistant may warn about the size, and I did not read its limit.
- The "Run actions" test in Home Assistant, and the menu names in Discord and Home Assistant. Both apps change their menus.
- The meaning of code 7400. The planefence template lists the code. I did not find the FAA text for it on the check date.
- That each map zoom step doubles the scale. It is the usual rule for web maps. I did not confirm it in the tar1090 documents.
- The start value of the zoom. I started at 9. The project's compose file now ships `zoom=7`, and the helper's own README gives 11 as its default. I do not know which value a new install had when I built mine.
- The default wait. The project's compose file ships `LOAD_SLEEP_TIME=15`. The helper's own README gives 1 second as the default when the line is absent.
- The spelling of `CANVAS_SIZE`. My station uses the comma, as the compose file does.
- The licence of the screenshot helper. GitHub reports no standard licence.
- Prices for the Raspberry Pi 4 and the ZimaBlade. Read the listings.
- The numbers of alerts for each day are from my station, in my region. Yours will differ.

## 17. Sources

All were read on 2026-10-10, unless a line says another date.

**The episode.** Episode 2 of Spurious Emissions, "Planefence and Plane-Alert": its fact sheet, script and link list.

**The full-station guide.** [Build the station](/guides/build-the-station/), section 8 and section 17. Its commands were checked against my station on 2026-10-07. The compose services and the test command in this guide come from there.

**Planefence and plane-alert.** <https://github.com/sdr-enthusiasts/docker-planefence>: `README.md`, `README-discord-alerts.md`, `README-privacy.md`, `docker-compose.yml`, the settings template `planefence.config.RENAME-and-EDIT-me`, and the script `get-pa-alertlist` (the 6-hour interval). Last change to the repository that I saw: 2026-10-04.

**The watchlist.** <https://github.com/sdr-enthusiasts/plane-alert-db>: `README.md` (the counts, the file list, the order of lists, the licence).

**The screenshot helper.** <https://github.com/kx1t/browser-screenshot-service>: `README.md` (the settings and their defaults).

**The receiver.** <https://github.com/sdr-enthusiasts/docker-adsb-ultrafeeder>: `README.md` (the output at `30003/tcp`, `READSB_FORWARD_MLAT_SBS`, `TAR1090_ENABLE_AC_DB`, `aircraft.json`). Map options: <https://github.com/wiedehopf/tar1090/blob/master/README-query.md>. Fields of `aircraft.json`: <https://github.com/wiedehopf/readsb/blob/dev/README-json.md>.

**Home Assistant.** I read these through the Context7 documentation service, from the source of the project's documents. RESTful sensor, automation syntax (the release note of 2024.10), the numeric state trigger and the notify action: <https://github.com/home-assistant/home-assistant.io>. A notification that opens an address (`docs/notifications/basic.md`): <https://github.com/home-assistant/companion.home-assistant>.

**Rules and public facts.** FAA Privacy ICAO Address program: <https://www.faa.gov/air_traffic/technology/equipadsb/privacy>. FAA Aeronautical Information Manual, paragraphs 4-1-20, 6-2-2, 6-3-4 and 6-4-2: <https://www.faa.gov/air_traffic/publications/atpubs/aim_html/>. 47 U.S.C. 605: <https://www.law.cornell.edu/uscode/text/47/605> (read for the full-station guide on 2026-10-07, not again for this guide).

**Parts.** The maker pages linked from section 2. Prices are from the full-station guide, checked on 2026-10-07.

[^layout]: My aircraft radio is on a Raspberry Pi 4 with FlightAware's own PiAware system. A small server runs ultrafeeder, planefence and the screenshot helper, and reads the Pi 4 over the network. Planefence reads ultrafeeder, not the Pi 4. One computer for all of it is simpler.
