ChannelSPURIOUS EMISSIONS

Sheet 4 · The written guides

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.

Rev A · checked 2026-10-10

Download the Markdown

The Markdown file is the whole guide in plain text. You can give it to a coding agent.

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. How links work.

Contents

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

  1. Log in to vhf with SSH.

  2. 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.

  3. Read the aircraft file.

    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.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

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

    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:.

      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.

    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.

    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.

    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.

    nano /opt/station/planefence/config/planefence.config
    
  2. 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 it FEEDER_LONG or READSB_LON.

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

    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, 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.

    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.

    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.

    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.

    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.

    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.

    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.

    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 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

  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.

    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.

    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>.

    - 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

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.

  1. The containers run.

    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, 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.

Footnotes

  1. 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. ↩ ↩2

SpurI did not run these steps on a new, empty computer. The settings are the ones on my station, checked against each project's documents on the check date.