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 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 gives all three items with no change. For other decoders, read section 4.
Hardware
| ☐ | Part | Job | Price | Note |
|---|---|---|---|---|
| ☐ | ◆ FlightAware Pro Stick Plus | The ADS-B radio, already in your receiver | $45.99, FlightAware store | Any radio that your decoder supports is correct |
| ☐ | ◆ Raspberry Pi 4 | The computer at my antenna cable. It runs the radio and the decoder | See listing | My layout uses two computers1 |
| ☐ | ◆ ZimaBlade | 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, 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 | Planefence and plane-alert in one container | Free, GPL-3.0 |
| ☐ | ◆ plane-alert-db | The community watchlist | Free, Open Database License |
| ☐ | ◆ 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 account and a server of your own | Receives the alert messages | Free |
| ☐ | ◆ Home Assistant 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
-
Log in to
vhfwith SSH. -
Read three lines of the stream.
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. -
Read the aircraft file.
curl -s http://localhost:8080/data/aircraft.json | head -c 300Result: text that starts with
{ "now" :and contains"hex":entries.8080is the map address from the ultrafeeder compose file. Use yours if it is different. -
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-protobufcontainer, the README gives two environment lines:READSB_NET_SBS_OUTPUT_PORT=30003andREADSB_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.1
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.
- 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
-
Open the compose file that holds your
ultrafeederservice.cd /opt/station && nano docker-compose.yml -
Add these two services below the last service. Keep the two spaces at the start of the lines
planefence:andscreenshot:.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,1200The host name of the second container must be
screenshot. Planefence looks for that name. For the lowest-cost path, leave out thescreenshotservice. -
Save the file. Start the containers.
docker compose up -dResult: Docker downloads two images and starts them. The first start can take a minute or more.
-
Read the log of planefence.
docker logs -f planefenceResult: warnings that
planefence.configis not available. That is correct at this point. Press Ctrl-C to stop the log. -
Make the settings file from the template that the container wrote.
cd /opt/station/planefence/config cp planefence.config.RENAME-and-EDIT-me planefence.configResult: a file
planefence.configwith every setting and a comment for each one.
Caution. The settings page at
9999and the log page at8088have 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:
- In the compose file of the receiver, add
- 30003:30003to theports:list ofultrafeeder. Rundocker compose up -dthere. - In section 6, set
PF_SOCK30003HOSTto the name or the address of the receiver computer. - In the
screenshotservice, setBASE_URLto the full address of your map, for examplehttp://vhf:8080.
6. Set the fence
-
Open the settings file.
nano /opt/station/planefence/config/planefence.config -
Find each of these lines and set its value. Do not rename a setting.
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 itFEEDER_LONGorREADSB_LON. -
Save the file. Start the container again with the new settings.
cd /opt/station && docker compose up -d planefence --force-recreate -
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, 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
-
Open
planefence.configagain. -
Set this line. It is one long line with one comma and no space.
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 -
Make sure that this line says
ON.PLANEALERT=ON -
Save the file. Run
docker compose up -d planefence --force-recreatein/opt/station. -
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 |
-
Find this line in
planefence.config. The template already holds this value.PF_PA_SQUAWKS=7400,7500,7600,7700 -
Leave it as it is, or remove a code that you do not want. An empty value turns the function off. An
xstands for any one digit, for example7x00.
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.configholds it in plain text: do not publish that file.
Make the webhook
- In Discord, create a server of your own, or open one that you own.
- Right-click the channel that you want the alerts in. Click “Edit Channel”.
- Open the page “Integrations”. Click “Create Webhook”.
- Give the webhook a name, for example “plane-alert”.
- Click “Copy Webhook URL”. This is
<WEBHOOK>.
Set planefence
-
Open
planefence.config. -
Set these lines.
PA_DISCORD=ON PA_DISCORD_WEBHOOKS=<WEBHOOK> PF_DISCORD=OFF DISCORD_FEEDER_NAME=<NAME> DISCORD_MEDIA=screenshot+photo PF_SCREENSHOTURL=http://screenshot:5042 -
Save the file. Run
docker compose up -d planefence --force-recreatein/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.
-
Find the address of an aircraft that is on your map now.
curl -s http://localhost:8080/data/aircraft.json | head -c 300Result: text with
"hex":"......"entries. Copy the six characters of one entry. This is<hex>. -
Ask the helper for a picture of that aircraft, from inside the planefence container.
docker exec planefence curl -s -o /tmp/t.png http://screenshot:5042/snap/<hex> docker exec planefence ls -l /tmp/t.pngResult: 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.
-
Copy the picture out and look at it.
docker cp planefence:/tmp/t.png /tmp/t.pngThen copy
/tmp/t.pngfromvhfto your own computer, for example withscp 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 widthxheight |
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.
- 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
-
Open
configuration.yamlof Home Assistant in its file editor. -
Add this sensor. If the file already has a
sensor:line, add only the lines below it.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: - aircraftHome Assistant must reach that address. If it cannot find the name
vhf, use the fixed address of the receiver computer. -
Add these two template sensors. If the file already has a
template:line, add only the lines below it.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 %} -
In Home Assistant, open Developer tools, then the YAML page. Click “Check configuration”. If it reports no error, restart Home Assistant.
-
Open Developer tools, then States. Search for
adsb.Result:
sensor.adsb_aircraft_countshows a number.sensor.adsb_lowest_altitudeshows an altitude in feet.sensor.adsb_emergency_squawkshowsclear.
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
-
Open
automations.yaml, or the file where you keep automations. -
Add these two automations. Put the name of your phone in place of
<DEVICE>, and your altitude in place of<LOW_FT>.- 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 -
Open Developer tools, then the YAML page. Click “Check configuration”. Then click “Automations” to load the file again.
-
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:
urlandclickAction. 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_intervalsets 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
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.
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.
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.
-
The containers run.
docker ps --filter name=planefence --filter name=screenshotPass: two lines, both with “Up” in the status column.
-
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. -
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.
-
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. -
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.
-
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.
-
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.
- 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. - 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_TIMEfrom 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 needsbashandtimeoutin the ultrafeeder container. - The form of the SBS lines (
MSG,) and of the start ofaircraft.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
screenshotservice andDISCORD_MEDIA=photo. The setting is in the template. I did not run it. - The copy of the test picture with
docker cpin section 10. My station runs the first two commands of that test only. - That the settings page at
9999and the log page at8088have no login. I found none in the README. I did not look for one in the program. PA_EXCLUSIONS,planefence-ignore.txt,PF_ELEVATIONandPF_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, 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.